From f62686fdeb1304e3eaabd829d95f77adbd7ad9d4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 21:05:56 +0800 Subject: [PATCH 01/74] chore: support local chat --- frontend/rust-lib/Cargo.lock | 42 +++++++------------ frontend/rust-lib/Cargo.toml | 4 +- .../src/persistence/chat_message_sql.rs | 5 ++- frontend/rust-lib/flowy-server/src/lib.rs | 1 - .../impls/chat.rs} | 39 +++++++++++------ .../src/local_server/impls/mod.rs | 2 + .../flowy-server/src/local_server/server.rs | 13 ++++-- frontend/rust-lib/flowy-server/src/server.rs | 5 +-- 8 files changed, 59 insertions(+), 52 deletions(-) rename frontend/rust-lib/flowy-server/src/{default_impl.rs => local_server/impls/chat.rs} (83%) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 671d8bccf2..342e454b9d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "async-trait", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "bincode", "bytes", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "app-error", "jsonwebtoken", @@ -4098,7 +4098,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "bytes", @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,7 +5209,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5277,19 +5276,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -6784,7 +6770,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=873415478ed58686c98df578e2c39d07ddce6773#873415478ed58686c98df578e2c39d07ddce6773" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=39057f85a408a40dc702a9e54fd1b9dc91bb36e4#39057f85a408a40dc702a9e54fd1b9dc91bb36e4" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8d8e11f6a0..f1d811bf2d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,8 +105,8 @@ tantivy = { version = "0.24.0" } # 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 = "873415478ed58686c98df578e2c39d07ddce6773" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "873415478ed58686c98df578e2c39d07ddce6773" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "39057f85a408a40dc702a9e54fd1b9dc91bb36e4" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "39057f85a408a40dc702a9e54fd1b9dc91bb36e4" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs index aa4dd8215d..6eaf6798e3 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs @@ -65,7 +65,10 @@ pub fn select_chat_messages( query = query.filter(chat_message_table::message_id.lt(before_message_id)); } query = query - .order((chat_message_table::message_id.desc(),)) + .order(( + chat_message_table::created_at.desc(), + chat_message_table::message_id.desc(), + )) .limit(limit_val); let messages: Vec = query.load::(&mut *conn)?; diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 33f4b0c0d8..034991a984 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -5,5 +5,4 @@ pub mod local_server; mod response; mod server; -mod default_impl; pub mod util; diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs similarity index 83% rename from frontend/rust-lib/flowy-server/src/default_impl.rs rename to frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 02e313115f..a68dbc8b4f 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -6,15 +6,16 @@ use flowy_ai_pub::cloud::{ }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; +use lib_infra::util::timestamp; use serde_json::Value; use std::collections::HashMap; use std::path::Path; use uuid::Uuid; -pub(crate) struct DefaultChatCloudServiceImpl; +pub(crate) struct LocalServerChatServiceImpl; #[async_trait] -impl ChatCloudService for DefaultChatCloudServiceImpl { +impl ChatCloudService for LocalServerChatServiceImpl { async fn create_chat( &self, _uid: &i64, @@ -22,29 +23,40 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { _chat_id: &Uuid, _rag_ids: Vec, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(()) } async fn create_question( &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message: &str, - _message_type: ChatMessageType, + message: &str, + message_type: ChatMessageType, _metadata: &[ChatMessageMetadata], ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + match message_type { + ChatMessageType::System => Ok(ChatMessage::new_system(timestamp(), message.to_string())), + ChatMessageType::User => Ok(ChatMessage::new_human( + timestamp(), + message.to_string(), + None, + )), + } } async fn create_answer( &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message: &str, - _question_id: i64, - _metadata: Option, + message: &str, + question_id: i64, + metadata: Option, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let mut message = ChatMessage::new_ai(timestamp(), message.to_string(), Some(question_id)); + if let Some(metadata) = metadata { + message.metadata = metadata; + } + Ok(message) } async fn stream_answer( @@ -81,9 +93,12 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { &self, _workspace_id: &Uuid, _chat_id: &Uuid, - _message_id: i64, + message_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], + }) } async fn get_answer( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs index 0280cfbefb..f63265e734 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs @@ -1,8 +1,10 @@ +pub(crate) use chat::*; pub(crate) use database::*; pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; +mod chat; mod database; mod document; mod folder; 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 cb8b545c53..280a56cfe0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,13 +1,13 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use tokio::sync::mpsc; - +use flowy_ai_pub::cloud::ChatCloudService; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::FlowyError; use flowy_folder_pub::cloud::FolderCloudService; use flowy_storage_pub::cloud::StorageCloudService; +use tokio::sync::mpsc; // use flowy_user::services::database::{ // get_user_profile, get_user_workspace, open_collab_db, open_user_db, // }; @@ -15,8 +15,9 @@ use flowy_user_pub::cloud::UserCloudService; use flowy_user_pub::entities::*; use crate::local_server::impls::{ - LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, - LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl, + LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, + LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, + LocalServerUserAuthServiceImpl, }; use crate::AppFlowyServer; @@ -78,4 +79,8 @@ impl AppFlowyServer for LocalServer { fn database_ai_service(&self) -> Option> { None } + + fn chat_service(&self) -> Arc { + Arc::new(LocalServerChatServiceImpl) + } } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index ee07eefa5a..4c92fe28d2 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -12,7 +12,6 @@ use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage}; -use crate::default_impl::DefaultChatCloudServiceImpl; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; @@ -103,9 +102,7 @@ pub trait AppFlowyServer: Send + Sync + 'static { /// An `Arc` wrapping the `DocumentCloudService` interface. fn document_service(&self) -> Arc; - fn chat_service(&self) -> Arc { - Arc::new(DefaultChatCloudServiceImpl) - } + fn chat_service(&self) -> Arc; /// Bridge for the Cloud AI Search features /// From c89f33e2f89f5cc82b3c3ff5b72f2f8b7e683605 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 20:59:25 +0800 Subject: [PATCH 02/74] chore: remove unused code --- .../application/workspace_error_bloc.dart | 15 +--- .../screens/workspace_error_screen.dart | 41 --------- frontend/rust-lib/Cargo.lock | 29 +------ frontend/rust-lib/flowy-ai/src/chat.rs | 3 +- frontend/rust-lib/flowy-ai/src/entities.rs | 4 + frontend/rust-lib/flowy-core/src/lib.rs | 9 ++ .../rust-lib/flowy-core/src/server_layer.rs | 32 +------ frontend/rust-lib/flowy-server/Cargo.toml | 12 +-- .../flowy-server/src/af_cloud/define.rs | 7 +- .../af_cloud/impls/user/cloud_service_impl.rs | 4 - .../src/local_server/impls/chat.rs | 6 +- .../src/local_server/impls/folder.rs | 6 +- .../src/local_server/impls/user.rs | 84 +++++++------------ .../flowy-server/src/local_server/server.rs | 58 +++++-------- .../flowy-server/tests/af_cloud_test/util.rs | 14 +++- frontend/rust-lib/flowy-user-pub/Cargo.toml | 1 + frontend/rust-lib/flowy-user-pub/src/cloud.rs | 2 - .../rust-lib/flowy-user/src/event_handler.rs | 18 ---- frontend/rust-lib/flowy-user/src/event_map.rs | 4 - .../src/services/authenticate_user.rs | 20 ----- .../src/services/sqlite_sql/user_sql.rs | 7 -- .../flowy-user/src/user_manager/manager.rs | 1 - .../user_manager/manager_user_workspace.rs | 18 ---- 23 files changed, 101 insertions(+), 294 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart index ce51fdd10b..7ff50dbd02 100644 --- a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart @@ -1,9 +1,7 @@ import 'package:appflowy/plugins/database/application/defines.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -22,20 +20,10 @@ class WorkspaceErrorBloc void _dispatch() { on( (event, emit) async { - await event.when( + event.when( init: () { // _loadSnapshots(); }, - resetWorkspace: () async { - emit(state.copyWith(loadingState: const LoadingState.loading())); - final payload = ResetWorkspacePB.create() - ..workspaceId = userFolder.workspaceId - ..uid = userFolder.uid; - final result = await UserEventResetWorkspace(payload).send(); - if (!isClosed) { - add(WorkspaceErrorEvent.didResetWorkspace(result)); - } - }, didResetWorkspace: (result) { result.fold( (_) { @@ -68,7 +56,6 @@ class WorkspaceErrorBloc class WorkspaceErrorEvent with _$WorkspaceErrorEvent { const factory WorkspaceErrorEvent.init() = _Init; const factory WorkspaceErrorEvent.logout() = _DidLogout; - const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace; const factory WorkspaceErrorEvent.didResetWorkspace( FlowyResult result, ) = _DidResetWorkspace; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index d79127e04c..bd32696514 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -86,7 +86,6 @@ class WorkspaceErrorScreen extends StatelessWidget { const VSpace(50), const LogoutButton(), const VSpace(20), - const ResetWorkspaceButton(), ]); return Center( @@ -157,43 +156,3 @@ class LogoutButton extends StatelessWidget { ); } } - -class ResetWorkspaceButton extends StatelessWidget { - const ResetWorkspaceButton({super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 200, - height: 40, - child: BlocBuilder( - builder: (context, state) { - final isLoading = state.loadingState?.isLoading() ?? false; - final icon = isLoading - ? const Center( - child: CircularProgressIndicator.adaptive(), - ) - : null; - - return FlowyButton( - text: FlowyText.medium( - LocaleKeys.workspace_reset.tr(), - textAlign: TextAlign.center, - ), - onTap: () { - NavigatorAlertDialog( - title: LocaleKeys.workspace_resetWorkspacePrompt.tr(), - confirm: () { - context.read().add( - const WorkspaceErrorEvent.resetWorkspace(), - ); - }, - ).show(context); - }, - rightIcon: icon, - ); - }, - ), - ); - } -} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 8e46148ea9..ac47524944 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2957,7 +2957,6 @@ dependencies = [ "arc-swap", "assert-json-diff", "bytes", - "chrono", "client-api", "collab", "collab-database", @@ -2966,7 +2965,6 @@ dependencies = [ "collab-folder", "collab-plugins", "collab-user", - "dashmap 6.0.1", "dotenv", "flowy-ai-pub", "flowy-database-pub", @@ -2975,33 +2973,25 @@ dependencies = [ "flowy-folder-pub", "flowy-search-pub", "flowy-server-pub", + "flowy-sqlite", "flowy-storage", "flowy-storage-pub", "flowy-user-pub", "futures", "futures-util", - "hex", - "hyper 0.14.27", "lazy_static", - "lib-dispatch", "lib-infra", - "mime_guess", - "postgrest", "rand 0.8.5", - "reqwest 0.11.27", "semver", "serde", "serde_json", "thiserror 1.0.64", "tokio", - "tokio-retry", "tokio-stream", "tokio-util", "tracing", "tracing-subscriber", - "url", "uuid", - "yrs", ] [[package]] @@ -3138,6 +3128,7 @@ name = "flowy-user-pub" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "base64 0.21.5", "chrono", "client-api", @@ -5382,15 +5373,6 @@ dependencies = [ "postgres-protocol", ] -[[package]] -name = "postgrest" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" -dependencies = [ - "reqwest 0.11.27", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -6130,7 +6112,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.2", "winreg 0.50.0", ] @@ -8443,12 +8424,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - [[package]] name = "webpki-roots" version = "0.26.7" diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 5be2b7b014..a8655ff647 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -581,6 +581,7 @@ impl Chat { author_type: record.author_type, author_id: record.author_id, reply_message_id: record.reply_message_id, + metadata: record.metadata, }) .collect::>(); @@ -641,7 +642,7 @@ fn save_chat_message_disk( author_type: message.author.author_type as i64, author_id: message.author.author_id.to_string(), reply_message_id: message.reply_message_id, - metadata: None, + metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), }) .collect::>(); insert_chat_messages(conn, &records)?; diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index d0cb7eda02..b62899eca3 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -296,6 +296,9 @@ pub struct ChatMessagePB { #[pb(index = 6, one_of)] pub reply_message_id: Option, + + #[pb(index = 7, one_of)] + pub metadata: Option, } #[derive(Debug, Clone, Default, ProtoBuf)] @@ -316,6 +319,7 @@ impl From for ChatMessagePB { author_type: chat_message.author.author_type as i64, author_id: chat_message.author.author_id.to_string(), reply_message_id: None, + metadata: Some(serde_json::to_string(&chat_message.metadata).unwrap_or_default()), } } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 31b98da5e6..21f09c1dad 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -35,6 +35,7 @@ use crate::deps_resolve::*; use crate::log_filter::init_log; use crate::server_layer::{current_server_type, Server, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; +use flowy_sqlite::DBConnection; use user_state_callback::UserStatusCallbackImpl; pub mod config; @@ -334,4 +335,12 @@ impl ServerUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } + + fn user_id(&self) -> FlowyResult { + self.upgrade_user()?.user_id() + } + + fn get_sqlite_db(&self, uid: i64) -> Result { + self.upgrade_user()?.get_sqlite_connection(uid) + } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 0d304c6063..8157748b4f 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,15 +1,15 @@ use arc_swap::ArcSwapOption; use dashmap::DashMap; +use diesel::Connection; +use serde_repr::*; use std::fmt::{Display, Formatter}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -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::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; @@ -115,10 +115,7 @@ impl ServerProvider { let server = match server_type { Server::Local => { - let local_db = Arc::new(LocalServerDBImpl { - storage_path: self.config.storage_path.clone(), - }); - let server = Arc::new(LocalServer::new(local_db)); + let server = Arc::new(LocalServer::new(self.user.clone())); Ok::, FlowyError>(server) }, Server::AppFlowyCloud => { @@ -171,24 +168,3 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } - -struct LocalServerDBImpl { - #[allow(dead_code)] - storage_path: String, -} - -impl LocalServerDB for LocalServerDBImpl { - fn get_user_profile(&self, _uid: i64) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("LocalServer doesn't support get_user_profile"), - ) - } - - fn get_user_workspace(&self, _uid: i64) -> Result, FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("LocalServer doesn't support get_user_workspace"), - ) - } -} diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 9e67081eb7..e94570cc4a 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -12,20 +12,15 @@ crate-type = ["cdylib", "rlib"] tracing.workspace = true futures.workspace = true futures-util = "0.3.26" -reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } -hyper = "0.14" serde.workspace = true serde_json.workspace = true thiserror = "1.0" tokio = { workspace = true, features = ["sync"] } 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 } collab-plugins = { workspace = true } collab-document = { workspace = true } @@ -33,8 +28,6 @@ collab-entity = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } collab-user = { workspace = true } -hex = "0.4.3" -postgrest = "1.0" lib-infra = { workspace = true } flowy-user-pub = { workspace = true } flowy-folder-pub = { workspace = true } @@ -46,14 +39,11 @@ flowy-search-pub = { workspace = true } flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-ai-pub = { workspace = true } -mime_guess = "2.0" -url = "2.4" tokio-util = "0.7" tokio-stream = { workspace = true, features = ["sync"] } -lib-dispatch = { workspace = true } -yrs.workspace = true rand = "0.8.5" semver = "1.0.23" +flowy-sqlite = { workspace = true } [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index db75ea93fe..3b2895b8f8 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,4 +1,5 @@ -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::DBConnection; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -10,4 +11,8 @@ pub const USER_DEVICE_ID: &str = "device_id"; pub trait ServerUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; + + fn user_id(&self) -> FlowyResult; + + fn get_sqlite_db(&self, uid: i64) -> Result; } 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 e59166fc37..6d7d9d743b 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 @@ -378,10 +378,6 @@ where Arc::into_inner(rx) } - async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { - Ok(()) - } - async fn create_collab_object( &self, collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index a68dbc8b4f..a6bfea53b4 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,3 +1,4 @@ +use crate::af_cloud::define::ServerUser; use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, @@ -10,9 +11,12 @@ use lib_infra::util::timestamp; use serde_json::Value; use std::collections::HashMap; use std::path::Path; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerChatServiceImpl; +pub struct LocalServerChatServiceImpl { + pub user: Arc, +} #[async_trait] impl ChatCloudService for LocalServerChatServiceImpl { 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 7bb3139953..bd2372e9d4 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 @@ -1,7 +1,6 @@ #![allow(unused_variables)] use std::sync::Arc; -use crate::local_server::LocalServerDB; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab_entity::CollabType; @@ -14,10 +13,7 @@ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; use uuid::Uuid; -pub(crate) struct LocalServerFolderCloudServiceImpl { - #[allow(dead_code)] - pub db: Arc, -} +pub(crate) struct LocalServerFolderCloudServiceImpl; #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { 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 8d9e342e85..df9050623e 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 @@ -18,24 +18,19 @@ use lib_infra::box_any::BoxAny; 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)); } -pub(crate) struct LocalServerUserAuthServiceImpl { - #[allow(dead_code)] - pub db: Arc, -} - +pub(crate) struct LocalServerUserServiceImpl; #[async_trait] -impl UserCloudService for LocalServerUserAuthServiceImpl { +impl UserCloudService for LocalServerUserServiceImpl { 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 workspace_id = Uuid::new_v4().to_string(); let user_workspace = UserWorkspace::new_local(&workspace_id, uid); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() @@ -58,13 +53,9 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { } async fn sign_in(&self, params: BoxAny) -> Result { - let db = self.db.clone(); 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); + let user_workspace = make_user_workspace(); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -132,16 +123,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { } 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| { - // We don't want to expose the email in the local server - profile.email = "".to_string(); - profile - }) - }, - } + Err(FlowyError::local_version_not_support().with_context("Not support")) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { @@ -155,6 +137,32 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { Ok(vec![]) } + async fn create_workspace(&self, _workspace_name: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + + async fn patch_workspace( + &self, + workspace_id: &Uuid, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + + async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } + async fn get_user_awareness_doc_state( &self, uid: i64, @@ -172,10 +180,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { Ok(encode_collab.doc_state.to_vec()) } - async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { - Ok(()) - } - async fn create_collab_object( &self, _collab_object: &CollabObject, @@ -194,32 +198,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { .with_context("local server doesn't support batch create collab object"), ) } - - async fn create_workspace(&self, _workspace_name: &str) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } - - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } - - async fn patch_workspace( - &self, - workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, - ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) - } } fn make_user_workspace() -> UserWorkspace { 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 280a56cfe0..282d118203 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,40 +1,30 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; -use flowy_document_pub::cloud::DocumentCloudService; -use flowy_error::FlowyError; -use flowy_folder_pub::cloud::FolderCloudService; -use flowy_storage_pub::cloud::StorageCloudService; -use tokio::sync::mpsc; -// use flowy_user::services::database::{ -// get_user_profile, get_user_workspace, open_collab_db, open_user_db, -// }; -use flowy_user_pub::cloud::UserCloudService; -use flowy_user_pub::entities::*; - +use crate::af_cloud::define::ServerUser; use crate::local_server::impls::{ LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, - LocalServerUserAuthServiceImpl, + LocalServerUserServiceImpl, }; use crate::AppFlowyServer; - -pub trait LocalServerDB: Send + Sync + 'static { - fn get_user_profile(&self, uid: i64) -> Result; - fn get_user_workspace(&self, uid: i64) -> Result, FlowyError>; -} +use flowy_ai_pub::cloud::ChatCloudService; +use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; +use flowy_document_pub::cloud::DocumentCloudService; +use flowy_folder_pub::cloud::FolderCloudService; +use flowy_storage_pub::cloud::StorageCloudService; +use flowy_user_pub::cloud::UserCloudService; +use tokio::sync::mpsc; pub struct LocalServer { - local_db: Arc, + user: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(local_db: Arc) -> Self { + pub fn new(user: Arc) -> Self { Self { - local_db, + user, stop_tx: Default::default(), } } @@ -49,38 +39,36 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { - Arc::new(LocalServerUserAuthServiceImpl { - db: self.local_db.clone(), - }) + Arc::new(LocalServerUserServiceImpl) } fn folder_service(&self) -> Arc { - Arc::new(LocalServerFolderCloudServiceImpl { - db: self.local_db.clone(), - }) + Arc::new(LocalServerFolderCloudServiceImpl) } fn database_service(&self) -> Arc { Arc::new(LocalServerDatabaseCloudServiceImpl()) } + fn database_ai_service(&self) -> Option> { + None + } + fn document_service(&self) -> Arc { Arc::new(LocalServerDocumentCloudServiceImpl()) } - fn file_storage(&self) -> Option> { - None + fn chat_service(&self) -> Arc { + Arc::new(LocalServerChatServiceImpl { + user: self.user.clone(), + }) } fn search_service(&self) -> Option> { None } - fn database_ai_service(&self) -> Option> { + fn file_storage(&self) -> Option> { None } - - fn chat_service(&self) -> Arc { - Arc::new(LocalServerChatServiceImpl) - } } 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 9c88917df8..d00f484068 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 @@ -3,14 +3,14 @@ use semver::Version; use std::collections::HashMap; use std::sync::Arc; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; +use crate::setup_log; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; - -use crate::setup_log; +use flowy_sqlite::DBConnection; /// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: /// @@ -42,6 +42,14 @@ impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } + + fn user_id(&self) -> FlowyResult { + todo!() + } + + fn get_sqlite_db(&self, uid: i64) -> Result { + todo!() + } } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index 0228e25d35..80d087e88e 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -23,3 +23,4 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } +arc-swap = "1.7.1" diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 3f7f39910b..dde94aebdc 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -263,8 +263,6 @@ pub trait UserCloudService: Send + Sync + 'static { None } - async fn reset_workspace(&self, collab_object: CollabObject) -> Result<(), FlowyError>; - async fn create_collab_object( &self, collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 85b9274df3..c0b7a8d6c2 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -614,24 +614,6 @@ pub async fn get_all_reminder_event_handler( data_result_ok(reminders.into()) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn reset_workspace_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let reset_pb = data.into_inner(); - if reset_pb.workspace_id.is_empty() { - return Err(FlowyError::new( - ErrorCode::WorkspaceInitializeError, - "The workspace id is empty", - )); - } - let _session = manager.get_session()?; - manager.reset_workspace(reset_pb).await?; - Ok(()) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub async fn remove_reminder_event_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 2de1fdfbdc..7ed4948771 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -49,7 +49,6 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetAllReminders, get_all_reminder_event_handler) .event(UserEvent::RemoveReminder, remove_reminder_event_handler) .event(UserEvent::UpdateReminder, update_reminder_event_handler) - .event(UserEvent::ResetWorkspace, reset_workspace_handler) .event(UserEvent::SetDateTimeSettings, set_date_time_settings) .event(UserEvent::GetDateTimeSettings, get_date_time_settings) .event(UserEvent::SetNotificationSettings, set_notification_settings) @@ -183,9 +182,6 @@ pub enum UserEvent { #[event(input = "ReminderPB")] UpdateReminder = 31, - #[event(input = "ResetWorkspacePB")] - ResetWorkspace = 32, - /// Change the Date/Time formats globally #[event(input = "DateTimeSettingsPB")] SetDateTimeSettings = 33, 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 ab9a35b483..bc9603e26a 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -1,7 +1,6 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; -use crate::services::sqlite_sql::user_sql::vacuum_database; use collab_integrate::CollabKVDB; use arc_swap::ArcSwapOption; @@ -18,8 +17,6 @@ use std::sync::{Arc, Weak}; use tracing::{error, info}; use uuid::Uuid; -const SQLITE_VACUUM_042: &str = "sqlite_vacuum_042_version"; - pub struct AuthenticateUser { pub user_config: UserConfig, pub(crate) database: Arc, @@ -44,23 +41,6 @@ impl AuthenticateUser { } } - pub fn vacuum_database_if_need(&self) { - if !self - .store_preferences - .get_bool_or_default(SQLITE_VACUUM_042) - { - if let Ok(session) = self.get_session() { - let _ = self.store_preferences.set_bool(SQLITE_VACUUM_042, true); - if let Ok(conn) = self.database.get_connection(session.user_id) { - info!("vacuum database 042"); - if let Err(err) = vacuum_database(conn) { - error!("vacuum database error: {:?}", err); - } - } - } - } - } - pub fn user_id(&self) -> FlowyResult { let session = self.get_session()?; Ok(session.user_id) diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index 6da6f183cb..c63764a055 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -148,10 +148,3 @@ pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result Result<(), FlowyError> { - sql_query("VACUUM") - .execute(&mut *conn) - .map_err(internal_error)?; - Ok(()) -} 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 b4bc8911e1..5b5d060539 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -268,7 +268,6 @@ impl UserManager { }, _ => error!("Failed to get collab db or sqlite pool"), } - self.authenticate_user.vacuum_database_if_need(); // migrations should run before set the first time installed version self.set_first_time_installed_version(); 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 dae5a51ccc..2fff0c260b 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 @@ -414,24 +414,6 @@ impl UserManager { Ok(workspaces) } - /// Reset the remote workspace using local workspace data. This is useful when a user wishes to - /// open a workspace on a new device that hasn't fully synchronized with the server. - pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> { - let collab_object = CollabObject::new( - reset.uid, - reset.workspace_id.clone(), - CollabType::Folder, - reset.workspace_id.clone(), - self.authenticate_user.user_config.device_id.clone(), - ); - self - .cloud_services - .get_user_service()? - .reset_workspace(collab_object) - .await?; - Ok(()) - } - #[instrument(level = "info", skip(self), err)] pub async fn subscribe_workspace( &self, From 77fbf0f8a355bca2caaf123dd6b60e6ad06ab643 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 21:26:09 +0800 Subject: [PATCH 03/74] chore: local ai initialize --- frontend/rust-lib/Cargo.lock | 2 + frontend/rust-lib/Cargo.toml | 2 + frontend/rust-lib/flowy-ai/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai/src/lib.rs | 2 +- .../flowy-ai/src/local_ai/controller.rs | 117 +++++++++++------- frontend/rust-lib/flowy-core/Cargo.toml | 3 +- .../rust-lib/flowy-core/src/server_layer.rs | 27 ++-- frontend/rust-lib/flowy-server/Cargo.toml | 1 + 8 files changed, 98 insertions(+), 60 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ac47524944..2e85c57326 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2609,6 +2609,7 @@ name = "flowy-core" version = "0.1.0" dependencies = [ "af-local-ai", + "af-plugin", "anyhow", "arc-swap", "base64 0.21.5", @@ -2966,6 +2967,7 @@ dependencies = [ "collab-plugins", "collab-user", "dotenv", + "flowy-ai", "flowy-ai-pub", "flowy-database-pub", "flowy-document-pub", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 581715985f..6e93b13a19 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -99,6 +99,8 @@ zip = "2.2.0" dashmap = "6.0.1" derive_builder = "0.20.2" tantivy = { version = "0.24.0" } +af-plugin = { version = "0.1" } +af-local-ai = { version = "0.1" } # Please using the following command to update the revision id # Current directory: frontend diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index a31d42da61..3a6aaf5898 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -35,8 +35,8 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -af-local-ai = { version = "0.1.0" } -af-plugin = { version = "0.1.0" } +af-local-ai = { workspace = true } +af-plugin = { workspace = true } reqwest = { version = "0.11.27", features = ["json"] } sha2 = "0.10.7" base64 = "0.21.5" diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 6ab100fd6e..ccd3920e0d 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -5,7 +5,7 @@ pub mod ai_manager; mod chat; mod completion; pub mod entities; -mod local_ai; +pub mod local_ai; // #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] // pub mod mcp; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index ac44e9ad55..494e11d073 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -24,10 +24,10 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use tokio::select; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument}; +use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -53,7 +53,7 @@ pub struct LocalAIController { ai_plugin: Arc, resource: Arc, current_chat_id: ArcSwapOption, - store_preferences: Arc, + store_preferences: Weak, user_service: Arc, #[allow(dead_code)] cloud_service: Arc, @@ -70,7 +70,7 @@ impl Deref for LocalAIController { impl LocalAIController { pub fn new( plugin_manager: Arc, - store_preferences: Arc, + store_preferences: Weak, user_service: Arc, cloud_service: Arc, ) -> Self { @@ -94,7 +94,7 @@ impl LocalAIController { let mut running_state_rx = local_ai.subscribe_running_state(); let cloned_llm_res = Arc::clone(&local_ai_resource); - let cloned_store_preferences = Arc::clone(&store_preferences); + let cloned_store_preferences = store_preferences.clone(); let cloned_local_ai = Arc::clone(&local_ai); let cloned_user_service = Arc::clone(&user_service); @@ -110,44 +110,47 @@ impl LocalAIController { info!("[AI Plugin] state: {:?}", state); // Read whether plugin is enabled from store; default to true - let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + if let Some(store_preferences) = cloned_store_preferences.upgrade() { + let enabled = store_preferences.get_bool(&key).unwrap_or(true); + // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled + let (plugin_downloaded, lack_of_resource) = + if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { + // Possibly check plugin readiness and resource concurrency in parallel, + // but here we do it sequentially for clarity. + let downloaded = is_plugin_ready(); + let resource_lack = cloned_llm_res.get_lack_of_resource().await; + (downloaded, resource_lack) + } else { + (false, None) + }; - // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled - let (plugin_downloaded, lack_of_resource) = - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - // Possibly check plugin readiness and resource concurrency in parallel, - // but here we do it sequentially for clarity. - let downloaded = is_plugin_ready(); - let resource_lack = cloned_llm_res.get_lack_of_resource().await; - (downloaded, resource_lack) + // If plugin is running, retrieve version + let plugin_version = if matches!(state, RunningState::Running { .. }) { + match cloned_local_ai.plugin_info().await { + Ok(info) => Some(info.version), + Err(_) => None, + } } else { - (false, None) + None }; - // If plugin is running, retrieve version - let plugin_version = if matches!(state, RunningState::Running { .. }) { - match cloned_local_ai.plugin_info().await { - Ok(info) => Some(info.version), - Err(_) => None, - } + // Broadcast the new local AI state + let new_state = RunningStatePB::from(state); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + plugin_downloaded, + lack_of_resource, + state: new_state, + plugin_version, + }) + .send(); } else { - None - }; - - // Broadcast the new local AI state - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version, - }) - .send(); + warn!("[AI Plugin] store preferences is dropped"); + } } }); @@ -207,6 +210,13 @@ impl LocalAIController { Ok(()) } + fn upgrade_store_preferences(&self) -> FlowyResult> { + self + .store_preferences + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) + } + /// Indicate whether the local AI plugin is running. pub fn is_running(&self) -> bool { if !self.is_enabled() { @@ -228,7 +238,10 @@ impl LocalAIController { .workspace_id() .map(|workspace_id| local_ai_enabled_key(&workspace_id)) { - self.store_preferences.get_bool(&key).unwrap_or(false) + match self.upgrade_store_preferences() { + Ok(store) => store.get_bool(&key).unwrap_or(false), + Err(_) => false, + } } else { false } @@ -373,8 +386,9 @@ impl LocalAIController { pub async fn toggle_local_ai(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let key = local_ai_enabled_key(&workspace_id); - let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); - self.store_preferences.set_bool(&key, enabled)?; + let store_preferences = self.upgrade_store_preferences()?; + let enabled = !store_preferences.get_bool(&key).unwrap_or(true); + store_preferences.set_bool(&key, enabled)?; self.toggle_plugin(enabled).await?; Ok(enabled) } @@ -591,7 +605,16 @@ async fn initialize_ai_plugin( pub struct LLMResourceServiceImpl { user_service: Arc, cloud_service: Arc, - store_preferences: Arc, + store_preferences: Weak, +} + +impl LLMResourceServiceImpl { + fn upgrade_store_preferences(&self) -> FlowyResult> { + self + .store_preferences + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) + } } #[async_trait] impl LLMResourceService for LLMResourceServiceImpl { @@ -605,16 +628,14 @@ impl LLMResourceService for LLMResourceServiceImpl { } fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { - self - .store_preferences - .set_object(LOCAL_AI_SETTING_KEY, &setting)?; + let store_preferences = self.upgrade_store_preferences()?; + store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; Ok(()) } fn retrieve_setting(&self) -> Option { - self - .store_preferences - .get_object::(LOCAL_AI_SETTING_KEY) + let store_preferences = self.upgrade_store_preferences().ok()?; + store_preferences.get_object::(LOCAL_AI_SETTING_KEY) } } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 6499f79284..b4e7bd5fec 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -37,7 +37,8 @@ flowy-storage-pub = { workspace = true } client-api.workspace = true flowy-ai = { workspace = true } flowy-ai-pub = { workspace = true } -af-local-ai = { version = "0.1.0" } +af-local-ai = { workspace = true } +af-plugin = { workspace = true } tracing.workspace = true diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 8157748b4f..176818abdf 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,11 +1,9 @@ +use crate::AppFlowyCoreConfig; +use af_plugin::manager::PluginManager; use arc_swap::ArcSwapOption; use dashmap::DashMap; -use diesel::Connection; -use serde_repr::*; -use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; -use std::sync::{Arc, Weak}; - +use flowy_ai::ai_manager::AIUserService; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; @@ -14,8 +12,10 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; - -use crate::AppFlowyCoreConfig; +use serde_repr::*; +use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::{Arc, Weak}; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -66,10 +66,21 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, + user_service: impl AIUserService, server_user: impl ServerUser + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); + + let user_service = Arc::new(user_service); + let plugin_manager = Arc::new(PluginManager::new()); + let local_ai = Arc::new(LocalAIController::new( + plugin_manager.clone(), + store_preferences.clone(), + user_service.clone(), + chat_cloud_service.clone(), + )); + Self { config, providers: DashMap::new(), diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index e94570cc4a..5225eb817d 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -44,6 +44,7 @@ tokio-stream = { workspace = true, features = ["sync"] } rand = "0.8.5" semver = "1.0.23" flowy-sqlite = { workspace = true } +flowy-ai = { workspace = true } [dependencies.client-api] workspace = true From e2896b29117b7f51eb805ff0c718d3f6057e19ce Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 16 Apr 2025 22:03:14 +0800 Subject: [PATCH 04/74] chore: remove inline view id reference --- .../cell/bloc/relation_cell_bloc.dart | 5 ++- .../relation_type_option_cubit.dart | 10 +++--- .../cell_editor/relation_cell_editor.dart | 2 +- frontend/rust-lib/Cargo.lock | 34 ++++++------------- frontend/rust-lib/Cargo.toml | 16 ++++----- .../src/entities/database_entities.rs | 5 +-- .../flowy-database2/src/event_handler.rs | 2 +- .../rust-lib/flowy-database2/src/manager.rs | 9 ----- .../src/services/database/database_editor.rs | 4 +-- .../src/services/share/csv/export.rs | 8 +++-- 10 files changed, 34 insertions(+), 61 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart index 70c5e074ab..ec789b03a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart @@ -143,12 +143,11 @@ class RelationCellBloc extends Bloc { (f) => null, ); if (databaseMeta != null) { - final result = - await ViewBackendService.getView(databaseMeta.inlineViewId); + final result = await ViewBackendService.getView(databaseMeta.viewId); return result.fold( (s) => DatabaseMeta( databaseId: databaseId, - inlineViewId: databaseMeta.inlineViewId, + viewId: databaseMeta.viewId, databaseName: s.name, ), (f) => null, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart index 691b6b7227..4ddde80b79 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart @@ -17,11 +17,11 @@ class RelationDatabaseListCubit extends Cubit { .send() .fold>((s) => s.items, (f) => []); final futures = metaPBs.map((meta) { - return ViewBackendService.getView(meta.inlineViewId).then( + return ViewBackendService.getView(meta.viewId).then( (result) => result.fold( (s) => DatabaseMeta( databaseId: meta.databaseId, - inlineViewId: meta.inlineViewId, + viewId: meta.viewId, databaseName: s.name, ), (f) => null, @@ -43,10 +43,10 @@ class DatabaseMeta with _$DatabaseMeta { /// id of the database required String databaseId, - /// id of the inline view - required String inlineViewId, + /// id of the view + required String viewId, - /// name of the database, currently identical to the name of the inline view + /// name of the database required String databaseName, }) = _DatabaseMeta; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index e68e77cd97..7f6960de9d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -256,7 +256,7 @@ class _CellEditorTitle extends StatelessWidget { } void _openRelatedDatbase(BuildContext context) { - FolderEventGetView(ViewIdPB(value: databaseMeta.inlineViewId)) + FolderEventGetView(ViewIdPB(value: databaseMeta.viewId)) .send() .then((result) { result.fold( diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a84de4cdd6..4ae0275383 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "bytes", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "arc-swap", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-recursion", @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "async-stream", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" dependencies = [ "anyhow", "collab", @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -5189,7 +5189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5209,7 +5209,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5277,19 +5276,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 88972e5bfb..1efaa0cc97 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -141,14 +141,14 @@ 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 = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } # Working directory: frontend # To update the commit ID, run: diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 8c16db4379..2562bd84f7 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -26,9 +26,6 @@ pub struct DatabasePB { #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, - - #[pb(index = 5)] - pub is_linked: bool, } #[derive(ProtoBuf, Default)] @@ -208,7 +205,7 @@ pub struct DatabaseMetaPB { pub database_id: String, #[pb(index = 2)] - pub inline_view_id: String, + pub view_id: String, } #[derive(Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 9164550fe4..63d6fdf2c3 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -900,7 +900,7 @@ pub(crate) async fn get_databases_handler( if let Some(link_view) = meta.linked_views.first() { items.push(DatabaseMetaPB { database_id: meta.database_id, - inline_view_id: link_view.clone(), + view_id: link_view.clone(), }) } } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 49a946d108..a7cca9960b 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -148,15 +148,6 @@ impl DatabaseManager { Ok(()) } - pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { - let lock = self.workspace_database()?; - let wdb = lock.read().await; - let database_collab = wdb.get_or_init_database(database_id).await?; - drop(wdb); - 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 Some(lock) = self.workspace_database_manager.load_full() { 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 d079bdc8c2..227b96df4f 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 @@ -1505,7 +1505,7 @@ impl DatabaseEditor { view_editor.set_row_orders(row_orders.clone()).await; // Collect database details in a single block holding the `read` lock - let (database_id, fields, is_linked) = { + let (database_id, fields) = { let database = self.database.read().await; ( database.get_database_id(), @@ -1514,7 +1514,6 @@ impl DatabaseEditor { .into_iter() .map(FieldIdPB::from) .collect::>(), - database.is_inline_view(view_id), ) }; @@ -1557,7 +1556,6 @@ impl DatabaseEditor { fields, rows: order_rows, layout_type: view_layout.into(), - is_linked, }); // Mark that the opening process is complete if let Some(tx) = self.is_loading_rows.load_full() { 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 dd704f43d5..3eab243fd7 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 @@ -28,8 +28,10 @@ impl CSVExport { 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); + let view_id = database + .get_first_database_view_id() + .ok_or_else(|| FlowyError::internal().with_context("failed to get first database view"))?; + let fields = database.get_fields_in_view(&view_id, None); // Write fields let field_records = fields @@ -49,7 +51,7 @@ impl CSVExport { field_by_field_id.insert(field.id.clone(), field); }); let rows = database - .get_rows_for_view(&inline_view_id, 20, None) + .get_rows_for_view(&view_id, 20, None) .await .filter_map(|result| async { result.ok() }) .collect::>() From c633dd0919304a160f6ac06e0c523da3354f5797 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 11:11:54 +0800 Subject: [PATCH 05/74] chore: remove unused code --- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 8 +-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 14 +---- frontend/rust-lib/flowy-ai/src/chat.rs | 3 +- .../flowy-ai/src/local_ai/controller.rs | 21 +------ .../flowy-ai/src/local_ai/resource.rs | 20 ------ .../src/middleware/chat_service_mw.rs | 61 ++++++++++--------- .../flowy-core/src/deps_resolve/chat_deps.rs | 7 +-- .../src/deps_resolve/cloud_service_impl.rs | 15 ++--- frontend/rust-lib/flowy-core/src/lib.rs | 8 +++ .../rust-lib/flowy-core/src/server_layer.rs | 30 +++++++-- .../flowy-server/src/af_cloud/define.rs | 2 + .../flowy-server/src/af_cloud/impls/chat.rs | 23 +------ .../src/local_server/impls/chat.rs | 10 +-- .../src/local_server/impls/folder.rs | 1 - .../src/local_server/impls/user.rs | 1 - .../flowy-server/tests/af_cloud_test/util.rs | 5 ++ .../src/services/authenticate_user.rs | 2 +- .../src/services/sqlite_sql/user_sql.rs | 4 +- .../user_manager/manager_user_workspace.rs | 5 +- 19 files changed, 97 insertions(+), 143 deletions(-) diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 79478f64fc..5e9923d464 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -141,6 +141,7 @@ pub trait ChatCloudService: Send + Sync + 'static { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result; async fn stream_complete( @@ -158,13 +159,6 @@ pub trait ChatCloudService: Send + Sync + 'static { metadata: Option>, ) -> Result<(), FlowyError>; - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result; - - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError>; - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index ec12ac4963..d619b8ab7e 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -8,7 +8,6 @@ use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; -use af_plugin::manager::PluginManager; use dashmap::DashMap; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, @@ -35,7 +34,6 @@ use uuid::Uuid; pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; - fn device_id(&self) -> Result; fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; @@ -85,16 +83,9 @@ impl AIManager { store_preferences: Arc, storage_service: Weak, query_service: impl AIExternalService, + local_ai: Arc, ) -> AIManager { let user_service = Arc::new(user_service); - let plugin_manager = Arc::new(PluginManager::new()); - let local_ai = Arc::new(LocalAIController::new( - plugin_manager.clone(), - store_preferences.clone(), - user_service.clone(), - chat_cloud_service.clone(), - )); - let cloned_local_ai = local_ai.clone(); tokio::spawn(async move { cloned_local_ai.observe_plugin_resource().await; @@ -596,7 +587,8 @@ impl AIManager { message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; - let resp = chat.get_related_question(message_id).await?; + let ai_model = self.get_active_model(&chat_id.to_string()).await; + let resp = chat.get_related_question(message_id, ai_model).await?; Ok(resp) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index a8655ff647..a496e067eb 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -524,11 +524,12 @@ impl Chat { pub async fn get_related_question( &self, message_id: i64, + ai_model: Option, ) -> Result { let workspace_id = self.user_service.workspace_id()?; let resp = self .chat_service - .get_related_message(&workspace_id, &self.chat_id, message_id) + .get_related_message(&workspace_id, &self.chat_id, message_id, ai_model) .await?; trace!( diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 494e11d073..431d8ff681 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -6,7 +6,7 @@ use crate::notification::{ }; use af_plugin::manager::PluginManager; use anyhow::Error; -use flowy_ai_pub::cloud::{ChatCloudService, ChatMessageMetadata, ContextLoader, LocalAIConfig}; +use flowy_ai_pub::cloud::{ChatMessageMetadata, ContextLoader}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; @@ -55,8 +55,6 @@ pub struct LocalAIController { current_chat_id: ArcSwapOption, store_preferences: Weak, user_service: Arc, - #[allow(dead_code)] - cloud_service: Arc, } impl Deref for LocalAIController { @@ -72,7 +70,6 @@ impl LocalAIController { plugin_manager: Arc, store_preferences: Weak, user_service: Arc, - cloud_service: Arc, ) -> Self { debug!( "[AI Plugin] init local ai controller, thread: {:?}", @@ -83,7 +80,6 @@ impl LocalAIController { let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), - cloud_service: cloud_service.clone(), store_preferences: store_preferences.clone(), }; let local_ai_resource = Arc::new(LocalAIResourceController::new( @@ -160,7 +156,6 @@ impl LocalAIController { current_chat_id: ArcSwapOption::default(), store_preferences, user_service, - cloud_service, } } #[instrument(level = "debug", skip_all)] @@ -379,10 +374,6 @@ impl LocalAIController { .map(|path| path.to_string_lossy().to_string()) } - pub async fn get_plugin_download_link(&self) -> FlowyResult { - self.resource.get_plugin_download_link().await - } - pub async fn toggle_local_ai(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; let key = local_ai_enabled_key(&workspace_id); @@ -604,7 +595,6 @@ async fn initialize_ai_plugin( pub struct LLMResourceServiceImpl { user_service: Arc, - cloud_service: Arc, store_preferences: Weak, } @@ -618,15 +608,6 @@ impl LLMResourceServiceImpl { } #[async_trait] impl LLMResourceService for LLMResourceServiceImpl { - async fn fetch_local_ai_config(&self) -> Result { - let workspace_id = self.user_service.workspace_id()?; - let config = self - .cloud_service - .get_local_ai_config(&workspace_id) - .await?; - Ok(config) - } - fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { let store_preferences = self.upgrade_store_preferences()?; store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index f2c1d1d041..6251ef8de5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -1,6 +1,5 @@ use crate::ai_manager::AIUserService; use crate::local_ai::controller::LocalAISetting; -use flowy_ai_pub::cloud::LocalAIConfig; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; @@ -33,7 +32,6 @@ struct ModelEntry { #[async_trait] pub trait LLMResourceService: Send + Sync + 'static { /// Get local ai configuration from remote server - async fn fetch_local_ai_config(&self) -> Result; fn store_setting(&self, setting: LocalAISetting) -> Result<(), anyhow::Error>; fn retrieve_setting(&self) -> Option; } @@ -125,11 +123,6 @@ impl LocalAIResourceController { .is_ok_and(|r| r.is_none()) } - pub async fn get_plugin_download_link(&self) -> FlowyResult { - let ai_config = self.get_local_ai_configuration().await?; - Ok(ai_config.plugin.url) - } - /// Retrieves model information and updates the current model settings. pub fn get_llm_setting(&self) -> LocalAISetting { self.resource_service.retrieve_setting().unwrap_or_default() @@ -271,19 +264,6 @@ impl LocalAIResourceController { Ok(config) } - /// Fetches the local AI configuration from the resource service. - async fn get_local_ai_configuration(&self) -> FlowyResult { - self - .resource_service - .fetch_local_ai_config() - .await - .map_err(|err| { - error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); - FlowyError::local_ai() - .with_context("Can't retrieve model info. Please try again later".to_string()) - }) - } - pub(crate) fn user_model_folder(&self) -> FlowyResult { self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 8c27268139..54f1c929dd 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -10,9 +10,9 @@ use std::collections::HashMap; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, LocalAIConfig, - MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, - ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, + RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, + StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; @@ -50,10 +50,6 @@ impl AICloudServiceMiddleware { } } - pub fn is_local_ai_enabled(&self) -> bool { - self.local_ai.is_enabled() - } - pub async fn index_message_metadata( &self, chat_id: &Uuid, @@ -63,7 +59,7 @@ impl AICloudServiceMiddleware { if metadata_list.is_empty() { return Ok(()); } - if self.is_local_ai_enabled() { + if self.local_ai.is_enabled() { let _ = index_process_sink .send(StreamMessage::IndexStart.to_string()) .await; @@ -262,28 +258,41 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); + let use_local_ai = match &ai_model { + None => false, + Some(model) => model.is_local, + }; - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, + if use_local_ai { + if self.local_ai.is_running() { + let questions = self + .local_ai + .get_related_question(&chat_id.to_string()) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + trace!("LocalAI related questions: {:?}", questions); + + let items = questions + .into_iter() + .map(|content| RelatedQuestion { + content, + metadata: None, + }) + .collect::>(); + + Ok(RepeatedRelatedQuestion { message_id, items }) + } else { + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], }) - .collect::>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) + } } else { self .cloud_service - .get_related_message(workspace_id, chat_id, message_id) + .get_related_message(workspace_id, chat_id, message_id, ai_model) .await } } @@ -359,10 +368,6 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - self.cloud_service.get_local_ai_config(workspace_id).await - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 2d9a57b331..a8280f9f66 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -6,6 +6,7 @@ use collab::util::is_change_since_sv; use collab_entity::CollabType; use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; use flowy_error::FlowyError; use flowy_folder::ViewLayout; @@ -33,6 +34,7 @@ impl ChatDepsResolver { storage_service: Weak, folder_cloud_service: Arc, folder_service: impl FolderService, + local_ai: Arc, ) -> Arc { let user_service = ChatUserServiceImpl(authenticate_user); Arc::new(AIManager::new( @@ -44,6 +46,7 @@ impl ChatDepsResolver { folder_service: Box::new(folder_service), folder_cloud_service, }, + local_ai, )) } } @@ -166,10 +169,6 @@ impl AIUserService for ChatUserServiceImpl { self.upgrade_user()?.user_id() } - fn device_id(&self) -> Result { - self.upgrade_user()?.device_id() - } - fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 836bd6b32b..30fa6d0ae4 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -15,8 +15,8 @@ use flowy_ai_pub::cloud::search_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, - StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, + StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -753,11 +753,12 @@ impl ChatCloudService for ServerProvider { workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { self .get_server()? .chat_service() - .get_related_message(workspace_id, chat_id, message_id) + .get_related_message(workspace_id, chat_id, message_id, ai_model) .await } @@ -801,14 +802,6 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - self - .get_server()? - .chat_service() - .get_local_ai_config(workspace_id) - .await - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 21f09c1dad..7584628056 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -9,6 +9,7 @@ use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; use flowy_server::af_cloud::define::ServerUser; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; @@ -191,6 +192,7 @@ impl AppFlowyCore { Arc::downgrade(&storage_manager.storage_service), server_provider.clone(), folder_query_service.clone(), + server_provider.local_ai.clone(), ); let database_manager = DatabaseDepsResolver::resolve( @@ -343,4 +345,10 @@ impl ServerUser for ServerUserImpl { fn get_sqlite_db(&self, uid: i64) -> Result { self.upgrade_user()?.get_sqlite_connection(uid) } + + fn application_root_dir(&self) -> Result { + Ok(PathBuf::from( + self.upgrade_user()?.get_application_root_dir(), + )) + } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 176818abdf..0c7bc88859 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -11,11 +11,14 @@ use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; use serde_repr::*; use std::fmt::{Display, Formatter}; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; +use uuid::Uuid; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -59,6 +62,7 @@ pub struct ServerProvider { authenticator: AtomicU8, user: Arc, pub(crate) uid: Arc>, + pub local_ai: Arc, } impl ServerProvider { @@ -66,19 +70,16 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, - user_service: impl AIUserService, server_user: impl ServerUser + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); - - let user_service = Arc::new(user_service); + let user_service = Arc::new(AIUserServiceImpl(user.clone())); let plugin_manager = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( plugin_manager.clone(), store_preferences.clone(), user_service.clone(), - chat_cloud_service.clone(), )); Self { @@ -90,6 +91,7 @@ impl ServerProvider { store_preferences, uid: Default::default(), user, + local_ai, } } @@ -179,3 +181,23 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } + +struct AIUserServiceImpl(Arc); + +impl AIUserService for AIUserServiceImpl { + fn user_id(&self) -> Result { + self.0.user_id() + } + + fn workspace_id(&self) -> Result { + self.0.workspace_id() + } + + fn sqlite_connection(&self, uid: i64) -> Result { + self.0.get_sqlite_db(uid) + } + + fn application_root_dir(&self) -> Result { + self.0.application_root_dir() + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 3b2895b8f8..cf98cf5214 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,5 +1,6 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; +use std::path::PathBuf; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -15,4 +16,5 @@ pub trait ServerUser: Send + Sync { fn user_id(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; + fn application_root_dir(&self) -> Result; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 4081d659ba..a92cd8ffa9 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -9,12 +9,11 @@ use client_api::entity::chat_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - LocalAIConfig, ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; -use lib_infra::util::{get_operating_system, OperatingSystem}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; @@ -186,6 +185,7 @@ where workspace_id: &Uuid, chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? @@ -226,25 +226,6 @@ where ); } - async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result { - let system = get_operating_system(); - let platform = match system { - OperatingSystem::MacOS => "macos", - _ => { - return Err( - FlowyError::not_support() - .with_context("local ai is not supported on this operating system"), - ); - }, - }; - let config = self - .inner - .try_get_client()? - .get_local_ai_config(workspace_id.to_string().as_str(), platform) - .await?; - Ok(config) - } - async fn get_workspace_plan( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index a6bfea53b4..bd49ffb75b 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,5 +1,5 @@ use crate::af_cloud::define::ServerUser; -use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; +use client_api::entity::ai_dto::RepeatedRelatedQuestion; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, @@ -98,6 +98,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { _workspace_id: &Uuid, _chat_id: &Uuid, message_id: i64, + ai_model: Option, ) -> Result { Ok(RepeatedRelatedQuestion { message_id, @@ -133,13 +134,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_local_ai_config(&self, _workspace_id: &Uuid) -> Result { - Err( - FlowyError::not_support() - .with_context("Get local ai config is not supported in local server."), - ) - } - async fn get_workspace_plan( &self, _workspace_id: &Uuid, 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 bd2372e9d4..31a65e7fa9 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 @@ -1,5 +1,4 @@ #![allow(unused_variables)] -use std::sync::Arc; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; 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 df9050623e..706e7f0597 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 @@ -5,7 +5,6 @@ use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; use lazy_static::lazy_static; -use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; 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 d00f484068..b747c0b2ae 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 @@ -1,6 +1,7 @@ use client_api::ClientConfiguration; use semver::Version; use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use flowy_error::{FlowyError, FlowyResult}; @@ -50,6 +51,10 @@ impl ServerUser for FakeServerUserImpl { fn get_sqlite_db(&self, uid: i64) -> Result { todo!() } + + fn application_root_dir(&self) -> Result { + todo!() + } } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { 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 bc9603e26a..7ec63f93f7 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -14,7 +14,7 @@ use flowy_user_pub::session::Session; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::{error, info}; +use tracing::info; use uuid::Uuid; pub struct AuthenticateUser { diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index c63764a055..e2b6628832 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -1,5 +1,5 @@ -use diesel::{sql_query, RunQueryDsl}; -use flowy_error::{internal_error, FlowyError}; +use diesel::RunQueryDsl; +use flowy_error::FlowyError; use std::str::FromStr; use flowy_user_pub::cloud::UserUpdate; 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 2fff0c260b..b5ae54309b 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 @@ -6,7 +6,6 @@ use std::convert::TryFrom; use std::str::FromStr; use std::sync::Arc; -use collab_entity::{CollabObject, CollabType}; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; @@ -20,7 +19,7 @@ use tracing::{error, info, instrument, trace, warn}; use uuid::Uuid; use crate::entities::{ - RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, + RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, UpdateUserWorkspaceSettingPB, UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB, }; use crate::migrations::AnonUser; @@ -575,7 +574,7 @@ impl UserManager { if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self - .get_workspace_member_info_from_remote(&workspace_id, uid) + .get_workspace_member_info_from_remote(workspace_id, uid) .await?; } From 98b835227ed59bb64b572ba886bef04aa7d038d4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 11:22:14 +0800 Subject: [PATCH 06/74] chore: remove unused code --- .../flowy-ai/src/middleware/chat_service_mw.rs | 7 ------- .../rust-lib/flowy-ai/src/persistence/chat_sql.rs | 1 - .../src/deps_resolve/cloud_service_impl.rs | 11 ----------- .../rust-lib/flowy-server/src/af_cloud/impls/chat.rs | 12 ------------ .../flowy-server/src/local_server/impls/chat.rs | 10 ---------- 5 files changed, 41 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 54f1c929dd..5a529d4201 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -368,13 +368,6 @@ impl ChatCloudService for AICloudServiceMiddleware { } } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - self.cloud_service.get_workspace_plan(workspace_id).await - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs index e962f2c880..45e839ae7e 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs @@ -76,7 +76,6 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< .execute(&mut *conn) } -#[allow(dead_code)] pub fn update_chat( conn: &mut SqliteConnection, changeset: ChatTableChangeset, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 30fa6d0ae4..a84aff211c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -802,17 +802,6 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - self - .get_server()? - .chat_service() - .get_workspace_plan(workspace_id) - .await - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index a92cd8ffa9..9bfd0e23fd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -226,18 +226,6 @@ where ); } - async fn get_workspace_plan( - &self, - workspace_id: &Uuid, - ) -> Result, FlowyError> { - let plans = self - .inner - .try_get_client()? - .get_active_workspace_subscriptions(workspace_id.to_string().as_str()) - .await?; - Ok(plans) - } - async fn get_chat_settings( &self, workspace_id: &Uuid, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index bd49ffb75b..fe90f45e77 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -134,16 +134,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) } - async fn get_workspace_plan( - &self, - _workspace_id: &Uuid, - ) -> Result, FlowyError> { - Err( - FlowyError::not_support() - .with_context("Get local ai config is not supported in local server."), - ) - } - async fn get_chat_settings( &self, _workspace_id: &Uuid, From af91a72187a5bd199c3f95a951608b6493f48998 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 14:07:01 +0800 Subject: [PATCH 07/74] chore: select message --- frontend/rust-lib/Cargo.lock | 1 + frontend/rust-lib/flowy-ai-pub/Cargo.toml | 3 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 2 + frontend/rust-lib/flowy-ai-pub/src/lib.rs | 1 + .../src/persistence/chat_message_sql.rs | 35 +++-- .../src/persistence/chat_sql.rs | 67 +++++++-- .../src/persistence/mod.rs | 0 frontend/rust-lib/flowy-ai/src/ai_manager.rs | 33 +++-- frontend/rust-lib/flowy-ai/src/chat.rs | 30 ++-- frontend/rust-lib/flowy-ai/src/lib.rs | 1 - .../src/middleware/chat_service_mw.rs | 19 +-- .../src/deps_resolve/cloud_service_impl.rs | 4 +- .../rust-lib/flowy-core/src/server_layer.rs | 2 +- .../flowy-server/src/af_cloud/impls/chat.rs | 4 +- .../src/local_server/impls/chat.rs | 131 ++++++++++++++---- .../flowy-server/src/local_server/server.rs | 6 +- .../2025-04-17-042326_chat_metadata/down.sql | 9 ++ .../2025-04-17-042326_chat_metadata/up.sql | 4 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 26 ++-- 19 files changed, 276 insertions(+), 102 deletions(-) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/chat_message_sql.rs (75%) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/chat_sql.rs (66%) rename frontend/rust-lib/{flowy-ai => flowy-ai-pub}/src/persistence/mod.rs (100%) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 2e85c57326..29547be823 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2564,6 +2564,7 @@ version = "0.1.0" dependencies = [ "client-api", "flowy-error", + "flowy-sqlite", "futures", "lib-infra", "serde", diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml index dfb67490ac..93ea79bcab 100644 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-ai-pub/Cargo.toml @@ -12,4 +12,5 @@ client-api = { workspace = true } futures.workspace = true serde_json.workspace = true serde.workspace = true -uuid.workspace = true \ No newline at end of file +uuid.workspace = true +flowy-sqlite = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 5e9923d464..0acf3c5bd3 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -85,6 +85,8 @@ pub trait ChatCloudService: Send + Sync + 'static { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError>; async fn create_question( diff --git a/frontend/rust-lib/flowy-ai-pub/src/lib.rs b/frontend/rust-lib/flowy-ai-pub/src/lib.rs index 1ede32218e..9a7423ec3f 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/lib.rs @@ -1 +1,2 @@ pub mod cloud; +pub mod persistence; diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs similarity index 75% rename from frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index 6eaf6798e3..ac3192064b 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -1,3 +1,4 @@ +use crate::cloud::MessageCursor; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::upsert::excluded; use flowy_sqlite::{ @@ -51,19 +52,25 @@ pub fn select_chat_messages( mut conn: DBConnection, chat_id_val: &str, limit_val: i64, - after_message_id: Option, - before_message_id: Option, + offset: MessageCursor, ) -> QueryResult> { let mut query = dsl::chat_message_table .filter(chat_message_table::chat_id.eq(chat_id_val)) .into_boxed(); - if let Some(after_message_id) = after_message_id { - query = query.filter(chat_message_table::message_id.gt(after_message_id)); + + match offset { + MessageCursor::AfterMessageId(after_message_id) => { + query = query.filter(chat_message_table::message_id.gt(after_message_id)); + }, + MessageCursor::BeforeMessageId(before_message_id) => { + query = query.filter(chat_message_table::message_id.lt(before_message_id)); + }, + MessageCursor::Offset(offset_val) => { + query = query.offset(offset_val as i64); + }, + MessageCursor::NextBack => {}, } - if let Some(before_message_id) = before_message_id { - query = query.filter(chat_message_table::message_id.lt(before_message_id)); - } query = query .order(( chat_message_table::created_at.desc(), @@ -75,7 +82,7 @@ pub fn select_chat_messages( Ok(messages) } -pub fn select_single_message( +pub fn select_message( mut conn: DBConnection, message_id_val: i64, ) -> QueryResult> { @@ -86,6 +93,18 @@ pub fn select_single_message( Ok(message) } +pub fn select_message_content( + mut conn: DBConnection, + message_id_val: i64, +) -> QueryResult> { + let message = dsl::chat_message_table + .filter(chat_message_table::message_id.eq(message_id_val)) + .select(chat_message_table::content) + .first::(&mut *conn) + .optional()?; + Ok(message) +} + pub fn select_message_where_match_reply_message_id( mut conn: DBConnection, answer_message_id_val: i64, diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs similarity index 66% rename from frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs index 45e839ae7e..218f6222c3 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs @@ -16,10 +16,8 @@ pub struct ChatTable { pub chat_id: String, pub created_at: i64, pub name: String, - pub local_files: String, pub metadata: String, - pub local_enabled: bool, - pub sync_to_cloud: bool, + pub rag_ids: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -49,22 +47,57 @@ pub struct ChatTableFile { pub struct ChatTableChangeset { pub chat_id: String, pub name: Option, - pub local_files: Option, pub metadata: Option, - pub local_enabled: Option, - pub sync_to_cloud: Option, + pub rag_ids: Option, } impl ChatTableChangeset { pub fn from_metadata(metadata: ChatTableMetadata) -> Self { ChatTableChangeset { + chat_id: Default::default(), metadata: serde_json::to_string(&metadata).ok(), - ..Default::default() + name: None, + rag_ids: None, + } + } + + pub fn from_rag_ids(rag_ids: Vec) -> Self { + ChatTableChangeset { + chat_id: Default::default(), + // Serialize the Vec to a JSON array string + rag_ids: Some(serde_json::to_string(&rag_ids).unwrap_or_default()), + name: None, + metadata: None, } } } -pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { +pub fn serialize_rag_ids(rag_ids: &[String]) -> String { + serde_json::to_string(rag_ids).unwrap_or_default() +} + +pub fn deserialize_rag_ids(rag_ids_str: &Option) -> Vec { + match rag_ids_str { + Some(str) => serde_json::from_str(str).unwrap_or_default(), + None => Vec::new(), + } +} + +pub fn deserialize_chat_metadata(metadata: &str) -> T +where + T: serde::de::DeserializeOwned + Default, +{ + serde_json::from_str(metadata).unwrap_or_default() +} + +pub fn serialize_chat_metadata(metadata: &T) -> String +where + T: Serialize, +{ + serde_json::to_string(metadata).unwrap_or_default() +} + +pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { diesel::insert_into(chat_table::table) .values(new_chat) .on_conflict(chat_table::chat_id) @@ -72,6 +105,8 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< .set(( chat_table::created_at.eq(excluded(chat_table::created_at)), chat_table::name.eq(excluded(chat_table::name)), + chat_table::metadata.eq(excluded(chat_table::metadata)), + chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), )) .execute(&mut *conn) } @@ -85,7 +120,6 @@ pub fn update_chat( Ok(affected_row) } -#[allow(dead_code)] pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { let row = dsl::chat_table .filter(chat_table::chat_id.eq(chat_id_val)) @@ -93,7 +127,17 @@ pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult FlowyResult> { + let chat = dsl::chat_table + .filter(chat_table::chat_id.eq(chat_id_val)) + .first::(conn)?; + + Ok(deserialize_rag_ids(&chat.rag_ids)) +} + pub fn read_chat_metadata( conn: &mut SqliteConnection, chat_id_val: &str, @@ -102,8 +146,7 @@ pub fn read_chat_metadata( .select(chat_table::metadata) .filter(chat_table::chat_id.eq(chat_id_val)) .first::(&mut *conn)?; - let value = serde_json::from_str(&metadata_str).unwrap_or_default(); - Ok(value) + Ok(deserialize_chat_metadata(&metadata_str)) } #[allow(dead_code)] diff --git a/frontend/rust-lib/flowy-ai/src/persistence/mod.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-ai/src/persistence/mod.rs rename to frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index d619b8ab7e..1bb05e19b6 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -5,7 +5,9 @@ use crate::entities::{ }; use crate::local_ai::controller::{LocalAIController, LocalAISetting}; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; -use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; +use flowy_ai_pub::persistence::{ + read_chat_metadata, serialize_chat_metadata, serialize_rag_ids, upsert_chat, ChatTable, +}; use std::collections::HashMap; use dashmap::DashMap; @@ -25,6 +27,7 @@ use flowy_ai_pub::cloud::ai_dto::AvailableModel; use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; +use serde_json::json; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; @@ -221,11 +224,17 @@ impl AIManager { .unwrap_or_default(); info!("[Chat] create chat with rag_ids: {:?}", rag_ids); + save_chat( + self.user_service.sqlite_connection(*uid)?, + chat_id, + "", + rag_ids.iter().map(|v| v.to_string()).collect(), + json!({}), + )?; self .cloud_service_wm - .create_chat(uid, &workspace_id, chat_id, rag_ids) + .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) .await?; - save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; let chat = Arc::new(Chat::new( self.user_service.user_id()?, @@ -705,18 +714,22 @@ async fn sync_chat_documents( Ok(()) } -fn save_chat(conn: DBConnection, chat_id: &Uuid) -> FlowyResult<()> { +fn save_chat( + conn: DBConnection, + chat_id: &Uuid, + name: &str, + rag_ids: Vec, + metadata: serde_json::Value, +) -> FlowyResult<()> { let row = ChatTable { chat_id: chat_id.to_string(), created_at: timestamp(), - name: "".to_string(), - local_files: "".to_string(), - metadata: "".to_string(), - local_enabled: false, - sync_to_cloud: false, + name: name.to_string(), + metadata: serialize_chat_metadata(&metadata), + rag_ids: Some(serialize_rag_ids(&rag_ids)), }; - insert_chat(conn, &row)?; + upsert_chat(conn, &row)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index a496e067eb..0610799f03 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -5,15 +5,15 @@ use crate::entities::{ }; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::persistence::{ - insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, - ChatMessageTable, -}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; +use flowy_ai_pub::persistence::{ + insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + ChatMessageTable, +}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; @@ -349,9 +349,9 @@ impl Chat { limit, before_message_id ); - let messages = self - .load_local_chat_messages(limit, None, before_message_id) - .await?; + + let offset = before_message_id.map_or(MessageCursor::NextBack, MessageCursor::BeforeMessageId); + let messages = self.load_local_chat_messages(limit, offset).await?; // If the number of messages equals the limit, then no need to load more messages from remote if messages.len() == limit as usize { @@ -397,9 +397,8 @@ impl Chat { limit, after_message_id, ); - let messages = self - .load_local_chat_messages(limit, after_message_id, None) - .await?; + let offset = after_message_id.map_or(MessageCursor::NextBack, MessageCursor::AfterMessageId); + let messages = self.load_local_chat_messages(limit, offset).await?; trace!( "[Chat] Loaded local chat messages: chat_id={}, messages={}", @@ -562,17 +561,10 @@ impl Chat { async fn load_local_chat_messages( &self, limit: i64, - after_message_id: Option, - before_message_id: Option, + offset: MessageCursor, ) -> Result, FlowyError> { let conn = self.user_service.sqlite_connection(self.uid)?; - let records = select_chat_messages( - conn, - &self.chat_id.to_string(), - limit, - after_message_id, - before_message_id, - )?; + let records = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?; let messages = records .into_iter() .map(|record| ChatMessagePB { diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index ccd3920e0d..400afd1507 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -12,7 +12,6 @@ pub mod local_ai; mod middleware; pub mod notification; -mod persistence; mod protobuf; mod stream_message; mod util; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 5a529d4201..a9a4022573 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -4,8 +4,8 @@ use crate::local_ai::controller::LocalAIController; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; -use crate::persistence::{select_single_message, ChatMessageTable}; use af_plugin::error::PluginError; +use flowy_ai_pub::persistence::{select_message, select_message_content, ChatMessageTable}; use std::collections::HashMap; use flowy_ai_pub::cloud::{ @@ -78,14 +78,13 @@ impl AICloudServiceMiddleware { Ok(()) } - fn get_message_record(&self, message_id: i64) -> FlowyResult { + fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; - let row = select_single_message(conn, message_id)?.ok_or_else(|| { + let content = select_message_content(conn, message_id)?.ok_or_else(|| { FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) })?; - - Ok(row) + Ok(content) } fn handle_plugin_error(&self, err: PluginError) { @@ -114,10 +113,12 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { self .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids) + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) .await } @@ -165,12 +166,12 @@ impl ChatCloudService for AICloudServiceMiddleware { info!("stream_answer use model: {:?}", ai_model); if use_local_ai { if self.local_ai.is_running() { - let row = self.get_message_record(message_id)?; + let content = self.get_message_content(message_id)?; match self .local_ai .stream_question( &chat_id.to_string(), - &row.content, + &content, Some(json!(format)), json!({}), ) @@ -202,7 +203,7 @@ impl ChatCloudService for AICloudServiceMiddleware { question_message_id: i64, ) -> Result { if self.local_ai.is_running() { - let content = self.get_message_record(question_message_id)?.content; + let content = self.get_message_content(question_message_id)?; match self .local_ai .ask_question(&chat_id.to_string(), &content) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index a84aff211c..c916157ff0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -667,11 +667,13 @@ impl ChatCloudService for ServerProvider { workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { let server = self.get_server(); server? .chat_service() - .create_chat(uid, workspace_id, chat_id, rag_ids) + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) .await } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 0c7bc88859..e194f614cd 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -128,7 +128,7 @@ impl ServerProvider { let server = match server_type { Server::Local => { - let server = Arc::new(LocalServer::new(self.user.clone())); + let server = Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())); Ok::, FlowyError>(server) }, Server::AppFlowyCloud => { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 9bfd0e23fd..4b0611e497 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -35,12 +35,14 @@ where workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, + name: &str, + metadata: serde_json::Value, ) -> Result<(), FlowyError> { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatParams { chat_id, - name: "".to_string(), + name: name.to_string(), rag_ids, }; try_get_client? diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index fe90f45e77..23562d2b07 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,14 +1,22 @@ use crate::af_cloud::define::ServerUser; use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use flowy_ai::local_ai::controller::LocalAIController; +use flowy_ai::local_ai::stream_util::QuestionStream; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; -use flowy_error::FlowyError; +use flowy_ai_pub::persistence::{ + deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_message_content, + serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, ChatTable, + ChatTableChangeset, +}; +use flowy_error::{FlowyError, FlowyResult}; +use futures_util::{stream, FutureExt, StreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; -use serde_json::Value; +use serde_json::{json, Value}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; @@ -16,6 +24,18 @@ use uuid::Uuid; pub struct LocalServerChatServiceImpl { pub user: Arc, + pub local_ai: Arc, +} + +impl LocalServerChatServiceImpl { + fn get_message_content(&self, message_id: i64) -> FlowyResult { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let content = select_message_content(db, message_id)?.ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) + })?; + Ok(content) + } } #[async_trait] @@ -24,9 +44,28 @@ impl ChatCloudService for LocalServerChatServiceImpl { &self, _uid: &i64, _workspace_id: &Uuid, - _chat_id: &Uuid, - _rag_ids: Vec, + chat_id: &Uuid, + rag_ids: Vec, + name: &str, + metadata: Value, ) -> Result<(), FlowyError> { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + + let rag_ids = rag_ids + .iter() + .map(|v| v.to_string()) + .collect::>(); + + let row = ChatTable { + chat_id: chat_id.to_string(), + created_at: timestamp(), + name: name.to_string(), + metadata: serialize_chat_metadata(&metadata), + rag_ids: Some(serialize_rag_ids(&rag_ids)), + }; + + upsert_chat(db, &row)?; Ok(()) } @@ -66,21 +105,52 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn stream_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _message_id: i64, - _format: ResponseFormat, + chat_id: &Uuid, + message_id: i64, + format: ResponseFormat, _ai_model: Option, ) -> Result { + if self.local_ai.is_running() { + let content = self.get_message_content(message_id)?; + match self + .local_ai + .stream_question( + &chat_id.to_string(), + &content, + Some(json!(format)), + json!({}), + ) + .await + { + Ok(stream) => Ok(QuestionStream::new(stream).boxed()), + Err(err) => Ok( + stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), + ), + } + } else { + Err(FlowyError::local_ai_not_ready()) + } + } + + async fn get_answer( + &self, + _workspace_id: &Uuid, + _chat_id: &Uuid, + _question_message_id: i64, + ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } async fn get_chat_messages( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _offset: MessageCursor, - _limit: u64, + chat_id: &Uuid, + offset: MessageCursor, + limit: u64, ) -> Result { + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } @@ -106,15 +176,6 @@ impl ChatCloudService for LocalServerChatServiceImpl { }) } - async fn get_answer( - &self, - _workspace_id: &Uuid, - _chat_id: &Uuid, - _question_message_id: i64, - ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) - } - async fn stream_complete( &self, _workspace_id: &Uuid, @@ -137,18 +198,40 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn get_chat_settings( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let chat_id = chat_id.to_string(); + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let row = read_chat(db, &chat_id)?; + let rag_ids = deserialize_rag_ids(&row.rag_ids); + let metadata = deserialize_chat_metadata::(&row.metadata); + let setting = ChatSettings { + name: row.name, + rag_ids, + metadata, + }; + + Ok(setting) } async fn update_chat_settings( &self, _workspace_id: &Uuid, - _id: &Uuid, - _s: UpdateChatParams, + id: &Uuid, + s: UpdateChatParams, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let uid = self.user.user_id()?; + let mut db = self.user.get_sqlite_db(uid)?; + let changeset = ChatTableChangeset { + chat_id: id.to_string(), + name: s.name, + metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), + rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), + }; + + update_chat(&mut db, changeset)?; + Ok(()) } async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { 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 282d118203..0ef930320e 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -8,6 +8,7 @@ use crate::local_server::impls::{ LocalServerUserServiceImpl, }; use crate::AppFlowyServer; +use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; @@ -18,13 +19,15 @@ use tokio::sync::mpsc; pub struct LocalServer { user: Arc, + local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, + local_ai, stop_tx: Default::default(), } } @@ -61,6 +64,7 @@ impl AppFlowyServer for LocalServer { fn chat_service(&self) -> Arc { Arc::new(LocalServerChatServiceImpl { user: self.user.clone(), + local_ai: self.local_ai.clone(), }) } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql new file mode 100644 index 0000000000..6edd4a5113 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql @@ -0,0 +1,9 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE chat_table + ADD COLUMN local_enabled INTEGER; +ALTER TABLE chat_table + ADD COLUMN sync_to_cloud INTEGER; +ALTER TABLE chat_table + ADD COLUMN local_files TEXT; + +ALTER TABLE chat_table DROP COLUMN rag_ids; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql new file mode 100644 index 0000000000..0604601486 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE chat_table DROP COLUMN local_enabled; +ALTER TABLE chat_table DROP COLUMN local_files; +ALTER TABLE chat_table DROP COLUMN sync_to_cloud; +ALTER TABLE chat_table ADD COLUMN rag_ids TEXT; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 4ff70bf3c6..eda95c29f7 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -35,10 +35,8 @@ diesel::table! { chat_id -> Text, created_at -> BigInt, name -> Text, - local_files -> Text, metadata -> Text, - local_enabled -> Bool, - sync_to_cloud -> Bool, + rag_ids -> Nullable, } } @@ -128,15 +126,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 13065ac726e8e52ab9f0ff39838079b9e29eb926 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 15:47:17 +0800 Subject: [PATCH 08/74] chore: add tests --- frontend/rust-lib/Cargo.lock | 169 +---- .../event-integration-test/Cargo.toml | 9 - .../event-integration-test/tests/main.rs | 2 + .../tests/sql_test/chat_message_test.rs | 596 ++++++++++++++++++ .../tests/sql_test/mod.rs | 1 + .../src/persistence/chat_message_sql.rs | 48 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 6 +- frontend/rust-lib/flowy-ai/src/chat.rs | 32 +- .../rust-lib/flowy-ai/src/event_handler.rs | 4 +- .../flowy-ai/src/local_ai/controller.rs | 2 - .../src/middleware/chat_service_mw.rs | 4 +- .../src/deps_resolve/cloud_service_impl.rs | 2 +- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../flowy-server/src/af_cloud/impls/chat.rs | 2 +- .../src/local_server/impls/chat.rs | 151 ++++- .../flowy-server/tests/af_cloud_test/util.rs | 2 +- .../2025-04-17-042326_chat_metadata/down.sql | 2 +- 17 files changed, 795 insertions(+), 238 deletions(-) create mode 100644 frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs create mode 100644 frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 29547be823..cd06dc4870 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2313,7 +2313,6 @@ dependencies = [ name = "event-integration-test" version = "0.1.0" dependencies = [ - "anyhow", "assert-json-diff", "bytes", "chrono", @@ -2322,15 +2321,11 @@ dependencies = [ "collab-document", "collab-entity", "collab-folder", - "collab-plugins", - "dotenv", "flowy-ai", "flowy-ai-pub", "flowy-core", - "flowy-database-pub", "flowy-database2", "flowy-document", - "flowy-document-pub", "flowy-folder", "flowy-folder-pub", "flowy-notification", @@ -2342,7 +2337,6 @@ dependencies = [ "flowy-user", "flowy-user-pub", "futures", - "futures-util", "lib-dispatch", "lib-infra", "nanoid", @@ -2352,10 +2346,7 @@ dependencies = [ "serde", "serde_json", "strum", - "tempdir", - "thread-id", "tokio", - "tokio-postgres", "tracing", "uuid", "walkdir", @@ -2415,12 +2406,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fancy-regex" version = "0.10.0" @@ -2491,12 +2476,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -2959,6 +2938,7 @@ dependencies = [ "arc-swap", "assert-json-diff", "bytes", + "chrono", "client-api", "collab", "collab-database", @@ -3199,12 +3179,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "funty" version = "2.0.0" @@ -4621,15 +4595,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "md-5" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" -dependencies = [ - "digest", -] - [[package]] name = "md5" version = "0.7.0" @@ -5347,35 +5312,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "postgres-protocol" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" -dependencies = [ - "base64 0.21.5", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.8.5", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" -dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -5796,19 +5732,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.7.3" @@ -5854,21 +5777,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -5944,15 +5852,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -6053,15 +5952,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "rend" version = "0.4.0" @@ -6948,17 +6838,6 @@ dependencies = [ "quote", ] -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "strsim" version = "0.10.0" @@ -7296,16 +7175,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - [[package]] name = "tempfile" version = "3.12.0" @@ -7538,32 +7407,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.1", - "percent-encoding", - "phf 0.11.2", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.8.5", - "socket2 0.5.5", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-retry" version = "0.3.0" @@ -8448,16 +8291,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 21d94ae28a..6b2d5af7ba 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -12,16 +12,13 @@ flowy-user-pub = { workspace = true } flowy-folder = { path = "../flowy-folder", features = ["test_helper"] } flowy-folder-pub = { workspace = true } flowy-database2 = { path = "../flowy-database2" } -flowy-database-pub = { workspace = true } flowy-document = { path = "../flowy-document" } -flowy-document-pub = { workspace = true } flowy-ai = { workspace = true } lib-dispatch = { workspace = true } lib-infra = { workspace = true } flowy-server = { path = "../flowy-server" } flowy-server-pub = { workspace = true } flowy-notification = { workspace = true } -anyhow.workspace = true flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-search = { workspace = true } @@ -31,8 +28,6 @@ serde.workspace = true serde_json.workspace = true protobuf.workspace = true tokio = { workspace = true, features = ["full"] } -futures-util = "0.3.26" -thread-id = "3.3.0" bytes.workspace = true nanoid = "0.4.0" tracing.workspace = true @@ -41,17 +36,13 @@ collab = { workspace = true } collab-document = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } -collab-plugins = { workspace = true } collab-entity = { workspace = true } rand = { version = "0.8.5", features = [] } strum = "0.25.0" [dev-dependencies] -dotenv = "0.15.0" -tempdir = "0.3.7" uuid.workspace = true assert-json-diff = "2.0.2" -tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" zip.workspace = true walkdir = "2.5.0" diff --git a/frontend/rust-lib/event-integration-test/tests/main.rs b/frontend/rust-lib/event-integration-test/tests/main.rs index 05f19e9b75..cf4c1591ac 100644 --- a/frontend/rust-lib/event-integration-test/tests/main.rs +++ b/frontend/rust-lib/event-integration-test/tests/main.rs @@ -4,6 +4,8 @@ mod folder; // TODO(Mathias): Enable tests for search // mod search; + +mod sql_test; mod user; pub mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs new file mode 100644 index 0000000000..549548ccf1 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs @@ -0,0 +1,596 @@ +use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_ai_pub::cloud::MessageCursor; +use flowy_ai_pub::persistence::{ + insert_chat_messages, select_chat_messages, select_message, select_message_content, + select_message_where_match_reply_message_id, total_message_count, ChatMessageTable, +}; +use uuid::Uuid; + +#[tokio::test] +async fn chat_message_table_insert_select_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id_1 = 1000; + let message_id_2 = 2000; + + // Create test messages + let messages = vec![ + ChatMessageTable { + message_id: message_id_1, + chat_id: chat_id.clone(), + content: "Hello, this is a test message".to_string(), + created_at: 1625097600, // 2021-07-01 + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }, + ChatMessageTable { + message_id: message_id_2, + chat_id: chat_id.clone(), + content: "This is a reply to the test message".to_string(), + created_at: 1625097700, // 2021-07-01, 100 seconds later + author_type: 0, // AI + author_id: "ai".to_string(), + reply_message_id: Some(message_id_1), + metadata: Some(r#"{"source": "test"}"#.to_string()), + }, + ]; + + // Test insert_chat_messages + let result = insert_chat_messages(db_conn, &messages); + assert!( + result.is_ok(), + "Failed to insert chat messages: {:?}", + result + ); + + // Test select_chat_messages + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let messages_result = + select_chat_messages(db_conn, &chat_id, 10, MessageCursor::Offset(0)).unwrap(); + + assert_eq!(messages_result.messages.len(), 2); + assert_eq!(messages_result.total_count, 2); + assert!(!messages_result.has_more); + + // Verify the content of the returned messages + let first_message = messages_result + .messages + .iter() + .find(|m| m.message_id == message_id_1) + .unwrap(); + assert_eq!(first_message.content, "Hello, this is a test message"); + assert_eq!(first_message.author_type, 1); + + let second_message = messages_result + .messages + .iter() + .find(|m| m.message_id == message_id_2) + .unwrap(); + assert_eq!( + second_message.content, + "This is a reply to the test message" + ); + assert_eq!(second_message.reply_message_id, Some(message_id_1)); +} + +#[tokio::test] +async fn chat_message_table_cursor_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create multiple test messages with sequential IDs + let mut messages = Vec::new(); + for i in 1..6 { + messages.push(ChatMessageTable { + message_id: i * 1000, + chat_id: chat_id.clone(), + content: format!("Message {}", i), + created_at: 1625097600 + (i * 100), // Increasing timestamps + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }); + } + + // Insert messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Test MessageCursor::Offset + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_offset = select_chat_messages( + db_conn, + &chat_id, + 2, // Limit to 2 messages + MessageCursor::Offset(0), + ) + .unwrap(); + + assert_eq!(result_offset.messages.len(), 2); + assert!(result_offset.has_more); + + // Test MessageCursor::AfterMessageId + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_after = select_chat_messages( + db_conn, + &chat_id, + 3, // Limit to 3 messages + MessageCursor::AfterMessageId(2000), + ) + .unwrap(); + + assert_eq!(result_after.messages.len(), 3); // Should get message IDs 3000, 4000, 5000 + assert!(result_after.messages.iter().all(|m| m.message_id > 2000)); + + // Test MessageCursor::BeforeMessageId + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_before = select_chat_messages( + db_conn, + &chat_id, + 2, // Limit to 2 messages + MessageCursor::BeforeMessageId(4000), + ) + .unwrap(); + + assert_eq!(result_before.messages.len(), 2); // Should get message IDs 1000, 2000, 3000 + assert!(result_before.messages.iter().all(|m| m.message_id < 4000)); +} + +#[tokio::test] +async fn chat_message_total_count_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create test messages + let messages = vec![ + ChatMessageTable { + message_id: 1001, + chat_id: chat_id.clone(), + content: "Message 1".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }, + ChatMessageTable { + message_id: 1002, + chat_id: chat_id.clone(), + content: "Message 2".to_string(), + created_at: 1625097700, + author_type: 0, + author_id: "ai".to_string(), + reply_message_id: None, + metadata: None, + }, + ]; + + // Insert messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Test total_message_count + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 2); + + // Add one more message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let additional_message = ChatMessageTable { + message_id: 1003, + chat_id: chat_id.clone(), + content: "Message 3".to_string(), + created_at: 1625097800, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + insert_chat_messages(db_conn, &[additional_message]).unwrap(); + + // Verify count increased + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let updated_count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(updated_count, 3); + + // Test count for non-existent chat + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let empty_count = total_message_count(db_conn, "non_existent_chat").unwrap(); + assert_eq!(empty_count, 0); +} + +#[tokio::test] +async fn chat_message_select_message_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 2001; + + // Create test message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: "This is a test message for select_message".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Test select_message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message(db_conn, message_id).unwrap(); + assert!(result.is_some()); + + let retrieved_message = result.unwrap(); + assert_eq!(retrieved_message.message_id, message_id); + assert_eq!(retrieved_message.chat_id, chat_id); + assert_eq!( + retrieved_message.content, + "This is a test message for select_message" + ); + assert_eq!(retrieved_message.author_id, "user_1"); + assert_eq!( + retrieved_message.metadata, + Some(r#"{"test_key": "test_value"}"#.to_string()) + ); + + // Test select_message with non-existent ID + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let non_existent = select_message(db_conn, 9999).unwrap(); + assert!(non_existent.is_none()); +} + +#[tokio::test] +async fn chat_message_select_content_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 3001; + let message_content = "This is the content to retrieve"; + + // Create test message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: message_content.to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Test select_message_content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let content = select_message_content(db_conn, message_id).unwrap(); + assert!(content.is_some()); + assert_eq!(content.unwrap(), message_content); + + // Test with non-existent message + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let no_content = select_message_content(db_conn, 9999).unwrap(); + assert!(no_content.is_none()); +} + +#[tokio::test] +async fn chat_message_reply_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let question_id = 4001; + let answer_id = 4002; + + // Create question and answer messages + let question = ChatMessageTable { + message_id: question_id, + chat_id: chat_id.clone(), + content: "What is the question?".to_string(), + created_at: 1625097600, + author_type: 1, // User + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + let answer = ChatMessageTable { + message_id: answer_id, + chat_id: chat_id.clone(), + content: "This is the answer".to_string(), + created_at: 1625097700, + author_type: 0, // AI + author_id: "ai".to_string(), + reply_message_id: Some(question_id), // Link to question + metadata: None, + }; + + // Insert messages + insert_chat_messages(db_conn, &[question, answer]).unwrap(); + + // Test select_message_where_match_reply_message_id + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); + + assert!(result.is_some()); + let reply = result.unwrap(); + assert_eq!(reply.message_id, answer_id); + assert_eq!(reply.content, "This is the answer"); + assert_eq!(reply.reply_message_id, Some(question_id)); + + // Test with non-existent reply relation + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let no_reply = select_message_where_match_reply_message_id( + db_conn, &chat_id, 9999, // Non-existent question ID + ) + .unwrap(); + + assert!(no_reply.is_none()); + + // Test with wrong chat_id + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let wrong_chat = + select_message_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); + + assert!(wrong_chat.is_none()); +} + +#[tokio::test] +async fn chat_message_upsert_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + let message_id = 5001; + + // Create initial message + let message = ChatMessageTable { + message_id, + chat_id: chat_id.clone(), + content: "Original content".to_string(), + created_at: 1625097600, + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: None, + metadata: None, + }; + + // Insert message + insert_chat_messages(db_conn, &[message]).unwrap(); + + // Check original content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let original = select_message(db_conn, message_id).unwrap().unwrap(); + assert_eq!(original.content, "Original content"); + + // Create updated message with same ID but different content + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let updated_message = ChatMessageTable { + message_id, // Same ID + chat_id: chat_id.clone(), + content: "Updated content".to_string(), // New content + created_at: 1625097700, // Updated timestamp + author_type: 1, + author_id: "user_1".to_string(), + reply_message_id: Some(1000), // Added reply ID + metadata: Some(r#"{"updated": true}"#.to_string()), + }; + + // Upsert message + insert_chat_messages(db_conn, &[updated_message]).unwrap(); + + // Verify update + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result = select_message(db_conn, message_id).unwrap().unwrap(); + assert_eq!(result.content, "Updated content"); + assert_eq!(result.created_at, 1625097700); + assert_eq!(result.reply_message_id, Some(1000)); + assert_eq!(result.metadata, Some(r#"{"updated": true}"#.to_string())); + + // Count should still be 1 (update, not insert) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 1); +} + +#[tokio::test] +async fn chat_message_select_with_large_dataset() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.sign_up_as_anon().await; + + let uid = test.user_manager.get_anon_user().await.unwrap().id; + let db_conn = test.user_manager.db_connection(uid).unwrap(); + + let chat_id = Uuid::new_v4().to_string(); + + // Create 100 test messages with sequential IDs + let mut messages = Vec::new(); + for i in 1..=100 { + messages.push(ChatMessageTable { + message_id: i * 100, + chat_id: chat_id.clone(), + content: format!("Message {}", i), + created_at: 1625097600 + (i * 10), // Increasing timestamps + author_type: if i % 2 == 0 { 0 } else { 1 }, // Alternate between AI and User + author_id: if i % 2 == 0 { + "ai".to_string() + } else { + "user_1".to_string() + }, + reply_message_id: if i > 1 && i % 2 == 0 { + Some((i - 1) * 100) + } else { + None + }, // Even messages reply to previous message + metadata: if i % 5 == 0 { + Some(format!(r#"{{"index": {}}}"#, i)) + } else { + None + }, + }); + } + + // Insert all 100 messages + insert_chat_messages(db_conn, &messages).unwrap(); + + // Verify total count + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let count = total_message_count(db_conn, &chat_id).unwrap(); + assert_eq!(count, 100, "Should have 100 messages in the database"); + + // Test 1: MessageCursor::Offset with small page size + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let page_size = 10; + let result_offset = + select_chat_messages(db_conn, &chat_id, page_size, MessageCursor::Offset(0)).unwrap(); + + assert_eq!( + result_offset.messages.len(), + page_size as usize, + "Should return exactly {page_size} messages" + ); + assert!( + result_offset.has_more, + "Should have more messages available" + ); + assert_eq!(result_offset.total_count, 100, "Total count should be 100"); + + // Test 2: Pagination with offset + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_page2 = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::Offset(page_size), + ) + .unwrap(); + + assert_eq!(result_page2.messages.len(), page_size as usize); + assert!( + result_page2.messages[0].message_id != result_offset.messages[0].message_id, + "Second page should have different messages than first page" + ); + + // Test 3: MessageCursor::AfterMessageId (forward pagination) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let middle_message_id = 5000; // Message ID from the middle + let result_after = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::AfterMessageId(middle_message_id), + ) + .unwrap(); + + assert_eq!(result_after.messages.len(), page_size as usize); + assert!( + result_after + .messages + .iter() + .all(|m| m.message_id > middle_message_id), + "All messages should have ID greater than the cursor" + ); + + // Test 4: MessageCursor::BeforeMessageId (backward pagination) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_before = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::BeforeMessageId(middle_message_id), + ) + .unwrap(); + + assert_eq!(result_before.messages.len(), page_size as usize); + assert!( + result_before + .messages + .iter() + .all(|m| m.message_id < middle_message_id), + "All messages should have ID less than the cursor" + ); + + // Test 5: Large page size (retrieve all) + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_all = select_chat_messages( + db_conn, + &chat_id, + 200, // More than we have + MessageCursor::Offset(0), + ) + .unwrap(); + + assert_eq!( + result_all.messages.len(), + 100, + "Should return all 100 messages" + ); + assert!(!result_all.has_more, "Should not have more messages"); + + // Test 6: Empty result when using out of range cursor + let db_conn = test.user_manager.db_connection(uid).unwrap(); + let result_out_of_range = select_chat_messages( + db_conn, + &chat_id, + page_size, + MessageCursor::AfterMessageId(10000), // After the last message + ) + .unwrap(); + + assert_eq!( + result_out_of_range.messages.len(), + 0, + "Should return no messages" + ); + assert!( + !result_out_of_range.has_more, + "Should not have more messages" + ); +} diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs new file mode 100644 index 0000000000..773bdab81f --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs @@ -0,0 +1 @@ +mod chat_message_test; diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index ac3192064b..f5f985575a 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -35,6 +35,7 @@ pub fn insert_chat_messages( .do_update() .set(( chat_message_table::content.eq(excluded(chat_message_table::content)), + chat_message_table::metadata.eq(excluded(chat_message_table::metadata)), chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), @@ -48,12 +49,18 @@ pub fn insert_chat_messages( Ok(()) } +pub struct ChatMessagesResult { + pub messages: Vec, + pub total_count: i64, + pub has_more: bool, +} + pub fn select_chat_messages( mut conn: DBConnection, chat_id_val: &str, - limit_val: i64, + limit_val: u64, offset: MessageCursor, -) -> QueryResult> { +) -> QueryResult { let mut query = dsl::chat_message_table .filter(chat_message_table::chat_id.eq(chat_id_val)) .into_boxed(); @@ -71,15 +78,46 @@ pub fn select_chat_messages( MessageCursor::NextBack => {}, } + // Get total count before applying limit + let total_count = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .count() + .first::(&mut *conn)?; + query = query .order(( chat_message_table::created_at.desc(), chat_message_table::message_id.desc(), )) - .limit(limit_val); + .limit(limit_val as i64); let messages: Vec = query.load::(&mut *conn)?; - Ok(messages) + + // Check if there are more messages + let has_more = if let Some(last_message) = messages.last() { + let remaining_count = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .filter(chat_message_table::message_id.lt(last_message.message_id)) + .count() + .first::(&mut *conn)?; + + remaining_count > 0 + } else { + false + }; + + Ok(ChatMessagesResult { + messages, + total_count, + has_more, + }) +} + +pub fn total_message_count(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { + dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .count() + .first::(&mut *conn) } pub fn select_message( @@ -107,10 +145,12 @@ pub fn select_message_content( pub fn select_message_where_match_reply_message_id( mut conn: DBConnection, + chat_id: &str, answer_message_id_val: i64, ) -> QueryResult> { dsl::chat_message_table .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) + .filter(chat_message_table::chat_id.eq(chat_id)) .first::(&mut *conn) .optional() } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 1bb05e19b6..a49771a544 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -270,7 +270,7 @@ impl AIManager { ) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; let question_message_id = chat - .get_question_id_from_answer_id(answer_message_id) + .get_question_id_from_answer_id(chat_id, answer_message_id) .await?; let model = model.map_or_else( @@ -567,7 +567,7 @@ impl AIManager { pub async fn load_prev_chat_messages( &self, chat_id: &Uuid, - limit: i64, + limit: u64, before_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -580,7 +580,7 @@ impl AIManager { pub async fn load_latest_chat_messages( &self, chat_id: &Uuid, - limit: i64, + limit: u64, after_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 0610799f03..976acee3e4 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -63,18 +63,6 @@ impl Chat { pub fn close(&self) {} - #[allow(dead_code)] - pub async fn pull_latest_message(&self, limit: i64) { - let latest_message_id = self - .latest_message_id - .load(std::sync::atomic::Ordering::Relaxed); - if latest_message_id > 0 { - let _ = self - .load_remote_chat_messages(limit, None, Some(latest_message_id)) - .await; - } - } - pub async fn stop_stream_message(&self) { self .stop_stream @@ -340,7 +328,7 @@ impl Chat { /// - `before_message_id` is the first message ID in the current chat messages. pub async fn load_prev_chat_messages( &self, - limit: i64, + limit: u64, before_message_id: Option, ) -> Result { trace!( @@ -388,7 +376,7 @@ impl Chat { pub async fn load_latest_chat_messages( &self, - limit: i64, + limit: u64, after_message_id: Option, ) -> Result { trace!( @@ -420,7 +408,7 @@ impl Chat { async fn load_remote_chat_messages( &self, - limit: i64, + limit: u64, before_message_id: Option, after_message_id: Option, ) -> FlowyResult<()> { @@ -445,7 +433,7 @@ impl Chat { _ => MessageCursor::NextBack, }; match cloud_service - .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit as u64) + .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit) .await { Ok(resp) => { @@ -498,12 +486,14 @@ impl Chat { pub async fn get_question_id_from_answer_id( &self, + chat_id: &Uuid, answer_message_id: i64, ) -> Result { let conn = self.user_service.sqlite_connection(self.uid)?; - let local_result = select_message_where_match_reply_message_id(conn, answer_message_id)? - .map(|message| message.message_id); + let local_result = + select_message_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? + .map(|message| message.message_id); if let Some(message_id) = local_result { return Ok(message_id); @@ -560,12 +550,12 @@ impl Chat { async fn load_local_chat_messages( &self, - limit: i64, + limit: u64, offset: MessageCursor, ) -> Result, FlowyError> { let conn = self.user_service.sqlite_connection(self.uid)?; - let records = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?; - let messages = records + let rows = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?.messages; + let messages = rows .into_iter() .map(|record| ChatMessagePB { message_id: record.message_id, diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index b8334ffe8d..6788685102 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -152,7 +152,7 @@ pub(crate) async fn load_prev_message_handler( let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_prev_chat_messages(&chat_id, data.limit, data.before_message_id) + .load_prev_chat_messages(&chat_id, data.limit as u64, data.before_message_id) .await?; data_result_ok(messages) } @@ -168,7 +168,7 @@ pub(crate) async fn load_next_message_handler( let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_latest_chat_messages(&chat_id, data.limit, data.after_message_id) + .load_latest_chat_messages(&chat_id, data.limit as u64, data.after_message_id) .await?; data_result_ok(messages) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 431d8ff681..3c4ccb1f9d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -79,7 +79,6 @@ impl LocalAIController { // Create the core plugin and resource controller let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { - user_service: user_service.clone(), store_preferences: store_preferences.clone(), }; let local_ai_resource = Arc::new(LocalAIResourceController::new( @@ -594,7 +593,6 @@ async fn initialize_ai_plugin( } pub struct LLMResourceServiceImpl { - user_service: Arc, store_preferences: Weak, } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index a9a4022573..e3c3d671ec 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -5,14 +5,14 @@ use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use af_plugin::error::PluginError; -use flowy_ai_pub::persistence::{select_message, select_message_content, ChatMessageTable}; +use flowy_ai_pub::persistence::select_message_content; use std::collections::HashMap; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + StreamComplete, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index c916157ff0..e4bffbfb69 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -16,7 +16,7 @@ use flowy_ai_pub::cloud::search_dto::{ use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + StreamComplete, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 5225eb817d..c8710470b0 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -45,6 +45,7 @@ rand = "0.8.5" semver = "1.0.23" flowy-sqlite = { workspace = true } flowy-ai = { workspace = true } +chrono.workspace = true [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 4b0611e497..3f8803c7fc 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -9,7 +9,7 @@ use client_api::entity::chat_dto::{ }; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + ModelList, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 23562d2b07..09ef644bfc 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,25 +1,29 @@ use crate::af_cloud::define::ServerUser; +use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::CompletionStream; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai::local_ai::stream_util::QuestionStream; +use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, SubscriptionPlan, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, + ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, + RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_message_content, - serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, ChatTable, - ChatTableChangeset, + deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_chat_messages, + select_message_content, select_message_where_match_reply_message_id, serialize_chat_metadata, + serialize_rag_ids, update_chat, upsert_chat, ChatMessageTable, ChatTable, ChatTableChangeset, }; use flowy_error::{FlowyError, FlowyResult}; -use futures_util::{stream, FutureExt, StreamExt}; +use futures_util::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; use serde_json::{json, Value}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; +use tracing::trace; use uuid::Uuid; pub struct LocalServerChatServiceImpl { @@ -148,51 +152,119 @@ impl ChatCloudService for LocalServerChatServiceImpl { offset: MessageCursor, limit: u64, ) -> Result { + let chat_id = chat_id.to_string(); let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; + let result = select_chat_messages(db, &chat_id, limit, offset)?; - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let messages = result + .messages + .into_iter() + .map(chat_message_from_row) + .collect(); + + Ok(RepeatedChatMessage { + messages, + has_more: result.has_more, + total: result.total_count, + }) } async fn get_question_from_answer_id( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _answer_message_id: i64, + chat_id: &Uuid, + answer_message_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let chat_id = chat_id.to_string(); + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + let row = select_message_where_match_reply_message_id(db, &chat_id, answer_message_id)? + .map(chat_message_from_row) + .ok_or_else(FlowyError::record_not_found)?; + Ok(row) } async fn get_related_message( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message_id: i64, - ai_model: Option, + _ai_model: Option, ) -> Result { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], - }) + if self.local_ai.is_running() { + let questions = self + .local_ai + .get_related_question(&chat_id.to_string()) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + trace!("LocalAI related questions: {:?}", questions); + + let items = questions + .into_iter() + .map(|content| RelatedQuestion { + content, + metadata: None, + }) + .collect::>(); + + Ok(RepeatedRelatedQuestion { message_id, items }) + } else { + Ok(RepeatedRelatedQuestion { + message_id, + items: vec![], + }) + } } async fn stream_complete( &self, _workspace_id: &Uuid, - _params: CompleteTextParams, + params: CompleteTextParams, _ai_model: Option, ) -> Result { - Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) + if self.local_ai.is_running() { + match self + .local_ai + .complete_text_v2( + ¶ms.text, + params.completion_type.unwrap() as u8, + Some(json!(params.format)), + Some(json!(params.metadata)), + ) + .await + { + Ok(stream) => Ok( + CompletionStream::new( + stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), + ) + .map_err(FlowyError::from) + .boxed(), + ), + Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), + } + } else { + Err(FlowyError::local_ai_not_ready()) + } } async fn embed_file( &self, _workspace_id: &Uuid, - _file_path: &Path, - _chat_id: &Uuid, - _metadata: Option>, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) + if self.local_ai.is_running() { + self + .local_ai + .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + Ok(()) + } else { + Err(FlowyError::local_ai_not_ready()) + } } async fn get_chat_settings( @@ -242,3 +314,36 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } } + +fn chat_message_from_row(row: ChatMessageTable) -> ChatMessage { + let created_at = Utc + .timestamp_opt(row.created_at, 0) + .single() + .unwrap_or_else(Utc::now); + + let author_id = row.author_id.parse::().unwrap_or_default(); + let author_type = match row.author_type { + 1 => ChatAuthorType::Human, + 2 => ChatAuthorType::System, + 3 => ChatAuthorType::AI, + _ => ChatAuthorType::Unknown, + }; + + let metadata = row + .metadata + .map(|s| deserialize_chat_metadata::(&s)) + .unwrap_or_else(|| json!({})); + + ChatMessage { + author: ChatAuthor { + author_id, + author_type, + meta: None, + }, + message_id: row.message_id, + content: row.content, + created_at, + metadata, + reply_message_id: row.reply_message_id, + } +} 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 b747c0b2ae..3004ce2163 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 @@ -48,7 +48,7 @@ impl ServerUser for FakeServerUserImpl { todo!() } - fn get_sqlite_db(&self, uid: i64) -> Result { + fn get_sqlite_db(&self, _uid: i64) -> Result { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql index 6edd4a5113..8b07e6189d 100644 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql @@ -6,4 +6,4 @@ ALTER TABLE chat_table ALTER TABLE chat_table ADD COLUMN local_files TEXT; -ALTER TABLE chat_table DROP COLUMN rag_ids; +ALTER TABLE chat_table DROP COLUMN rag_ids; \ No newline at end of file From 57e4d269eb097a527b5982fee2a7a5fd13bea0e4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 16:27:53 +0800 Subject: [PATCH 09/74] chore: enable local chat --- .../lib/plugins/ai_chat/chat_page.dart | 16 ++++++------- .../setting_ai_view/settings_ai_view.dart | 17 ------------- .../settings/settings_dialog.dart | 7 +++++- frontend/rust-lib/Cargo.lock | 24 +++++++++---------- frontend/rust-lib/Cargo.toml | 4 ++-- 5 files changed, 28 insertions(+), 40 deletions(-) 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 4f843d447b..52617f1292 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -51,14 +51,14 @@ class AIChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { - return Center( - child: FlowyText( - LocaleKeys.chat_unsupportedCloudPrompt.tr(), - fontSize: 20, - ), - ); - } + // if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + // return Center( + // child: FlowyText( + // LocaleKeys.chat_unsupportedCloudPrompt.tr(), + // fontSize: 20, + // ), + // ); + // } return MultiBlocProvider( providers: [ 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 efb969700e..c2e75ff2f2 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 @@ -10,23 +10,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { - const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), - child: FlowyText( - LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), - maxLines: null, - fontSize: 16, - lineHeight: 1.6, - ), - ); - } -} - class SettingsAIView extends StatelessWidget { const SettingsAIView({ super.key, 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 08747b95da..512d673407 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -32,6 +32,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'pages/setting_ai_view/local_settings_ai_view.dart'; import 'widgets/setting_cloud.dart'; @visibleForTesting @@ -147,7 +148,11 @@ class SettingsDialog extends StatelessWidget { workspaceId: workspaceId, ); } else { - return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); + return LocalSettingsAIView( + key: ValueKey(workspaceId), + userProfile: user, + workspaceId: workspaceId, + ); } case SettingsPage.member: return WorkspaceMembersPage( diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index cd06dc4870..ec114f205a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "async-trait", @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "bincode", "bytes", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3442,7 +3442,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "app-error", "jsonwebtoken", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "bytes", @@ -6644,7 +6644,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3e98e6811f8e5cf1a5ab5a92dc9b35006e254579#3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6e93b13a19..e4f0eab545 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -107,8 +107,8 @@ af-local-ai = { version = "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 = "3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3e98e6811f8e5cf1a5ab5a92dc9b35006e254579" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } [profile.dev] opt-level = 0 From 3a4d17f054687c41854d2fcfe94b58c9b9f1f83e Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 16:53:52 +0800 Subject: [PATCH 10/74] chore: enable anon ai writer --- .../ai/ai_writer_toolbar_item.dart | 14 ++++---- .../local_settings_ai_view.dart | 34 +++++++++++++++++++ .../src/local_server/impls/chat.rs | 12 +++++-- 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 467a847c53..a8071e9286 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -142,7 +142,7 @@ class _AiWriterToolbarActionListState extends State { ], ), onPressed: () { - if (_isAIEnabled(widget.editorState)) { + if (_isAIWriterEnabled(widget.editorState)) { keepEditorFocusNotifier.increase(); popoverController.show(); setState(() { @@ -159,7 +159,7 @@ class _AiWriterToolbarActionListState extends State { return widget.tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIEnabled(widget.editorState) + _isAIWriterEnabled(widget.editorState) ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -191,7 +191,7 @@ class ImproveWritingButton extends StatelessWidget { color: theme.iconColorTheme.primary, ), onPressed: () { - if (_isAIEnabled(editorState)) { + if (_isAIWriterEnabled(editorState)) { keepEditorFocusNotifier.increase(); _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { @@ -205,7 +205,7 @@ class ImproveWritingButton extends StatelessWidget { return tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIEnabled(editorState) + _isAIWriterEnabled(editorState) ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -240,10 +240,8 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async { ); } -bool _isAIEnabled(EditorState editorState) { - final documentContext = editorState.document.root.context; - return documentContext == null || - !documentContext.read().isLocalMode; +bool _isAIWriterEnabled(EditorState editorState) { + return true; } bool onlyShowInTextTypeAndExcludeTable( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart new file mode 100644 index 0000000000..e90c42444f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalSettingsAIView extends StatelessWidget { + const LocalSettingsAIView({ + super.key, + required this.userProfile, + required this.workspaceId, + }); + + final UserProfilePB userProfile; + final String workspaceId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => SettingsAIBloc(userProfile, workspaceId) + ..add(const SettingsAIEvent.started()), + child: SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: "", + children: [ + const LocalAISetting(), + ], + ), + ); + } +} diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 09ef644bfc..9057288f88 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -132,7 +132,11 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } @@ -244,7 +248,11 @@ impl ChatCloudService for LocalServerChatServiceImpl { Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } } else { - Err(FlowyError::local_ai_not_ready()) + if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) + } else { + Err(FlowyError::local_ai_disabled()) + } } } From ac659066c674bd1f95d3e637dac040767d5d3951 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 20:49:24 +0800 Subject: [PATCH 11/74] chore: return local model --- .../lib/plugins/ai_chat/chat_page.dart | 3 - .../ai/ai_writer_toolbar_item.dart | 2 - frontend/rust-lib/flowy-ai/src/ai_manager.rs | 178 ++++++++++-------- .../flowy-core/src/deps_resolve/chat_deps.rs | 7 +- frontend/rust-lib/flowy-core/src/lib.rs | 7 + .../rust-lib/flowy-core/src/server_layer.rs | 6 + .../flowy-server/src/af_cloud/define.rs | 3 + .../flowy-server/tests/af_cloud_test/util.rs | 6 + .../src/services/authenticate_user.rs | 24 +++ .../src/user_manager/manager_history_user.rs | 2 +- 10 files changed, 150 insertions(+), 88 deletions(-) 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 52617f1292..c154b0e3a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -9,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:desktop_drop/desktop_drop.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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index a8071e9286..ab695b31a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -9,7 +8,6 @@ import 'package:appflowy_ui/appflowy_ui.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 'operations/ai_writer_entities.dart'; diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a49771a544..c2f5b223c0 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -35,8 +35,10 @@ use tokio::sync::RwLock; use tracing::{error, info, instrument, trace}; use uuid::Uuid; +#[async_trait] pub trait AIUserService: Send + Sync + 'static { fn user_id(&self) -> Result; + async fn is_local_model(&self) -> FlowyResult; fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; @@ -437,99 +439,113 @@ impl AIManager { } pub async fn get_available_models(&self, source: String) -> FlowyResult { - // Build the models list from server models and mark them as non-local. - let mut models: Vec = self - .get_server_available_models() - .await? - .into_iter() - .map(AIModel::from) - .collect(); + let is_local_mode = self.user_service.is_local_model().await?; + if is_local_mode { + let mut selected_model = AIModel::default(); + let mut models = vec![]; + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + let model = AIModel::local(local_model, "".to_string()); + selected_model = model.clone(); + models.push(model); + } - trace!("[Model Selection]: Available models: {:?}", models); - - let mut current_active_local_ai_model = None; - - // If user enable local ai, then add local ai model to the list. - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - let model = AIModel::local(local_model, "".to_string()); - current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); - models.push(model); - } - - if models.is_empty() { - return Ok(AvailableModelsPB { + Ok(AvailableModelsPB { models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::default(), - }); - } + selected_model: AIModelPB::from(selected_model), + }) + } else { + // Build the models list from server models and mark them as non-local. + let mut models: Vec = self + .get_server_available_models() + .await? + .into_iter() + .map(AIModel::from) + .collect(); - // Global active model is the model selected by the user in the workspace settings. - let mut server_active_model = self - .get_workspace_select_model() - .await - .map(|m| AIModel::server(m, "".to_string())) - .unwrap_or_else(|_| AIModel::default()); + trace!("[Model Selection]: Available models: {:?}", models); + let mut current_active_local_ai_model = None; - trace!( - "[Model Selection] server active model: {:?}", - server_active_model - ); + // If user enable local ai, then add local ai model to the list. + if let Some(local_model) = self.local_ai.get_plugin_chat_model() { + let model = AIModel::local(local_model, "".to_string()); + current_active_local_ai_model = Some(model.clone()); + trace!("[Model Selection] current local ai model: {}", model.name); + models.push(model); + } - let mut user_selected_model = server_active_model.clone(); - // when current select model is deprecated, reset the model to default - if !models.iter().any(|m| m.name == server_active_model.name) { - server_active_model = AIModel::default(); - } + if models.is_empty() { + return Ok(AvailableModelsPB { + models: models.into_iter().map(|m| m.into()).collect(), + selected_model: AIModelPB::default(), + }); + } - let source_key = ai_available_models_key(&source); + // Global active model is the model selected by the user in the workspace settings. + let mut server_active_model = self + .get_workspace_select_model() + .await + .map(|m| AIModel::server(m, "".to_string())) + .unwrap_or_else(|_| AIModel::default()); - // We use source to identify user selected model. source can be document id or chat id. - match self.store_preferences.get_object::(&source_key) { - None => { - // when there is selected model and current local ai is active, then use local ai - if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { - user_selected_model = local_ai_model.clone(); - } - }, - Some(mut model) => { - trace!("[Model Selection] user previous select model: {:?}", model); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. - if model.is_local { - if let Some(local_ai_model) = ¤t_active_local_ai_model { - if local_ai_model.name != model.name { - model = local_ai_model.clone(); + trace!( + "[Model Selection] server active model: {:?}", + server_active_model + ); + + let mut user_selected_model = server_active_model.clone(); + // when current select model is deprecated, reset the model to default + if !models.iter().any(|m| m.name == server_active_model.name) { + server_active_model = AIModel::default(); + } + + let source_key = ai_available_models_key(&source); + // We use source to identify user selected model. source can be document id or chat id. + match self.store_preferences.get_object::(&source_key) { + None => { + // when there is selected model and current local ai is active, then use local ai + if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { + user_selected_model = local_ai_model.clone(); + } + }, + Some(mut model) => { + trace!("[Model Selection] user previous select model: {:?}", model); + // If source is provided, try to get the user-selected model from the store. User selected + // model will be used as the active model if it exists. + if model.is_local { + if let Some(local_ai_model) = ¤t_active_local_ai_model { + if local_ai_model.name != model.name { + model = local_ai_model.clone(); + } } } - } - user_selected_model = model; - }, - } - - // If user selected model is not available in the list, use the global active model. - let active_model = models - .iter() - .find(|m| m.name == user_selected_model.name) - .cloned() - .or(Some(server_active_model.clone())); - - // Update the stored preference if a different model is used. - if let Some(ref active_model) = active_model { - if active_model.name != user_selected_model.name { - self - .store_preferences - .set_object::(&source_key, &active_model.clone())?; + user_selected_model = model; + }, } - } - trace!("[Model Selection] final active model: {:?}", active_model); - let selected_model = AIModelPB::from(active_model.unwrap_or_default()); - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model, - }) + // If user selected model is not available in the list, use the global active model. + let active_model = models + .iter() + .find(|m| m.name == user_selected_model.name) + .cloned() + .or(Some(server_active_model.clone())); + + // Update the stored preference if a different model is used. + if let Some(ref active_model) = active_model { + if active_model.name != user_selected_model.name { + self + .store_preferences + .set_object::(&source_key, &active_model.clone())?; + } + } + + trace!("[Model Selection] final active model: {:?}", active_model); + let selected_model = AIModelPB::from(active_model.unwrap_or_default()); + Ok(AvailableModelsPB { + models: models.into_iter().map(|m| m.into()).collect(), + selected_model, + }) + } } pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result, FlowyError> { diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index a8280f9f66..c8c93a7f4c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -8,7 +8,7 @@ use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; -use flowy_error::FlowyError; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::ViewLayout; use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams}; use flowy_folder_pub::query::FolderService; @@ -164,11 +164,16 @@ impl ChatUserServiceImpl { } } +#[async_trait] impl AIUserService for ChatUserServiceImpl { fn user_id(&self) -> Result { self.upgrade_user()?.user_id() } + async fn is_local_model(&self) -> FlowyResult { + self.upgrade_user()?.is_local_mode().await + } + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 7584628056..1cf748d089 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -37,6 +37,7 @@ use crate::log_filter::init_log; use crate::server_layer::{current_server_type, Server, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; use user_state_callback::UserStatusCallbackImpl; pub mod config; @@ -333,6 +334,8 @@ impl ServerUserImpl { Ok(user) } } + +#[async_trait] impl ServerUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() @@ -342,6 +345,10 @@ impl ServerUser for ServerUserImpl { self.upgrade_user()?.user_id() } + async fn is_local_mode(&self) -> FlowyResult { + self.upgrade_user()?.is_local_mode().await + } + fn get_sqlite_db(&self, uid: i64) -> Result { self.upgrade_user()?.get_sqlite_connection(uid) } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index e194f614cd..3f5d16b165 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -13,6 +13,7 @@ use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; +use lib_infra::async_trait::async_trait; use serde_repr::*; use std::fmt::{Display, Formatter}; use std::path::PathBuf; @@ -184,11 +185,16 @@ pub fn current_server_type() -> Server { struct AIUserServiceImpl(Arc); +#[async_trait] impl AIUserService for AIUserServiceImpl { fn user_id(&self) -> Result { self.0.user_id() } + async fn is_local_model(&self) -> FlowyResult { + self.0.is_local_mode().await + } + fn workspace_id(&self) -> Result { self.0.workspace_id() } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index cf98cf5214..e6d8a7fac2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,5 +1,6 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; use std::path::PathBuf; use uuid::Uuid; @@ -9,11 +10,13 @@ pub const USER_EMAIL: &str = "email"; pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. +#[async_trait] pub trait ServerUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; fn user_id(&self) -> FlowyResult; + async fn is_local_mode(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; 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 3004ce2163..11027028ff 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 @@ -12,6 +12,7 @@ use flowy_server::af_cloud::define::ServerUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; /// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: /// @@ -38,6 +39,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc )) } +#[async_trait] struct FakeServerUserImpl; impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { @@ -48,6 +50,10 @@ impl ServerUser for FakeServerUserImpl { todo!() } + async fn is_local_mode(&self) -> FlowyResult { + Ok(true) + } + fn get_sqlite_db(&self, _uid: i64) -> Result { todo!() } 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 7ec63f93f7..84c1e9afe9 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -3,6 +3,7 @@ use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; use collab_integrate::CollabKVDB; +use crate::user_manager::manager_history_user::ANON_USER; use arc_swap::ArcSwapOption; use collab_plugins::local_storage::kv::doc::CollabKVAction; use collab_plugins::local_storage::kv::KVTransactionDB; @@ -46,6 +47,17 @@ impl AuthenticateUser { Ok(session.user_id) } + pub async fn is_local_mode(&self) -> FlowyResult { + let uid = self.user_id()?; + if let Ok(anon_user) = self.get_anon_user().await { + if anon_user == uid { + return Ok(true); + } + } + + Ok(false) + } + pub fn device_id(&self) -> FlowyResult { Ok(self.user_config.device_id.to_string()) } @@ -150,4 +162,16 @@ impl AuthenticateUser { }, } } + + async fn get_anon_user(&self) -> FlowyResult { + let anon_session = self + .store_preferences + .get_object::(ANON_USER) + .ok_or(FlowyError::new( + ErrorCode::RecordNotFound, + "Anon user not found", + ))?; + + Ok(anon_session.user_id) + } } 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 8d20bae427..b7f4789f9a 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 @@ -9,7 +9,7 @@ use flowy_user_pub::entities::Authenticator; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; -const ANON_USER: &str = "anon_user"; +pub const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] pub async fn get_migration_user( From ed64719560e71c399db522b4165f02836f3eaafe Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 21:09:24 +0800 Subject: [PATCH 12/74] chore: clippy --- .../lib/user/presentation/screens/workspace_error_screen.dart | 1 - frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index bd32696514..af6d4ad770 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; 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 11027028ff..f196e44ea9 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 @@ -39,8 +39,9 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc )) } -#[async_trait] struct FakeServerUserImpl; + +#[async_trait] impl ServerUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() From 5436277ada39450fd9cac00068bc2338ed455139 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 17 Apr 2025 21:39:49 +0800 Subject: [PATCH 13/74] chore: fmt --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index eda95c29f7..9504d126ed 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -126,15 +126,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 31d8653ba61f5885630f7bad13e2d08f4e0968d1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 13:20:05 +0800 Subject: [PATCH 14/74] refactor: save chat and chat message --- .../tests/chat/chat_message_test.rs | 2 - .../tests/sql_test/chat_message_test.rs | 43 ++- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 5 +- .../src/persistence/chat_message_sql.rs | 36 ++- .../flowy-ai-pub/src/persistence/chat_sql.rs | 53 ++-- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 38 +-- frontend/rust-lib/flowy-ai/src/chat.rs | 37 +-- frontend/rust-lib/flowy-ai/src/lib.rs | 1 + .../src/middleware/chat_service_mw.rs | 23 +- frontend/rust-lib/flowy-ai/src/offline/mod.rs | 1 + .../src/offline/offline_message_sync.rs | 258 ++++++++++++++++++ .../src/deps_resolve/cloud_service_impl.rs | 17 +- frontend/rust-lib/flowy-core/src/lib.rs | 4 +- .../rust-lib/flowy-core/src/server_layer.rs | 36 +-- .../flowy-server/src/af_cloud/define.rs | 29 +- .../flowy-server/src/af_cloud/impls/chat.rs | 17 +- .../src/af_cloud/impls/database.rs | 8 +- .../src/af_cloud/impls/document.rs | 8 +- .../flowy-server/src/af_cloud/impls/folder.rs | 8 +- .../af_cloud/impls/user/cloud_service_impl.rs | 6 +- .../flowy-server/src/af_cloud/impls/util.rs | 4 +- .../flowy-server/src/af_cloud/server.rs | 34 ++- .../src/local_server/impls/chat.rs | 102 ++++--- .../flowy-server/src/local_server/server.rs | 13 +- .../flowy-server/tests/af_cloud_test/util.rs | 4 +- .../down.sql | 3 + .../up.sql | 5 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 +- 28 files changed, 548 insertions(+), 271 deletions(-) create mode 100644 frontend/rust-lib/flowy-ai/src/offline/mod.rs create mode 100644 frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index c5b30d68c3..5b29258142 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -27,7 +27,6 @@ async fn af_cloud_create_chat_message_test() { &Uuid::from_str(&chat_id).unwrap(), &format!("hello world {}", i), ChatMessageType::System, - &[], ) .await .unwrap(); @@ -83,7 +82,6 @@ async fn af_cloud_load_remote_system_message_test() { &Uuid::from_str(&chat_id).unwrap(), &format!("hello server {}", i), ChatMessageType::System, - &[], ) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs index 549548ccf1..3294ad26db 100644 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs @@ -2,8 +2,8 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_ai_pub::cloud::MessageCursor; use flowy_ai_pub::persistence::{ - insert_chat_messages, select_chat_messages, select_message, select_message_content, - select_message_where_match_reply_message_id, total_message_count, ChatMessageTable, + select_answer_where_match_reply_message_id, select_chat_messages, select_message, + select_message_content, total_message_count, upsert_chat_messages, ChatMessageTable, }; use uuid::Uuid; @@ -31,6 +31,7 @@ async fn chat_message_table_insert_select_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ChatMessageTable { message_id: message_id_2, @@ -41,11 +42,12 @@ async fn chat_message_table_insert_select_test() { author_id: "ai".to_string(), reply_message_id: Some(message_id_1), metadata: Some(r#"{"source": "test"}"#.to_string()), + is_sync: false, }, ]; // Test insert_chat_messages - let result = insert_chat_messages(db_conn, &messages); + let result = upsert_chat_messages(db_conn, &messages); assert!( result.is_ok(), "Failed to insert chat messages: {:?}", @@ -105,11 +107,12 @@ async fn chat_message_table_cursor_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }); } // Insert messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Test MessageCursor::Offset let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -173,6 +176,7 @@ async fn chat_message_total_count_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ChatMessageTable { message_id: 1002, @@ -183,11 +187,12 @@ async fn chat_message_total_count_test() { author_id: "ai".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }, ]; // Insert messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Test total_message_count let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -205,9 +210,10 @@ async fn chat_message_total_count_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; - insert_chat_messages(db_conn, &[additional_message]).unwrap(); + upsert_chat_messages(db_conn, &[additional_message]).unwrap(); // Verify count increased let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -242,10 +248,11 @@ async fn chat_message_select_message_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Test select_message let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -294,10 +301,11 @@ async fn chat_message_select_content_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Test select_message_content let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -334,6 +342,7 @@ async fn chat_message_reply_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; let answer = ChatMessageTable { @@ -345,14 +354,15 @@ async fn chat_message_reply_test() { author_id: "ai".to_string(), reply_message_id: Some(question_id), // Link to question metadata: None, + is_sync: false, }; // Insert messages - insert_chat_messages(db_conn, &[question, answer]).unwrap(); + upsert_chat_messages(db_conn, &[question, answer]).unwrap(); // Test select_message_where_match_reply_message_id let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); + let result = select_answer_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); assert!(result.is_some()); let reply = result.unwrap(); @@ -362,7 +372,7 @@ async fn chat_message_reply_test() { // Test with non-existent reply relation let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_reply = select_message_where_match_reply_message_id( + let no_reply = select_answer_where_match_reply_message_id( db_conn, &chat_id, 9999, // Non-existent question ID ) .unwrap(); @@ -372,7 +382,7 @@ async fn chat_message_reply_test() { // Test with wrong chat_id let db_conn = test.user_manager.db_connection(uid).unwrap(); let wrong_chat = - select_message_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); + select_answer_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); assert!(wrong_chat.is_none()); } @@ -399,10 +409,11 @@ async fn chat_message_upsert_test() { author_id: "user_1".to_string(), reply_message_id: None, metadata: None, + is_sync: false, }; // Insert message - insert_chat_messages(db_conn, &[message]).unwrap(); + upsert_chat_messages(db_conn, &[message]).unwrap(); // Check original content let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -420,10 +431,11 @@ async fn chat_message_upsert_test() { author_id: "user_1".to_string(), reply_message_id: Some(1000), // Added reply ID metadata: Some(r#"{"updated": true}"#.to_string()), + is_sync: false, }; // Upsert message - insert_chat_messages(db_conn, &[updated_message]).unwrap(); + upsert_chat_messages(db_conn, &[updated_message]).unwrap(); // Verify update let db_conn = test.user_manager.db_connection(uid).unwrap(); @@ -474,11 +486,12 @@ async fn chat_message_select_with_large_dataset() { } else { None }, + is_sync: false, }); } // Insert all 100 messages - insert_chat_messages(db_conn, &messages).unwrap(); + upsert_chat_messages(db_conn, &messages).unwrap(); // Verify total count let db_conn = test.user_manager.db_connection(uid).unwrap(); diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 0acf3c5bd3..b91021142e 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -95,7 +95,6 @@ pub trait ChatCloudService: Send + Sync + 'static { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result; async fn create_answer( @@ -111,7 +110,7 @@ pub trait ChatCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result; @@ -120,7 +119,7 @@ pub trait ChatCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result; async fn get_chat_messages( diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs index f5f985575a..230e5761d2 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs @@ -1,4 +1,5 @@ use crate::cloud::MessageCursor; +use client_api::entity::chat_dto::ChatMessage; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::upsert::excluded; use flowy_sqlite::{ @@ -21,9 +22,40 @@ pub struct ChatMessageTable { pub author_id: String, pub reply_message_id: Option, pub metadata: Option, + pub is_sync: bool, +} +impl ChatMessageTable { + pub fn from_message(chat_id: String, message: ChatMessage, is_sync: bool) -> Self { + ChatMessageTable { + message_id: message.message_id, + chat_id, + content: message.content, + created_at: message.created_at.timestamp(), + author_type: message.author.author_type as i64, + author_id: message.author.author_id.to_string(), + reply_message_id: message.reply_message_id, + metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), + is_sync, + } + } } -pub fn insert_chat_messages( +pub fn update_chat_message_is_sync( + mut conn: DBConnection, + chat_id_val: &str, + message_id_val: i64, + is_sync_val: bool, +) -> FlowyResult<()> { + diesel::update(chat_message_table::table) + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .filter(chat_message_table::message_id.eq(message_id_val)) + .set(chat_message_table::is_sync.eq(is_sync_val)) + .execute(&mut *conn)?; + + Ok(()) +} + +pub fn upsert_chat_messages( mut conn: DBConnection, new_messages: &[ChatMessageTable], ) -> FlowyResult<()> { @@ -143,7 +175,7 @@ pub fn select_message_content( Ok(message) } -pub fn select_message_where_match_reply_message_id( +pub fn select_answer_where_match_reply_message_id( mut conn: DBConnection, chat_id: &str, answer_message_id_val: i64, diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs index 218f6222c3..f5398c48c0 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs @@ -7,7 +7,10 @@ use flowy_sqlite::{ schema::{chat_table, chat_table::dsl}, AsChangeset, DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, }; +use lib_infra::util::timestamp; use serde::{Deserialize, Serialize}; +use serde_json::Value; +use uuid::Uuid; #[derive(Clone, Default, Queryable, Insertable, Identifiable)] #[diesel(table_name = chat_table)] @@ -18,6 +21,23 @@ pub struct ChatTable { pub name: String, pub metadata: String, pub rag_ids: Option, + pub is_sync: bool, +} + +impl ChatTable { + pub fn new(chat_id: String, metadata: Value, rag_ids: Vec, is_sync: bool) -> Self { + let rag_ids = rag_ids.iter().map(|v| v.to_string()).collect::>(); + let metadata = serialize_chat_metadata(&metadata); + let rag_ids = Some(serialize_rag_ids(&rag_ids)); + Self { + chat_id, + created_at: timestamp(), + name: "".to_string(), + metadata, + rag_ids, + is_sync, + } + } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -49,27 +69,7 @@ pub struct ChatTableChangeset { pub name: Option, pub metadata: Option, pub rag_ids: Option, -} - -impl ChatTableChangeset { - pub fn from_metadata(metadata: ChatTableMetadata) -> Self { - ChatTableChangeset { - chat_id: Default::default(), - metadata: serde_json::to_string(&metadata).ok(), - name: None, - rag_ids: None, - } - } - - pub fn from_rag_ids(rag_ids: Vec) -> Self { - ChatTableChangeset { - chat_id: Default::default(), - // Serialize the Vec to a JSON array string - rag_ids: Some(serde_json::to_string(&rag_ids).unwrap_or_default()), - name: None, - metadata: None, - } - } + pub is_sync: Option, } pub fn serialize_rag_ids(rag_ids: &[String]) -> String { @@ -107,6 +107,7 @@ pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< chat_table::name.eq(excluded(chat_table::name)), chat_table::metadata.eq(excluded(chat_table::metadata)), chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), + chat_table::is_sync.eq(excluded(chat_table::is_sync)), )) .execute(&mut *conn) } @@ -120,6 +121,16 @@ pub fn update_chat( Ok(affected_row) } +pub fn update_chat_is_sync( + mut conn: DBConnection, + chat_id_val: &str, + is_sync_val: bool, +) -> QueryResult { + diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) + .set(chat_table::is_sync.eq(is_sync_val)) + .execute(&mut *conn) +} + pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { let row = dsl::chat_table .filter(chat_table::chat_id.eq(chat_id_val)) diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index c2f5b223c0..7602ff8040 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -4,10 +4,8 @@ use crate::entities::{ FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; use crate::local_ai::controller::{LocalAIController, LocalAISetting}; -use crate::middleware::chat_service_mw::AICloudServiceMiddleware; -use flowy_ai_pub::persistence::{ - read_chat_metadata, serialize_chat_metadata, serialize_rag_ids, upsert_chat, ChatTable, -}; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; +use flowy_ai_pub::persistence::read_chat_metadata; use std::collections::HashMap; use dashmap::DashMap; @@ -72,7 +70,7 @@ struct ServerModelsCache { pub const GLOBAL_ACTIVE_MODEL_KEY: &str = "global_active_model"; pub struct AIManager { - pub cloud_service_wm: Arc, + pub cloud_service_wm: Arc, pub user_service: Arc, pub external_service: Arc, chats: Arc>>, @@ -97,7 +95,7 @@ impl AIManager { }); let external_service = Arc::new(query_service); - let cloud_service_wm = Arc::new(AICloudServiceMiddleware::new( + let cloud_service_wm = Arc::new(ChatServiceMiddleware::new( user_service.clone(), chat_cloud_service, local_ai.clone(), @@ -226,13 +224,6 @@ impl AIManager { .unwrap_or_default(); info!("[Chat] create chat with rag_ids: {:?}", rag_ids); - save_chat( - self.user_service.sqlite_connection(*uid)?, - chat_id, - "", - rag_ids.iter().map(|v| v.to_string()).collect(), - json!({}), - )?; self .cloud_service_wm .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) @@ -730,28 +721,9 @@ async fn sync_chat_documents( Ok(()) } -fn save_chat( - conn: DBConnection, - chat_id: &Uuid, - name: &str, - rag_ids: Vec, - metadata: serde_json::Value, -) -> FlowyResult<()> { - let row = ChatTable { - chat_id: chat_id.to_string(), - created_at: timestamp(), - name: name.to_string(), - metadata: serialize_chat_metadata(&metadata), - rag_ids: Some(serialize_rag_ids(&rag_ids)), - }; - - upsert_chat(conn, &row)?; - Ok(()) -} - async fn refresh_chat_setting( user_service: &Arc, - cloud_service: &Arc, + cloud_service: &Arc, store_preferences: &Arc, chat_id: &Uuid, ) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 976acee3e4..ba5ff431e9 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -3,7 +3,7 @@ use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::middleware::chat_service_mw::AICloudServiceMiddleware; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; @@ -11,7 +11,7 @@ use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; use flowy_ai_pub::persistence::{ - insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages, ChatMessageTable, }; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -35,7 +35,7 @@ pub struct Chat { chat_id: Uuid, uid: i64, user_service: Arc, - chat_service: Arc, + chat_service: Arc, prev_message_state: Arc>, latest_message_id: Arc, stop_stream: Arc, @@ -47,7 +47,7 @@ impl Chat { uid: i64, chat_id: Uuid, user_service: Arc, - chat_service: Arc, + chat_service: Arc, ) -> Chat { Chat { uid, @@ -105,7 +105,6 @@ impl Chat { &self.chat_id, ¶ms.message, params.message_type.clone(), - &[], ) .await .map_err(|err| { @@ -126,7 +125,7 @@ impl Chat { } // Save message to disk - save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; + notify_message(&self.chat_id, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); self.stream_response( params.answer_stream_port, @@ -185,7 +184,7 @@ impl Chat { &self, answer_stream_port: i64, answer_stream_buffer: Arc>, - uid: i64, + _uid: i64, workspace_id: Uuid, question_id: i64, format: ResponseFormat, @@ -194,7 +193,6 @@ impl Chat { let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id; let cloud_service = self.chat_service.clone(); - let user_service = self.user_service.clone(); tokio::spawn(async move { let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service @@ -309,7 +307,7 @@ impl Chat { metadata, ) .await?; - save_and_notify_message(uid, &chat_id, &user_service, answer)?; + notify_message(&chat_id, answer)?; Ok::<(), FlowyError>(()) }); } @@ -442,6 +440,7 @@ impl Chat { user_service.sqlite_connection(uid)?, &chat_id, resp.messages.clone(), + true, ) { error!("Failed to save chat:{} messages: {}", chat_id, err); } @@ -492,7 +491,7 @@ impl Chat { let conn = self.user_service.sqlite_connection(self.uid)?; let local_result = - select_message_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? + select_answer_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? .map(|message| message.message_id); if let Some(message_id) = local_result { @@ -543,7 +542,7 @@ impl Chat { .get_answer(&workspace_id, &self.chat_id, question_message_id) .await?; - save_and_notify_message(self.uid, &self.chat_id, &self.user_service, answer.clone())?; + notify_message(&self.chat_id, answer.clone())?; let pb = ChatMessagePB::from(answer); Ok(pb) } @@ -614,6 +613,7 @@ fn save_chat_message_disk( conn: DBConnection, chat_id: &Uuid, messages: Vec, + is_sync: bool, ) -> FlowyResult<()> { let records = messages .into_iter() @@ -626,9 +626,10 @@ fn save_chat_message_disk( author_id: message.author.author_id.to_string(), reply_message_id: message.reply_message_id, metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), + is_sync, }) .collect::>(); - insert_chat_messages(conn, &records)?; + upsert_chat_messages(conn, &records)?; Ok(()) } @@ -665,18 +666,8 @@ impl StringBuffer { } } -pub(crate) fn save_and_notify_message( - uid: i64, - chat_id: &Uuid, - user_service: &Arc, - message: ChatMessage, -) -> Result<(), FlowyError> { +pub(crate) fn notify_message(chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { trace!("[Chat] save answer: answer={:?}", message); - save_chat_message_disk( - user_service.sqlite_connection(uid)?, - chat_id, - vec![message.clone()], - )?; let pb = ChatMessagePB::from(message); chat_notification_builder(chat_id, ChatNotification::DidReceiveChatMessage) .payload(pb) diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 400afd1507..5b582b2577 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -12,6 +12,7 @@ pub mod local_ai; mod middleware; pub mod notification; +pub mod offline; mod protobuf; mod stream_message; mod util; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index e3c3d671ec..ff5c608f22 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -28,14 +28,14 @@ use std::sync::{Arc, Weak}; use tracing::{info, trace}; use uuid::Uuid; -pub struct AICloudServiceMiddleware { +pub struct ChatServiceMiddleware { cloud_service: Arc, user_service: Arc, local_ai: Arc, storage_service: Weak, } -impl AICloudServiceMiddleware { +impl ChatServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, @@ -106,7 +106,7 @@ impl AICloudServiceMiddleware { } #[async_trait] -impl ChatCloudService for AICloudServiceMiddleware { +impl ChatCloudService for ChatServiceMiddleware { async fn create_chat( &self, uid: &i64, @@ -128,11 +128,10 @@ impl ChatCloudService for AICloudServiceMiddleware { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { self .cloud_service - .create_question(workspace_id, chat_id, message, message_type, metadata) + .create_question(workspace_id, chat_id, message, message_type) .await } @@ -154,7 +153,7 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result { @@ -166,7 +165,7 @@ impl ChatCloudService for AICloudServiceMiddleware { info!("stream_answer use model: {:?}", ai_model); if use_local_ai { if self.local_ai.is_running() { - let content = self.get_message_content(message_id)?; + let content = self.get_message_content(question_id)?; match self .local_ai .stream_question( @@ -191,7 +190,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .stream_answer(workspace_id, chat_id, message_id, format, ai_model) + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) .await } } @@ -200,10 +199,10 @@ impl ChatCloudService for AICloudServiceMiddleware { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { if self.local_ai.is_running() { - let content = self.get_message_content(question_message_id)?; + let content = self.get_message_content(question_id)?; match self .local_ai .ask_question(&chat_id.to_string(), &content) @@ -212,7 +211,7 @@ impl ChatCloudService for AICloudServiceMiddleware { Ok(answer) => { let message = self .cloud_service - .create_answer(workspace_id, chat_id, &answer, question_message_id, None) + .create_answer(workspace_id, chat_id, &answer, question_id, None) .await?; Ok(message) }, @@ -224,7 +223,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .get_answer(workspace_id, chat_id, question_message_id) + .get_answer(workspace_id, chat_id, question_id) .await } } diff --git a/frontend/rust-lib/flowy-ai/src/offline/mod.rs b/frontend/rust-lib/flowy-ai/src/offline/mod.rs new file mode 100644 index 0000000000..e55b43fdb2 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/offline/mod.rs @@ -0,0 +1 @@ +pub mod offline_message_sync; diff --git a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs new file mode 100644 index 0000000000..8d7e8d2e42 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs @@ -0,0 +1,258 @@ +use crate::ai_manager::AIUserService; +use flowy_ai_pub::cloud::{ + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, + MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, + StreamAnswer, StreamComplete, UpdateChatParams, +}; +use flowy_ai_pub::persistence::{ + update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages, + ChatMessageTable, ChatTable, +}; +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; +use serde_json::Value; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use uuid::Uuid; + +pub struct AutoSyncChatService { + cloud_service: Arc, + user_service: Arc, +} + +impl AutoSyncChatService { + pub fn new( + cloud_service: Arc, + user_service: Arc, + ) -> Self { + Self { + cloud_service, + user_service, + } + } + + async fn upsert_message( + &self, + chat_id: &Uuid, + message: ChatMessage, + is_sync: bool, + ) -> Result<(), FlowyError> { + let uid = self.user_service.user_id()?; + let conn = self.user_service.sqlite_connection(uid)?; + let row = ChatMessageTable::from_message(chat_id.to_string(), message, is_sync); + upsert_chat_messages(conn, &[row])?; + Ok(()) + } + + #[allow(dead_code)] + async fn update_message_is_sync( + &self, + chat_id: &Uuid, + message_id: i64, + ) -> Result<(), FlowyError> { + let uid = self.user_service.user_id()?; + let conn = self.user_service.sqlite_connection(uid)?; + update_chat_message_is_sync(conn, &chat_id.to_string(), message_id, true)?; + Ok(()) + } +} + +#[async_trait] +impl ChatCloudService for AutoSyncChatService { + async fn create_chat( + &self, + uid: &i64, + workspace_id: &Uuid, + chat_id: &Uuid, + rag_ids: Vec, + name: &str, + metadata: Value, + ) -> Result<(), FlowyError> { + let conn = self.user_service.sqlite_connection(*uid)?; + let chat = ChatTable::new( + chat_id.to_string(), + metadata.clone(), + rag_ids.clone(), + false, + ); + upsert_chat(conn, &chat)?; + + if self + .cloud_service + .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) + .await + .is_ok() + { + let conn = self.user_service.sqlite_connection(*uid)?; + update_chat_is_sync(conn, &chat_id.to_string(), true)?; + } + Ok(()) + } + + async fn create_question( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + message_type: ChatMessageType, + ) -> Result { + let message = self + .cloud_service + .create_question(workspace_id, chat_id, message, message_type) + .await?; + self.upsert_message(chat_id, message.clone(), true).await?; + // TODO: implement background sync + // self + // .update_message_is_sync(chat_id, message.message_id) + // .await?; + Ok(message) + } + + async fn create_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message: &str, + question_id: i64, + metadata: Option, + ) -> Result { + let message = self + .cloud_service + .create_answer(workspace_id, chat_id, message, question_id, metadata) + .await?; + + // TODO: implement background sync + self.upsert_message(chat_id, message.clone(), true).await?; + Ok(message) + } + + async fn stream_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + question_id: i64, + format: ResponseFormat, + ai_model: Option, + ) -> Result { + self + .cloud_service + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) + .await + } + + async fn get_answer( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + question_id: i64, + ) -> Result { + let message = self + .cloud_service + .get_answer(workspace_id, chat_id, question_id) + .await?; + + // TODO: implement background sync + self.upsert_message(chat_id, message.clone(), true).await?; + Ok(message) + } + + async fn get_chat_messages( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + offset: MessageCursor, + limit: u64, + ) -> Result { + self + .cloud_service + .get_chat_messages(workspace_id, chat_id, offset, limit) + .await + } + + async fn get_question_from_answer_id( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + answer_message_id: i64, + ) -> Result { + self + .cloud_service + .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) + .await + } + + async fn get_related_message( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + message_id: i64, + ai_model: Option, + ) -> Result { + self + .cloud_service + .get_related_message(workspace_id, chat_id, message_id, ai_model) + .await + } + + async fn stream_complete( + &self, + workspace_id: &Uuid, + params: CompleteTextParams, + ai_model: Option, + ) -> Result { + self + .cloud_service + .stream_complete(workspace_id, params, ai_model) + .await + } + + async fn embed_file( + &self, + workspace_id: &Uuid, + file_path: &Path, + chat_id: &Uuid, + metadata: Option>, + ) -> Result<(), FlowyError> { + self + .cloud_service + .embed_file(workspace_id, file_path, chat_id, metadata) + .await + } + + async fn get_chat_settings( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + ) -> Result { + // TODO: implement background sync + self + .cloud_service + .get_chat_settings(workspace_id, chat_id) + .await + } + + async fn update_chat_settings( + &self, + workspace_id: &Uuid, + chat_id: &Uuid, + params: UpdateChatParams, + ) -> Result<(), FlowyError> { + // TODO: implement background sync + self + .cloud_service + .update_chat_settings(workspace_id, chat_id, params) + .await + } + + async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + self.cloud_service.get_available_models(workspace_id).await + } + + async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { + self + .cloud_service + .get_workspace_default_model(workspace_id) + .await + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index e4bffbfb69..3a7d648133 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -14,9 +14,9 @@ use flowy_ai_pub::cloud::search_dto::{ SearchDocumentResponseItem, SearchResult, SearchSummaryResult, }; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, - StreamComplete, UpdateChatParams, + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, + MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, + UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -683,13 +683,12 @@ impl ChatCloudService for ServerProvider { chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { let message = message.to_string(); self .get_server()? .chat_service() - .create_question(workspace_id, chat_id, &message, message_type, metadata) + .create_question(workspace_id, chat_id, &message, message_type) .await } @@ -712,14 +711,14 @@ impl ChatCloudService for ServerProvider { &self, workspace_id: &Uuid, chat_id: &Uuid, - message_id: i64, + question_id: i64, format: ResponseFormat, ai_model: Option, ) -> Result { let server = self.get_server()?; server .chat_service() - .stream_answer(workspace_id, chat_id, message_id, format, ai_model) + .stream_answer(workspace_id, chat_id, question_id, format, ai_model) .await } @@ -768,12 +767,12 @@ impl ChatCloudService for ServerProvider { &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { let server = self.get_server(); server? .chat_service() - .get_answer(workspace_id, chat_id, question_message_id) + .get_answer(workspace_id, chat_id, question_id) .await } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 1cf748d089..dbbce02136 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::LoginUserService; use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -336,7 +336,7 @@ impl ServerUserImpl { } #[async_trait] -impl ServerUser for ServerUserImpl { +impl LoginUserService for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 3f5d16b165..a3d6de7003 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -2,24 +2,19 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; use arc_swap::ArcSwapOption; use dashmap::DashMap; -use flowy_ai::ai_manager::AIUserService; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::{AIUserServiceImpl, LoginUserService}; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; -use flowy_sqlite::DBConnection; use flowy_user_pub::entities::*; -use lib_infra::async_trait::async_trait; use serde_repr::*; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -use uuid::Uuid; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] @@ -61,7 +56,7 @@ pub struct ServerProvider { /// The authenticator type of the user. authenticator: AtomicU8, - user: Arc, + user: Arc, pub(crate) uid: Arc>, pub local_ai: Arc, } @@ -71,7 +66,7 @@ impl ServerProvider { config: AppFlowyCoreConfig, server: Server, store_preferences: Weak, - server_user: impl ServerUser + 'static, + server_user: impl LoginUserService + 'static, ) -> Self { let user = Arc::new(server_user); let encryption = EncryptionImpl::new(None); @@ -182,28 +177,3 @@ pub fn current_server_type() -> Server { AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, } } - -struct AIUserServiceImpl(Arc); - -#[async_trait] -impl AIUserService for AIUserServiceImpl { - fn user_id(&self) -> Result { - self.0.user_id() - } - - async fn is_local_model(&self) -> FlowyResult { - self.0.is_local_mode().await - } - - fn workspace_id(&self) -> Result { - self.0.workspace_id() - } - - fn sqlite_connection(&self, uid: i64) -> Result { - self.0.get_sqlite_db(uid) - } - - fn application_root_dir(&self) -> Result { - self.0.application_root_dir() - } -} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index e6d8a7fac2..0cebe3371f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,7 +1,9 @@ +use flowy_ai::ai_manager::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; use std::path::PathBuf; +use std::sync::Arc; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -11,7 +13,7 @@ pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. #[async_trait] -pub trait ServerUser: Send + Sync { +pub trait LoginUserService: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; @@ -21,3 +23,28 @@ pub trait ServerUser: Send + Sync { fn get_sqlite_db(&self, uid: i64) -> Result; fn application_root_dir(&self) -> Result; } + +pub struct AIUserServiceImpl(pub Arc); + +#[async_trait] +impl AIUserService for AIUserServiceImpl { + fn user_id(&self) -> Result { + self.0.user_id() + } + + async fn is_local_model(&self) -> FlowyResult { + self.0.is_local_mode().await + } + + fn workspace_id(&self) -> Result { + self.0.workspace_id() + } + + fn sqlite_connection(&self, uid: i64) -> Result { + self.0.get_sqlite_db(uid) + } + + fn application_root_dir(&self) -> Result { + self.0.application_root_dir() + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 3f8803c7fc..14a26078f5 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -8,8 +8,8 @@ use client_api::entity::chat_dto::{ RepeatedChatMessage, }; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, - ModelList, StreamAnswer, StreamComplete, UpdateChatParams, + AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, ModelList, StreamAnswer, + StreamComplete, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; @@ -20,12 +20,12 @@ use std::path::Path; use tracing::trace; use uuid::Uuid; -pub(crate) struct AFCloudChatCloudServiceImpl { +pub(crate) struct CloudChatServiceImpl { pub inner: T, } #[async_trait] -impl ChatCloudService for AFCloudChatCloudServiceImpl +impl ChatCloudService for CloudChatServiceImpl where T: AFServer, { @@ -59,7 +59,6 @@ where chat_id: &Uuid, message: &str, message_type: ChatMessageType, - metadata: &[ChatMessageMetadata], ) -> Result { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); @@ -132,15 +131,11 @@ where &self, workspace_id: &Uuid, chat_id: &Uuid, - question_message_id: i64, + question_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_answer( - workspace_id, - chat_id.to_string().as_str(), - question_message_id, - ) + .get_answer(workspace_id, chat_id.to_string().as_str(), question_id) .await .map_err(FlowyError::from)?; Ok(resp) 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 c493dda344..d6a22a2f73 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 @@ -1,5 +1,5 @@ #![allow(unused_variables)] -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ @@ -23,7 +23,7 @@ use uuid::Uuid; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -40,7 +40,7 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -95,7 +95,7 @@ where workspace_id: &Uuid, ) -> Result { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let client = try_get_client?; let params = object_ids .into_iter() 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 4909d96fef..ae67fc6d26 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 @@ -13,13 +13,13 @@ use std::sync::Arc; use tracing::instrument; use uuid::Uuid; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -49,7 +49,7 @@ where check_request_workspace_id_is_match( workspace_id, - &self.user, + &self.logged_user, format!("get document doc state:{}", document_id), )?; @@ -85,7 +85,7 @@ where .to_vec(); check_request_workspace_id_is_match( workspace_id, - &self.user, + &self.logged_user, format!("Get {} document", document_id), )?; let collab = Collab::new_with_source( 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 5b8efa4b32..116651a734 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 @@ -22,13 +22,13 @@ use flowy_folder_pub::cloud::{ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl { pub inner: T, - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -91,7 +91,7 @@ where ) -> Result, FlowyError> { let uid = *uid; let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*workspace_id, CollabType::Folder), @@ -131,7 +131,7 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), 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 6d7d9d743b..0ac4555d6e 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 @@ -31,7 +31,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use uuid::Uuid; -use crate::af_cloud::define::{ServerUser, USER_SIGN_IN_URL}; +use crate::af_cloud::define::{LoginUserService, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; @@ -44,14 +44,14 @@ use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_st pub(crate) struct AFCloudUserAuthServiceImpl { server: T, user_change_recv: ArcSwapOption>, - user: Arc, + user: Arc, } impl AFCloudUserAuthServiceImpl { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver, - user: Arc, + user: Arc, ) -> Self { Self { server, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index 0d91de8412..bedcc90ca0 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use flowy_error::{FlowyError, FlowyResult}; use std::sync::Arc; use tracing::warn; @@ -9,7 +9,7 @@ use uuid::Uuid; /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( expected_workspace_id: &Uuid, - user: &Arc, + user: &Arc, action: impl AsRef, ) -> FlowyResult<()> { let actual_workspace_id = user.workspace_id()?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 06e56a8c05..bb2d11cdbd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::{AIUserServiceImpl, LoginUserService}; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -24,6 +24,11 @@ use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; use flowy_user_pub::entities::UserTokenState; +use crate::af_cloud::impls::{ + AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, + AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl, +}; +use flowy_ai::offline::offline_message_sync::AutoSyncChatService; use rand::Rng; use semver::Version; use tokio::select; @@ -34,11 +39,6 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use uuid::Uuid; -use crate::af_cloud::impls::{ - AFCloudChatCloudServiceImpl, AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, - AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, -}; - use crate::AppFlowyServer; use super::impls::AFCloudSearchCloudServiceImpl; @@ -53,7 +53,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc, pub device_id: String, ws_client: Arc, - user: Arc, + logged_user: Arc, } impl AppFlowyCloudServer { @@ -62,7 +62,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - user: Arc, + auth_user_service: Arc, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -100,7 +100,7 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - user, + logged_user: auth_user_service, } } @@ -187,7 +187,7 @@ impl AppFlowyServer for AppFlowyCloudServer { Arc::new(AFCloudUserAuthServiceImpl::new( server, rx, - self.user.clone(), + self.logged_user.clone(), )) } @@ -197,7 +197,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudFolderCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -207,7 +207,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudDatabaseCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -217,7 +217,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Some(Arc::new(AFCloudDatabaseCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), })) } @@ -227,7 +227,7 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Arc::new(AFCloudDocumentCloudServiceImpl { inner: server, - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } @@ -235,7 +235,11 @@ impl AppFlowyServer for AppFlowyCloudServer { let server = AFServerImpl { client: self.get_client(), }; - Arc::new(AFCloudChatCloudServiceImpl { inner: server }) + + Arc::new(AutoSyncChatService::new( + Arc::new(CloudChatServiceImpl { inner: server }), + Arc::new(AIUserServiceImpl(self.logged_user.clone())), + )) } fn subscribe_ws_state(&self) -> Option { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 9057288f88..8e731edb84 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::CompletionStream; @@ -6,14 +6,15 @@ use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai::local_ai::stream_util::QuestionStream; use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, - RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, + ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, + ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, }; use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, select_chat_messages, - select_message_content, select_message_where_match_reply_message_id, serialize_chat_metadata, - serialize_rag_ids, update_chat, upsert_chat, ChatMessageTable, ChatTable, ChatTableChangeset, + deserialize_chat_metadata, deserialize_rag_ids, read_chat, + select_answer_where_match_reply_message_id, select_chat_messages, select_message_content, + serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, upsert_chat_messages, + ChatMessageTable, ChatTable, ChatTableChangeset, }; use flowy_error::{FlowyError, FlowyResult}; use futures_util::{stream, StreamExt, TryStreamExt}; @@ -26,12 +27,12 @@ use std::sync::Arc; use tracing::trace; use uuid::Uuid; -pub struct LocalServerChatServiceImpl { - pub user: Arc, +pub struct LocalChatServiceImpl { + pub user: Arc, pub local_ai: Arc, } -impl LocalServerChatServiceImpl { +impl LocalChatServiceImpl { fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; @@ -40,35 +41,30 @@ impl LocalServerChatServiceImpl { })?; Ok(content) } + + async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { + let uid = self.user.user_id()?; + let conn = self.user.get_sqlite_db(uid)?; + let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); + upsert_chat_messages(conn, &[row])?; + Ok(()) + } } #[async_trait] -impl ChatCloudService for LocalServerChatServiceImpl { +impl ChatCloudService for LocalChatServiceImpl { async fn create_chat( &self, _uid: &i64, _workspace_id: &Uuid, chat_id: &Uuid, rag_ids: Vec, - name: &str, + _name: &str, metadata: Value, ) -> Result<(), FlowyError> { let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; - - let rag_ids = rag_ids - .iter() - .map(|v| v.to_string()) - .collect::>(); - - let row = ChatTable { - chat_id: chat_id.to_string(), - created_at: timestamp(), - name: name.to_string(), - metadata: serialize_chat_metadata(&metadata), - rag_ids: Some(serialize_rag_ids(&rag_ids)), - }; - + let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); upsert_chat(db, &row)?; Ok(()) } @@ -76,25 +72,23 @@ impl ChatCloudService for LocalServerChatServiceImpl { async fn create_question( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message: &str, message_type: ChatMessageType, - _metadata: &[ChatMessageMetadata], ) -> Result { - match message_type { - ChatMessageType::System => Ok(ChatMessage::new_system(timestamp(), message.to_string())), - ChatMessageType::User => Ok(ChatMessage::new_human( - timestamp(), - message.to_string(), - None, - )), - } + let message = match message_type { + ChatMessageType::System => ChatMessage::new_system(timestamp(), message.to_string()), + ChatMessageType::User => ChatMessage::new_human(timestamp(), message.to_string(), None), + }; + + self.upsert_message(chat_id, message.clone()).await?; + Ok(message) } async fn create_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, + chat_id: &Uuid, message: &str, question_id: i64, metadata: Option, @@ -103,6 +97,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { if let Some(metadata) = metadata { message.metadata = metadata; } + self.upsert_message(chat_id, message.clone()).await?; Ok(message) } @@ -131,22 +126,26 @@ impl ChatCloudService for LocalServerChatServiceImpl { stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), ), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } async fn get_answer( &self, _workspace_id: &Uuid, - _chat_id: &Uuid, - _question_message_id: i64, + chat_id: &Uuid, + question_id: i64, ) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + let uid = self.user.user_id()?; + let db = self.user.get_sqlite_db(uid)?; + + match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { + None => Err(FlowyError::record_not_found()), + Some(message) => Ok(chat_message_from_row(message)), + } } async fn get_chat_messages( @@ -183,7 +182,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { let chat_id = chat_id.to_string(); let uid = self.user.user_id()?; let db = self.user.get_sqlite_db(uid)?; - let row = select_message_where_match_reply_message_id(db, &chat_id, answer_message_id)? + let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? .map(chat_message_from_row) .ok_or_else(FlowyError::record_not_found)?; Ok(row) @@ -247,12 +246,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } @@ -285,7 +282,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { let db = self.user.get_sqlite_db(uid)?; let row = read_chat(db, &chat_id)?; let rag_ids = deserialize_rag_ids(&row.rag_ids); - let metadata = deserialize_chat_metadata::(&row.metadata); + let metadata = deserialize_chat_metadata::(&row.metadata); let setting = ChatSettings { name: row.name, rag_ids, @@ -308,6 +305,7 @@ impl ChatCloudService for LocalServerChatServiceImpl { name: s.name, metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), + is_sync: None, }; update_chat(&mut db, changeset)?; 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 0ef930320e..719dd59c95 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,11 +1,10 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use crate::af_cloud::define::ServerUser; +use crate::af_cloud::define::LoginUserService; use crate::local_server::impls::{ - LocalServerChatServiceImpl, LocalServerDatabaseCloudServiceImpl, - LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, - LocalServerUserServiceImpl, + LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, + LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, }; use crate::AppFlowyServer; use flowy_ai::local_ai::controller::LocalAIController; @@ -18,13 +17,13 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, local_ai, @@ -62,7 +61,7 @@ impl AppFlowyServer for LocalServer { } fn chat_service(&self) -> Arc { - Arc::new(LocalServerChatServiceImpl { + Arc::new(LocalChatServiceImpl { user: self.user.clone(), local_ai: self.local_ai.clone(), }) 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 f196e44ea9..a8dcdb507f 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,7 @@ use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; use crate::setup_log; -use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::define::LoginUserService; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; @@ -42,7 +42,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc struct FakeServerUserImpl; #[async_trait] -impl ServerUser for FakeServerUserImpl { +impl LoginUserService for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql new file mode 100644 index 0000000000..65dec0f30a --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE chat_table DROP COLUMN is_sync; +ALTER TABLE chat_message_table DROP COLUMN is_sync; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql new file mode 100644 index 0000000000..ff8dce94bc --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE chat_table + ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE chat_message_table + ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 9504d126ed..f3caa86e66 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -27,6 +27,7 @@ diesel::table! { author_id -> Text, reply_message_id -> Nullable, metadata -> Nullable, + is_sync -> Bool, } } @@ -37,6 +38,7 @@ diesel::table! { name -> Text, metadata -> Text, rag_ids -> Nullable, + is_sync -> Bool, } } @@ -126,15 +128,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 165e95c480663f989b32a1be7d59dbc878f58525 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 14:36:47 +0800 Subject: [PATCH 15/74] chore: fix test --- .../event-integration-test/tests/chat/chat_message_test.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index 5b29258142..a9c928db5b 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -91,10 +91,8 @@ async fn af_cloud_load_remote_system_message_test() { .notification_sender .subscribe::(&chat_id, ChatNotification::DidLoadLatestChatMessage); - // Previous messages were created by the server, so there are no messages in the local cache. - // It will try to load messages in the background. let all = test.load_next_message(&chat_id, 5, None).await; - assert!(all.messages.is_empty()); + assert_eq!(all.messages.len(), 5); // Wait for the messages to be loaded. let next_back_five = receive_with_timeout(rx, Duration::from_secs(60)) @@ -119,7 +117,6 @@ async fn af_cloud_load_remote_system_message_test() { let first_five_messages = receive_with_timeout(rx, Duration::from_secs(60)) .await .unwrap(); - assert!(!first_five_messages.has_more); assert_eq!(first_five_messages.messages[0].content, "hello server 4"); assert_eq!(first_five_messages.messages[1].content, "hello server 3"); assert_eq!(first_five_messages.messages[2].content, "hello server 2"); From 59efb7d9e54d2c044721e2be6b884a86914cbce7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 14:43:01 +0800 Subject: [PATCH 16/74] chore: fix test --- frontend/rust-lib/Cargo.lock | 24 ++++++++++++------------ frontend/rust-lib/Cargo.toml | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ec114f205a..fd0307fca7 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bincode", @@ -513,7 +513,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bytes", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "again", "anyhow", @@ -1214,7 +1214,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "futures-channel", "futures-util", @@ -1499,7 +1499,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bincode", @@ -1521,7 +1521,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "async-trait", @@ -1969,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "bincode", "bytes", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3442,7 +3442,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "app-error", "jsonwebtoken", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "bytes", @@ -6644,7 +6644,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=72a71205ebb3ec227b44ed48473abe4f1c7663e8#72a71205ebb3ec227b44ed48473abe4f1c7663e8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index e4f0eab545..ee99fcacf1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -107,8 +107,8 @@ af-local-ai = { version = "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 = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "72a71205ebb3ec227b44ed48473abe4f1c7663e8" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } [profile.dev] opt-level = 0 From 3bb5075a9830354965a2551feece6039f2f56c3f Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 18 Apr 2025 14:46:46 +0800 Subject: [PATCH 17/74] feat: setup/change password in settings page (#7752) * feat: change password in settings page * feat: add change password api * feat: add password service * feat: add setup password * feat: refacotor account page * chor: update i18n * chore: i18n * chore: i18n * feat: add error message under text field * fix: flutter tests * chore: remove long password test * fix: cloud integration test * fix: cargo clippy * fix: replace border color --- .../integration_test/shared/settings.dart | 2 +- .../personal_info_setting_group.dart | 2 +- .../application/password/password_bloc.dart | 241 +++++++++++++ .../password/password_http_service.dart | 183 ++++++++++ .../lib/user/application/sign_in_bloc.dart | 2 + .../lib/user/application/user_service.dart | 11 + .../continue_with_email_and_password.dart | 89 ++--- ...inue_with_magic_link_or_passcode_page.dart | 4 +- .../continue_with_password_page.dart | 43 ++- .../widgets/flowy_logo_title.dart | 2 +- .../application/user/settings_user_bloc.dart | 37 +- .../menu/sidebar/footer/sidebar_toast.dart | 6 +- .../menu/sidebar/shared/sidebar_setting.dart | 67 +++- .../settings/pages/about/app_version.dart | 30 +- .../pages/account/account_deletion.dart | 43 +-- .../pages/account/account_sign_in_out.dart | 105 +++++- .../pages/account/account_user_profile.dart | 21 +- .../pages/account/email/email_section.dart | 38 ++ .../account/password/change_password.dart | 330 ++++++++++++++++++ .../password/password_suffix_icon.dart | 30 ++ .../account/password/setup_password.dart | 254 ++++++++++++++ .../settings/pages/settings_account_view.dart | 17 +- .../settings/shared/settings_category.dart | 10 +- .../shared/settings_category_spacer.dart | 10 +- .../settings/shared/settings_header.dart | 12 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 +-- .../component/button/base_button/base.dart | 2 +- .../button/base_button/base_text_button.dart | 4 + .../filled_button/filled_text_button.dart | 10 +- .../outlined_button/outlined_text_button.dart | 10 +- .../src/component/textfield/textfield.dart | 31 ++ .../text_style/base/default_text_style.dart | 295 ++++++++++++++-- .../lib/src/theme/text_style/text_style.dart | 10 +- .../flowy_icons/20x/hide_password.svg | 5 + .../flowy_icons/20x/password_close.svg | 5 + .../flowy_icons/20x/show_password.svg | 4 + frontend/resources/translations/en.json | 36 +- frontend/rust-lib/Cargo.lock | 18 +- .../tests/user/local_test/auth_test.rs | 23 -- .../src/local_server/impls/chat.rs | 16 +- .../rust-lib/flowy-user/src/entities/auth.rs | 7 +- .../flowy-user/src/entities/user_profile.rs | 7 +- 42 files changed, 1854 insertions(+), 264 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart create mode 100644 frontend/resources/flowy_icons/20x/hide_password.svg create mode 100644 frontend/resources/flowy_icons/20x/password_close.svg create mode 100644 frontend/resources/flowy_icons/20x/show_password.svg diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index aade7bb4c9..bfc5efedde 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester { // Enable editing username final editUsernameFinder = find.descendant( of: find.byType(AccountUserProfile), - matching: find.byFlowySvg(FlowySvgs.edit_s), + matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m), ); await tap(editUsernameFinder, warnIfMissed: false); await pumpAndSettle(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index 37191a2ae2..28ebdb750e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -58,7 +58,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { userName: userName, onSubmitted: (value) => context .read() - .add(SettingsUserEvent.updateUserName(value)), + .add(SettingsUserEvent.updateUserName(name: value)), ); }, ); diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart new file mode 100644 index 0000000000..34e8514e4c --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -0,0 +1,241 @@ +import 'dart:convert'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/user/application/password/password_http_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'password_bloc.freezed.dart'; + +class PasswordBloc extends Bloc { + PasswordBloc(this.userProfile) : super(PasswordState.initial()) { + on( + (event, emit) async { + await event.when( + init: () async => _init(), + changePassword: (oldPassword, newPassword) async => _onChangePassword( + emit, + oldPassword: oldPassword, + newPassword: newPassword, + ), + setupPassword: (newPassword) async => _onSetupPassword( + emit, + newPassword: newPassword, + ), + forgotPassword: (email) async => _onForgotPassword( + emit, + email: email, + ), + checkHasPassword: () async => _onCheckHasPassword( + emit, + ), + cancel: () {}, + ); + }, + ); + } + + final UserProfilePB userProfile; + late final PasswordHttpService passwordHttpService; + + bool _isInitialized = false; + + Future _init() async { + if (userProfile.authenticator == AuthenticatorPB.Local) { + Log.debug('PasswordBloc: skip init because user is local authenticator'); + return; + } + + final baseUrl = await getAppFlowyCloudUrl(); + try { + final authToken = jsonDecode(userProfile.token)['access_token']; + passwordHttpService = PasswordHttpService( + baseUrl: baseUrl, + authToken: authToken, + ); + _isInitialized = true; + } catch (e) { + Log.error('PasswordBloc: _init: error: $e'); + } + } + + Future _onChangePassword( + Emitter emit, { + required String oldPassword, + required String newPassword, + }) async { + if (!_isInitialized) { + Log.info('changePassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('changePassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.changePassword( + currentPassword: oldPassword, + newPassword: newPassword, + ); + + emit( + state.copyWith( + isSubmitting: false, + changePasswordResult: result, + ), + ); + } + + Future _onSetupPassword( + Emitter emit, { + required String newPassword, + }) async { + if (!_isInitialized) { + Log.info('setupPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('setupPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.setupPassword( + newPassword: newPassword, + ); + + emit( + state.copyWith( + isSubmitting: false, + hasPassword: result.fold( + (success) => true, + (error) => false, + ), + setupPasswordResult: result, + ), + ); + } + + Future _onForgotPassword( + Emitter emit, { + required String email, + }) async { + if (!_isInitialized) { + Log.info('forgotPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('forgotPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.forgotPassword(email: email); + + emit( + state.copyWith( + isSubmitting: false, + forgotPasswordResult: result, + ), + ); + } + + Future _onCheckHasPassword(Emitter emit) async { + if (!_isInitialized) { + Log.info('checkHasPassword: not initialized'); + return; + } + + if (state.isSubmitting) { + Log.info('checkHasPassword: already submitting'); + return; + } + + _clearState(emit, true); + + final result = await passwordHttpService.checkHasPassword(); + + emit( + state.copyWith( + isSubmitting: false, + hasPassword: result.fold( + (success) => success, + (error) => false, + ), + checkHasPasswordResult: result, + ), + ); + } + + void _clearState(Emitter emit, bool isSubmitting) { + emit( + state.copyWith( + isSubmitting: isSubmitting, + changePasswordResult: null, + setupPasswordResult: null, + forgotPasswordResult: null, + checkHasPasswordResult: null, + ), + ); + } +} + +@freezed +class PasswordEvent with _$PasswordEvent { + const factory PasswordEvent.init() = Init; + + // Change password + const factory PasswordEvent.changePassword({ + required String oldPassword, + required String newPassword, + }) = ChangePassword; + + // Setup password + const factory PasswordEvent.setupPassword({ + required String newPassword, + }) = SetupPassword; + + // Forgot password + const factory PasswordEvent.forgotPassword({ + required String email, + }) = ForgotPassword; + + // Check has password + const factory PasswordEvent.checkHasPassword() = CheckHasPassword; + + // Cancel operation + const factory PasswordEvent.cancel() = Cancel; +} + +@freezed +class PasswordState with _$PasswordState { + const factory PasswordState({ + required bool isSubmitting, + required bool hasPassword, + required FlowyResult? changePasswordResult, + required FlowyResult? setupPasswordResult, + required FlowyResult? forgotPasswordResult, + required FlowyResult? checkHasPasswordResult, + }) = _PasswordState; + + factory PasswordState.initial() => const PasswordState( + isSubmitting: false, + hasPassword: false, + changePasswordResult: null, + setupPasswordResult: null, + forgotPasswordResult: null, + checkHasPasswordResult: null, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart new file mode 100644 index 0000000000..723ded57e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:http/http.dart' as http; + +enum PasswordEndpoint { + changePassword, + forgotPassword, + setupPassword, + checkHasPassword; + + String get path { + switch (this) { + case PasswordEndpoint.changePassword: + return '/gotrue/user/change-password'; + case PasswordEndpoint.forgotPassword: + return '/gotrue/user/recover'; + case PasswordEndpoint.setupPassword: + return '/gotrue/user/change-password'; + case PasswordEndpoint.checkHasPassword: + return '/gotrue/user/auth-info'; + } + } + + String get method { + switch (this) { + case PasswordEndpoint.changePassword: + case PasswordEndpoint.setupPassword: + case PasswordEndpoint.forgotPassword: + return 'POST'; + case PasswordEndpoint.checkHasPassword: + return 'GET'; + } + } + + Uri uri(String baseUrl) => Uri.parse('$baseUrl$path'); +} + +class PasswordHttpService { + PasswordHttpService({ + required this.baseUrl, + required this.authToken, + }); + + final String baseUrl; + final String authToken; + + final http.Client client = http.Client(); + + Map get headers => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $authToken', + }; + + /// Changes the user's password + /// + /// [currentPassword] - The user's current password + /// [newPassword] - The new password to set + Future> changePassword({ + required String currentPassword, + required String newPassword, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.changePassword, + body: { + 'current_password': currentPassword, + 'password': newPassword, + }, + errorMessage: 'Failed to change password', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Sends a password reset email to the user + /// + /// [email] - The email address of the user + Future> forgotPassword({ + required String email, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.forgotPassword, + body: {'email': email}, + errorMessage: 'Failed to send password reset email', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Sets up a password for a user that doesn't have one + /// + /// [newPassword] - The new password to set + Future> setupPassword({ + required String newPassword, + }) async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.setupPassword, + body: {'password': newPassword}, + errorMessage: 'Failed to setup password', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Checks if the user has a password set + Future> checkHasPassword() async { + final result = await _makeRequest( + endpoint: PasswordEndpoint.checkHasPassword, + errorMessage: 'Failed to check password status', + ); + + return result.fold( + (data) => FlowyResult.success(data['has_password'] ?? false), + (error) => FlowyResult.failure(error), + ); + } + + /// Makes a request to the specified endpoint with the given body + Future> _makeRequest({ + required PasswordEndpoint endpoint, + Map? body, + String errorMessage = 'Request failed', + }) async { + try { + final uri = endpoint.uri(baseUrl); + http.Response response; + + if (endpoint.method == 'POST') { + response = await client.post( + uri, + headers: headers, + body: body != null ? jsonEncode(body) : null, + ); + } else if (endpoint.method == 'GET') { + response = await client.get( + uri, + headers: headers, + ); + } else { + return FlowyResult.failure( + FlowyError(msg: 'Invalid request method: ${endpoint.method}'), + ); + } + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + return FlowyResult.success(jsonDecode(response.body)); + } + return FlowyResult.success(true); + } else { + final errorBody = + response.body.isNotEmpty ? jsonDecode(response.body) : {}; + + Log.info( + '${endpoint.name} request failed: ${response.statusCode}, $errorBody ', + ); + + return FlowyResult.failure( + FlowyError( + msg: errorBody['msg'] ?? errorMessage, + ), + ); + } + } catch (e) { + Log.error('${endpoint.name} request failed: error: $e'); + + return FlowyResult.failure( + FlowyError(msg: 'Network error: ${e.toString()}'), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 339af51f9f..9691a1269b 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -303,6 +303,8 @@ class SignInBloc extends Bloc { msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); } else if (errorMsg.contains('invalid')) { msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); + } else if (errorMsg.contains('Invalid login credentials')) { + msg = LocaleKeys.signIn_invalidLoginCredentials.tr(); } return state.copyWith( isSubmitting: false, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 644a115641..4359a23753 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -95,6 +95,17 @@ class UserBackendService implements IUserBackendService { return UserEventPasscodeSignIn(payload).send(); } + Future> signInWithPassword( + String email, + String password, + ) { + final payload = SignInPayloadPB( + email: email, + password: password, + ); + return UserEventSignInWithEmailPassword(payload).send(); + } + static Future> signOut() { return UserEventSignOut().send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index 8034dccd32..349dd7e0d4 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -2,6 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -50,11 +52,6 @@ class _ContinueWithEmailAndPasswordState ); } else if (successOrFail == null && !state.isSubmitting) { emailKey.currentState?.clearError(); - - // _pushContinueWithMagicLinkOrPasscodePage( - // context, - // controller.text, - // ); } }, child: Column( @@ -76,13 +73,24 @@ class _ContinueWithEmailAndPasswordState controller.text, ), ), - // VSpace(theme.spacing.l), - // ContinueWithPassword( - // onTap: () => _pushContinueWithPasswordPage( - // context, - // controller.text, - // ), - // ), + VSpace(theme.spacing.l), + ContinueWithPassword( + onTap: () { + final email = controller.text; + + if (!isEmail(email)) { + emailKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidEmail.tr(), + ); + return; + } + + _pushContinueWithPasswordPage( + context, + email, + ); + }, + ), ], ), ); @@ -147,31 +155,34 @@ class _ContinueWithEmailAndPasswordState _hasPushedContinueWithMagicLinkOrPasscodePage = true; } - // void _pushContinueWithPasswordPage( - // BuildContext context, - // String email, - // ) { - // final signInBloc = context.read(); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => BlocProvider.value( - // value: signInBloc, - // child: ContinueWithPasswordPage( - // email: email, - // backToLogin: () => Navigator.pop(context), - // onEnterPassword: (password) => signInBloc.add( - // SignInEvent.signInWithEmailAndPassword( - // email: email, - // password: password, - // ), - // ), - // onForgotPassword: () { - // // todo: implement forgot password - // }, - // ), - // ), - // ), - // ); - // } + void _pushContinueWithPasswordPage( + BuildContext context, + String email, + ) { + final signInBloc = context.read(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: signInBloc, + child: ContinueWithPasswordPage( + email: email, + backToLogin: () { + emailKey.currentState?.clearError(); + Navigator.pop(context); + }, + onEnterPassword: (password) => signInBloc.add( + SignInEvent.signInWithEmailAndPassword( + email: email, + password: password, + ), + ), + onForgotPassword: () { + // todo: implement forgot password + }, + ), + ), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index 5be30ef84c..8cfc4c1157 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -167,7 +167,7 @@ class _ContinueWithMagicLinkOrPasscodePageState // title Text( LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), @@ -199,7 +199,7 @@ class _ContinueWithMagicLinkOrPasscodePageState // title Text( LocaleKeys.signIn_enterCode.tr(), - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart index 4ab40011d2..1e2ed6e100 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart @@ -1,6 +1,9 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; @@ -43,9 +46,16 @@ class _ContinueWithPasswordPageState extends State { width: 320, child: BlocListener( listener: (context, state) { - if (state.passwordError != null) { + final successOrFail = state.successOrFail; + if (successOrFail != null && successOrFail.isFailure) { + successOrFail.onFailure((error) { + inputPasswordKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), + ); + }); + } else if (state.passwordError != null) { inputPasswordKey.currentState?.syncError( - errorText: 'Incorrect password. Please try again.', + errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), ); } else { inputPasswordKey.currentState?.clearError(); @@ -80,8 +90,8 @@ class _ContinueWithPasswordPageState extends State { // title Text( - 'Enter password', - style: theme.textStyle.heading.h3( + LocaleKeys.signIn_enterPassword.tr(), + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), @@ -92,13 +102,13 @@ class _ContinueWithPasswordPageState extends State { text: TextSpan( children: [ TextSpan( - text: 'Login as ', + text: LocaleKeys.signIn_loginAs.tr(), style: theme.textStyle.body.standard( color: theme.textColorScheme.primary, ), ), TextSpan( - text: widget.email, + text: ' ${widget.email}', style: theme.textStyle.body.enhanced( color: theme.textColorScheme.primary, ), @@ -111,13 +121,26 @@ class _ContinueWithPasswordPageState extends State { } List _buildPasswordSection() { + final theme = AppFlowyTheme.of(context); + final iconSize = 20.0; return [ // Password input AFTextField( key: inputPasswordKey, controller: passwordController, - hintText: 'Enter password', + hintText: LocaleKeys.signIn_enterPassword.tr(), autoFocus: true, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + inputPasswordKey.currentState?.syncObscured(!isObscured); + }, + ), onSubmitted: widget.onEnterPassword, ), // todo: ask designer to provide the spacing @@ -127,7 +150,7 @@ class _ContinueWithPasswordPageState extends State { Align( alignment: Alignment.centerLeft, child: AFGhostTextButton( - text: 'Forget password?', + text: LocaleKeys.signIn_forgotPassword.tr(), size: AFButtonSize.s, padding: EdgeInsets.zero, onTap: widget.onForgotPassword, @@ -144,7 +167,7 @@ class _ContinueWithPasswordPageState extends State { // Continue button AFFilledTextButton.primary( - text: 'Continue', + text: LocaleKeys.web_continue.tr(), onTap: () => widget.onEnterPassword(passwordController.text), size: AFButtonSize.l, alignment: Alignment.center, @@ -156,7 +179,7 @@ class _ContinueWithPasswordPageState extends State { List _buildBackToLogin() { return [ AFGhostTextButton( - text: 'Back to Login', + text: LocaleKeys.signIn_backToLogin.tr(), size: AFButtonSize.s, onTap: widget.backToLogin, padding: EdgeInsets.zero, diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart index 93ccea25d0..14b1c896a9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart @@ -25,7 +25,7 @@ class FlowyLogoTitle extends StatelessWidget { const VSpace(20), Text( title, - style: theme.textStyle.heading.h3( + style: theme.textStyle.heading3.enhanced( color: theme.textColorScheme.primary, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 56faa9f8d8..2f62177661 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -54,17 +54,27 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - removeUserIcon: () { - // Empty Icon URL = No icon - _userService.updateUserProfile(iconUrl: "").then((result) { + updateUserEmail: (String email) { + _userService.updateUserProfile(email: email).then((result) { result.fold( (l) => null, (err) => Log.error(err), ); }); }, - updateUserEmail: (String email) { - _userService.updateUserProfile(email: email).then((result) { + updateUserPassword: (String oldPassword, String newPassword) { + _userService + .updateUserProfile(password: newPassword) + .then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + removeUserIcon: () { + // Empty Icon URL = No icon + _userService.updateUserProfile(iconUrl: "").then((result) { result.fold( (l) => null, (err) => Log.error(err), @@ -104,10 +114,19 @@ class SettingsUserViewBloc extends Bloc { @freezed class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; - const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; - const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail; - const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) = - _UpdateUserIcon; + const factory SettingsUserEvent.updateUserName({ + required String name, + }) = _UpdateUserName; + const factory SettingsUserEvent.updateUserEmail({ + required String email, + }) = _UpdateEmail; + const factory SettingsUserEvent.updateUserIcon({ + required String iconUrl, + }) = _UpdateUserIcon; + const factory SettingsUserEvent.updateUserPassword({ + required String oldPassword, + required String newPassword, + }) = _UpdateUserPassword; const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart index 5e1a6f90e0..05e6d46957 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart @@ -105,9 +105,9 @@ class SidebarToast extends StatelessWidget { if (role.isOwner) { showSettingsDialog( context, - userProfile, - userWorkspaceBloc, - SettingsPage.plan, + userProfile: userProfile, + userWorkspaceBloc: userWorkspaceBloc, + initPage: SettingsPage.plan, ); } else { final String message; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 84a76cfe83..0bd5dafe91 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; @@ -33,7 +34,7 @@ HotKeyItem openSettingsHotKey( ), keyDownHandler: (_) { if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context, userProfile); + showSettingsDialog(context, userProfile: userProfile); } else { Navigator.of(context, rootNavigator: true) .popUntil((route) => route.isFirst); @@ -57,37 +58,55 @@ class UserSettingButton extends StatefulWidget { class _UserSettingButtonState extends State { late UserWorkspaceBloc _userWorkspaceBloc; + late PasswordBloc _passwordBloc; @override void initState() { super.initState(); + _userWorkspaceBloc = context.read(); + _passwordBloc = PasswordBloc(widget.userProfile) + ..add(PasswordEvent.init()) + ..add(PasswordEvent.checkHasPassword()); } @override void didChangeDependencies() { _userWorkspaceBloc = context.read(); + super.didChangeDependencies(); } + @override + void dispose() { + _passwordBloc.close(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return SizedBox.square( dimension: 24.0, child: FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), - child: FlowyButton( - onTap: () => showSettingsDialog( - context, - widget.userProfile, - _userWorkspaceBloc, - ), - margin: EdgeInsets.zero, - text: FlowySvg( - FlowySvgs.settings_s, - color: - widget.isHover ? Theme.of(context).colorScheme.onSurface : null, - opacity: 0.7, + child: BlocProvider.value( + value: _passwordBloc, + child: FlowyButton( + onTap: () => showSettingsDialog( + context, + userProfile: widget.userProfile, + userWorkspaceBloc: _userWorkspaceBloc, + passwordBloc: _passwordBloc, + ), + margin: EdgeInsets.zero, + text: FlowySvg( + FlowySvgs.settings_s, + color: widget.isHover + ? Theme.of(context).colorScheme.onSurface + : null, + opacity: 0.7, + ), ), ), ), @@ -96,21 +115,33 @@ class _UserSettingButtonState extends State { } void showSettingsDialog( - BuildContext context, - UserProfilePB userProfile, [ - UserWorkspaceBloc? bloc, + BuildContext context, { + required UserProfilePB userProfile, + UserWorkspaceBloc? userWorkspaceBloc, + PasswordBloc? passwordBloc, SettingsPage? initPage, -]) { +}) { AFFocusManager.maybeOf(context)?.notifyLoseFocus(); showDialog( context: context, builder: (dialogContext) => MultiBlocProvider( key: _settingsDialogKey, providers: [ + passwordBloc != null + ? BlocProvider.value( + value: passwordBloc, + ) + : BlocProvider( + create: (context) => PasswordBloc(userProfile) + ..add(PasswordEvent.init()) + ..add(PasswordEvent.checkHasPassword()), + ), BlocProvider.value( value: BlocProvider.of(dialogContext), ), - BlocProvider.value(value: bloc ?? context.read()), + BlocProvider.value( + value: userWorkspaceBloc ?? context.read(), + ), ], child: SettingsDialog( userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart index 71dfdde7a9..2125ea4b66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -16,28 +17,29 @@ class SettingsAppVersion extends StatelessWidget { Widget build(BuildContext context) { return ApplicationInfo.isUpdateAvailable ? const _UpdateAppSection() - : _buildIsUpToDate(); + : _buildIsUpToDate(context); } - Widget _buildIsUpToDate() { + Widget _buildIsUpToDate(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.regular( + Text( LocaleKeys.settings_accountPage_isUpToDate.tr(), - figmaLineHeight: 17, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), ), const VSpace(4), - Opacity( - opacity: 0.7, - child: FlowyText.regular( - LocaleKeys.settings_accountPage_officialVersion.tr( - namedArgs: { - 'version': ApplicationInfo.applicationVersion, - }, - ), - fontSize: 12, - figmaLineHeight: 13, + Text( + LocaleKeys.settings_accountPage_officialVersion.tr( + namedArgs: { + 'version': ApplicationInfo.applicationVersion, + }, + ), + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index e6c011156b..04d078ec0d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -8,8 +8,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -43,43 +43,36 @@ class _AccountDeletionButtonState extends State { @override Widget build(BuildContext context) { - final textColor = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF4F4F4F) - : const Color(0xFFB0B0B0); + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText( + Text( LocaleKeys.button_deleteAccount.tr(), - fontSize: 14.0, - fontWeight: FontWeight.w500, - figmaLineHeight: 21.0, - color: textColor, + style: theme.textStyle.heading4.enhanced( + color: theme.textColorScheme.primary, + ), ), const VSpace(8), Row( children: [ Expanded( - child: FlowyText.regular( + child: Text( LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(), - fontSize: 12.0, - figmaLineHeight: 13.0, maxLines: 2, - color: textColor, + overflow: TextOverflow.ellipsis, + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, + ), ), ), - FlowyTextButton( - LocaleKeys.button_deleteAccount.tr(), - constraints: const BoxConstraints(minHeight: 32), - padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10), - fillColor: Colors.transparent, - radius: Corners.s8Border, - hoverColor: - Theme.of(context).colorScheme.error.withValues(alpha: 0.1), - fontColor: Theme.of(context).colorScheme.error, - fontSize: 12, - isDangerous: true, - onPressed: () { + AFOutlinedTextButton.destructive( + text: LocaleKeys.button_deleteAccount.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.error, + weight: FontWeight.w400, + ), + onTap: () { isCheckedNotifier.value = false; textEditingController.clear(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 984598f29c..89066ea649 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -3,12 +3,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/user/application/prelude.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart'; import 'package:appflowy/util/navigator_context_extension.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -28,9 +32,15 @@ class AccountSignInOutSection extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Row( children: [ - FlowyText.regular(LocaleKeys.settings_accountPage_login_title.tr()), + Text( + LocaleKeys.settings_accountPage_login_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), const Spacer(), AccountSignInOutButton( userProfile: userProfile, @@ -56,13 +66,10 @@ class AccountSignInOutButton extends StatelessWidget { @override Widget build(BuildContext context) { - return PrimaryRoundedButton( + return AFFilledTextButton.primary( text: signIn ? LocaleKeys.settings_accountPage_login_loginLabel.tr() : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - fontWeight: FontWeight.w500, - radius: 8.0, onTap: () => signIn ? _showSignInDialog(context) : _showLogoutDialog(context), ); @@ -96,6 +103,94 @@ class AccountSignInOutButton extends StatelessWidget { } } +class ChangePasswordSection extends StatelessWidget { + const ChangePasswordSection({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + Text( + LocaleKeys.newSettings_myAccount_password_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + state.hasPassword + ? AFFilledTextButton.primary( + text: LocaleKeys + .newSettings_myAccount_password_changePassword + .tr(), + onTap: () => _showChangePasswordDialog(context), + ) + : AFFilledTextButton.primary( + text: LocaleKeys + .newSettings_myAccount_password_setupPassword + .tr(), + onTap: () => _showSetPasswordDialog(context), + ), + ], + ); + }, + ); + } + + Future _showChangePasswordDialog(BuildContext context) async { + final theme = AppFlowyTheme.of(context); + await showDialog( + context: context, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: getIt(), + ), + ], + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + ), + child: ChangePasswordDialogContent( + userProfile: userProfile, + ), + ), + ), + ); + } + + Future _showSetPasswordDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: getIt(), + ), + ], + child: Dialog( + child: SetupPasswordDialogContent( + userProfile: userProfile, + ), + ), + ), + ); + } +} + class _SignInDialogContent extends StatelessWidget { const _SignInDialogContent(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart index bd08501ae4..62a6232c4a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart @@ -4,6 +4,7 @@ import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -96,27 +97,29 @@ class _AccountUserProfileState extends State { } Widget _buildNameDisplay() { + final theme = AppFlowyTheme.of(context); return Padding( padding: const EdgeInsets.only(top: 12), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: FlowyText.medium( + child: Text( widget.name, overflow: TextOverflow.ellipsis, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), ), ), const HSpace(4), - GestureDetector( - behavior: HitTestBehavior.opaque, + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), onTap: () => setState(() => isEditing = true), - child: const FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: EdgeInsets.all(4), - child: FlowySvg(FlowySvgs.edit_s), - ), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.toolbar_link_edit_m, + size: const Size.square(20), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart new file mode 100644 index 0000000000..d606f870ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart @@ -0,0 +1,38 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/widgets.dart'; + +class SettingsEmailSection extends StatelessWidget { + const SettingsEmailSection({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.settings_accountPage_email_title.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + VSpace(theme.spacing.s), + Text( + userProfile.email, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart new file mode 100644 index 0000000000..194254c869 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart @@ -0,0 +1,330 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.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 ChangePasswordDialogContent extends StatefulWidget { + const ChangePasswordDialogContent({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => + _ChangePasswordDialogContentState(); +} + +class _ChangePasswordDialogContentState + extends State { + final currentPasswordTextFieldKey = GlobalKey(); + final newPasswordTextFieldKey = GlobalKey(); + final confirmPasswordTextFieldKey = GlobalKey(); + + final currentPasswordController = TextEditingController(); + final newPasswordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + final iconSize = 20.0; + + @override + void dispose() { + currentPasswordController.dispose(); + newPasswordController.dispose(); + confirmPasswordController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocListener( + listener: _onPasswordStateChanged, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(context), + VSpace(theme.spacing.l), + ..._buildCurrentPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildNewPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildConfirmPasswordFields(context), + VSpace(theme.spacing.l), + _buildSubmitButton(context), + ], + ), + ), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Change password', + style: theme.textStyle.heading4.prominent( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.password_close_m, + size: const Size.square(20), + ), + ), + ], + ); + } + + List _buildCurrentPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_currentPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: currentPasswordTextFieldKey, + controller: currentPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_enterYourCurrentPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildNewPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_newPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: newPasswordTextFieldKey, + controller: newPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_enterYourNewPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + newPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildConfirmPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(), + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: confirmPasswordTextFieldKey, + controller: confirmPasswordController, + hintText: LocaleKeys + .newSettings_myAccount_password_hint_confirmYourNewPassword + .tr(), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + Widget _buildSubmitButton(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AFOutlinedTextButton.normal( + text: LocaleKeys.button_cancel.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + weight: FontWeight.w400, + ), + onTap: () => Navigator.of(context).pop(), + ), + const HSpace(16), + AFFilledTextButton.primary( + text: LocaleKeys.button_save.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.onFill, + weight: FontWeight.w400, + ), + onTap: () => _save(context), + ), + ], + ); + } + + void _save(BuildContext context) async { + _resetError(); + + final currentPassword = currentPasswordController.text; + final newPassword = newPasswordController.text; + final confirmPassword = confirmPasswordController.text; + + if (newPassword.isEmpty) { + newPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsRequired + .tr(), + ); + return; + } + + if (confirmPassword.isEmpty) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_confirmPasswordIsRequired + .tr(), + ); + return; + } + + if (newPassword != confirmPassword) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_passwordsDoNotMatch + .tr(), + ); + return; + } + + if (newPassword == currentPassword) { + newPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsSameAsCurrent + .tr(), + ); + return; + } + + // all the verification passed, save the new password + context.read().add( + PasswordEvent.changePassword( + oldPassword: currentPassword, + newPassword: newPassword, + ), + ); + } + + void _resetError() { + currentPasswordTextFieldKey.currentState?.clearError(); + newPasswordTextFieldKey.currentState?.clearError(); + confirmPasswordTextFieldKey.currentState?.clearError(); + } + + void _onPasswordStateChanged(BuildContext context, PasswordState state) { + bool hasError = false; + String message = ''; + String description = ''; + + final changePasswordResult = state.changePasswordResult; + final setPasswordResult = state.setupPasswordResult; + + if (changePasswordResult != null) { + changePasswordResult.fold( + (success) { + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordUpdatedSuccessfully + .tr(); + }, + (error) { + hasError = true; + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordUpdatedFailed + .tr(); + description = error.msg; + }, + ); + } else if (setPasswordResult != null) { + setPasswordResult.fold( + (success) { + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordSetupSuccessfully + .tr(); + }, + (error) { + hasError = true; + message = LocaleKeys + .newSettings_myAccount_password_toast_passwordSetupFailed + .tr(); + description = error.msg; + }, + ); + } + + if (!state.isSubmitting && message.isNotEmpty) { + showToastNotification( + message: message, + description: description, + type: hasError ? ToastificationType.error : ToastificationType.success, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart new file mode 100644 index 0000000000..5417b1a0eb --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart @@ -0,0 +1,30 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class PasswordSuffixIcon extends StatelessWidget { + const PasswordSuffixIcon({ + super.key, + required this.isObscured, + required this.onTap, + }); + + final bool isObscured; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Padding( + padding: EdgeInsets.only(right: theme.spacing.m), + child: GestureDetector( + onTap: onTap, + child: FlowySvg( + isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s, + color: theme.textColorScheme.secondary, + size: const Size.square(20), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart new file mode 100644 index 0000000000..2fdfd8b934 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart @@ -0,0 +1,254 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/password/password_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_ui/appflowy_ui.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 SetupPasswordDialogContent extends StatefulWidget { + const SetupPasswordDialogContent({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => + _SetupPasswordDialogContentState(); +} + +class _SetupPasswordDialogContentState + extends State { + final passwordTextFieldKey = GlobalKey(); + final confirmPasswordTextFieldKey = GlobalKey(); + + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + + final iconSize = 20.0; + + @override + void dispose() { + passwordController.dispose(); + confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return BlocListener( + listener: _onPasswordStateChanged, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(context), + VSpace(theme.spacing.l), + ..._buildPasswordFields(context), + VSpace(theme.spacing.l), + ..._buildConfirmPasswordFields(context), + VSpace(theme.spacing.l), + _buildSubmitButton(context), + ], + ), + ), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.newSettings_myAccount_password_setupPassword.tr(), + style: theme.textStyle.heading4.prominent( + color: theme.textColorScheme.primary, + ), + ), + const Spacer(), + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.password_close_m, + size: const Size.square(20), + ), + ), + ], + ); + } + + List _buildPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + 'Password', + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: passwordTextFieldKey, + controller: passwordController, + hintText: 'Enter your password', + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + passwordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + List _buildConfirmPasswordFields(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return [ + Text( + 'Confirm password', + style: theme.textStyle.caption.enhanced( + color: theme.textColorScheme.secondary, + ), + ), + VSpace(theme.spacing.xs), + AFTextField( + key: confirmPasswordTextFieldKey, + controller: confirmPasswordController, + hintText: 'Confirm your password', + keyboardType: TextInputType.visiblePassword, + obscureText: true, + suffixIconConstraints: BoxConstraints.tightFor( + width: iconSize + theme.spacing.m, + height: iconSize, + ), + suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( + isObscured: isObscured, + onTap: () { + confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); + }, + ), + ), + ]; + } + + Widget _buildSubmitButton(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AFOutlinedTextButton.normal( + text: 'Cancel', + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + weight: FontWeight.w400, + ), + onTap: () => Navigator.of(context).pop(), + ), + const HSpace(16), + AFFilledTextButton.primary( + text: 'Save', + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.onFill, + weight: FontWeight.w400, + ), + onTap: () => _save(context), + ), + ], + ); + } + + void _save(BuildContext context) async { + _resetError(); + + final password = passwordController.text; + final confirmPassword = confirmPasswordController.text; + + if (password.isEmpty) { + passwordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_newPasswordIsRequired + .tr(), + ); + return; + } + + if (confirmPassword.isEmpty) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_confirmPasswordIsRequired + .tr(), + ); + return; + } + + if (password != confirmPassword) { + confirmPasswordTextFieldKey.currentState?.syncError( + errorText: LocaleKeys + .newSettings_myAccount_password_error_passwordsDoNotMatch + .tr(), + ); + return; + } + + // all the verification passed, save the password + context.read().add( + PasswordEvent.setupPassword( + newPassword: password, + ), + ); + } + + void _resetError() { + passwordTextFieldKey.currentState?.clearError(); + confirmPasswordTextFieldKey.currentState?.clearError(); + } + + void _onPasswordStateChanged(BuildContext context, PasswordState state) { + bool hasError = false; + String message = ''; + String description = ''; + + final setPasswordResult = state.setupPasswordResult; + + if (setPasswordResult != null) { + setPasswordResult.fold( + (success) { + message = 'Password set'; + description = 'Your password has been set'; + }, + (error) { + hasError = true; + message = 'Failed to set password'; + description = error.msg; + }, + ); + } + + if (!state.isSubmitting && message.isNotEmpty) { + showToastNotification( + message: message, + description: description, + type: hasError ? ToastificationType.error : ToastificationType.success, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index 701d1cb565..e223b2d063 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -4,12 +4,12 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart'; import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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'; @@ -45,11 +45,11 @@ class _SettingsAccountViewState extends State { child: BlocBuilder( builder: (context, state) { return SettingsBody( - title: LocaleKeys.settings_accountPage_title.tr(), + title: LocaleKeys.newSettings_myAccount_title.tr(), children: [ // user profile SettingsCategory( - title: LocaleKeys.settings_accountPage_general_title.tr(), + title: LocaleKeys.newSettings_myAccount_myProfile.tr(), children: [ AccountUserProfile( name: userName, @@ -61,7 +61,7 @@ class _SettingsAccountViewState extends State { setState(() => userName = newName); context .read() - .add(SettingsUserEvent.updateUserName(newName)); + .add(SettingsUserEvent.updateUserName(name: newName)); }, ), ], @@ -72,9 +72,14 @@ class _SettingsAccountViewState extends State { if (isAuthEnabled && state.userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( - title: LocaleKeys.settings_accountPage_email_title.tr(), + title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ - FlowyText.regular(state.userProfile.email), + SettingsEmailSection( + userProfile: state.userProfile, + ), + ChangePasswordSection( + userProfile: state.userProfile, + ), AccountSignInOutSection( userProfile: state.userProfile, onAction: state.userProfile.authenticator == diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index a111fa2626..33c81b99e8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -25,15 +26,18 @@ class SettingsCategory extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - FlowyText.semibold( + Text( title, + style: theme.textStyle.heading4.enhanced( + color: theme.textColorScheme.primary, + ), maxLines: 2, - fontSize: 16, overflow: TextOverflow.ellipsis, ), if (tooltip != null) ...[ @@ -47,7 +51,7 @@ class SettingsCategory extends StatelessWidget { if (actions != null) ...actions!, ], ), - const VSpace(8), + const VSpace(16), if (description?.isNotEmpty ?? false) ...[ FlowyText.regular( description!, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index 5637fdd20c..deec09c1d8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; /// This is used to create a uniform space and divider @@ -7,6 +8,11 @@ class SettingsCategorySpacer extends StatelessWidget { const SettingsCategorySpacer({super.key}); @override - Widget build(BuildContext context) => - const Divider(height: 32, color: Color(0xFFF2F2F2)); + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Divider( + height: 32, + color: theme.borderColorScheme.greyPrimary, + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart index c028e6886d..7409070ba9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; /// Renders a simple header for the settings view /// @@ -13,10 +13,16 @@ class SettingsHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.semibold(title, fontSize: 24), + Text( + title, + style: theme.textStyle.heading2.enhanced( + color: theme.textColorScheme.primary, + ), + ), if (description?.isNotEmpty == true) ...[ const VSpace(8), FlowyText( diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart index dc85a3ee55..39d5175af1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart @@ -27,7 +27,7 @@ enum AFButtonSize { vertical: theme.spacing.xs, ), AFButtonSize.m => EdgeInsets.symmetric( - horizontal: theme.spacing.l, + horizontal: theme.spacing.xl, vertical: theme.spacing.s, ), AFButtonSize.l => EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart index ced936cf0a..035307d10b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart @@ -13,6 +13,7 @@ class AFBaseTextButton extends StatelessWidget { this.textColor, this.backgroundColor, this.alignment, + this.textStyle, }); /// The text of the button. @@ -44,6 +45,9 @@ class AFBaseTextButton extends StatelessWidget { /// If it's null, the button size will be the size of the text with padding. final Alignment? alignment; + /// The text style of the button. + final TextStyle? textStyle; + @override Widget build(BuildContext context) { throw UnimplementedError(); diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart index 353d5ac785..889ad1e429 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -14,6 +14,7 @@ class AFFilledTextButton extends AFBaseTextButton { super.borderRadius, super.disabled = false, super.alignment, + super.textStyle, }); /// Primary text button. @@ -26,6 +27,7 @@ class AFFilledTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -36,6 +38,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.onFill, backgroundColor: (context, isHovering, disabled) { @@ -60,6 +63,7 @@ class AFFilledTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -70,6 +74,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.onFill, backgroundColor: (context, isHovering, disabled) { @@ -92,6 +97,7 @@ class AFFilledTextButton extends AFBaseTextButton { EdgeInsetsGeometry? padding, double? borderRadius, Alignment? alignment, + TextStyle? textStyle, }) { return AFFilledTextButton( key: key, @@ -102,6 +108,7 @@ class AFFilledTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: true, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) => AppFlowyTheme.of(context).textColorScheme.tertiary, backgroundColor: (context, isHovering, disabled) => @@ -123,7 +130,8 @@ class AFFilledTextButton extends AFBaseTextButton { AppFlowyTheme.of(context).textColorScheme.onFill; Widget child = Text( text, - style: size.buildTextStyle(context).copyWith(color: textColor), + style: textStyle ?? + size.buildTextStyle(context).copyWith(color: textColor), ); final alignment = this.alignment; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart index 7cb5f2d609..d5ae580583 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -8,6 +8,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { required super.text, required super.onTap, this.borderColor, + super.textStyle, super.textColor, super.backgroundColor, super.size = AFButtonSize.m, @@ -27,6 +28,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -37,6 +39,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, borderColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (disabled) { @@ -80,6 +83,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { double? borderRadius, bool disabled = false, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -90,6 +94,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: disabled, alignment: alignment, + textStyle: textStyle, borderColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); if (disabled) { @@ -127,6 +132,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { EdgeInsetsGeometry? padding, double? borderRadius, Alignment? alignment, + TextStyle? textStyle, }) { return AFOutlinedTextButton._( key: key, @@ -137,6 +143,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { borderRadius: borderRadius, disabled: true, alignment: alignment, + textStyle: textStyle, textColor: (context, isHovering, disabled) { final theme = AppFlowyTheme.of(context); return disabled @@ -185,7 +192,8 @@ class AFOutlinedTextButton extends AFBaseTextButton { Widget child = Text( text, - style: size.buildTextStyle(context).copyWith(color: textColor), + style: textStyle ?? + size.buildTextStyle(context).copyWith(color: textColor), ); final alignment = this.alignment; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 595d4bb859..a934734983 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -6,8 +6,12 @@ typedef AFTextFieldValidator = (bool result, String errorText) Function( ); abstract class AFTextFieldState extends State { + // Error handler void syncError({required String errorText}) {} void clearError() {} + + /// Obscure the text. + void syncObscured(bool isObscured) {} } class AFTextField extends StatefulWidget { @@ -23,6 +27,9 @@ class AFTextField extends StatefulWidget { this.onSubmitted, this.autoFocus, this.height = 40.0, + this.obscureText = false, + this.suffixIconBuilder, + this.suffixIconConstraints, }); /// The height of the text field. @@ -57,6 +64,16 @@ class AFTextField extends StatefulWidget { /// Enable auto focus. final bool? autoFocus; + /// Obscure the text. + final bool obscureText; + + /// The trailing widget to display. + final Widget Function(BuildContext context, bool isObscured)? + suffixIconBuilder; + + /// The size of the suffix icon. + final BoxConstraints? suffixIconConstraints; + @override State createState() => _AFTextFieldState(); } @@ -67,6 +84,8 @@ class _AFTextFieldState extends AFTextFieldState { bool hasError = false; String errorText = ''; + bool isObscured = false; + @override void initState() { super.initState(); @@ -79,6 +98,8 @@ class _AFTextFieldState extends AFTextFieldState { } effectiveController.addListener(_validate); + + isObscured = widget.obscureText; } @override @@ -107,6 +128,7 @@ class _AFTextFieldState extends AFTextFieldState { style: theme.textStyle.body.standard( color: theme.textColorScheme.primary, ), + obscureText: isObscured, onChanged: widget.onChanged, onSubmitted: widget.onSubmitted, autofocus: widget.autoFocus ?? false, @@ -152,6 +174,8 @@ class _AFTextFieldState extends AFTextFieldState { borderRadius: borderRadius, ), hoverColor: theme.borderColorScheme.greyTertiaryHover, + suffixIcon: widget.suffixIconBuilder?.call(context, isObscured), + suffixIconConstraints: widget.suffixIconConstraints, ), ); @@ -204,4 +228,11 @@ class _AFTextFieldState extends AFTextFieldState { errorText = ''; }); } + + @override + void syncObscured(bool isObscured) { + setState(() { + this.isObscured = isObscured; + }); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart index a85ffbb6cb..3cdf267fe0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart @@ -6,66 +6,86 @@ abstract class TextThemeType { TextStyle standard({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle enhanced({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle prominent({ String family = '', Color? color, + FontWeight? weight, }); + TextStyle underline({ String family = '', Color? color, + FontWeight? weight, }); } -class TextThemeHeading { - const TextThemeHeading(); +class TextThemeHeading1 extends TextThemeType { + const TextThemeHeading1(); - TextStyle h1({ + @override + TextStyle standard({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, fontSize: 36, height: 40 / 36, color: color, + weight: weight ?? FontWeight.w400, ); - TextStyle h2({ + @override + TextStyle enhanced({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 24, - height: 32 / 24, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.w600, ); - TextStyle h3({ + @override + TextStyle prominent({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 20, - height: 28 / 20, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.w700, ); - TextStyle h4({ + @override + TextStyle underline({ String family = '', Color? color, + FontWeight? weight, }) => _defaultTextStyle( family: family, - fontSize: 16, - height: 22 / 16, + fontSize: 36, + height: 40 / 36, color: color, + weight: weight ?? FontWeight.bold, + decoration: TextDecoration.underline, ); static TextStyle _defaultTextStyle({ @@ -74,13 +94,188 @@ class TextThemeHeading { required double height, TextDecoration decoration = TextDecoration.none, Color? color, + FontWeight weight = FontWeight.bold, }) => TextStyle( inherit: false, fontSize: fontSize, decoration: decoration, fontStyle: FontStyle.normal, - fontWeight: FontWeight.bold, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading2 extends TextThemeType { + const TextThemeHeading2(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 24, + double height = 32 / 24, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading3 extends TextThemeType { + const TextThemeHeading3(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 20, + double height = 28 / 20, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, + height: height, + fontFamily: family, + color: color, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + ); +} + +class TextThemeHeading4 extends TextThemeType { + const TextThemeHeading4(); + + @override + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + ); + + @override + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w600, + ); + + @override + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w700, + ); + + @override + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( + family: family, + color: color, + weight: weight ?? FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle _defaultTextStyle({ + required String family, + double fontSize = 16, + double height = 22 / 16, + TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.w400, + Color? color, + }) => + TextStyle( + inherit: false, + fontSize: fontSize, + decoration: decoration, + fontStyle: FontStyle.normal, + fontWeight: weight, height: height, fontFamily: family, color: color, @@ -93,29 +288,35 @@ class TextThemeHeadline extends TextThemeType { const TextThemeHeadline(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -123,8 +324,8 @@ class TextThemeHeadline extends TextThemeType { required String family, double fontSize = 24, double height = 36 / 24, - FontWeight weight = FontWeight.normal, TextDecoration decoration = TextDecoration.none, + FontWeight weight = FontWeight.normal, Color? color, }) => TextStyle( @@ -145,29 +346,35 @@ class TextThemeTitle extends TextThemeType { const TextThemeTitle(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -197,29 +404,35 @@ class TextThemeBody extends TextThemeType { const TextThemeBody(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); @@ -249,29 +462,35 @@ class TextThemeCaption extends TextThemeType { const TextThemeCaption(); @override - TextStyle standard({String family = '', Color? color}) => _defaultTextStyle( + TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color}) => _defaultTextStyle( + TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.w600, + weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color}) => _defaultTextStyle( + TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, - weight: FontWeight.bold, + weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color}) => _defaultTextStyle( + TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + _defaultTextStyle( family: family, color: color, + weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart index 64daae2370..1caaa288e8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart @@ -2,14 +2,20 @@ import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart'; class AppFlowyBaseTextStyle { const AppFlowyBaseTextStyle({ - this.heading = const TextThemeHeading(), + this.heading1 = const TextThemeHeading1(), + this.heading2 = const TextThemeHeading2(), + this.heading3 = const TextThemeHeading3(), + this.heading4 = const TextThemeHeading4(), this.headline = const TextThemeHeadline(), this.title = const TextThemeTitle(), this.body = const TextThemeBody(), this.caption = const TextThemeCaption(), }); - final TextThemeHeading heading; + final TextThemeType heading1; + final TextThemeType heading2; + final TextThemeType heading3; + final TextThemeType heading4; final TextThemeType headline; final TextThemeType title; final TextThemeType body; diff --git a/frontend/resources/flowy_icons/20x/hide_password.svg b/frontend/resources/flowy_icons/20x/hide_password.svg new file mode 100644 index 0000000000..2ebd274866 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/hide_password.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/20x/password_close.svg b/frontend/resources/flowy_icons/20x/password_close.svg new file mode 100644 index 0000000000..52a44e1a8e --- /dev/null +++ b/frontend/resources/flowy_icons/20x/password_close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/20x/show_password.svg b/frontend/resources/flowy_icons/20x/show_password.svg new file mode 100644 index 0000000000..ac8d092b37 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/show_password.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 6274540914..30e8c476ae 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -81,8 +81,11 @@ "enterCode": "Enter code", "enterCodeManually": "Enter code manually", "continueWithEmail": "Continue with email", + "enterPassword": "Enter password", + "loginAs": "Login as", "invalidVerificationCode": "Please enter a valid verification code", - "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later." + "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later.", + "invalidLoginCredentials": "Your password is incorrect, please try again" }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -2618,7 +2621,7 @@ "noLogFiles": "There're no log files", "newSettings": { "myAccount": { - "title": "My account", + "title": "Account & App", "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", "profileLabel": "Account name & Profile image", "profileNamePlaceholder": "Enter your name", @@ -2644,7 +2647,34 @@ "failedToGetCurrentUser": "Failed to get current user email", "confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", "deleteAccountSuccess": "Account deleted successfully" - } + }, + "password": { + "title": "Password", + "changePassword": "Change password", + "currentPassword": "Current password", + "newPassword": "New password", + "confirmNewPassword": "Confirm new password", + "setupPassword": "Setup password", + "error": { + "newPasswordIsRequired": "New password is required", + "confirmPasswordIsRequired": "Confirm password is required", + "passwordsDoNotMatch": "Passwords do not match", + "newPasswordIsSameAsCurrent": "New password is same as current password" + }, + "toast": { + "passwordUpdatedSuccessfully": "Password updated successfully", + "passwordUpdatedFailed": "Failed to update password", + "passwordSetupSuccessfully": "Password setup successfully", + "passwordSetupFailed": "Failed to setup password" + }, + "hint": { + "enterYourCurrentPassword": "Enter your current password", + "enterYourNewPassword": "Enter your new password", + "confirmYourNewPassword": "Confirm your new password" + } + }, + "myAccount": "My Account", + "myProfile": "My Profile" }, "workplace": { "name": "Workplace", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ec114f205a..b2bb89e3b9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1786,7 +1786,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -5148,7 +5148,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5168,6 +5168,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.2", ] @@ -5235,6 +5236,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 3cd3733837..3c73fd01eb 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -31,29 +31,6 @@ async fn sign_up_with_invalid_email() { ); } } -#[tokio::test] -async fn sign_up_with_long_password() { - let sdk = EventIntegrationTest::new().await; - let request = SignUpPayloadPB { - email: unique_email(), - name: valid_name(), - password: "1234".repeat(100).as_str().to_string(), - auth_type: AuthenticatorPB::Local, - device_id: "".to_string(), - }; - - assert_eq!( - EventBuilder::new(sdk) - .event(SignUp) - .payload(request) - .async_send() - .await - .error() - .unwrap() - .code, - ErrorCode::PasswordTooLong - ); -} #[tokio::test] async fn sign_in_with_invalid_email() { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 9057288f88..98ed822901 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -131,12 +131,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), ), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } @@ -247,12 +245,10 @@ impl ChatCloudService for LocalServerChatServiceImpl { ), Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), } + } else if self.local_ai.is_enabled() { + Err(FlowyError::local_ai_not_ready()) } else { - if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } + Err(FlowyError::local_ai_disabled()) } } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 02ae333aa0..87e828199c 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -31,11 +31,10 @@ impl TryInto for SignInPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; - let password = UserPassword::parse(self.password)?; Ok(SignInParams { email: email.0, - password: password.0, + password: self.password, name: self.name, auth_type: self.auth_type.into(), }) @@ -65,13 +64,13 @@ impl TryInto for SignUpPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; - let password = UserPassword::parse(self.password)?; + let password = self.password; let name = UserName::parse(self.name)?; Ok(SignUpParams { email: email.0, name: name.0, - password: password.0, + password, auth_type: self.auth_type.into(), device_id: self.device_id, }) 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 36d5232bd2..7e62958c68 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -4,7 +4,7 @@ use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; +use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey}; use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; @@ -171,10 +171,7 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(email) => Some(UserEmail::parse(email)?.0), }; - let password = match self.password { - None => None, - Some(password) => Some(UserPassword::parse(password)?.0), - }; + let password = self.password; let icon_url = match self.icon_url { None => None, From 4925e991660a5a6d89ada9705473051553e06f8b Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:02:02 +0800 Subject: [PATCH 18/74] chore: fix compile --- .../ai_chat/application/chat_bloc.dart | 2 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 4 +- frontend/rust-lib/flowy-ai/src/chat.rs | 11 +- frontend/rust-lib/flowy-ai/src/entities.rs | 9 +- .../rust-lib/flowy-ai/src/event_handler.rs | 33 +----- .../flowy-ai/src/local_ai/controller.rs | 107 +++++++++--------- .../src/middleware/chat_service_mw.rs | 41 +------ .../rust-lib/flowy-ai/src/stream_message.rs | 1 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 22 ++-- 9 files changed, 78 insertions(+), 152 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 4924a42c0d..602b46f97a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -435,7 +435,7 @@ class ChatBloc extends Bloc { messageType: ChatMessageTypePB.User, questionStreamPort: Int64(questionStream.nativePort), answerStreamPort: Int64(answerStream!.nativePort), - metadata: await metadataPBFromMetadata(metadata), + //metadata: await metadataPBFromMetadata(metadata), ); if (format != null) { payload.format = format.toPB(); diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index b91021142e..2292e0f332 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -6,8 +6,8 @@ pub use client_api::entity::ai_dto::{ }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ - ChatMessage, ChatMessageMetadata, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, - MessageCursor, RepeatedChatMessage, UpdateChatParams, + ChatMessage, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, MessageCursor, + RepeatedChatMessage, UpdateChatParams, }; pub use client_api::entity::QuestionStreamValue; pub use client_api::entity::*; diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index ba5ff431e9..3180227ed0 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -76,11 +76,10 @@ impl Chat { preferred_ai_model: Option, ) -> Result { trace!( - "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", + "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, format={:?}", self.chat_id, params.message, params.message_type, - params.metadata, params.format, ); @@ -116,14 +115,6 @@ impl Chat { .send(StreamMessage::MessageId(question.message_id).to_string()) .await; - if let Err(err) = self - .chat_service - .index_message_metadata(&self.chat_id, ¶ms.metadata, &mut question_sink) - .await - { - error!("Failed to index file: {}", err); - } - // Save message to disk notify_message(&self.chat_id, question.clone())?; let format = params.format.clone().map(Into::into).unwrap_or_default(); diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index b62899eca3..5a4aecbbd7 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -2,9 +2,8 @@ use crate::local_ai::controller::LocalAISetting; use crate::local_ai::resource::PendingResource; use af_plugin::core::plugin::RunningState; use flowy_ai_pub::cloud::{ - AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel, - OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, - ResponseFormat, + AIModel, ChatMessage, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, + RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -71,9 +70,6 @@ pub struct StreamChatPayloadPB { #[pb(index = 6, one_of)] pub format: Option, - - #[pb(index = 7)] - pub metadata: Vec, } #[derive(Default, Debug)] @@ -84,7 +80,6 @@ pub struct StreamMessageParams { pub answer_stream_port: i64, pub question_stream_port: i64, pub format: Option, - pub metadata: Vec, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 6788685102..85f2dc8306 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -2,16 +2,13 @@ use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; use crate::completion::AICompletion; use crate::entities::*; use crate::util::ai_available_models_key; -use flowy_ai_pub::cloud::{ - AIModel, ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader, -}; +use flowy_ai_pub::cloud::{AIModel, ChatMessageType}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use std::fs; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::trace; use uuid::Uuid; use validator::Validate; @@ -37,7 +34,6 @@ pub(crate) async fn stream_chat_message_handler( answer_stream_port, question_stream_port, format, - metadata, } = data; let message_type = match message_type { @@ -45,32 +41,6 @@ pub(crate) async fn stream_chat_message_handler( ChatMessageTypePB::User => ChatMessageType::User, }; - let metadata = metadata - .into_iter() - .map(|metadata| { - let (content_type, content_len) = match metadata.loader_type { - ContextLoaderTypePB::Txt => (ContextLoader::Text, metadata.data.len()), - ContextLoaderTypePB::Markdown => (ContextLoader::Markdown, metadata.data.len()), - ContextLoaderTypePB::PDF => (ContextLoader::PDF, 0), - ContextLoaderTypePB::UnknownLoaderType => (ContextLoader::Unknown, 0), - }; - - ChatMessageMetadata { - data: ChatRAGData { - content: metadata.data, - content_type, - size: content_len as i64, - }, - id: metadata.id, - name: metadata.name.clone(), - source: metadata.source, - extra: None, - } - }) - .collect::>(); - - trace!("Stream chat message with metadata: {:?}", metadata); - let chat_id = Uuid::from_str(&chat_id)?; let params = StreamMessageParams { chat_id, @@ -79,7 +49,6 @@ pub(crate) async fn stream_chat_message_handler( answer_stream_port, question_stream_port, format, - metadata, }; let ai_manager = upgrade_ai_manager(ai_manager)?; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 3c4ccb1f9d..b9dc7a73c1 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -6,7 +6,6 @@ use crate::notification::{ }; use af_plugin::manager::PluginManager; use anyhow::Error; -use flowy_ai_pub::cloud::{ChatMessageMetadata, ContextLoader}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; @@ -21,9 +20,8 @@ use arc_swap::ArcSwapOption; use futures_util::SinkExt; use lib_infra::util::get_operating_system; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use tokio::select; use tokio_stream::StreamExt; @@ -383,58 +381,59 @@ impl LocalAIController { Ok(enabled) } - #[instrument(level = "debug", skip_all)] - pub async fn index_message_metadata( - &self, - chat_id: &Uuid, - metadata_list: &[ChatMessageMetadata], - index_process_sink: &mut (impl Sink + Unpin), - ) -> FlowyResult<()> { - if !self.is_enabled() { - info!("[AI Plugin] local ai is disabled, skip indexing"); - return Ok(()); - } - - for metadata in metadata_list { - let mut file_metadata = HashMap::new(); - file_metadata.insert("id".to_string(), json!(&metadata.id)); - file_metadata.insert("name".to_string(), json!(&metadata.name)); - file_metadata.insert("source".to_string(), json!(&metadata.source)); - - let file_path = Path::new(&metadata.data.content); - if !file_path.exists() { - return Err( - FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), - ); - } - info!( - "[AI Plugin] embed file: {:?}, with metadata: {:?}", - file_path, file_metadata - ); - - match &metadata.data.content_type { - ContextLoader::Unknown => { - error!( - "[AI Plugin] unsupported content type: {:?}", - metadata.data.content_type - ); - }, - ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { - self - .process_index_file( - chat_id, - file_path.to_path_buf(), - &file_metadata, - index_process_sink, - ) - .await?; - }, - } - } - - Ok(()) - } + // #[instrument(level = "debug", skip_all)] + // pub async fn index_message_metadata( + // &self, + // chat_id: &Uuid, + // metadata_list: &[ChatMessageMetadata], + // index_process_sink: &mut (impl Sink + Unpin), + // ) -> FlowyResult<()> { + // if !self.is_enabled() { + // info!("[AI Plugin] local ai is disabled, skip indexing"); + // return Ok(()); + // } + // + // for metadata in metadata_list { + // let mut file_metadata = HashMap::new(); + // file_metadata.insert("id".to_string(), json!(&metadata.id)); + // file_metadata.insert("name".to_string(), json!(&metadata.name)); + // file_metadata.insert("source".to_string(), json!(&metadata.source)); + // + // let file_path = Path::new(&metadata.data.content); + // if !file_path.exists() { + // return Err( + // FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), + // ); + // } + // info!( + // "[AI Plugin] embed file: {:?}, with metadata: {:?}", + // file_path, file_metadata + // ); + // + // match &metadata.data.content_type { + // ContextLoader::Unknown => { + // error!( + // "[AI Plugin] unsupported content type: {:?}", + // metadata.data.content_type + // ); + // }, + // ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { + // self + // .process_index_file( + // chat_id, + // file_path.to_path_buf(), + // &file_metadata, + // index_process_sink, + // ) + // .await?; + // }, + // } + // } + // + // Ok(()) + // } + #[allow(dead_code)] async fn process_index_file( &self, chat_id: &Uuid, diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index ff5c608f22..22a2bec674 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -9,19 +9,17 @@ use flowy_ai_pub::persistence::select_message_content; use std::collections::HashMap; use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageMetadata, - ChatMessageType, ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, - RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, - StreamComplete, UpdateChatParams, + AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, + ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, + RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, + UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; -use futures::{stream, Sink, StreamExt, TryStreamExt}; +use futures::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use crate::local_ai::stream_util::QuestionStream; -use crate::stream_message::StreamMessage; use flowy_storage_pub::storage::StorageService; -use futures_util::SinkExt; use serde_json::{json, Value}; use std::path::Path; use std::sync::{Arc, Weak}; @@ -32,6 +30,7 @@ pub struct ChatServiceMiddleware { cloud_service: Arc, user_service: Arc, local_ai: Arc, + #[allow(dead_code)] storage_service: Weak, } @@ -50,34 +49,6 @@ impl ChatServiceMiddleware { } } - pub async fn index_message_metadata( - &self, - chat_id: &Uuid, - metadata_list: &[ChatMessageMetadata], - index_process_sink: &mut (impl Sink + Unpin), - ) -> Result<(), FlowyError> { - if metadata_list.is_empty() { - return Ok(()); - } - if self.local_ai.is_enabled() { - let _ = index_process_sink - .send(StreamMessage::IndexStart.to_string()) - .await; - let result = self - .local_ai - .index_message_metadata(chat_id, metadata_list, index_process_sink) - .await; - let _ = index_process_sink - .send(StreamMessage::IndexEnd.to_string()) - .await; - - result? - } else if let Some(_storage_service) = self.storage_service.upgrade() { - // - } - Ok(()) - } - fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs index 3e76282aa8..3f7b37bd34 100644 --- a/frontend/rust-lib/flowy-ai/src/stream_message.rs +++ b/frontend/rust-lib/flowy-ai/src/stream_message.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +#[allow(dead_code)] pub enum StreamMessage { MessageId(i64), IndexStart, diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f3caa86e66..91c9aa8162 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -128,15 +128,15 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, ); From 394ac85c3265ae4c071206dc629ea84a8b9a6ae2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:31:50 +0800 Subject: [PATCH 19/74] refactor: server type name --- .../src/deps_resolve/cloud_service_impl.rs | 24 +-- frontend/rust-lib/flowy-core/src/lib.rs | 10 +- .../rust-lib/flowy-core/src/server_layer.rs | 199 ++++++++---------- .../flowy-core/src/user_state_callback.rs | 34 ++- .../src/af_cloud/impls/user/dto.rs | 7 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 10 +- .../rust-lib/flowy-user-pub/src/entities.rs | 34 +-- .../rust-lib/flowy-user/src/entities/auth.rs | 14 +- .../rust-lib/flowy-user/src/event_handler.rs | 22 +- frontend/rust-lib/flowy-user/src/event_map.rs | 22 +- .../src/migrations/doc_key_with_workspace.rs | 4 +- .../src/migrations/document_empty_content.rs | 6 +- .../flowy-user/src/migrations/migration.rs | 6 +- .../migrations/workspace_and_favorite_v1.rs | 4 +- .../src/migrations/workspace_trash_v1.rs | 4 +- .../data_import/appflowy_data_import.rs | 4 +- .../src/services/sqlite_sql/user_sql.rs | 6 +- .../flowy-user/src/user_manager/manager.rs | 60 +++--- .../src/user_manager/manager_history_user.rs | 7 +- .../user_manager/manager_user_awareness.rs | 15 +- .../src/user_manager/user_login_state.rs | 4 +- 21 files changed, 224 insertions(+), 272 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 3a7d648133..9c1ee9462c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,4 +1,4 @@ -use crate::server_layer::{Server, ServerProvider}; +use crate::server_layer::{ServerProvider, ServerType}; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::workspace_dto::PublishInfoView; @@ -35,7 +35,7 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration; 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 flowy_user_pub::entities::{AuthType, UserTokenState}; use lib_infra::async_trait::async_trait; use serde_json::Value; use std::collections::HashMap; @@ -186,18 +186,18 @@ impl UserCloudServiceProvider for ServerProvider { } } - /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. + /// When user login, the provider type is set by the [AuthType] and save to disk for next use. /// - /// Each [Authenticator] has a corresponding [Server]. The [Server] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [Server] is set, + /// Each [AuthType] has a corresponding [ServerType]. The [ServerType] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, /// it will be used when user open the app again. /// - fn set_user_authenticator(&self, authenticator: &Authenticator) { - self.set_authenticator(authenticator.clone()); + fn set_auth_type(&self, auth_type: &AuthType) { + self.set_auth_type(*auth_type); } - fn get_user_authenticator(&self) -> Authenticator { - self.get_authenticator() + fn get_auth_type(&self) -> AuthType { + self.get_auth_type() } fn set_network_reachable(&self, reachable: bool) { @@ -211,7 +211,7 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.set_secret(secret); } - /// Returns the [UserCloudService] base on the current [Server]. + /// Returns the [UserCloudService] base on the current [ServerType]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let user_service = self.get_server()?.user_service(); @@ -220,8 +220,8 @@ impl UserCloudServiceProvider for ServerProvider { fn service_url(&self) -> String { match self.get_server_type() { - Server::Local => "".to_string(), - Server::AppFlowyCloud => AFCloudConfiguration::from_env() + ServerType::Local => "".to_string(), + ServerType::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index dbbce02136..bd55c8db1d 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, Server, ServerProvider}; +use crate::server_layer::{current_server_type, ServerProvider, ServerType}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -314,11 +314,11 @@ impl AppFlowyCore { } } -impl From for CollabPluginProviderType { - fn from(server_type: Server) -> Self { +impl From for CollabPluginProviderType { + fn from(server_type: ServerType) -> Self { match server_type { - Server::Local => CollabPluginProviderType::Local, - Server::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + ServerType::Local => CollabPluginProviderType::Local, + ServerType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index a3d6de7003..c0bf043d27 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,179 +1,156 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; -use arc_swap::ArcSwapOption; +use arc_swap::{ArcSwap, ArcSwapOption}; use dashmap::DashMap; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::{AIUserServiceImpl, LoginUserService}; -use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::af_cloud::{ + define::{AIUserServiceImpl, LoginUserService}, + AppFlowyCloudServer, +}; use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; -use serde_repr::*; -use std::fmt::{Display, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +/// ServerType: local or cloud #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] -pub enum Server { - /// Local server provider. - /// Offline mode, no user authentication and the data is stored locally. +pub enum ServerType { Local = 0, - /// AppFlowy Cloud server provider. - /// See: https://github.com/AppFlowy-IO/AppFlowy-Cloud AppFlowyCloud = 1, } -impl Server { +impl ServerType { pub fn is_local(&self) -> bool { - matches!(self, Server::Local) + matches!(self, Self::Local) } } -impl Display for Server { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Server::Local => write!(f, "Local"), - Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), +impl Display for ServerType { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{:?}", self) + } +} + +/// Conversion between AuthType and ServerType +impl From<&AuthType> for ServerType { + fn from(a: &AuthType) -> Self { + match a { + AuthType::Local => ServerType::Local, + AuthType::AppFlowyCloud => ServerType::AppFlowyCloud, + } + } +} +impl From for AuthType { + fn from(s: ServerType) -> Self { + match s { + ServerType::Local => AuthType::Local, + ServerType::AppFlowyCloud => AuthType::AppFlowyCloud, } } } -/// The [ServerProvider] provides list of [AppFlowyServer] base on the [Authenticator]. Using -/// the auth type, the [ServerProvider] will create a new [AppFlowyServer] if it doesn't -/// exist. -/// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: DashMap>, - pub(crate) encryption: Arc, - #[allow(dead_code)] - pub(crate) store_preferences: Weak, - pub(crate) user_enable_sync: AtomicBool, - - /// The authenticator type of the user. - authenticator: AtomicU8, + providers: DashMap>, + auth_type: ArcSwap, user: Arc, - pub(crate) uid: Arc>, pub local_ai: Arc, + pub uid: Arc>, + pub user_enable_sync: Arc, + pub encryption: Arc, } impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - server: Server, + initial: ServerType, store_preferences: Weak, - server_user: impl LoginUserService + 'static, + user_service: impl LoginUserService + 'static, ) -> Self { - let user = Arc::new(server_user); - let encryption = EncryptionImpl::new(None); - let user_service = Arc::new(AIUserServiceImpl(user.clone())); - let plugin_manager = Arc::new(PluginManager::new()); + let user = Arc::new(user_service); + let initial_auth = AuthType::from(initial); + let auth_type = ArcSwap::from(Arc::new(initial_auth)); + let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; + let ai_user = Arc::new(AIUserServiceImpl(user.clone())); + let plugins = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( - plugin_manager.clone(), - store_preferences.clone(), - user_service.clone(), + plugins, + store_preferences, + ai_user.clone(), )); - Self { + ServerProvider { config, 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(), + encryption, + user_enable_sync: Arc::new(AtomicBool::new(true)), + auth_type, user, + uid: Default::default(), local_ai, } } - pub fn get_server_type(&self) -> Server { - match Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) { - Authenticator::Local => Server::Local, - Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + /// Reads current type + pub fn get_server_type(&self) -> ServerType { + let auth_type = self.auth_type.load_full(); + ServerType::from(auth_type.as_ref()) + } + + pub fn set_auth_type(&self, a: AuthType) { + let old_type = self.get_server_type(); + self.auth_type.store(Arc::new(a)); + let new_type = self.get_server_type(); + if old_type != new_type { + self.providers.remove(&old_type); } } - pub fn set_authenticator(&self, authenticator: Authenticator) { - let old_server_type = self.get_server_type(); - 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.remove(&old_server_type); - } + pub fn get_auth_type(&self) -> AuthType { + *self.auth_type.load_full().as_ref() } - pub fn get_authenticator(&self) -> Authenticator { - Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) - } - - /// Returns a [AppFlowyServer] trait implementation base on the provider_type. + /// Lazily create or fetch an AppFlowyServer instance pub fn get_server(&self) -> FlowyResult> { - let server_type = self.get_server_type(); - - if let Some(provider) = self.providers.get(&server_type) { - return Ok(provider.value().clone()); + let key = self.get_server_type(); + if let Some(entry) = self.providers.get(&key) { + return Ok(entry.clone()); } - let server = match server_type { - Server::Local => { - let server = Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())); - Ok::, FlowyError>(server) - }, - Server::AppFlowyCloud => { - let config = self.config.cloud_config.clone().ok_or_else(|| { - FlowyError::internal().with_context("AppFlowyCloud configuration is missing") - })?; - let server = Arc::new(AppFlowyCloudServer::new( - config, + let server: Arc = match key { + ServerType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + ServerType::AppFlowyCloud => { + let cfg = self + .config + .cloud_config + .clone() + .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?; + Arc::new(AppFlowyCloudServer::new( + cfg, self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), self.user.clone(), - )); - - Ok::, FlowyError>(server) + )) }, - }?; + }; - self.providers.insert(server_type.clone(), server.clone()); + self.providers.insert(key.clone(), server.clone()); Ok(server) } } -impl From for Server { - fn from(auth_provider: Authenticator) -> Self { - match auth_provider { - Authenticator::Local => Server::Local, - Authenticator::AppFlowyCloud => Server::AppFlowyCloud, - } - } -} - -impl From for Authenticator { - fn from(ty: Server) -> Self { - match ty { - Server::Local => Authenticator::Local, - Server::AppFlowyCloud => Authenticator::AppFlowyCloud, - } - } -} -impl From<&Authenticator> for Server { - fn from(auth_provider: &Authenticator) -> Self { - Self::from(auth_provider.clone()) - } -} - -pub fn current_server_type() -> Server { +/// Determine current server type from ENV +pub fn current_server_type() -> ServerType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => Server::Local, - AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, + AuthenticatorType::Local => ServerType::Local, + AuthenticatorType::AppFlowyCloud => ServerType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 2882b9b050..89ba14e6d2 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -14,11 +14,11 @@ use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; -use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; +use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; -use crate::server_layer::{Server, ServerProvider}; +use crate::server_layer::{ServerProvider, ServerType}; pub(crate) struct UserStatusCallbackImpl { pub(crate) collab_builder: Arc, @@ -49,16 +49,14 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn did_init( &self, user_id: i64, - user_authenticator: &Authenticator, + auth_type: &AuthType, cloud_config: &Option, user_workspace: &UserWorkspace, _device_id: &str, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; - self - .server_provider - .set_user_authenticator(user_authenticator); + self.server_provider.set_auth_type(*auth_type); if let Some(cloud_config) = cloud_config { self @@ -83,7 +81,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator == &Authenticator::Local) + .initialize(user_id, authenticator == &AuthType::Local) .await?; self.document_manager.initialize(user_id).await?; @@ -97,7 +95,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_id: i64, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { event!( tracing::Level::TRACE, @@ -127,11 +125,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_profile: &UserProfile, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { - self - .server_provider - .set_user_authenticator(&user_profile.authenticator); + self.server_provider.set_auth_type(*auth_type); let server_type = self.server_provider.get_server_type(); event!( @@ -159,16 +155,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await { Ok(doc_state) => match server_type { - Server::Local => FolderInitDataSource::LocalDisk { + ServerType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - Server::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + ServerType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), }, Err(err) => match server_type { - Server::Local => FolderInitDataSource::LocalDisk { + ServerType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - Server::AppFlowyCloud => { + ServerType::AppFlowyCloud => { return Err(err); }, }, @@ -188,7 +184,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .database_manager - .initialize_with_new_user(user_profile.uid, authenticator.is_local()) + .initialize_with_new_user(user_profile.uid, auth_type.is_local()) .await .context("DatabaseManager error")?; @@ -212,7 +208,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { &self, user_id: i64, user_workspace: &UserWorkspace, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { self .folder_manager diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index 0710bcc2b2..eb2bf26698 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -3,9 +3,8 @@ use client_api::entity::auth_dto::{UpdateUserParams, UserMetaData}; use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFWorkspaceMember}; use flowy_user_pub::entities::{ - Authenticator, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, - WorkspaceMember, USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, - USER_METADATA_STABILITY_AI_KEY, + AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, + USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, }; use crate::af_cloud::impls::user::util::encryption_type_from_profile; @@ -60,7 +59,7 @@ pub fn user_profile_from_af_profile( icon_url: icon_url.unwrap_or_default(), openai_key: openai_key.unwrap_or_default(), stability_ai_key: stability_ai_key.unwrap_or_default(), - authenticator: Authenticator::AppFlowyCloud, + authenticator: AuthType::AppFlowyCloud, encryption_type, uid: profile.uid, updated_at: profile.updated_at, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dde94aebdc..fd283360de 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -20,7 +20,7 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, Authenticator, Role, UpdateUserProfileParams, UserCredentials, UserProfile, + AuthResponse, AuthType, Role, UpdateUserProfileParams, UserCredentials, UserProfile, UserTokenState, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; @@ -84,13 +84,9 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - /// Sets the authenticator when user sign in or sign up. - /// - /// # Arguments - /// * `authenticator`: An `Authenticator` object. - fn set_user_authenticator(&self, authenticator: &Authenticator); + fn set_auth_type(&self, auth_type: &AuthType); - fn get_user_authenticator(&self) -> Authenticator; + fn get_auth_type(&self) -> AuthType; /// Sets the network reachability /// diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 857a735edb..c09b536ccf 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -32,7 +32,7 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, - pub auth_type: Authenticator, + pub auth_type: AuthType, } #[derive(Serialize, Deserialize, Default, Debug)] @@ -40,7 +40,7 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, - pub auth_type: Authenticator, + pub auth_type: AuthType, pub device_id: String, } @@ -103,7 +103,7 @@ impl UserAuthResponse for AuthResponse { #[derive(Clone, Debug)] pub struct UserCredentials { - /// Currently, the token is only used when the [Authenticator] is AppFlowyCloud + /// Currently, the token is only used when the [AuthType] is AppFlowyCloud pub token: Option, /// The user id @@ -180,7 +180,7 @@ pub struct UserProfile { pub icon_url: String, pub openai_key: String, pub stability_ai_key: String, - pub authenticator: Authenticator, + pub authenticator: AuthType, // If the encryption_sign is not empty, which means the user has enabled the encryption. pub encryption_type: EncryptionType, pub updated_at: i64, @@ -226,11 +226,11 @@ impl FromStr for EncryptionType { } } -impl From<(&T, &Authenticator)> for UserProfile +impl From<(&T, &AuthType)> for UserProfile where T: UserAuthResponse, { - fn from(params: (&T, &Authenticator)) -> Self { + fn from(params: (&T, &AuthType)) -> Self { let (value, auth_type) = params; let (icon_url, openai_key, stability_ai_key) = { value @@ -258,7 +258,7 @@ where token: value.user_token().unwrap_or_default(), icon_url, openai_key, - authenticator: auth_type.clone(), + authenticator: *auth_type, encryption_type: value.encryption_type(), stability_ai_key, updated_at: value.updated_at(), @@ -349,9 +349,9 @@ impl UpdateUserProfileParams { } } -#[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] -pub enum Authenticator { +pub enum AuthType { /// It's a local server, we do fake sign in default. Local = 0, /// Currently not supported. It will be supported in the future when the @@ -359,28 +359,28 @@ pub enum Authenticator { AppFlowyCloud = 1, } -impl Default for Authenticator { +impl Default for AuthType { fn default() -> Self { Self::Local } } -impl Authenticator { +impl AuthType { pub fn is_local(&self) -> bool { - matches!(self, Authenticator::Local) + matches!(self, AuthType::Local) } pub fn is_appflowy_cloud(&self) -> bool { - matches!(self, Authenticator::AppFlowyCloud) + matches!(self, AuthType::AppFlowyCloud) } } -impl From for Authenticator { +impl From for AuthType { fn from(value: i32) -> Self { match value { - 0 => Authenticator::Local, - 1 => Authenticator::AppFlowyCloud, - _ => Authenticator::Local, + 0 => AuthType::Local, + 1 => AuthType::AppFlowyCloud, + _ => AuthType::Local, } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 87e828199c..6a889359c1 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -235,20 +235,20 @@ pub enum AuthenticatorPB { AppFlowyCloud = 2, } -impl From for AuthenticatorPB { - fn from(auth_type: Authenticator) -> Self { +impl From for AuthenticatorPB { + fn from(auth_type: AuthType) -> Self { match auth_type { - Authenticator::Local => AuthenticatorPB::Local, - Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, + AuthType::Local => AuthenticatorPB::Local, + AuthType::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, } } } -impl From for Authenticator { +impl From for AuthType { fn from(pb: AuthenticatorPB) -> Self { match pb { - AuthenticatorPB::Local => Authenticator::Local, - AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, + AuthenticatorPB::Local => AuthType::Local, + AuthenticatorPB::AppFlowyCloud => AuthType::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index c0b7a8d6c2..a20229b6d0 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -45,16 +45,14 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_user_authenticator(); + let old_authenticator = manager.cloud_services.get_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), Err(err) => { - manager - .cloud_services - .set_user_authenticator(&old_authenticator); + manager.cloud_services.set_auth_type(&old_authenticator); return Err(err); }, } @@ -76,15 +74,13 @@ pub async fn sign_up( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: SignUpParams = data.into_inner().try_into()?; - let authenticator = params.auth_type.clone(); + let auth_type = params.auth_type; - let prev_authenticator = manager.cloud_services.get_user_authenticator(); - match manager.sign_up(authenticator, BoxAny::new(params)).await { + let prev_auth_type = manager.cloud_services.get_auth_type(); + match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager - .cloud_services - .set_user_authenticator(&prev_authenticator); + manager.cloud_services.set_auth_type(&prev_auth_type); Err(err) }, } @@ -119,7 +115,7 @@ pub async fn get_user_profile_handler( // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.authenticator == Authenticator::Local { + if user_profile.authenticator == AuthType::Local { user_profile.email = "".to_string(); } @@ -341,7 +337,7 @@ pub async fn oauth_sign_in_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: Authenticator = params.authenticator.into(); + let authenticator: AuthType = params.authenticator.into(); let user_profile = manager .sign_up(authenticator, BoxAny::new(params.map)) .await?; @@ -355,7 +351,7 @@ pub async fn gen_sign_in_url_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: Authenticator = params.authenticator.into(); + let authenticator: AuthType = params.authenticator.into(); let sign_in_url = manager .generate_sign_in_url_with_email(&authenticator, ¶ms.email) .await?; diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 7ed4948771..953011bc1c 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -86,12 +86,12 @@ pub fn init(user_manager: Weak) -> AFPlugin { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { - /// Only use when the [Authenticator] is Local or SelfHosted + /// Only use when the [AuthType] is Local or SelfHosted /// Logging into an account using a register email and password #[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")] SignInWithEmailPassword = 0, - /// Only use when the [Authenticator] is Local or SelfHosted + /// Only use when the [AuthType] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, @@ -129,7 +129,7 @@ pub enum UserEvent { OauthSignIn = 10, /// Get the OAuth callback url - /// Only use when the [Authenticator] is AFCloud + /// Only use when the [AuthType] is AFCloud #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")] GenerateSignInURL = 11, @@ -165,7 +165,7 @@ pub enum UserEvent { OpenAnonUser = 26, /// Push a realtime event to the user. Currently, the realtime event - /// is only used when the auth type is: [Authenticator::Supabase]. + /// is only used when the auth type is: [AuthType::Supabase]. /// #[event(input = "RealtimePayloadPB")] PushRealtimeEvent = 27, @@ -281,19 +281,19 @@ pub enum UserEvent { #[async_trait] pub trait UserStatusCallback: Send + Sync + 'static { - /// When the [Authenticator] changed, this method will be called. Currently, the auth type + /// When the [AuthType] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn authenticator_did_changed(&self, _authenticator: Authenticator) {} + fn authenticator_did_changed(&self, _authenticator: AuthType) {} /// This will be called after the application launches if the user is already signed in. /// If the user is not signed in, this method will not be called async fn did_init( &self, _user_id: i64, - _user_authenticator: &Authenticator, + _user_authenticator: &AuthType, _cloud_config: &Option, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -303,7 +303,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -314,7 +314,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_profile: &UserProfile, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &Authenticator, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -326,7 +326,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { &self, _user_id: i64, _user_workspace: &UserWorkspace, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 3056f4d945..5a8bb4d516 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -7,7 +7,7 @@ use tracing::{instrument, trace}; use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use flowy_user_pub::session::Session; @@ -39,7 +39,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { trace!( "migrate key with workspace id:{}", 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 e557c22450..ab828e18d8 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 @@ -11,7 +11,7 @@ use tracing::{event, instrument}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -41,12 +41,12 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { &self, session: &Session, collab_db: &Arc, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. // - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version. - if !matches!(authenticator, Authenticator::Local) { + if !matches!(authenticator, AuthType::Local) { return Ok(()); } collab_db.with_write_txn(|write_txn| { diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index c604e47e8d..1330d1995b 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -7,7 +7,7 @@ use flowy_error::FlowyResult; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_data_migration_records; use flowy_sqlite::ConnectionPool; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use flowy_user_pub::session::Session; use semver::Version; use tracing::info; @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - authenticator: &Authenticator, + authenticator: &AuthType, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -98,7 +98,7 @@ pub trait UserDataMigration { &self, user: &Session, collab_db: &Arc, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> FlowyResult<()>; } 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 ec55b5fe29..51894f9a04 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 @@ -7,7 +7,7 @@ use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -39,7 +39,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( 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 d631e32e78..70123edb2c 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 @@ -7,7 +7,7 @@ use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -37,7 +37,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &Authenticator, + _authenticator: &AuthType, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( 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 9efbf81932..106e911c1e 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 @@ -30,7 +30,7 @@ use flowy_folder_pub::entities::{ }; 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::entities::{user_awareness_object_id, AuthType}; use flowy_user_pub::session::Session; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; @@ -1175,7 +1175,7 @@ pub async fn upload_collab_objects_data( uid: i64, user_collab_db: Weak, workspace_id: &Uuid, - user_authenticator: &Authenticator, + user_authenticator: &AuthType, collab_data: ImportedCollabData, user_cloud_service: Arc, ) -> Result<(), FlowyError> { diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index e2b6628832..1138efd092 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -31,8 +31,8 @@ pub struct UserTable { } #[allow(deprecated)] -impl From<(UserProfile, Authenticator)> for UserTable { - fn from(value: (UserProfile, Authenticator)) -> Self { +impl From<(UserProfile, AuthType)> for UserTable { + fn from(value: (UserProfile, AuthType)) -> Self { let (user_profile, auth_type) = value; let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTable { @@ -62,7 +62,7 @@ impl From for UserProfile { token: table.token, icon_url: table.icon_url, openai_key: table.openai_key, - authenticator: Authenticator::from(table.auth_type), + authenticator: AuthType::from(table.auth_type), encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), stability_ai_key: table.stability_ai_key, updated_at: table.updated_at, 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 5b5d060539..89a05539b6 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -141,7 +141,7 @@ impl UserManager { // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.authenticator != Authenticator::Local && user.authenticator != current_authenticator { + if user.authenticator != AuthType::Local && user.authenticator != current_authenticator { event!( tracing::Level::INFO, "Authenticator changed from {:?} to {:?}", @@ -349,9 +349,9 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - authenticator: Authenticator, + authenticator: AuthType, ) -> Result { - self.cloud_services.set_user_authenticator(&authenticator); + self.cloud_services.set_auth_type(&authenticator); let response: AuthResponse = self .cloud_services @@ -398,25 +398,25 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_up( &self, - authenticator: Authenticator, + auth_type: AuthType, params: BoxAny, ) -> Result { // sign out the current user if there is one - let migration_user = self.get_migration_user(&authenticator).await; - self.cloud_services.set_user_authenticator(&authenticator); + let migration_user = self.get_migration_user(&auth_type).await; + self.cloud_services.set_auth_type(&auth_type); let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; - let new_user_profile = UserProfile::from((&response, &authenticator)); + let new_user_profile = UserProfile::from((&response, &auth_type)); if new_user_profile.encryption_type.require_encrypt_secret() { self.auth_process.lock().await.replace(UserAuthProcess { user_profile: new_user_profile.clone(), migration_user, response, - authenticator, + authenticator: auth_type, }); } else { self - .continue_sign_up(&new_user_profile, migration_user, response, &authenticator) + .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) .await?; } Ok(new_user_profile) @@ -450,12 +450,12 @@ impl UserManager { new_user_profile: &UserProfile, migration_user: Option, response: AuthResponse, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, authenticator, &new_session) + .save_auth_data(&response, auth_type, &new_session) .await?; let _ = self .initial_user_awareness(&new_session, &new_user_profile.authenticator) @@ -469,7 +469,7 @@ impl UserManager { new_user_profile, &new_session.user_workspace, &self.authenticate_user.user_config.device_id, - authenticator, + auth_type, ) .await?; @@ -493,7 +493,7 @@ impl UserManager { new_user_profile.uid ); self - .migrate_anon_user_data_to_cloud(&old_user, &new_session, authenticator) + .migrate_anon_user_data_to_cloud(&old_user, &new_session, auth_type) .await?; self.remove_anon_user(); let _ = self @@ -709,10 +709,10 @@ impl UserManager { pub(crate) async fn generate_sign_in_url_with_email( &self, - authenticator: &Authenticator, + authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_user_authenticator(authenticator); + self.cloud_services.set_auth_type(authenticator); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; @@ -724,9 +724,7 @@ impl UserManager { email: &str, password: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) @@ -737,9 +735,7 @@ impl UserManager { email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) @@ -752,9 +748,7 @@ impl UserManager { email: &str, passcode: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) @@ -764,9 +758,7 @@ impl UserManager { &self, oauth_provider: &str, ) -> Result { - self - .cloud_services - .set_user_authenticator(&Authenticator::AppFlowyCloud); + self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) @@ -778,7 +770,7 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - authenticator: &Authenticator, + authenticator: &AuthType, session: &Session, ) -> Result<(), FlowyError> { let user_profile = UserProfile::from((response, authenticator)); @@ -798,7 +790,7 @@ impl UserManager { .authenticate_user .set_session(Some(session.clone().into()))?; self - .save_user(uid, (user_profile, authenticator.clone()).into()) + .save_user(uid, (user_profile, *authenticator).into()) .await?; Ok(()) } @@ -827,14 +819,14 @@ impl UserManager { &self, old_user: &AnonUser, _new_user_session: &Session, - authenticator: &Authenticator, + authenticator: &AuthType, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - if authenticator == &Authenticator::AppFlowyCloud { + if authenticator == &AuthType::AppFlowyCloud { self .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) .await?; @@ -853,10 +845,10 @@ impl UserManager { } } -fn current_authenticator() -> Authenticator { +fn current_authenticator() -> AuthType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => Authenticator::Local, - AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud, + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::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 b7f4789f9a..ddb73667c8 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 @@ -4,7 +4,7 @@ use tracing::instrument; use crate::entities::UserProfilePB; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::Authenticator; +use flowy_user_pub::entities::AuthType; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; @@ -12,10 +12,7 @@ use flowy_user_pub::session::Session; pub const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] - pub async fn get_migration_user( - &self, - current_authenticator: &Authenticator, - ) -> Option { + pub async fn get_migration_user(&self, current_authenticator: &AuthType) -> Option { // No need to migrate if the user is already local if current_authenticator.is_local() { return None; 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 d055621398..7de09995a0 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 @@ -12,7 +12,7 @@ use collab_integrate::CollabKVDB; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; use dashmap::try_result::TryResult; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; +use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; use tracing::{error, info, instrument, trace}; use uuid::Uuid; @@ -119,9 +119,8 @@ impl UserManager { pub(crate) async fn initial_user_awareness( &self, session: &Session, - authenticator: &Authenticator, + auth_type: &AuthType, ) -> FlowyResult<()> { - let authenticator = authenticator.clone(); let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); // Try to acquire mutable access to `is_loading_awareness`. @@ -156,11 +155,11 @@ impl UserManager { let is_exist_on_disk = self .authenticate_user .is_collab_on_disk(session.user_id, &object_id.to_string())?; - if authenticator.is_local() || is_exist_on_disk { + if auth_type.is_local() || is_exist_on_disk { trace!( "Initializing new user awareness from disk:{}, {:?}", object_id, - authenticator + auth_type ); let collab_db = self.get_collab_db(session.user_id)?; let workspace_id = session.user_workspace.workspace_id()?; @@ -185,9 +184,9 @@ impl UserManager { } else { info!( "Initializing new user awareness from server:{}, {:?}", - object_id, authenticator + object_id, auth_type ); - self.load_awareness_from_server(session, object_id, authenticator.clone())?; + self.load_awareness_from_server(session, object_id, *auth_type)?; } } else { return Err(FlowyError::new( @@ -209,7 +208,7 @@ impl UserManager { &self, session: &Session, object_id: Uuid, - authenticator: Authenticator, + authenticator: AuthType, ) -> FlowyResult<()> { // Clone necessary data let session = session.clone(); diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs index 906002ad10..ed7fdacf8d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs @@ -1,11 +1,11 @@ use crate::migrations::AnonUser; -use flowy_user_pub::entities::{AuthResponse, Authenticator, UserProfile}; +use flowy_user_pub::entities::{AuthResponse, AuthType, UserProfile}; /// recording the intermediate state of the sign-in/sign-up process #[derive(Clone)] pub struct UserAuthProcess { pub user_profile: UserProfile, pub response: AuthResponse, - pub authenticator: Authenticator, + pub authenticator: AuthType, pub migration_user: Option, } From d8401e09c9c48468cb9c61b0c77809dc74c19a0c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:10:17 +0800 Subject: [PATCH 20/74] feat: implement keyboard triggering on buttons and add focus state (#7724) * feat: implement keyboard triggering on buttons and add focus state * chore: pass isFocused to builders --- .../button/base_button/base_button.dart | 116 +++++++++++++++--- .../button/filled_button/filled_button.dart | 2 +- .../filled_button/filled_text_button.dart | 2 +- .../button/ghost_button/ghost_button.dart | 2 +- .../ghost_button/ghost_icon_text_button.dart | 2 +- .../ghost_button/ghost_text_button.dart | 2 +- .../outlined_button/outlined_button.dart | 8 +- .../outlined_icon_text_button.dart | 8 +- .../outlined_button/outlined_text_button.dart | 8 +- 9 files changed, 113 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart index 22c5325681..b62f2cce9b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -7,6 +7,13 @@ typedef AFBaseButtonColorBuilder = Color Function( bool disabled, ); +typedef AFBaseButtonBorderColorBuilder = Color Function( + BuildContext context, + bool isHovering, + bool disabled, + bool isFocused, +); + class AFBaseButton extends StatefulWidget { const AFBaseButton({ super.key, @@ -16,49 +23,99 @@ class AFBaseButton extends StatefulWidget { required this.borderRadius, this.borderColor, this.backgroundColor, + this.ringColor, this.disabled = false, }); final VoidCallback? onTap; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? ringColor; final AFBaseButtonColorBuilder? backgroundColor; final EdgeInsetsGeometry padding; final double borderRadius; final bool disabled; - final Widget Function(BuildContext context, bool isHovering, bool disabled) - builder; + final Widget Function( + BuildContext context, + bool isHovering, + bool disabled, + ) builder; @override State createState() => _AFBaseButtonState(); } class _AFBaseButtonState extends State { + final FocusNode focusNode = FocusNode(); + bool isHovering = false; + bool isFocused = false; + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final Color borderColor = _buildBorderColor(context); final Color backgroundColor = _buildBackgroundColor(context); + final Color ringColor = _buildRingColor(context); - return MouseRegion( - cursor: - widget.disabled ? SystemMouseCursors.basic : SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - onTap: widget.disabled ? null : widget.onTap, - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Padding( - padding: widget.padding, - child: widget.builder(context, isHovering, widget.disabled), + return Actions( + actions: { + ActivateIntent: CallbackAction( + onInvoke: (_) { + if (!widget.disabled) { + widget.onTap?.call(); + } + return; + }, + ), + }, + child: Focus( + focusNode: focusNode, + onFocusChange: (isFocused) { + setState(() => this.isFocused = isFocused); + }, + child: MouseRegion( + cursor: widget.disabled + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: GestureDetector( + onTap: widget.disabled ? null : widget.onTap, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + border: isFocused + ? Border.all( + color: ringColor, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Padding( + padding: widget.padding, + child: widget.builder( + context, + isHovering, + widget.disabled, + ), + ), + ), + ), ), ), ), @@ -67,7 +124,8 @@ class _AFBaseButtonState extends State { Color _buildBorderColor(BuildContext context) { final theme = AppFlowyTheme.of(context); - return widget.borderColor?.call(context, isHovering, widget.disabled) ?? + return widget.borderColor + ?.call(context, isHovering, widget.disabled, isFocused) ?? theme.borderColorScheme.greyTertiary; } @@ -76,4 +134,22 @@ class _AFBaseButtonState extends State { return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? theme.fillColorScheme.transparent; } + + Color _buildRingColor(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + if (widget.ringColor != null) { + return widget.ringColor! + .call(context, isHovering, widget.disabled, isFocused); + } + + if (isFocused) { + return AppFlowyTheme.of(context) + .borderColorScheme + .themeThick + .withAlpha(128); + } + + return theme.borderColorScheme.transparent; + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart index 68fb341827..e871626b59 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart @@ -115,7 +115,7 @@ class AFFilledButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart index 889ad1e429..d1b1d868d0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart @@ -121,7 +121,7 @@ class AFFilledTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart index 47ff96e878..6300c6f5a8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart @@ -86,7 +86,7 @@ class AFGhostButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart index e65eb2dd7e..af65599ea3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart @@ -109,7 +109,7 @@ class AFGhostIconTextButton extends StatelessWidget { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { return Colors.transparent; }, padding: padding ?? size.buildPadding(context), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart index 441b544f8a..d154d67dbd 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart @@ -88,7 +88,7 @@ class AFGhostTextButton extends AFBaseTextButton { return AFBaseButton( disabled: disabled, backgroundColor: backgroundColor, - borderColor: (_, __, ___) => Colors.transparent, + borderColor: (_, __, ___, ____) => Colors.transparent, padding: padding ?? size.buildPadding(context), borderRadius: borderRadius ?? size.buildBorderRadius(context), onTap: onTap, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart index 3b0ea7a06d..205d9931d6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart @@ -38,7 +38,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -79,7 +79,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: disabled, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -118,7 +118,7 @@ class AFOutlinedButton extends StatelessWidget { padding: padding, borderRadius: borderRadius, disabled: true, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -148,7 +148,7 @@ class AFOutlinedButton extends StatelessWidget { final EdgeInsetsGeometry? padding; final double? borderRadius; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; final AFOutlinedButtonWidgetBuilder builder; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart index 710a4ccca5..350594cd46 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart @@ -46,7 +46,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -101,7 +101,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { borderRadius: borderRadius, disabled: disabled, alignment: alignment, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -156,7 +156,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -190,7 +190,7 @@ class AFOutlinedIconTextButton extends StatelessWidget { final AFOutlinedIconBuilder iconBuilder; final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; final AFBaseButtonColorBuilder? backgroundColor; @override diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart index d5ae580583..d809d981b0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart @@ -40,7 +40,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -95,7 +95,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { disabled: disabled, alignment: alignment, textStyle: textStyle, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.fillColorScheme.errorThick; @@ -150,7 +150,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ? theme.textColorScheme.tertiary : theme.textColorScheme.primary; }, - borderColor: (context, isHovering, disabled) { + borderColor: (context, isHovering, disabled, isFocused) { final theme = AppFlowyTheme.of(context); if (disabled) { return theme.borderColorScheme.greyTertiary; @@ -173,7 +173,7 @@ class AFOutlinedTextButton extends AFBaseTextButton { ); } - final AFBaseButtonColorBuilder? borderColor; + final AFBaseButtonBorderColorBuilder? borderColor; @override Widget build(BuildContext context) { From 068f93c258eaad91d9a4025598d673fd1e1cacf5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:14:28 +0800 Subject: [PATCH 21/74] feat: add shadow tokens (#7726) --- .../document/presentation/editor_page.dart | 2 +- .../link/link_create_menu.dart | 2 +- .../link_preview/paste_as/paste_as_menu.dart | 2 +- .../lib/src/theme/data/builder.dart | 68 +++++++++++-------- .../appflowy_ui/lib/src/theme/data/data.dart | 22 +++--- .../lib/src/theme/shadow/shadow.dart | 9 ++- 6 files changed, 60 insertions(+), 45 deletions(-) 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 0ffb7de73a..edb19232be 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -445,7 +445,7 @@ class _AppFlowyEditorPageState extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(appTheme.borderRadius.l), color: appTheme.surfaceColorScheme.primary, - boxShadow: [appTheme.shadow.small], + boxShadow: appTheme.shadow.small, ), toolbarBuilder: (_, child, onDismiss, isMetricsChanged) => BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart index 6213896feb..002d569c7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart @@ -315,6 +315,6 @@ ShapeDecoration buildToolbarLinkDecoration( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), - shadows: [theme.shadow.small], + shadows: theme.shadow.small, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart index d31b2f8fd9..fb51cdcf47 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart @@ -148,7 +148,7 @@ class _PasteAsMenuState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: theme.surfaceColorScheme.primary, - boxShadow: [theme.shadow.medium], + boxShadow: theme.shadow.medium, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 8923f61e1f..06c6eb5d8e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -93,35 +93,6 @@ class AppFlowyThemeBuilder { }; } - AppFlowyShadow buildShadow(Brightness brightness) { - return switch (brightness) { - Brightness.light => AppFlowyShadow( - small: const BoxShadow( - offset: Offset(0.0, 2.0), - blurRadius: 16.0, - color: Color(0x1F000000), - ), - medium: const BoxShadow( - offset: Offset(0.0, 4.0), - blurRadius: 32.0, - color: Color(0x1F000000), - ), - ), - Brightness.dark => AppFlowyShadow( - small: BoxShadow( - offset: Offset(0.0, 2.0), - blurRadius: 16.0, - color: Color(0x7A000000), - ), - medium: BoxShadow( - offset: Offset(0.0, 4.0), - blurRadius: 32.0, - color: Color(0x7A000000), - ), - ), - }; - } - AppFlowyBorderColorScheme buildBorderColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, @@ -351,4 +322,43 @@ class AppFlowyThemeBuilder { xxl: AppFlowySpacingConstant.spacing600, ); } + + AppFlowyShadow buildShadow( + Brightness brightness, + ) { + return switch (brightness) { + Brightness.light => AppFlowyShadow( + small: [ + BoxShadow( + offset: Offset(0, 2), + blurRadius: 16, + color: Color(0x1F000000), + ), + ], + medium: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 32, + color: Color(0x1F000000), + ), + ], + ), + Brightness.dark => AppFlowyShadow( + small: [ + BoxShadow( + offset: Offset(0, 2), + blurRadius: 16, + color: Color(0x7A000000), + ), + ], + medium: [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 32, + color: Color(0x7A000000), + ), + ], + ), + }; + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart index 9494bdf0e2..60f7d1f1a4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart @@ -36,9 +36,9 @@ abstract class AppFlowyBaseTheme { AppFlowySpacing get spacing; - AppFlowyBrandColorScheme get brandColorScheme; - AppFlowyShadow get shadow; + + AppFlowyBrandColorScheme get brandColorScheme; } class AppFlowyThemeData extends AppFlowyBaseTheme { @@ -70,10 +70,11 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.light, ); - final shadow = themeBuilder.buildShadow(Brightness.light); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); + final shadow = themeBuilder.buildShadow(Brightness.light); + return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -85,8 +86,8 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { surfaceColorScheme: surfaceColorScheme, borderRadius: borderRadius, spacing: spacing, - brandColorScheme: brandColorScheme, shadow: shadow, + brandColorScheme: brandColorScheme, ); } @@ -117,10 +118,11 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { colorScheme, Brightness.dark, ); - final shadow = themeBuilder.buildShadow(Brightness.dark); final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); final borderRadius = themeBuilder.buildBorderRadius(colorScheme); final spacing = themeBuilder.buildSpacing(colorScheme); + final shadow = themeBuilder.buildShadow(Brightness.dark); + return AppFlowyThemeData( colorScheme: colorScheme, textColorScheme: textColorScheme, @@ -132,8 +134,8 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { surfaceColorScheme: surfaceColorScheme, borderRadius: borderRadius, spacing: spacing, - brandColorScheme: brandColorScheme, shadow: shadow, + brandColorScheme: brandColorScheme, ); } @@ -146,10 +148,10 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { required this.surfaceColorScheme, required this.borderRadius, required this.spacing, + required this.shadow, required this.brandColorScheme, required this.iconColorTheme, required this.backgroundColorScheme, - required this.shadow, this.brightness = Brightness.light, }); @@ -181,6 +183,9 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { @override final AppFlowySpacing spacing; + @override + final AppFlowyShadow shadow; + @override final AppFlowyBrandColorScheme brandColorScheme; @@ -190,9 +195,6 @@ class AppFlowyThemeData extends AppFlowyBaseTheme { @override final AppFlowyBackgroundColorScheme backgroundColorScheme; - @override - final AppFlowyShadow shadow; - static AppFlowyTextColorScheme buildTextColorScheme( AppFlowyBaseColorScheme colorScheme, Brightness brightness, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart index 9bb2ac1116..457b86265e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart @@ -1,8 +1,11 @@ import 'package:flutter/widgets.dart'; class AppFlowyShadow { - AppFlowyShadow({required this.small, required this.medium}); + AppFlowyShadow({ + required this.small, + required this.medium, + }); - final BoxShadow small; - final BoxShadow medium; + final List small; + final List medium; } From e1bfb7095b7d296350ef3a9d0bcb6232960a4f43 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:20:25 +0800 Subject: [PATCH 22/74] feat: improve select modal button (#7736) --- .../prompt_input/select_model_menu.dart | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index eef2663370..a611d84310 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -31,9 +31,6 @@ class _SelectModelMenuState extends State { ), child: BlocBuilder( builder: (context, state) { - if (state.selectedModel == null) { - return const SizedBox.shrink(); - } return AppFlowyPopover( offset: Offset(-12.0, 0.0), constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), @@ -55,8 +52,12 @@ class _SelectModelMenuState extends State { ); }, child: _CurrentModelButton( - model: state.selectedModel!, - onTap: () => popoverController.show(), + model: state.selectedModel, + onTap: () { + if (state.selectedModel != null) { + popoverController.show(); + } + }, ), ); }, @@ -202,7 +203,7 @@ class _CurrentModelButton extends StatelessWidget { required this.onTap, }); - final AIModelPB model; + final AIModelPB? model; final VoidCallback onTap; @override @@ -214,40 +215,45 @@ class _CurrentModelButton extends StatelessWidget { behavior: HitTestBehavior.opaque, child: SizedBox( height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(4.0), - child: Row( - children: [ - Padding( - // TODO: remove this after change icon to 20px - padding: EdgeInsets.all(2), - child: FlowySvg( - FlowySvgs.ai_sparks_s, - color: Theme.of(context).hintColor, - size: Size.square(16), - ), - ), - if (!model.isDefault) + child: AnimatedSize( + duration: const Duration(milliseconds: 50), + curve: Curves.easeInOut, + alignment: AlignmentDirectional.centerStart, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(4.0), + child: Row( + children: [ Padding( - padding: EdgeInsetsDirectional.only(end: 2.0), - child: FlowyText( - model.i18n, - fontSize: 12, - figmaLineHeight: 16, + // TODO: remove this after change icon to 20px + padding: EdgeInsets.all(2), + child: FlowySvg( + FlowySvgs.ai_sparks_s, color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, + size: Size.square(16), ), ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], + if (model != null && !model!.isDefault) + Padding( + padding: EdgeInsetsDirectional.only(end: 2.0), + child: FlowyText( + model!.i18n, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), ), ), ), From b12bd8ee8562bfffcea0778f296ff2f5eac7c7fd Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:03:49 +0800 Subject: [PATCH 23/74] feat: add medium sized text field (#7737) * feat: add medium sized text field * chore: remove height --- .../continue_with_email_and_password.dart | 1 - ...inue_with_magic_link_or_passcode_page.dart | 1 - .../appflowy_ui/example/lib/main.dart | 6 +-- .../lib/src/textfield/textfield_page.dart | 13 +++++ .../src/component/textfield/textfield.dart | 50 ++++++++++++------- 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart index 349dd7e0d4..5027874418 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart @@ -60,7 +60,6 @@ class _ContinueWithEmailAndPasswordState key: emailKey, controller: controller, hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), - radius: 10, onSubmitted: (value) => _signInWithEmail( context, value, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index 8cfc4c1157..ec4fd1bbee 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -101,7 +101,6 @@ class _ContinueWithMagicLinkOrPasscodePageState controller: passcodeController, hintText: LocaleKeys.signIn_enterCode.tr(), keyboardType: TextInputType.number, - radius: 10, autoFocus: true, onSubmitted: (passcode) { if (passcode.isEmpty) { diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 067e42858b..41548abd08 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -24,6 +24,8 @@ class MyApp extends StatelessWidget { return ValueListenableBuilder( valueListenable: themeMode, builder: (context, themeMode, child) { + final themeData = + themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); return AppFlowyTheme( data: themeMode == ThemeMode.light ? AppFlowyThemeData.light() @@ -31,9 +33,7 @@ class MyApp extends StatelessWidget { child: MaterialApp( debugShowCheckedModeBanner: false, title: 'AppFlowy UI Example', - theme: themeMode == ThemeMode.light - ? ThemeData.light() - : ThemeData.dark(), + theme: themeData.copyWith(visualDensity: VisualDensity.standard), home: const MyHomePage( title: 'AppFlowy UI', ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart index 280e43818c..9e3436ecd4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart @@ -11,6 +11,19 @@ class TextFieldPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildSection( + 'TextField Sizes', + [ + AFTextField( + hintText: 'Please enter your name', + size: AFTextFieldSize.m, + ), + AFTextField( + hintText: 'Please enter your name', + ), + ], + ), + const SizedBox(height: 32), _buildSection( 'TextField with hint text', [ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index a934734983..3f5ad4cfed 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:appflowy_ui/src/theme/theme.dart'; import 'package:flutter/material.dart'; typedef AFTextFieldValidator = (bool result, String errorText) Function( @@ -20,21 +20,17 @@ class AFTextField extends StatefulWidget { this.hintText, this.initialText, this.keyboardType, - this.radius, + this.size = AFTextFieldSize.l, this.validator, this.controller, this.onChanged, this.onSubmitted, this.autoFocus, - this.height = 40.0, this.obscureText = false, this.suffixIconBuilder, this.suffixIconConstraints, }); - /// The height of the text field. - final double height; - /// The hint text to display when the text field is empty. final String? hintText; @@ -44,8 +40,8 @@ class AFTextField extends StatefulWidget { /// The type of keyboard to display. final TextInputType? keyboardType; - /// The radius of the text field. - final double? radius; + /// The size variant of the text field. + final AFTextFieldSize size; /// The validator to use for the text field. final AFTextFieldValidator? validator; @@ -115,9 +111,8 @@ class _AFTextFieldState extends AFTextFieldState { @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - final borderRadius = BorderRadius.circular( - widget.radius ?? theme.borderRadius.l, - ); + final borderRadius = widget.size.borderRadius(theme); + final contentPadding = widget.size.contentPadding(theme); final errorBorderColor = theme.borderColorScheme.errorThick; final defaultBorderColor = theme.borderColorScheme.greyTertiary; @@ -137,10 +132,9 @@ class _AFTextFieldState extends AFTextFieldState { hintStyle: theme.textStyle.body.standard( color: theme.textColorScheme.tertiary, ), - contentPadding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: 10, - ), + isDense: true, + constraints: BoxConstraints(), + contentPadding: contentPadding, border: OutlineInputBorder( borderSide: BorderSide( color: hasError ? errorBorderColor : defaultBorderColor, @@ -179,8 +173,6 @@ class _AFTextFieldState extends AFTextFieldState { ), ); - child = SizedBox(height: widget.height, child: child); - if (hasError && errorText.isNotEmpty) { child = Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -236,3 +228,27 @@ class _AFTextFieldState extends AFTextFieldState { }); } } + +enum AFTextFieldSize { + m, + l; + + EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { + return EdgeInsets.symmetric( + vertical: switch (this) { + AFTextFieldSize.m => theme.spacing.s, + AFTextFieldSize.l => 10.0, + }, + horizontal: theme.spacing.m, + ); + } + + BorderRadius borderRadius(AppFlowyThemeData theme) { + return BorderRadius.circular( + switch (this) { + AFTextFieldSize.m => theme.borderRadius.m, + AFTextFieldSize.l => 10.0, + }, + ); + } +} From 0906febe957ffd982fb3283087c11651457c168a Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:48:17 +0800 Subject: [PATCH 24/74] refactor: remove server type --- .../src/deps_resolve/cloud_service_impl.rs | 21 ++--- frontend/rust-lib/flowy-core/src/lib.rs | 19 ++--- .../rust-lib/flowy-core/src/server_layer.rs | 78 ++++--------------- .../flowy-core/src/user_state_callback.rs | 15 ++-- .../rust-lib/flowy-user-pub/src/entities.rs | 10 +++ 5 files changed, 49 insertions(+), 94 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 9c1ee9462c..56bf4e965c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,4 +1,4 @@ -use crate::server_layer::{ServerProvider, ServerType}; +use crate::server_layer::ServerProvider; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::workspace_dto::PublishInfoView; @@ -188,8 +188,8 @@ impl UserCloudServiceProvider for ServerProvider { /// When user login, the provider type is set by the [AuthType] and save to disk for next use. /// - /// Each [AuthType] has a corresponding [ServerType]. The [ServerType] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, + /// Each [AuthType] has a corresponding [AuthType]. The [AuthType] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, /// it will be used when user open the app again. /// fn set_auth_type(&self, auth_type: &AuthType) { @@ -211,7 +211,7 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.set_secret(secret); } - /// Returns the [UserCloudService] base on the current [ServerType]. + /// Returns the [UserCloudService] base on the current [AuthType]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let user_service = self.get_server()?.user_service(); @@ -219,9 +219,9 @@ impl UserCloudServiceProvider for ServerProvider { } fn service_url(&self) -> String { - match self.get_server_type() { - ServerType::Local => "".to_string(), - ServerType::AppFlowyCloud => AFCloudConfiguration::from_env() + match self.get_auth_type() { + AuthType::Local => "".to_string(), + AuthType::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), } @@ -578,12 +578,15 @@ impl DocumentCloudService for ServerProvider { impl CollabCloudPluginProvider for ServerProvider { fn provider_type(&self) -> CollabPluginProviderType { - self.get_server_type().into() + match self.get_auth_type() { + AuthType::Local => CollabPluginProviderType::Local, + AuthType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + } } fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { // If the user is local, we don't need to create a sync plugin. - if self.get_server_type().is_local() { + if self.get_auth_type().is_local() { debug!( "User authenticator is local, skip create sync plugin for: {}", context diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index bd55c8db1d..d217f52785 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,6 +1,6 @@ #![allow(unused_doc_comments)] -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, ServerProvider, ServerType}; +use crate::server_layer::{current_server_type, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -131,12 +131,12 @@ impl AppFlowyCore { store_preference.clone(), )); - let server_type = current_server_type(); - debug!("🔥runtime:{}, server:{}", runtime, server_type); + let auth_type = current_server_type(); + debug!("🔥runtime:{}, server:{}", runtime, auth_type); let server_provider = Arc::new(ServerProvider::new( config.clone(), - server_type, + auth_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -314,15 +314,6 @@ impl AppFlowyCore { } } -impl From for CollabPluginProviderType { - fn from(server_type: ServerType) -> Self { - match server_type { - ServerType::Local => CollabPluginProviderType::Local, - ServerType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, - } - } -} - struct ServerUserImpl(Weak); impl ServerUserImpl { diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index c0bf043d27..ebb86c4417 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -13,52 +13,12 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::fmt::{Display, Formatter, Result as FmtResult}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; -/// ServerType: local or cloud -#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum ServerType { - Local = 0, - AppFlowyCloud = 1, -} - -impl ServerType { - pub fn is_local(&self) -> bool { - matches!(self, Self::Local) - } -} - -impl Display for ServerType { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{:?}", self) - } -} - -/// Conversion between AuthType and ServerType -impl From<&AuthType> for ServerType { - fn from(a: &AuthType) -> Self { - match a { - AuthType::Local => ServerType::Local, - AuthType::AppFlowyCloud => ServerType::AppFlowyCloud, - } - } -} -impl From for AuthType { - fn from(s: ServerType) -> Self { - match s { - ServerType::Local => AuthType::Local, - ServerType::AppFlowyCloud => AuthType::AppFlowyCloud, - } - } -} - pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: DashMap>, + providers: DashMap>, auth_type: ArcSwap, user: Arc, pub local_ai: Arc, @@ -70,12 +30,11 @@ pub struct ServerProvider { impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - initial: ServerType, + initial_auth: AuthType, store_preferences: Weak, user_service: impl LoginUserService + 'static, ) -> Self { let user = Arc::new(user_service); - let initial_auth = AuthType::from(initial); let auth_type = ArcSwap::from(Arc::new(initial_auth)); let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; let ai_user = Arc::new(AIUserServiceImpl(user.clone())); @@ -98,17 +57,10 @@ impl ServerProvider { } } - /// Reads current type - pub fn get_server_type(&self) -> ServerType { - let auth_type = self.auth_type.load_full(); - ServerType::from(auth_type.as_ref()) - } - - pub fn set_auth_type(&self, a: AuthType) { - let old_type = self.get_server_type(); - self.auth_type.store(Arc::new(a)); - let new_type = self.get_server_type(); - if old_type != new_type { + pub fn set_auth_type(&self, new_auth_type: AuthType) { + let old_type = self.get_auth_type(); + if old_type != new_auth_type { + self.auth_type.store(Arc::new(new_auth_type)); self.providers.remove(&old_type); } } @@ -119,14 +71,14 @@ impl ServerProvider { /// Lazily create or fetch an AppFlowyServer instance pub fn get_server(&self) -> FlowyResult> { - let key = self.get_server_type(); - if let Some(entry) = self.providers.get(&key) { + let auth_type = self.get_auth_type(); + if let Some(entry) = self.providers.get(&auth_type) { return Ok(entry.clone()); } - let server: Arc = match key { - ServerType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), - ServerType::AppFlowyCloud => { + let server: Arc = match auth_type { + AuthType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + AuthType::AppFlowyCloud => { let cfg = self .config .cloud_config @@ -142,15 +94,15 @@ impl ServerProvider { }, }; - self.providers.insert(key.clone(), server.clone()); + self.providers.insert(auth_type, server.clone()); Ok(server) } } /// Determine current server type from ENV -pub fn current_server_type() -> ServerType { +pub fn current_server_type() -> AuthType { match AuthenticatorType::from_env() { - AuthenticatorType::Local => ServerType::Local, - AuthenticatorType::AppFlowyCloud => ServerType::AppFlowyCloud, + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 89ba14e6d2..ff1c6b6699 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -18,7 +18,7 @@ use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; -use crate::server_layer::{ServerProvider, ServerType}; +use crate::server_layer::ServerProvider; pub(crate) struct UserStatusCallbackImpl { pub(crate) collab_builder: Arc, @@ -128,7 +128,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { auth_type: &AuthType, ) -> FlowyResult<()> { self.server_provider.set_auth_type(*auth_type); - let server_type = self.server_provider.get_server_type(); event!( tracing::Level::TRACE, @@ -154,17 +153,17 @@ impl UserStatusCallback for UserStatusCallbackImpl { ) .await { - Ok(doc_state) => match server_type { - ServerType::Local => FolderInitDataSource::LocalDisk { + Ok(doc_state) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - ServerType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), }, - Err(err) => match server_type { - ServerType::Local => FolderInitDataSource::LocalDisk { + Err(err) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, - ServerType::AppFlowyCloud => { + AuthType::AppFlowyCloud => { return Err(err); }, }, diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index c09b536ccf..d59f9cab47 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use std::str::FromStr; use chrono::{DateTime, Utc}; @@ -359,6 +360,15 @@ pub enum AuthType { AppFlowyCloud = 1, } +impl Display for AuthType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AuthType::Local => write!(f, "Local"), + AuthType::AppFlowyCloud => write!(f, "AppFlowyCloud"), + } + } +} + impl Default for AuthType { fn default() -> Self { Self::Local From 2dc22004a1e249994f4569c058b77130b6723767 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 18 Apr 2025 15:57:33 +0800 Subject: [PATCH 25/74] refactor: remove server type --- .../src/deps_resolve/cloud_service_impl.rs | 4 ++-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 4 ++-- .../rust-lib/flowy-user/src/event_handler.rs | 10 +++++---- .../flowy-user/src/user_manager/manager.rs | 22 +++++++++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 56bf4e965c..35300563d7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -192,11 +192,11 @@ impl UserCloudServiceProvider for ServerProvider { /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, /// it will be used when user open the app again. /// - fn set_auth_type(&self, auth_type: &AuthType) { + fn set_server_auth_type(&self, auth_type: &AuthType) { self.set_auth_type(*auth_type); } - fn get_auth_type(&self) -> AuthType { + fn get_server_auth_type(&self) -> AuthType { self.get_auth_type() } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index fd283360de..dd69e4aa37 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -84,9 +84,9 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - fn set_auth_type(&self, auth_type: &AuthType); + fn set_server_auth_type(&self, auth_type: &AuthType); - fn get_auth_type(&self) -> AuthType; + fn get_server_auth_type(&self) -> AuthType; /// Sets the network reachability /// diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index a20229b6d0..7a64c20e06 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -45,14 +45,16 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_auth_type(); + let old_authenticator = manager.cloud_services.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), Err(err) => { - manager.cloud_services.set_auth_type(&old_authenticator); + manager + .cloud_services + .set_server_auth_type(&old_authenticator); return Err(err); }, } @@ -76,11 +78,11 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_services.get_auth_type(); + let prev_auth_type = manager.cloud_services.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager.cloud_services.set_auth_type(&prev_auth_type); + manager.cloud_services.set_server_auth_type(&prev_auth_type); Err(err) }, } 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 89a05539b6..8fb4009991 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -351,7 +351,7 @@ impl UserManager { params: SignInParams, authenticator: AuthType, ) -> Result { - self.cloud_services.set_auth_type(&authenticator); + self.cloud_services.set_server_auth_type(&authenticator); let response: AuthResponse = self .cloud_services @@ -403,7 +403,7 @@ impl UserManager { ) -> Result { // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_services.set_auth_type(&auth_type); + self.cloud_services.set_server_auth_type(&auth_type); let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); @@ -712,7 +712,7 @@ impl UserManager { authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_auth_type(authenticator); + self.cloud_services.set_server_auth_type(authenticator); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; @@ -724,7 +724,9 @@ impl UserManager { email: &str, password: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) @@ -735,7 +737,9 @@ impl UserManager { email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) @@ -748,7 +752,9 @@ impl UserManager { email: &str, passcode: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) @@ -758,7 +764,9 @@ impl UserManager { &self, oauth_provider: &str, ) -> Result { - self.cloud_services.set_auth_type(&AuthType::AppFlowyCloud); + self + .cloud_services + .set_server_auth_type(&AuthType::AppFlowyCloud); let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) From 54b5e248e3aa7eccfdcff79b1de2abdea1a83b39 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:02:08 +0800 Subject: [PATCH 26/74] feat: implement modal (#7750) --- .../presentation/widgets/dialog_v2.dart | 109 +++++++++++++ .../appflowy_ui/example/lib/main.dart | 8 +- .../example/lib/src/modal/modal_page.dart | 153 ++++++++++++++++++ .../button/base_button/base_button.dart | 5 +- .../lib/src/component/component.dart | 1 + .../lib/src/component/modal/dimension.dart | 9 ++ .../lib/src/component/modal/modal.dart | 125 ++++++++++++++ 7 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart new file mode 100644 index 0000000000..43ab8897e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart @@ -0,0 +1,109 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +typedef SimpleAFDialogAction = (String, void Function(BuildContext)?); + +/// A simple dialog with a title, content, and actions. +/// +/// The primary button is a filled button and colored using theme or destructive +/// color depending on the [isDestructive] parameter. The secondary button is an +/// outlined button. +/// +Future showSimpleAFDialog({ + required BuildContext context, + required String title, + required String content, + bool isDestructive = false, + required SimpleAFDialogAction primaryAction, + SimpleAFDialogAction? secondaryAction, + bool barrierDismissible = true, +}) { + final theme = AppFlowyTheme.of(context); + + return showDialog( + context: context, + barrierColor: theme.surfaceColorScheme.overlay, + barrierDismissible: barrierDismissible, + builder: (_) { + return AFModal( + constraints: BoxConstraints( + maxWidth: AFModalDimension.S, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AFModalHeader( + leading: Text( + title, + style: theme.textStyle.heading4.standard( + color: theme.textColorScheme.primary, + ), + ), + trailing: [ + AFGhostButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return FlowySvg( + FlowySvgs.close_s, + size: Size.square(20), + ); + }, + ), + ], + ), + Flexible( + child: ConstrainedBox( + // AFModalDimension.dialogHeight - header - footer + constraints: BoxConstraints(minHeight: 108.0), + child: AFModalBody( + child: Text(content), + ), + ), + ), + AFModalFooter( + trailing: [ + if (secondaryAction != null) + AFOutlinedButton.normal( + onTap: () { + secondaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text(secondaryAction.$1); + }, + ), + isDestructive + ? AFFilledButton.destructive( + onTap: () { + primaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text( + primaryAction.$1, + style: TextStyle( + color: AppFlowyTheme.of(context) + .textColorScheme + .onFill, + ), + ); + }, + ) + : AFFilledButton.primary( + onTap: () { + primaryAction.$2?.call(context); + Navigator.of(context).pop(); + }, + builder: (context, isHovering, disabled) { + return Text(primaryAction.$1); + }, + ), + ], + ), + ], + ), + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 41548abd08..1dfc60b314 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -1,8 +1,10 @@ import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:appflowy_ui_example/src/buttons/buttons_page.dart'; -import 'package:appflowy_ui_example/src/textfield/textfield_page.dart'; import 'package:flutter/material.dart'; +import 'src/buttons/buttons_page.dart'; +import 'src/modal/modal_page.dart'; +import 'src/textfield/textfield_page.dart'; + enum ThemeMode { light, dark, @@ -60,6 +62,7 @@ class _MyHomePageState extends State { final tabs = [ Tab(text: 'Button'), Tab(text: 'TextField'), + Tab(text: 'Modal'), ]; @override @@ -92,6 +95,7 @@ class _MyHomePageState extends State { children: [ ButtonsPage(), TextFieldPage(), + ModalPage(), ], ), bottomNavigationBar: TabBar( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart new file mode 100644 index 0000000000..4a9480d1b9 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart @@ -0,0 +1,153 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +class ModalPage extends StatefulWidget { + const ModalPage({super.key}); + + @override + State createState() => _ModalPageState(); +} + +class _ModalPageState extends State { + double width = AFModalDimension.M; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: Container( + constraints: BoxConstraints(maxWidth: 600), + padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl), + child: Column( + spacing: theme.spacing.l, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: theme.spacing.m, + mainAxisSize: MainAxisSize.min, + children: [ + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.S), + builder: (context, isHovering, disabled) { + return Text( + 'S', + style: TextStyle( + color: width == AFModalDimension.S + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.M), + builder: (context, isHovering, disabled) { + return Text( + 'M', + style: TextStyle( + color: width == AFModalDimension.M + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + AFGhostButton.normal( + onTap: () => setState(() => width = AFModalDimension.L), + builder: (context, isHovering, disabled) { + return Text( + 'L', + style: TextStyle( + color: width == AFModalDimension.L + ? theme.textColorScheme.theme + : theme.textColorScheme.primary, + ), + ); + }, + ), + ], + ), + AFFilledButton.primary( + builder: (context, isHovering, disabled) { + return Text( + 'Show Modal', + style: TextStyle( + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + ); + }, + onTap: () { + showDialog( + context: context, + barrierColor: theme.surfaceColorScheme.overlay, + builder: (context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: AFModal( + constraints: BoxConstraints( + maxWidth: width, + maxHeight: AFModalDimension.dialogHeight, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AFModalHeader( + leading: Text( + 'Header', + style: theme.textStyle.heading4.standard( + color: theme.textColorScheme.primary, + ), + ), + trailing: [ + AFGhostButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return const Icon(Icons.close); + }, + ) + ], + ), + Expanded( + child: AFModalBody( + child: Text( + 'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'), + ), + ), + AFModalFooter( + trailing: [ + AFOutlinedButton.normal( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return const Text('Cancel'); + }, + ), + AFFilledButton.primary( + onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) { + return Text( + 'Apply', + style: TextStyle( + color: AppFlowyTheme.of(context) + .textColorScheme + .onFill, + ), + ); + }, + ), + ], + ) + ], + )), + ); + }, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart index b62f2cce9b..9bb36507e8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart @@ -144,10 +144,7 @@ class _AFBaseButtonState extends State { } if (isFocused) { - return AppFlowyTheme.of(context) - .borderColorScheme - .themeThick - .withAlpha(128); + return theme.borderColorScheme.themeThick.withAlpha(128); } return theme.borderColorScheme.transparent; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart index d01d64109c..584d50c07b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart @@ -1,2 +1,3 @@ export 'button/button.dart'; +export 'modal/modal.dart'; export 'textfield/textfield.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart new file mode 100644 index 0000000000..72a7dbb5cf --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart @@ -0,0 +1,9 @@ +class AFModalDimension { + const AFModalDimension._(); + + static const double S = 400.0; + static const double M = 560.0; + static const double L = 720.0; + + static const double dialogHeight = 200.0; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart new file mode 100644 index 0000000000..4b40aebcbd --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart @@ -0,0 +1,125 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +export 'dimension.dart'; + +class AFModal extends StatelessWidget { + const AFModal({ + super.key, + this.constraints = const BoxConstraints(), + required this.child, + }); + + final BoxConstraints constraints; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Center( + child: Padding( + padding: EdgeInsets.all(theme.spacing.xl), + child: ConstrainedBox( + constraints: constraints, + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: theme.shadow.medium, + borderRadius: BorderRadius.circular(theme.borderRadius.xl), + color: theme.surfaceColorScheme.primary, + ), + child: Material( + color: Colors.transparent, + child: child, + ), + ), + ), + ), + ); + } +} + +class AFModalHeader extends StatelessWidget { + const AFModalHeader({ + super.key, + required this.leading, + this.trailing = const [], + }); + + final Widget leading; + final List trailing; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.only( + top: theme.spacing.xl, + left: theme.spacing.xxl, + right: theme.spacing.xxl, + ), + child: Row( + spacing: theme.spacing.s, + children: [ + Expanded(child: leading), + ...trailing, + ], + ), + ); + } +} + +class AFModalFooter extends StatelessWidget { + const AFModalFooter({ + super.key, + this.leading = const [], + this.trailing = const [], + }); + + final List leading; + final List trailing; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.only( + bottom: theme.spacing.xl, + left: theme.spacing.xxl, + right: theme.spacing.xxl, + ), + child: Row( + spacing: theme.spacing.l, + children: [ + ...leading, + Spacer(), + ...trailing, + ], + ), + ); + } +} + +class AFModalBody extends StatelessWidget { + const AFModalBody({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Padding( + padding: EdgeInsets.symmetric( + vertical: theme.spacing.l, + horizontal: theme.spacing.xxl, + ), + child: child, + ); + } +} From 889756ebb09272447aec8e00b7a7d4b29435d288 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:54:28 +0800 Subject: [PATCH 27/74] refactor: use script to generate design tokens (#7751) * refactor: use script to generate design tokens * chore: improve code readaility * refactor: make builder reusable to built in themes * chore: improve code readability --- .../ai/ai_writer_toolbar_item.dart | 4 +- .../link_embed/link_embed_menu.dart | 6 +- .../link_preview/custom_link_preview.dart | 2 +- .../custom_format_toolbar_items.dart | 2 +- .../custom_hightlight_color_toolbar_item.dart | 2 +- .../custom_link_toolbar_item.dart | 2 +- .../custom_text_align_toolbar_item.dart | 2 +- .../custom_text_color_toolbar_item.dart | 2 +- .../text_heading_toolbar_item.dart | 2 +- .../text_suggestions_toolbar_item.dart | 2 +- .../appflowy_ui/example/lib/main.dart | 2 +- .../src/component/textfield/textfield.dart | 4 +- .../lib/src/theme/appflowy_theme.dart | 21 +- .../theme/color_scheme/base/base_scheme.dart | 33 - .../lib/src/theme/color_scheme/base/blue.dart | 27 - .../src/theme/color_scheme/base/green.dart | 24 - .../src/theme/color_scheme/base/magenta.dart | 25 - .../src/theme/color_scheme/base/neutral.dart | 49 - .../src/theme/color_scheme/base/orange.dart | 25 - .../src/theme/color_scheme/base/purple.dart | 25 - .../lib/src/theme/color_scheme/base/red.dart | 27 - .../src/theme/color_scheme/base/subtle.dart | 265 ----- .../src/theme/color_scheme/base/yellow.dart | 24 - .../src/theme/color_scheme/border/border.dart | 49 - .../src/theme/color_scheme/color_scheme.dart | 8 - .../data/appflowy_default/primitive.dart | 298 +++++ .../theme/data/appflowy_default/semantic.dart | 377 ++++++ .../lib/src/theme/data/builder.dart | 323 +---- .../lib/src/theme/data/built_in_themes.dart | 1 + .../appflowy_ui/lib/src/theme/data/data.dart | 249 ---- .../lib/src/theme/definition/base_theme.dart | 33 + .../border_radius/border_radius.dart | 0 .../background_color_scheme.dart | 0 .../color_scheme}/border_color_scheme.dart | 0 .../color_scheme}/brand_color_scheme.dart | 0 .../definition/color_scheme/color_scheme.dart | 8 + .../color_scheme}/fill_color_scheme.dart | 0 .../color_scheme/icon_color_scheme.dart} | 4 +- .../color_scheme/other_color_scheme.dart | 9 + .../color_scheme}/surface_color_scheme.dart | 0 .../color_scheme}/text_color_scheme.dart | 0 .../theme/{ => definition}/shadow/shadow.dart | 0 .../{ => definition}/spacing/spacing.dart | 0 .../text_style/base/default_text_style.dart | 0 .../text_style/text_style.dart | 2 +- .../appflowy_ui/lib/src/theme/dimensions.dart | 17 - .../appflowy_ui/lib/src/theme/theme.dart | 13 +- .../script/Primitive.Mode 1.tokens.json | 984 ++++++++++++++++ .../script/Semantic.Dark Mode.tokens.json | 1039 +++++++++++++++++ .../script/Semantic.Light Mode.tokens.json | 1039 +++++++++++++++++ .../appflowy_ui/script/generate_theme.dart | 269 +++++ 51 files changed, 4116 insertions(+), 1183 deletions(-) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/border_radius/border_radius.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/background => definition/color_scheme}/background_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/border => definition/color_scheme}/border_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/brand => definition/color_scheme}/brand_color_scheme.dart (100%) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/fill => definition/color_scheme}/fill_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/icon/icon_color_theme.dart => definition/color_scheme/icon_color_scheme.dart} (86%) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/surface => definition/color_scheme}/surface_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{color_scheme/text => definition/color_scheme}/text_color_scheme.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/shadow/shadow.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/spacing/spacing.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/text_style/base/default_text_style.dart (100%) rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/{ => definition}/text_style/text_style.dart (88%) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index ab695b31a6..70d627d327 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -117,7 +117,7 @@ class _AiWriterToolbarActionListState extends State { } Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; final child = FlowyIconButton( width: 48, height: 32, @@ -186,7 +186,7 @@ class ImproveWritingButton extends StatelessWidget { icon: FlowySvg( FlowySvgs.toolbar_ai_improve_writing_m, size: Size.square(20.0), - color: theme.iconColorTheme.primary, + color: theme.iconColorScheme.primary, ), onPressed: () { if (_isAIWriterEnabled(editorState)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart index bddfcb9b54..c3d2aebbcc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart @@ -63,7 +63,7 @@ class _LinkEmbedMenuState extends State { Widget buildChild() { final theme = AppFlowyTheme.of(context), - iconScheme = theme.iconColorTheme, + iconScheme = theme.iconColorScheme, fillScheme = theme.fillColorScheme; return Container( @@ -102,7 +102,7 @@ class _LinkEmbedMenuState extends State { } Widget buildconvertBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; return AppFlowyPopover( offset: Offset(0, 6), direction: PopoverDirection.bottomWithRightAligned, @@ -170,7 +170,7 @@ class _LinkEmbedMenuState extends State { } Widget buildMoreOptionBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme; + final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; return AppFlowyPopover( offset: Offset(0, 6), direction: PopoverDirection.bottomWithRightAligned, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 880a33b817..d7f3e26302 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -163,7 +163,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { Widget buildImage(BuildContext context) { final theme = AppFlowyTheme.of(context), fillScheme = theme.fillColorScheme, - iconScheme = theme.iconColorTheme; + iconScheme = theme.iconColorScheme; final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; Widget child; if (imageUrl?.isNotEmpty ?? false) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart index 7dbf192bae..d4f3d21f46 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart @@ -82,7 +82,7 @@ class _FormatToolbarItem extends ToolbarItem { size: Size.square(20.0), color: (isDark && isHighlight) ? Color(0xFF282E3A) - : theme.iconColorTheme.primary, + : theme.iconColorScheme.primary, ), onPressed: () => editorState.toggleAttribute( name, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart index 2e115d240d..46f2c02c5a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart @@ -83,7 +83,7 @@ class _HighlightColorPickerWidgetState Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 36, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart index cbbce9c943..8c9e6b69da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart @@ -45,7 +45,7 @@ final customLinkItem = ToolbarItem( size: Size.square(20.0), color: (isDark && isHref) ? Color(0xFF282E3A) - : theme.iconColorTheme.primary, + : theme.iconColorScheme.primary, ), onPressed: () { getIt().hideToolbar(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart index 2a1688db19..efaff532f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart @@ -97,7 +97,7 @@ class _TextAlignActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 48, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart index 80f2d3138d..9f5a917b89 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart @@ -82,7 +82,7 @@ class _TextColorPickerWidgetState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 36, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart index 8140a7b7f3..5778b6b8a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart @@ -79,7 +79,7 @@ class _TextHeadingActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyIconButton( width: 48, height: 32, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart index 3c8f55caef..48f5d3f403 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart @@ -120,7 +120,7 @@ class _SuggestionsActionListState extends State { Widget buildChild(BuildContext context) { final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorTheme.primary; + iconColor = theme.iconColorScheme.primary; final child = FlowyHover( isSelected: () => isSelected, style: HoverStyle( diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index 1dfc60b314..c02001745d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -82,7 +82,7 @@ class _MyHomePageState extends State { actions: [ IconButton( icon: Icon( - theme.brightness == Brightness.light + Theme.of(context).brightness == Brightness.light ? Icons.dark_mode : Icons.light_mode, ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 3f5ad4cfed..9e61b71709 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -233,7 +233,7 @@ enum AFTextFieldSize { m, l; - EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { + EdgeInsetsGeometry contentPadding(AppFlowyBaseThemeData theme) { return EdgeInsets.symmetric( vertical: switch (this) { AFTextFieldSize.m => theme.spacing.s, @@ -243,7 +243,7 @@ enum AFTextFieldSize { ); } - BorderRadius borderRadius(AppFlowyThemeData theme) { + BorderRadius borderRadius(AppFlowyBaseThemeData theme) { return BorderRadius.circular( switch (this) { AFTextFieldSize.m => theme.borderRadius.m, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart index 49deecc178..8a99b737ec 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/data/data.dart'; +import 'package:appflowy_ui/src/theme/definition/base_theme.dart'; import 'package:flutter/widgets.dart'; class AppFlowyTheme extends StatelessWidget { @@ -8,10 +8,10 @@ class AppFlowyTheme extends StatelessWidget { required this.child, }); - final AppFlowyThemeData data; + final AppFlowyBaseThemeData data; final Widget child; - static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { + static AppFlowyBaseThemeData of(BuildContext context, {bool listen = true}) { final provider = maybeOf(context, listen: listen); if (provider == null) { throw FlutterError( @@ -26,27 +26,26 @@ class AppFlowyTheme extends StatelessWidget { return provider; } - static AppFlowyThemeData? maybeOf( + static AppFlowyBaseThemeData? maybeOf( BuildContext context, { bool listen = true, }) { if (listen) { return context .dependOnInheritedWidgetOfExactType() - ?.theme - .data; + ?.theme; } final provider = context .getElementForInheritedWidgetOfExactType() ?.widget; - return (provider as AppFlowyInheritedTheme?)?.theme.data; + return (provider as AppFlowyInheritedTheme?)?.theme; } @override Widget build(BuildContext context) { return AppFlowyInheritedTheme( - theme: this, + theme: data, child: child, ); } @@ -59,14 +58,14 @@ class AppFlowyInheritedTheme extends InheritedTheme { required super.child, }); - final AppFlowyTheme theme; + final AppFlowyBaseThemeData theme; @override Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: theme.data, child: child); + return AppFlowyTheme(data: theme, child: child); } @override bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - theme.data != oldWidget.theme.data; + theme != oldWidget.theme; } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart deleted file mode 100644 index 5b843d97e2..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/base_scheme.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:appflowy_ui/src/theme/color_scheme/base/blue.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/green.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/magenta.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/neutral.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/orange.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/purple.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/red.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/subtle.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/yellow.dart'; - -class AppFlowyBaseColorScheme { - const AppFlowyBaseColorScheme({ - this.blue = const BlueColors(), - this.green = const GreenColors(), - this.yellow = const YellowColors(), - this.red = const RedColors(), - this.orange = const OrangeColors(), - this.magenta = const MagentaColors(), - this.purple = const PurpleColors(), - this.neutral = const NeutralColors(), - this.subtle = const SubtleColors(), - }); - - final BlueColors blue; - final GreenColors green; - final YellowColors yellow; - final RedColors red; - final OrangeColors orange; - final MagentaColors magenta; - final PurpleColors purple; - final NeutralColors neutral; - final SubtleColors subtle; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart deleted file mode 100644 index fb73b42f60..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/blue.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class BlueColors { - const BlueColors(); - - Color get blue100 => const Color(0xFFE3F6FF); - - Color get blue200 => const Color(0xFFA9E2FF); - - Color get blue300 => const Color(0xFF80D2FF); - - Color get blue400 => const Color(0xFF4EC1FF); - - Color get blue500 => const Color(0xFF00B5FF); - - Color get blue600 => const Color(0xFF0092D6); - - Color get blue700 => const Color(0xFF0078C0); - - Color get blue800 => const Color(0xFF0065A9); - - Color get blue900 => const Color(0xFF00508F); - - Color get blue1000 => const Color(0xFF003C77); - - Color get alphaBlue50015 => const Color(0x2600B5FF); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart deleted file mode 100644 index 652f5f5932..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/green.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class GreenColors { - const GreenColors(); - Color get green100 => const Color(0xFFECF9F5); - - Color get green200 => const Color(0xFFC3E5D8); - - Color get green300 => const Color(0xFF9AD1BC); - - Color get green400 => const Color(0xFF71BD9F); - - Color get green500 => const Color(0xFF48A982); - - Color get green600 => const Color(0xFF248569); - - Color get green700 => const Color(0xFF29725D); - - Color get green800 => const Color(0xFF2E6050); - - Color get green900 => const Color(0xFF305548); - - Color get green1000 => const Color(0xFF305244); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart deleted file mode 100644 index dec5617c67..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/magenta.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class MagentaColors { - const MagentaColors(); - - Color get magenta100 => const Color(0xFFFFE5EF); - - Color get magenta200 => const Color(0xFFFFB8D1); - - Color get magenta300 => const Color(0xFFFF8AB2); - - Color get magenta400 => const Color(0xFFFF5C93); - - Color get magenta500 => const Color(0xFFFB006D); - - Color get magenta600 => const Color(0xFFD2005F); - - Color get magenta700 => const Color(0xFFD2005F); - - Color get magenta800 => const Color(0xFF850040); - - Color get magenta900 => const Color(0xFF610031); - - Color get magenta1000 => const Color(0xFF400022); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart deleted file mode 100644 index 4b6c08b595..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/neutral.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class NeutralColors { - const NeutralColors(); - - Color get neutral100 => const Color(0xFFF8FAFF); - - Color get neutral200 => const Color(0xFFE4E8F5); - - Color get neutral300 => const Color(0xFFCED3E6); - - Color get neutral400 => const Color(0xFFB5BBD3); - - Color get neutral500 => const Color(0xFF989EB7); - - Color get neutral600 => const Color(0xFF6F748C); - - Color get neutral700 => const Color(0xFF54596E); - - Color get neutral800 => const Color(0xFF3C3F4E); - - Color get neutral900 => const Color(0xFF272930); - - Color get neutral1000 => const Color(0xFF21232A); - - Color get black => const Color(0xFF000000); - - Color get alphaBlack60 => const Color(0x99000000); - - Color get white => const Color(0xFFFFFFFF); - - Color get alphaWhite0 => const Color(0x00FFFFFF); - - Color get alphaWhite20 => const Color(0x33FFFFFF); - - Color get alphaWhite30 => const Color(0x4DFFFFFF); - - Color get alphaGrey10005 => const Color(0x0DF9FAFD); - - Color get alphaGrey10010 => const Color(0x1AF9FAFD); - - Color get alphaGrey100005 => const Color(0x0D1F2329); - - Color get alphaGrey100010 => const Color(0x1A1F2329); - - Color get alphaGrey100070 => const Color(0xB21F2329); - - Color get alphaGrey100080 => const Color(0xCC1F2329); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart deleted file mode 100644 index c9424bd960..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/orange.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class OrangeColors { - const OrangeColors(); - - Color get orange100 => const Color(0xFFFFF3D5); - - Color get orange200 => const Color(0xFFFFE4AB); - - Color get orange300 => const Color(0xFFFFD181); - - Color get orange400 => const Color(0xFFFFBE62); - - Color get orange500 => const Color(0xFFFFA02E); - - Color get orange600 => const Color(0xFFDB7E21); - - Color get orange700 => const Color(0xFFB75F17); - - Color get orange800 => const Color(0xFF93450E); - - Color get orange900 => const Color(0xFF7A3108); - - Color get orange1000 => const Color(0xFF602706); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart deleted file mode 100644 index fa3b9e54cf..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/purple.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class PurpleColors { - const PurpleColors(); - - Color get purple100 => const Color(0xFFF1E0FF); - - Color get purple200 => const Color(0xFFE1B3FF); - - Color get purple300 => const Color(0xFFD185FF); - - Color get purple400 => const Color(0xFFBC58FF); - - Color get purple500 => const Color(0xFF9327FF); - - Color get purple600 => const Color(0xFF7A1DCC); - - Color get purple700 => const Color(0xFF6617B3); - - Color get purple800 => const Color(0xFF55138F); - - Color get purple900 => const Color(0xFF470C72); - - Color get purple1000 => const Color(0xFF380758); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart deleted file mode 100644 index 030f2988bc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/red.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class RedColors { - const RedColors(); - - Color get red100 => const Color(0xFFFFD2DD); - - Color get red200 => const Color(0xFFFFA5B4); - - Color get red300 => const Color(0xFFFF7D87); - - Color get red400 => const Color(0xFFFF5050); - - Color get red500 => const Color(0xFFF33641); - - Color get red600 => const Color(0xFFE71D32); - - Color get red700 => const Color(0xFFAD1625); - - Color get red800 => const Color(0xFF8C101C); - - Color get red900 => const Color(0xFF6E0A1E); - - Color get red1000 => const Color(0xFF4C0A17); - - Color get alphaRed50010 => const Color(0x1AF33641); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart deleted file mode 100644 index 2fddd14f1a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/subtle.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'package:flutter/material.dart'; - -class SubtleColors { - const SubtleColors(); - - // Rose colors - Color get rose100 => const Color(0xFFFCF2F2); - - Color get rose200 => const Color(0xFFFAE3E3); - - Color get rose300 => const Color(0xFFFAD9D9); - - Color get rose400 => const Color(0xFFEDADAD); - - Color get rose500 => const Color(0xFFCC4E4E); - - Color get rose600 => const Color(0xFF702828); - - // Papaya colors - Color get papaya100 => const Color(0xFFFCF4F0); - - Color get papaya200 => const Color(0xFFFAE8DE); - - Color get papaya300 => const Color(0xFFFADFD2); - - Color get papaya400 => const Color(0xFFF0BDA3); - - Color get papaya500 => const Color(0xFFD67240); - - Color get papaya600 => const Color(0xFF6B3215); - - // Tangerine colors - Color get tangerine100 => const Color(0xFFFFF7ED); - - Color get tangerine200 => const Color(0xFFFCEDD9); - - Color get tangerine300 => const Color(0xFFFAE5CA); - - Color get tangerine400 => const Color(0xFFF2CB99); - - Color get tangerine500 => const Color(0xFFDB8F2C); - - Color get tangerine600 => const Color(0xFF613B0A); - - // Mango colors - Color get mango100 => const Color(0xFFFFF9EC); - - Color get mango200 => const Color(0xFFFCF1D7); - - Color get mango300 => const Color(0xFFFAE9C3); - - Color get mango400 => const Color(0xFFF5D68E); - - Color get mango500 => const Color(0xFFE0A416); - - Color get mango600 => const Color(0xFF5C4102); - - // Lemon colors - Color get lemon100 => const Color(0xFFFFFBE8); - - Color get lemon200 => const Color(0xFFFCF5CF); - - Color get lemon300 => const Color(0xFFFAEFB9); - - Color get lemon400 => const Color(0xFFF5E282); - - Color get lemon500 => const Color(0xFFE0BB00); - - Color get lemon600 => const Color(0xFF574800); - - // Olive colors - Color get olive100 => const Color(0xFFF9FAE6); - - Color get olive200 => const Color(0xFFF6F7D0); - - Color get olive300 => const Color(0xFFF0F2B3); - - Color get olive400 => const Color(0xFFDBDE83); - - Color get olive500 => const Color(0xFFADB204); - - Color get olive600 => const Color(0xFF4A4C03); - - // Lime colors - Color get lime100 => const Color(0xFFF6F9E6); - - Color get lime200 => const Color(0xFFEEF5CE); - - Color get lime300 => const Color(0xFFE7F0BB); - - Color get lime400 => const Color(0xFFCFDB91); - - Color get lime500 => const Color(0xFF92A822); - - Color get lime600 => const Color(0xFF414D05); - - // Grass colors - Color get grass100 => const Color(0xFFF4FAEB); - - Color get grass200 => const Color(0xFFE9F5D7); - - Color get grass300 => const Color(0xFFDEF0C5); - - Color get grass400 => const Color(0xFFBFD998); - - Color get grass500 => const Color(0xFF75A828); - - Color get grass600 => const Color(0xFF334D0C); - - // Forest colors - Color get forest100 => const Color(0xFFF1FAF0); - - Color get forest200 => const Color(0xFFE2F5DF); - - Color get forest300 => const Color(0xFFD7F0D3); - - Color get forest400 => const Color(0xFFA8D6A1); - - Color get forest500 => const Color(0xFF49A33B); - - Color get forest600 => const Color(0xFF1E4F16); - - // Jade colors - Color get jade100 => const Color(0xFFF0FAF6); - - Color get jade200 => const Color(0xFFDFF5EB); - - Color get jade300 => const Color(0xFFCEF0E1); - - Color get jade400 => const Color(0xFF90D1B5); - - Color get jade500 => const Color(0xFF1C9963); - - Color get jade600 => const Color(0xFF075231); - - // Aqua colors - Color get aqua100 => const Color(0xFFF0F9FA); - - Color get aqua200 => const Color(0xFFDFF3F5); - - Color get aqua300 => const Color(0xFFCCECF0); - - Color get aqua400 => const Color(0xFF83CCD4); - - Color get aqua500 => const Color(0xFF008E9E); - - Color get aqua600 => const Color(0xFF004E57); - - // Azure colors - Color get azure100 => const Color(0xFFF0F6FA); - - Color get azure200 => const Color(0xFFE1EEF7); - - Color get azure300 => const Color(0xFFD3E6F5); - - Color get azure400 => const Color(0xFF88C0EB); - - Color get azure500 => const Color(0xFF0877CC); - - Color get azure600 => const Color(0xFF154469); - - // Denim colors - Color get denim100 => const Color(0xFFF0F3FA); - - Color get denim200 => const Color(0xFFE3EBFA); - - Color get denim300 => const Color(0xFFD7E2F7); - - Color get denim400 => const Color(0xFF9AB6ED); - - Color get denim500 => const Color(0xFF3267D1); - - Color get denim600 => const Color(0xFF223C70); - - // Mauve colors - Color get mauve100 => const Color(0xFFF2F2FC); - - Color get mauve200 => const Color(0xFFE6E6FA); - - Color get mauve300 => const Color(0xFFDCDCF7); - - Color get mauve400 => const Color(0xFFAEAEF5); - - Color get mauve500 => const Color(0xFF5555E0); - - Color get mauve600 => const Color(0xFF36366B); - - // Lavender colors - Color get lavender100 => const Color(0xFFF6F3FC); - - Color get lavender200 => const Color(0xFFEBE3FA); - - Color get lavender300 => const Color(0xFFE4DAF7); - - Color get lavender400 => const Color(0xFFC1AAF0); - - Color get lavender500 => const Color(0xFF8153DB); - - Color get lavender600 => const Color(0xFF462F75); - - // Lilac colors - Color get lilac100 => const Color(0xFFF7F0FA); - - Color get lilac200 => const Color(0xFFF0E1F7); - - Color get lilac300 => const Color(0xFFEDD7F7); - - Color get lilac400 => const Color(0xFFD3A9E8); - - Color get lilac500 => const Color(0xFF9E4CC7); - - Color get lilac600 => const Color(0xFF562D6B); - - // Mallow colors - Color get mallow100 => const Color(0xFFFAF0FA); - - Color get mallow200 => const Color(0xFFF5E1F4); - - Color get mallow300 => const Color(0xFFF5D7F4); - - Color get mallow400 => const Color(0xFFDEA4DC); - - Color get mallow500 => const Color(0xFFB240AF); - - Color get mallow600 => const Color(0xFF632861); - - // Camellia colors - Color get camellia100 => const Color(0xFFF9EFF3); - - Color get camellia200 => const Color(0xFFF7E1EB); - - Color get camellia300 => const Color(0xFFF7D7E5); - - Color get camellia400 => const Color(0xFFE5A3C0); - - Color get camellia500 => const Color(0xFFC24279); - - Color get camellia600 => const Color(0xFF6E2343); - - // Smoke colors - Color get smoke100 => const Color(0xFFF5F5F5); - - Color get smoke200 => const Color(0xFFE8E8E8); - - Color get smoke300 => const Color(0xFFDEDEDE); - - Color get smoke400 => const Color(0xFFB8B8B8); - - Color get smoke500 => const Color(0xFF6E6E6E); - - Color get smoke600 => const Color(0xFF404040); - - // Iron colors - Color get iron100 => const Color(0xFFF2F4F7); - - Color get iron200 => const Color(0xFFE6E9F0); - - Color get iron300 => const Color(0xFFDADEE5); - - Color get iron400 => const Color(0xFFB0B5BF); - - Color get iron500 => const Color(0xFF666F80); - - Color get iron600 => const Color(0xFF394152); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart deleted file mode 100644 index a38cf2bd78..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/base/yellow.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class YellowColors { - const YellowColors(); - Color get yellow100 => const Color(0xFFFFF9B2); - - Color get yellow200 => const Color(0xFFFFEC66); - - Color get yellow300 => const Color(0xFFFFDF1A); - - Color get yellow400 => const Color(0xFFFFCC00); - - Color get yellow500 => const Color(0xFFFFCE00); - - Color get yellow600 => const Color(0xFFE6B800); - - Color get yellow700 => const Color(0xFFCC9F00); - - Color get yellow800 => const Color(0xFFB38A00); - - Color get yellow900 => const Color(0xFF9A7500); - - Color get yellow1000 => const Color(0xFF7F6200); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart deleted file mode 100644 index d1618b6cff..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBorderColorScheme { - const AppFlowyBorderColorScheme({ - required this.greyPrimary, - required this.greyPrimaryHover, - required this.greySecondary, - required this.greySecondaryHover, - required this.greyTertiary, - required this.greyTertiaryHover, - required this.greyQuaternary, - required this.greyQuaternaryHover, - required this.transparent, - required this.themeThick, - required this.themeThickHover, - required this.infoThick, - required this.infoThickHover, - required this.successThick, - required this.successThickHover, - required this.warningThick, - required this.warningThickHover, - required this.errorThick, - required this.errorThickHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color greyPrimary; - final Color greyPrimaryHover; - final Color greySecondary; - final Color greySecondaryHover; - final Color greyTertiary; - final Color greyTertiaryHover; - final Color greyQuaternary; - final Color greyQuaternaryHover; - final Color transparent; - final Color themeThick; - final Color themeThickHover; - final Color infoThick; - final Color infoThickHover; - final Color successThick; - final Color successThickHover; - final Color warningThick; - final Color warningThickHover; - final Color errorThick; - final Color errorThickHover; - final Color purpleThick; - final Color purpleThickHover; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart deleted file mode 100644 index 5a1e7debeb..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/color_scheme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'background/background_color_scheme.dart'; -export 'base/base_scheme.dart'; -export 'border/border_color_scheme.dart'; -export 'brand/brand_color_scheme.dart'; -export 'fill/fill_color_scheme.dart'; -export 'icon/icon_color_theme.dart'; -export 'surface/surface_color_scheme.dart'; -export 'text/text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart new file mode 100644 index 0000000000..1eee06953f --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -0,0 +1,298 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: 2025-04-15T23:05:39.278903 +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:flutter/material.dart'; + +class AppFlowyPrimitiveTokens { + AppFlowyPrimitiveTokens._(); + + /// #f8faff + static Color get neutral100 => Color(0xFFF8FAFF); + + /// #e4e8f5 + static Color get neutral200 => Color(0xFFE4E8F5); + + /// #ced3e6 + static Color get neutral300 => Color(0xFFCED3E6); + + /// #b5bbd3 + static Color get neutral400 => Color(0xFFB5BBD3); + + /// #989eb7 + static Color get neutral500 => Color(0xFF989EB7); + + /// #6f748c + static Color get neutral600 => Color(0xFF6F748C); + + /// #54596e + static Color get neutral700 => Color(0xFF54596E); + + /// #3c3f4e + static Color get neutral800 => Color(0xFF3C3F4E); + + /// #272930 + static Color get neutral900 => Color(0xFF272930); + + /// #21232a + static Color get neutral1000 => Color(0xFF21232A); + + /// #000000 + static Color get neutralBlack => Color(0xFF000000); + + /// #00000099 + static Color get neutralAlphaBlack60 => Color(0x99000000); + + /// #ffffff + static Color get neutralWhite => Color(0xFFFFFFFF); + + /// #ffffff00 + static Color get neutralAlphaWhite0 => Color(0x00FFFFFF); + + /// #ffffff33 + static Color get neutralAlphaWhite20 => Color(0x33FFFFFF); + + /// #ffffff4d + static Color get neutralAlphaWhite30 => Color(0x4DFFFFFF); + + /// #f9fafd0d + static Color get neutralAlphaGrey10005 => Color(0x0DF9FAFD); + + /// #f9fafd1a + static Color get neutralAlphaGrey10010 => Color(0x1AF9FAFD); + + /// #1f23290d + static Color get neutralAlphaGrey100005 => Color(0x0D1F2329); + + /// #1f23291a + static Color get neutralAlphaGrey100010 => Color(0x1A1F2329); + + /// #1f2329b2 + static Color get neutralAlphaGrey100070 => Color(0xB21F2329); + + /// #1f2329cc + static Color get neutralAlphaGrey100080 => Color(0xCC1F2329); + + /// #e3f6ff + static Color get blue100 => Color(0xFFE3F6FF); + + /// #a9e2ff + static Color get blue200 => Color(0xFFA9E2FF); + + /// #80d2ff + static Color get blue300 => Color(0xFF80D2FF); + + /// #4ec1ff + static Color get blue400 => Color(0xFF4EC1FF); + + /// #00b5ff + static Color get blue500 => Color(0xFF00B5FF); + + /// #0092d6 + static Color get blue600 => Color(0xFF0092D6); + + /// #0078c0 + static Color get blue700 => Color(0xFF0078C0); + + /// #0065a9 + static Color get blue800 => Color(0xFF0065A9); + + /// #00508f + static Color get blue900 => Color(0xFF00508F); + + /// #003c77 + static Color get blue1000 => Color(0xFF003C77); + + /// #00b5ff26 + static Color get blueAlphaBlue50015 => Color(0x2600B5FF); + + /// #ecf9f5 + static Color get green100 => Color(0xFFECF9F5); + + /// #c3e5d8 + static Color get green200 => Color(0xFFC3E5D8); + + /// #9ad1bc + static Color get green300 => Color(0xFF9AD1BC); + + /// #71bd9f + static Color get green400 => Color(0xFF71BD9F); + + /// #48a982 + static Color get green500 => Color(0xFF48A982); + + /// #248569 + static Color get green600 => Color(0xFF248569); + + /// #29725d + static Color get green700 => Color(0xFF29725D); + + /// #2e6050 + static Color get green800 => Color(0xFF2E6050); + + /// #305548 + static Color get green900 => Color(0xFF305548); + + /// #305244 + static Color get green1000 => Color(0xFF305244); + + /// #f1e0ff + static Color get purple100 => Color(0xFFF1E0FF); + + /// #e1b3ff + static Color get purple200 => Color(0xFFE1B3FF); + + /// #d185ff + static Color get purple300 => Color(0xFFD185FF); + + /// #bc58ff + static Color get purple400 => Color(0xFFBC58FF); + + /// #9327ff + static Color get purple500 => Color(0xFF9327FF); + + /// #7a1dcc + static Color get purple600 => Color(0xFF7A1DCC); + + /// #6617b3 + static Color get purple700 => Color(0xFF6617B3); + + /// #55138f + static Color get purple800 => Color(0xFF55138F); + + /// #470c72 + static Color get purple900 => Color(0xFF470C72); + + /// #380758 + static Color get purple1000 => Color(0xFF380758); + + /// #ffe5ef + static Color get magenta100 => Color(0xFFFFE5EF); + + /// #ffb8d1 + static Color get magenta200 => Color(0xFFFFB8D1); + + /// #ff8ab2 + static Color get magenta300 => Color(0xFFFF8AB2); + + /// #ff5c93 + static Color get magenta400 => Color(0xFFFF5C93); + + /// #fb006d + static Color get magenta500 => Color(0xFFFB006D); + + /// #d2005f + static Color get magenta600 => Color(0xFFD2005F); + + /// #d2005f + static Color get magenta700 => Color(0xFFD2005F); + + /// #850040 + static Color get magenta800 => Color(0xFF850040); + + /// #610031 + static Color get magenta900 => Color(0xFF610031); + + /// #400022 + static Color get magenta1000 => Color(0xFF400022); + + /// #ffd2dd + static Color get red100 => Color(0xFFFFD2DD); + + /// #ffa5b4 + static Color get red200 => Color(0xFFFFA5B4); + + /// #ff7d87 + static Color get red300 => Color(0xFFFF7D87); + + /// #ff5050 + static Color get red400 => Color(0xFFFF5050); + + /// #f33641 + static Color get red500 => Color(0xFFF33641); + + /// #e71d32 + static Color get red600 => Color(0xFFE71D32); + + /// #ad1625 + static Color get red700 => Color(0xFFAD1625); + + /// #8c101c + static Color get red800 => Color(0xFF8C101C); + + /// #6e0a1e + static Color get red900 => Color(0xFF6E0A1E); + + /// #4c0a17 + static Color get red1000 => Color(0xFF4C0A17); + + /// #f336411a + static Color get redAlphaRed50010 => Color(0x1AF33641); + + /// #fff3d5 + static Color get orange100 => Color(0xFFFFF3D5); + + /// #ffe4ab + static Color get orange200 => Color(0xFFFFE4AB); + + /// #ffd181 + static Color get orange300 => Color(0xFFFFD181); + + /// #ffbe62 + static Color get orange400 => Color(0xFFFFBE62); + + /// #ffa02e + static Color get orange500 => Color(0xFFFFA02E); + + /// #db7e21 + static Color get orange600 => Color(0xFFDB7E21); + + /// #b75f17 + static Color get orange700 => Color(0xFFB75F17); + + /// #93450e + static Color get orange800 => Color(0xFF93450E); + + /// #7a3108 + static Color get orange900 => Color(0xFF7A3108); + + /// #602706 + static Color get orange1000 => Color(0xFF602706); + + /// #fff9b2 + static Color get yellow100 => Color(0xFFFFF9B2); + + /// #ffec66 + static Color get yellow200 => Color(0xFFFFEC66); + + /// #ffdf1a + static Color get yellow300 => Color(0xFFFFDF1A); + + /// #ffcc00 + static Color get yellow400 => Color(0xFFFFCC00); + + /// #ffce00 + static Color get yellow500 => Color(0xFFFFCE00); + + /// #e6b800 + static Color get yellow600 => Color(0xFFE6B800); + + /// #cc9f00 + static Color get yellow700 => Color(0xFFCC9F00); + + /// #b38a00 + static Color get yellow800 => Color(0xFFB38A00); + + /// #9a7500 + static Color get yellow900 => Color(0xFF9A7500); + + /// #7f6200 + static Color get yellow1000 => Color(0xFF7F6200); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart new file mode 100644 index 0000000000..08206a6572 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -0,0 +1,377 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: 2025-04-15T23:05:39.288085 +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +import '../builder.dart'; +import 'primitive.dart'; + +class AppFlowyThemeData implements AppFlowyBaseThemeData { + const AppFlowyThemeData._({ + required this.textStyle, + required this.textColorScheme, + required this.borderColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.iconColorScheme, + required this.backgroundColorScheme, + required this.otherColorsColorScheme, + }); + + factory AppFlowyThemeData.light() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.light); + + final textColorScheme = AppFlowyTextColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral600, + tertiary: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral200, + inverse: AppFlowyPrimitiveTokens.neutralWhite, + onFill: AppFlowyPrimitiveTokens.neutralWhite, + theme: AppFlowyPrimitiveTokens.blue500, + themeHover: AppFlowyPrimitiveTokens.blue600, + action: AppFlowyPrimitiveTokens.blue500, + actionHover: AppFlowyPrimitiveTokens.blue600, + info: AppFlowyPrimitiveTokens.blue500, + infoHover: AppFlowyPrimitiveTokens.blue600, + success: AppFlowyPrimitiveTokens.green600, + successHover: AppFlowyPrimitiveTokens.green700, + warning: AppFlowyPrimitiveTokens.orange600, + warningHover: AppFlowyPrimitiveTokens.orange700, + error: AppFlowyPrimitiveTokens.red600, + errorHover: AppFlowyPrimitiveTokens.red700, + purple: AppFlowyPrimitiveTokens.purple500, + purpleHover: AppFlowyPrimitiveTokens.purple600, + ); + + final iconColorScheme = AppFlowyIconColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral600, + tertiary: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral200, + white: AppFlowyPrimitiveTokens.neutralWhite, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final borderColorScheme = AppFlowyBorderColorScheme( + greyPrimary: AppFlowyPrimitiveTokens.neutral1000, + greyPrimaryHover: AppFlowyPrimitiveTokens.neutral900, + greySecondary: AppFlowyPrimitiveTokens.neutral800, + greySecondaryHover: AppFlowyPrimitiveTokens.neutral700, + greyTertiary: AppFlowyPrimitiveTokens.neutral300, + greyTertiaryHover: AppFlowyPrimitiveTokens.neutral400, + greyQuaternary: AppFlowyPrimitiveTokens.neutral100, + greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral200, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red700, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final fillColorScheme = AppFlowyFillColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + primaryHover: AppFlowyPrimitiveTokens.neutral900, + secondary: AppFlowyPrimitiveTokens.neutral600, + secondaryHover: AppFlowyPrimitiveTokens.neutral500, + tertiary: AppFlowyPrimitiveTokens.neutral300, + tertiaryHover: AppFlowyPrimitiveTokens.neutral400, + quaternary: AppFlowyPrimitiveTokens.neutral100, + quaternaryHover: AppFlowyPrimitiveTokens.neutral200, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey100005, + primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100010, + primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, + primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, + white: AppFlowyPrimitiveTokens.neutralWhite, + whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, + whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, + black: AppFlowyPrimitiveTokens.neutralBlack, + themeLight: AppFlowyPrimitiveTokens.blue100, + themeLightHover: AppFlowyPrimitiveTokens.blue200, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, + infoLight: AppFlowyPrimitiveTokens.blue100, + infoLightHover: AppFlowyPrimitiveTokens.blue200, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successLight: AppFlowyPrimitiveTokens.green100, + successLightHover: AppFlowyPrimitiveTokens.green200, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningLight: AppFlowyPrimitiveTokens.orange100, + warningLightHover: AppFlowyPrimitiveTokens.orange200, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorLight: AppFlowyPrimitiveTokens.red100, + errorLightHover: AppFlowyPrimitiveTokens.red200, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red700, + errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, + purpleLight: AppFlowyPrimitiveTokens.purple100, + purpleLightHover: AppFlowyPrimitiveTokens.purple200, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + purpleThick: AppFlowyPrimitiveTokens.purple500, + ); + + final surfaceColorScheme = AppFlowySurfaceColorScheme( + primary: AppFlowyPrimitiveTokens.neutralWhite, + overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, + ); + + final backgroundColorScheme = AppFlowyBackgroundColorScheme( + primary: AppFlowyPrimitiveTokens.neutralWhite, + secondary: AppFlowyPrimitiveTokens.neutral100, + tertiary: AppFlowyPrimitiveTokens.neutral200, + quaternary: AppFlowyPrimitiveTokens.neutral300, + ); + + final brandColorScheme = AppFlowyBrandColorScheme( + skyline: Color(0xFF00B5FF), + aqua: Color(0xFF00C8FF), + violet: Color(0xFF9327FF), + amethyst: Color(0xFF8427E0), + berry: Color(0xFFE3006D), + coral: Color(0xFFFB006D), + golden: Color(0xFFF7931E), + amber: Color(0xFFFFBD00), + lemon: Color(0xFFFFCE00), + ); + + final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( + textHighlight: AppFlowyPrimitiveTokens.blue200, + ); + + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + } + + factory AppFlowyThemeData.dark() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.dark); + + final textColorScheme = AppFlowyTextColorScheme( + primary: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + inverse: AppFlowyPrimitiveTokens.neutral1000, + onFill: AppFlowyPrimitiveTokens.neutralWhite, + theme: AppFlowyPrimitiveTokens.blue500, + themeHover: AppFlowyPrimitiveTokens.blue600, + action: AppFlowyPrimitiveTokens.blue500, + actionHover: AppFlowyPrimitiveTokens.blue600, + info: AppFlowyPrimitiveTokens.blue500, + infoHover: AppFlowyPrimitiveTokens.blue600, + success: AppFlowyPrimitiveTokens.green600, + successHover: AppFlowyPrimitiveTokens.green700, + warning: AppFlowyPrimitiveTokens.orange600, + warningHover: AppFlowyPrimitiveTokens.orange700, + error: AppFlowyPrimitiveTokens.red500, + errorHover: AppFlowyPrimitiveTokens.red400, + purple: AppFlowyPrimitiveTokens.purple500, + purpleHover: AppFlowyPrimitiveTokens.purple600, + ); + + final iconColorScheme = AppFlowyIconColorScheme( + primary: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + white: AppFlowyPrimitiveTokens.neutralWhite, + purpleThick: Color(0xFFFFFFFF), + purpleThickHover: Color(0xFFFFFFFF), + ); + + final borderColorScheme = AppFlowyBorderColorScheme( + greyPrimary: AppFlowyPrimitiveTokens.neutral100, + greyPrimaryHover: AppFlowyPrimitiveTokens.neutral200, + greySecondary: AppFlowyPrimitiveTokens.neutral300, + greySecondaryHover: AppFlowyPrimitiveTokens.neutral400, + greyTertiary: AppFlowyPrimitiveTokens.neutral800, + greyTertiaryHover: AppFlowyPrimitiveTokens.neutral700, + greyQuaternary: AppFlowyPrimitiveTokens.neutral1000, + greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral900, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue600, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorThick: AppFlowyPrimitiveTokens.red500, + errorThickHover: AppFlowyPrimitiveTokens.red400, + purpleThick: AppFlowyPrimitiveTokens.purple500, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + ); + + final fillColorScheme = AppFlowyFillColorScheme( + primary: AppFlowyPrimitiveTokens.neutral100, + primaryHover: AppFlowyPrimitiveTokens.neutral200, + secondary: AppFlowyPrimitiveTokens.neutral300, + secondaryHover: AppFlowyPrimitiveTokens.neutral400, + tertiary: AppFlowyPrimitiveTokens.neutral600, + tertiaryHover: AppFlowyPrimitiveTokens.neutral500, + quaternary: AppFlowyPrimitiveTokens.neutral1000, + quaternaryHover: AppFlowyPrimitiveTokens.neutral900, + transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, + primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey10005, + primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey10010, + primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, + primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, + white: AppFlowyPrimitiveTokens.neutralWhite, + whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, + whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, + black: AppFlowyPrimitiveTokens.neutralBlack, + themeLight: AppFlowyPrimitiveTokens.blue100, + themeLightHover: AppFlowyPrimitiveTokens.blue200, + themeThick: AppFlowyPrimitiveTokens.blue500, + themeThickHover: AppFlowyPrimitiveTokens.blue400, + themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, + infoLight: AppFlowyPrimitiveTokens.blue100, + infoLightHover: AppFlowyPrimitiveTokens.blue200, + infoThick: AppFlowyPrimitiveTokens.blue500, + infoThickHover: AppFlowyPrimitiveTokens.blue600, + successLight: AppFlowyPrimitiveTokens.green100, + successLightHover: AppFlowyPrimitiveTokens.green200, + successThick: AppFlowyPrimitiveTokens.green600, + successThickHover: AppFlowyPrimitiveTokens.green700, + warningLight: AppFlowyPrimitiveTokens.orange100, + warningLightHover: AppFlowyPrimitiveTokens.orange200, + warningThick: AppFlowyPrimitiveTokens.orange600, + warningThickHover: AppFlowyPrimitiveTokens.orange700, + errorLight: AppFlowyPrimitiveTokens.red100, + errorLightHover: AppFlowyPrimitiveTokens.red200, + errorThick: AppFlowyPrimitiveTokens.red600, + errorThickHover: AppFlowyPrimitiveTokens.red500, + errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, + purpleLight: AppFlowyPrimitiveTokens.purple100, + purpleLightHover: AppFlowyPrimitiveTokens.purple200, + purpleThickHover: AppFlowyPrimitiveTokens.purple600, + purpleThick: AppFlowyPrimitiveTokens.purple500, + ); + + final surfaceColorScheme = AppFlowySurfaceColorScheme( + primary: AppFlowyPrimitiveTokens.neutral900, + overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, + ); + + final backgroundColorScheme = AppFlowyBackgroundColorScheme( + primary: AppFlowyPrimitiveTokens.neutral1000, + secondary: AppFlowyPrimitiveTokens.neutral900, + tertiary: AppFlowyPrimitiveTokens.neutral800, + quaternary: AppFlowyPrimitiveTokens.neutral700, + ); + + final brandColorScheme = AppFlowyBrandColorScheme( + skyline: Color(0xFF00B5FF), + aqua: Color(0xFF00C8FF), + violet: Color(0xFF9327FF), + amethyst: Color(0xFF8427E0), + berry: Color(0xFFE3006D), + coral: Color(0xFFFB006D), + golden: Color(0xFFF7931E), + amber: Color(0xFFFFBD00), + lemon: Color(0xFFFFCE00), + ); + + final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( + textHighlight: AppFlowyPrimitiveTokens.blue200, + ); + + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + } + + static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); + + @override + final AppFlowyBaseTextStyle textStyle; + + @override + final AppFlowyTextColorScheme textColorScheme; + + @override + final AppFlowyBorderColorScheme borderColorScheme; + + @override + final AppFlowyFillColorScheme fillColorScheme; + + @override + final AppFlowySurfaceColorScheme surfaceColorScheme; + + @override + final AppFlowyBorderRadius borderRadius; + + @override + final AppFlowySpacing spacing; + + @override + final AppFlowyShadow shadow; + + @override + final AppFlowyBrandColorScheme brandColorScheme; + + @override + final AppFlowyIconColorScheme iconColorScheme; + + @override + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + @override + final AppFlowyOtherColorsColorScheme otherColorsColorScheme; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart index 06c6eb5d8e..a4f83109cb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart @@ -1,305 +1,30 @@ -import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/dimensions.dart'; -import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; +import 'package:appflowy_ui/src/theme/definition/border_radius/border_radius.dart'; +import 'package:appflowy_ui/src/theme/definition/shadow/shadow.dart'; +import 'package:appflowy_ui/src/theme/definition/spacing/spacing.dart'; import 'package:flutter/material.dart'; +class AppFlowySpacingConstant { + static const double spacing100 = 4; + static const double spacing200 = 6; + static const double spacing300 = 8; + static const double spacing400 = 12; + static const double spacing500 = 16; + static const double spacing600 = 20; +} + +class AppFlowyBorderRadiusConstant { + static const double radius100 = 4; + static const double radius200 = 6; + static const double radius300 = 8; + static const double radius400 = 12; + static const double radius500 = 16; + static const double radius600 = 20; +} + class AppFlowyThemeBuilder { const AppFlowyThemeBuilder(); - AppFlowyTextColorScheme buildTextColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - inverse: colorScheme.neutral.white, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red600, - errorHover: colorScheme.red.red700, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - inverse: colorScheme.neutral.neutral1000, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red500, - errorHover: colorScheme.red.red400, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowyIconColorTheme buildIconColorTheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyIconColorTheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - white: colorScheme.neutral.white, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyIconColorTheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - white: colorScheme.neutral.white, - purpleThick: const Color(0xFFFFFFFF), - purpleThickHover: const Color(0xFFFFFFFF), - ), - }; - } - - AppFlowyBorderColorScheme buildBorderColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyBorderColorScheme( - greyPrimary: colorScheme.neutral.neutral1000, - greyPrimaryHover: colorScheme.neutral.neutral900, - greySecondary: colorScheme.neutral.neutral800, - greySecondaryHover: colorScheme.neutral.neutral700, - greyTertiary: colorScheme.neutral.neutral300, - greyTertiaryHover: colorScheme.neutral.neutral400, - greyQuaternary: colorScheme.neutral.neutral100, - greyQuaternaryHover: colorScheme.neutral.neutral200, - transparent: colorScheme.neutral.alphaWhite0, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyBorderColorScheme( - greyPrimary: colorScheme.neutral.neutral100, - greyPrimaryHover: colorScheme.neutral.neutral200, - greySecondary: colorScheme.neutral.neutral300, - greySecondaryHover: colorScheme.neutral.neutral400, - greyTertiary: colorScheme.neutral.neutral800, - greyTertiaryHover: colorScheme.neutral.neutral700, - greyQuaternary: colorScheme.neutral.neutral1000, - greyQuaternaryHover: colorScheme.neutral.neutral900, - transparent: colorScheme.neutral.alphaWhite0, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorThick: colorScheme.red.red500, - errorThickHover: colorScheme.red.red400, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowyFillColorScheme buildFillColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.dark => AppFlowyFillColorScheme( - primary: colorScheme.neutral.neutral100, - primaryHover: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral300, - secondaryHover: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - tertiaryHover: colorScheme.neutral.neutral500, - quaternary: colorScheme.neutral.neutral1000, - quaternaryHover: colorScheme.neutral.neutral900, - transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey10005, - primaryAlpha5Hover: colorScheme.neutral.alphaGrey10010, - primaryAlpha80: colorScheme.neutral.alphaGrey100080, - primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, - white: colorScheme.neutral.white, - whiteAlpha: colorScheme.neutral.alphaWhite20, - whiteAlphaHover: colorScheme.neutral.alphaWhite30, - black: colorScheme.neutral.black, - themeLight: colorScheme.blue.blue100, - themeLightHover: colorScheme.blue.blue200, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - themeSelect: colorScheme.blue.alphaBlue50015, - infoLight: colorScheme.blue.blue100, - infoLightHover: colorScheme.blue.blue200, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successLight: colorScheme.green.green100, - successLightHover: colorScheme.green.green200, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningLight: colorScheme.orange.orange100, - warningLightHover: colorScheme.orange.orange200, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorLight: colorScheme.red.red100, - errorLightHover: colorScheme.red.red200, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - errorSelect: colorScheme.red.alphaRed50010, - purpleLight: colorScheme.purple.purple100, - purpleLightHover: colorScheme.purple.purple200, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - Brightness.light => AppFlowyFillColorScheme( - primary: colorScheme.neutral.neutral1000, - primaryHover: colorScheme.neutral.neutral900, - secondary: colorScheme.neutral.neutral600, - secondaryHover: colorScheme.neutral.neutral500, - tertiary: colorScheme.neutral.neutral300, - tertiaryHover: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral100, - quaternaryHover: colorScheme.neutral.neutral200, - transparent: colorScheme.neutral.alphaWhite0, - primaryAlpha5: colorScheme.neutral.alphaGrey100005, - primaryAlpha5Hover: colorScheme.neutral.alphaGrey100010, - primaryAlpha80: colorScheme.neutral.alphaGrey100080, - primaryAlpha80Hover: colorScheme.neutral.alphaGrey100070, - white: colorScheme.neutral.white, - whiteAlpha: colorScheme.neutral.alphaWhite20, - whiteAlphaHover: colorScheme.neutral.alphaWhite30, - black: colorScheme.neutral.black, - themeLight: colorScheme.blue.blue100, - themeLightHover: colorScheme.blue.blue200, - themeThick: colorScheme.blue.blue500, - themeThickHover: colorScheme.blue.blue600, - themeSelect: colorScheme.blue.alphaBlue50015, - infoLight: colorScheme.blue.blue100, - infoLightHover: colorScheme.blue.blue200, - infoThick: colorScheme.blue.blue500, - infoThickHover: colorScheme.blue.blue600, - successLight: colorScheme.green.green100, - successLightHover: colorScheme.green.green200, - successThick: colorScheme.green.green600, - successThickHover: colorScheme.green.green700, - warningLight: colorScheme.orange.orange100, - warningLightHover: colorScheme.orange.orange200, - warningThick: colorScheme.orange.orange600, - warningThickHover: colorScheme.orange.orange700, - errorLight: colorScheme.red.red100, - errorLightHover: colorScheme.red.red200, - errorThick: colorScheme.red.red600, - errorThickHover: colorScheme.red.red700, - errorSelect: colorScheme.red.alphaRed50010, - purpleLight: colorScheme.purple.purple100, - purpleLightHover: colorScheme.purple.purple200, - purpleThick: colorScheme.purple.purple500, - purpleThickHover: colorScheme.purple.purple600, - ), - }; - } - - AppFlowySurfaceColorScheme buildSurfaceColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowySurfaceColorScheme( - primary: colorScheme.neutral.white, - overlay: colorScheme.neutral.alphaBlack60, - ), - Brightness.dark => AppFlowySurfaceColorScheme( - primary: colorScheme.neutral.neutral900, - overlay: colorScheme.neutral.alphaBlack60, - ), - }; - } - - AppFlowyBackgroundColorScheme buildBackgroundColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyBackgroundColorScheme( - primary: colorScheme.neutral.white, - secondary: colorScheme.neutral.neutral100, - tertiary: colorScheme.neutral.neutral200, - quaternary: colorScheme.neutral.neutral300, - ), - Brightness.dark => AppFlowyBackgroundColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral900, - tertiary: colorScheme.neutral.neutral800, - quaternary: colorScheme.neutral.neutral700, - ), - }; - } - - AppFlowyBrandColorScheme buildBrandColorScheme( - AppFlowyBaseColorScheme colorScheme, - ) { - return AppFlowyBrandColorScheme( - skyline: const Color(0xFF00B5FF), - aqua: const Color(0xFF00C8FF), - violet: const Color(0xFF9327FF), - amethyst: const Color(0xFF8427E0), - berry: const Color(0xFFE3006D), - coral: const Color(0xFFFB006D), - golden: const Color(0xFFF7931E), - amber: const Color(0xFFFFBD00), - lemon: const Color(0xFFFFCE00), - ); - } - - AppFlowyBorderRadius buildBorderRadius( - AppFlowyBaseColorScheme colorScheme, - ) { + AppFlowyBorderRadius buildBorderRadius() { return AppFlowyBorderRadius( xs: AppFlowyBorderRadiusConstant.radius100, s: AppFlowyBorderRadiusConstant.radius200, @@ -310,9 +35,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowySpacing buildSpacing( - AppFlowyBaseColorScheme colorScheme, - ) { + AppFlowySpacing buildSpacing() { return AppFlowySpacing( xs: AppFlowySpacingConstant.spacing100, s: AppFlowySpacingConstant.spacing200, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart new file mode 100644 index 0000000000..2b29371433 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart @@ -0,0 +1 @@ +export 'appflowy_default/semantic.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart deleted file mode 100644 index 60f7d1f1a4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/data.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:appflowy_ui/src/theme/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/background/background_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/base/base_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/border/border.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/brand/brand_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/fill/fill_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/icon/icon_color_theme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/surface/surface_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/color_scheme/text/text_color_scheme.dart'; -import 'package:appflowy_ui/src/theme/data/builder.dart'; -import 'package:appflowy_ui/src/theme/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/spacing/spacing.dart'; -import 'package:appflowy_ui/src/theme/text_style/text_style.dart'; -import 'package:flutter/material.dart'; - -abstract class AppFlowyBaseTheme { - const AppFlowyBaseTheme(); - - AppFlowyBaseColorScheme get colorScheme; - - AppFlowyTextColorScheme get textColorScheme; - - AppFlowyBaseTextStyle get textStyle; - - AppFlowyIconColorTheme get iconColorTheme; - - AppFlowyBorderColorScheme get borderColorScheme; - - AppFlowyBackgroundColorScheme get backgroundColorScheme; - - AppFlowyFillColorScheme get fillColorScheme; - - AppFlowySurfaceColorScheme get surfaceColorScheme; - - AppFlowyBorderRadius get borderRadius; - - AppFlowySpacing get spacing; - - AppFlowyShadow get shadow; - - AppFlowyBrandColorScheme get brandColorScheme; -} - -class AppFlowyThemeData extends AppFlowyBaseTheme { - factory AppFlowyThemeData.light() { - final colorScheme = AppFlowyBaseColorScheme(); - - final textStyle = AppFlowyBaseTextStyle(); - final textColorScheme = themeBuilder.buildTextColorScheme( - colorScheme, - Brightness.light, - ); - final borderColorScheme = themeBuilder.buildBorderColorScheme( - colorScheme, - Brightness.light, - ); - final fillColorScheme = themeBuilder.buildFillColorScheme( - colorScheme, - Brightness.light, - ); - final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( - colorScheme, - Brightness.light, - ); - final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( - colorScheme, - Brightness.light, - ); - final iconColorTheme = themeBuilder.buildIconColorTheme( - colorScheme, - Brightness.light, - ); - final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); - final borderRadius = themeBuilder.buildBorderRadius(colorScheme); - final spacing = themeBuilder.buildSpacing(colorScheme); - final shadow = themeBuilder.buildShadow(Brightness.light); - - return AppFlowyThemeData( - colorScheme: colorScheme, - textColorScheme: textColorScheme, - textStyle: textStyle, - iconColorTheme: iconColorTheme, - backgroundColorScheme: backgroundColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - brandColorScheme: brandColorScheme, - ); - } - - factory AppFlowyThemeData.dark() { - final colorScheme = AppFlowyBaseColorScheme(); - final textStyle = AppFlowyBaseTextStyle(); - final textColorScheme = themeBuilder.buildTextColorScheme( - colorScheme, - Brightness.dark, - ); - final borderColorScheme = themeBuilder.buildBorderColorScheme( - colorScheme, - Brightness.dark, - ); - final fillColorScheme = themeBuilder.buildFillColorScheme( - colorScheme, - Brightness.dark, - ); - final surfaceColorScheme = themeBuilder.buildSurfaceColorScheme( - colorScheme, - Brightness.dark, - ); - final backgroundColorScheme = themeBuilder.buildBackgroundColorScheme( - colorScheme, - Brightness.dark, - ); - final iconColorTheme = themeBuilder.buildIconColorTheme( - colorScheme, - Brightness.dark, - ); - final brandColorScheme = themeBuilder.buildBrandColorScheme(colorScheme); - final borderRadius = themeBuilder.buildBorderRadius(colorScheme); - final spacing = themeBuilder.buildSpacing(colorScheme); - final shadow = themeBuilder.buildShadow(Brightness.dark); - - return AppFlowyThemeData( - colorScheme: colorScheme, - textColorScheme: textColorScheme, - textStyle: textStyle, - iconColorTheme: iconColorTheme, - backgroundColorScheme: backgroundColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - brandColorScheme: brandColorScheme, - ); - } - - const AppFlowyThemeData({ - required this.colorScheme, - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorTheme, - required this.backgroundColorScheme, - this.brightness = Brightness.light, - }); - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - final Brightness brightness; - - @override - final AppFlowyBaseColorScheme colorScheme; - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorTheme iconColorTheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - static AppFlowyTextColorScheme buildTextColorScheme( - AppFlowyBaseColorScheme colorScheme, - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral1000, - secondary: colorScheme.neutral.neutral600, - tertiary: colorScheme.neutral.neutral400, - quaternary: colorScheme.neutral.neutral200, - inverse: colorScheme.neutral.white, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red600, - errorHover: colorScheme.red.red700, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - Brightness.dark => AppFlowyTextColorScheme( - primary: colorScheme.neutral.neutral200, - secondary: colorScheme.neutral.neutral400, - tertiary: colorScheme.neutral.neutral600, - quaternary: colorScheme.neutral.neutral1000, - inverse: colorScheme.neutral.neutral1000, - onFill: colorScheme.neutral.white, - theme: colorScheme.blue.blue500, - themeHover: colorScheme.blue.blue600, - action: colorScheme.blue.blue500, - actionHover: colorScheme.blue.blue600, - info: colorScheme.blue.blue500, - infoHover: colorScheme.blue.blue600, - success: colorScheme.green.green600, - successHover: colorScheme.green.green700, - warning: colorScheme.orange.orange600, - warningHover: colorScheme.orange.orange700, - error: colorScheme.red.red500, - errorHover: colorScheme.red.red400, - purple: colorScheme.purple.purple500, - purpleHover: colorScheme.purple.purple600, - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart new file mode 100644 index 0000000000..ca06c8da1b --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart @@ -0,0 +1,33 @@ +import 'border_radius/border_radius.dart'; +import 'color_scheme/color_scheme.dart'; +import 'shadow/shadow.dart'; +import 'spacing/spacing.dart'; +import 'text_style/text_style.dart'; + +abstract class AppFlowyBaseThemeData { + const AppFlowyBaseThemeData(); + + AppFlowyTextColorScheme get textColorScheme; + + AppFlowyBaseTextStyle get textStyle; + + AppFlowyIconColorScheme get iconColorScheme; + + AppFlowyBorderColorScheme get borderColorScheme; + + AppFlowyBackgroundColorScheme get backgroundColorScheme; + + AppFlowyFillColorScheme get fillColorScheme; + + AppFlowySurfaceColorScheme get surfaceColorScheme; + + AppFlowyBorderRadius get borderRadius; + + AppFlowySpacing get spacing; + + AppFlowyShadow get shadow; + + AppFlowyBrandColorScheme get brandColorScheme; + + AppFlowyOtherColorsColorScheme get otherColorsColorScheme; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/border_radius/border_radius.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/background/background_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/border/border_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/brand/brand_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart new file mode 100644 index 0000000000..01952e1461 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart @@ -0,0 +1,8 @@ +export 'background_color_scheme.dart'; +export 'border_color_scheme.dart'; +export 'brand_color_scheme.dart'; +export 'fill_color_scheme.dart'; +export 'icon_color_scheme.dart'; +export 'other_color_scheme.dart'; +export 'surface_color_scheme.dart'; +export 'text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/fill/fill_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart similarity index 86% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart index f9ece5339c..245a02aadb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/icon/icon_color_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class AppFlowyIconColorTheme { - const AppFlowyIconColorTheme({ +class AppFlowyIconColorScheme { + const AppFlowyIconColorScheme({ required this.primary, required this.secondary, required this.tertiary, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart new file mode 100644 index 0000000000..ed9b94695c --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart @@ -0,0 +1,9 @@ +import 'dart:ui'; + +class AppFlowyOtherColorsColorScheme { + const AppFlowyOtherColorsColorScheme({ + required this.textHighlight, + }); + + final Color textHighlight; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/surface/surface_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/color_scheme/text/text_color_scheme.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/shadow/shadow.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/spacing/spacing.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart similarity index 100% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/base/default_text_style.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart similarity index 88% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart index 1caaa288e8..d96ca0f557 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/text_style/text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_ui/src/theme/text_style/base/default_text_style.dart'; +import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; class AppFlowyBaseTextStyle { const AppFlowyBaseTextStyle({ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart deleted file mode 100644 index e502b3b875..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/dimensions.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowySpacingConstant { - static const double spacing100 = 4; - static const double spacing200 = 6; - static const double spacing300 = 8; - static const double spacing400 = 12; - static const double spacing500 = 16; - static const double spacing600 = 20; -} - -class AppFlowyBorderRadiusConstant { - static const double radius100 = 4; - static const double radius200 = 6; - static const double radius300 = 8; - static const double radius400 = 12; - static const double radius500 = 16; - static const double radius600 = 20; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart index b800b1bb6c..5f9f66cd2d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart @@ -1,7 +1,8 @@ export 'appflowy_theme.dart'; -export 'border_radius/border_radius.dart'; -export 'color_scheme/color_scheme.dart'; -export 'data/data.dart'; -export 'dimensions.dart'; -export 'spacing/spacing.dart'; -export 'text_style/text_style.dart'; +export 'data/built_in_themes.dart'; +export 'definition/border_radius/border_radius.dart'; +export 'definition/color_scheme/color_scheme.dart'; +export 'definition/base_theme.dart'; +export 'definition/spacing/spacing.dart'; +export 'definition/shadow/shadow.dart'; +export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json new file mode 100644 index 0000000000..c46354b599 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json @@ -0,0 +1,984 @@ +{ + "Neutral": { + "100": { + "$type": "color", + "$value": "#f8faff" + }, + "200": { + "$type": "color", + "$value": "#e4e8f5" + }, + "300": { + "$type": "color", + "$value": "#ced3e6" + }, + "400": { + "$type": "color", + "$value": "#b5bbd3" + }, + "500": { + "$type": "color", + "$value": "#989eb7" + }, + "600": { + "$type": "color", + "$value": "#6f748c" + }, + "700": { + "$type": "color", + "$value": "#54596e" + }, + "800": { + "$type": "color", + "$value": "#3c3f4e" + }, + "900": { + "$type": "color", + "$value": "#272930" + }, + "1000": { + "$type": "color", + "$value": "#21232a" + }, + "black": { + "$type": "color", + "$value": "#000000" + }, + "alpha-black-60": { + "$type": "color", + "$value": "#00000099" + }, + "white": { + "$type": "color", + "$value": "#ffffff" + }, + "alpha-white-0": { + "$type": "color", + "$value": "#ffffff00" + }, + "alpha-white-20": { + "$type": "color", + "$value": "#ffffff33" + }, + "alpha-white-30": { + "$type": "color", + "$value": "#ffffff4d" + }, + "alpha-grey-100-05": { + "$type": "color", + "$value": "#f9fafd0d" + }, + "alpha-grey-100-10": { + "$type": "color", + "$value": "#f9fafd1a" + }, + "alpha-grey-1000-05": { + "$type": "color", + "$value": "#1f23290d" + }, + "alpha-grey-1000-10": { + "$type": "color", + "$value": "#1f23291a" + }, + "alpha-grey-1000-70": { + "$type": "color", + "$value": "#1f2329b2" + }, + "alpha-grey-1000-80": { + "$type": "color", + "$value": "#1f2329cc" + } + }, + "Blue": { + "100": { + "$type": "color", + "$value": "#e3f6ff" + }, + "200": { + "$type": "color", + "$value": "#a9e2ff" + }, + "300": { + "$type": "color", + "$value": "#80d2ff" + }, + "400": { + "$type": "color", + "$value": "#4ec1ff" + }, + "500": { + "$type": "color", + "$value": "#00b5ff" + }, + "600": { + "$type": "color", + "$value": "#0092d6" + }, + "700": { + "$type": "color", + "$value": "#0078c0" + }, + "800": { + "$type": "color", + "$value": "#0065a9" + }, + "900": { + "$type": "color", + "$value": "#00508f" + }, + "1000": { + "$type": "color", + "$value": "#003c77" + }, + "alpha-blue-500-15": { + "$type": "color", + "$value": "#00b5ff26" + } + }, + "Green": { + "100": { + "$type": "color", + "$value": "#ecf9f5" + }, + "200": { + "$type": "color", + "$value": "#c3e5d8" + }, + "300": { + "$type": "color", + "$value": "#9ad1bc" + }, + "400": { + "$type": "color", + "$value": "#71bd9f" + }, + "500": { + "$type": "color", + "$value": "#48a982" + }, + "600": { + "$type": "color", + "$value": "#248569" + }, + "700": { + "$type": "color", + "$value": "#29725d" + }, + "800": { + "$type": "color", + "$value": "#2e6050" + }, + "900": { + "$type": "color", + "$value": "#305548" + }, + "1000": { + "$type": "color", + "$value": "#305244" + } + }, + "Purple": { + "100": { + "$type": "color", + "$value": "#f1e0ff" + }, + "200": { + "$type": "color", + "$value": "#e1b3ff" + }, + "300": { + "$type": "color", + "$value": "#d185ff" + }, + "400": { + "$type": "color", + "$value": "#bc58ff" + }, + "500": { + "$type": "color", + "$value": "#9327ff" + }, + "600": { + "$type": "color", + "$value": "#7a1dcc" + }, + "700": { + "$type": "color", + "$value": "#6617b3" + }, + "800": { + "$type": "color", + "$value": "#55138f" + }, + "900": { + "$type": "color", + "$value": "#470c72" + }, + "1000": { + "$type": "color", + "$value": "#380758" + } + }, + "Magenta": { + "100": { + "$type": "color", + "$value": "#ffe5ef" + }, + "200": { + "$type": "color", + "$value": "#ffb8d1" + }, + "300": { + "$type": "color", + "$value": "#ff8ab2" + }, + "400": { + "$type": "color", + "$value": "#ff5c93" + }, + "500": { + "$type": "color", + "$value": "#fb006d" + }, + "600": { + "$type": "color", + "$value": "#d2005f" + }, + "700": { + "$type": "color", + "$value": "#d2005f" + }, + "800": { + "$type": "color", + "$value": "#850040" + }, + "900": { + "$type": "color", + "$value": "#610031" + }, + "1000": { + "$type": "color", + "$value": "#400022" + } + }, + "Red": { + "100": { + "$type": "color", + "$value": "#ffd2dd" + }, + "200": { + "$type": "color", + "$value": "#ffa5b4" + }, + "300": { + "$type": "color", + "$value": "#ff7d87" + }, + "400": { + "$type": "color", + "$value": "#ff5050" + }, + "500": { + "$type": "color", + "$value": "#f33641" + }, + "600": { + "$type": "color", + "$value": "#e71d32" + }, + "700": { + "$type": "color", + "$value": "#ad1625" + }, + "800": { + "$type": "color", + "$value": "#8c101c" + }, + "900": { + "$type": "color", + "$value": "#6e0a1e" + }, + "1000": { + "$type": "color", + "$value": "#4c0a17" + }, + "alpha-red-500-10": { + "$type": "color", + "$value": "#f336411a" + } + }, + "Orange": { + "100": { + "$type": "color", + "$value": "#fff3d5" + }, + "200": { + "$type": "color", + "$value": "#ffe4ab" + }, + "300": { + "$type": "color", + "$value": "#ffd181" + }, + "400": { + "$type": "color", + "$value": "#ffbe62" + }, + "500": { + "$type": "color", + "$value": "#ffa02e" + }, + "600": { + "$type": "color", + "$value": "#db7e21" + }, + "700": { + "$type": "color", + "$value": "#b75f17" + }, + "800": { + "$type": "color", + "$value": "#93450e" + }, + "900": { + "$type": "color", + "$value": "#7a3108" + }, + "1000": { + "$type": "color", + "$value": "#602706" + } + }, + "Yellow": { + "100": { + "$type": "color", + "$value": "#fff9b2" + }, + "200": { + "$type": "color", + "$value": "#ffec66" + }, + "300": { + "$type": "color", + "$value": "#ffdf1a" + }, + "400": { + "$type": "color", + "$value": "#ffcc00" + }, + "500": { + "$type": "color", + "$value": "#ffce00" + }, + "600": { + "$type": "color", + "$value": "#e6b800" + }, + "700": { + "$type": "color", + "$value": "#cc9f00" + }, + "800": { + "$type": "color", + "$value": "#b38a00" + }, + "900": { + "$type": "color", + "$value": "#9a7500" + }, + "1000": { + "$type": "color", + "$value": "#7f6200" + } + }, + "Subtle_Color": { + "Rose": { + "100": { + "$type": "color", + "$value": "#fcf2f2" + }, + "200": { + "$type": "color", + "$value": "#fae3e3" + }, + "300": { + "$type": "color", + "$value": "#fad9d9" + }, + "400": { + "$type": "color", + "$value": "#edadad" + }, + "500": { + "$type": "color", + "$value": "#cc4e4e" + }, + "600": { + "$type": "color", + "$value": "#702828" + } + }, + "Papaya": { + "100": { + "$type": "color", + "$value": "#fcf4f0" + }, + "200": { + "$type": "color", + "$value": "#fae8de" + }, + "300": { + "$type": "color", + "$value": "#fadfd2" + }, + "400": { + "$type": "color", + "$value": "#f0bda3" + }, + "500": { + "$type": "color", + "$value": "#d67240" + }, + "600": { + "$type": "color", + "$value": "#6b3215" + } + }, + "Tangerine": { + "100": { + "$type": "color", + "$value": "#fff7ed" + }, + "200": { + "$type": "color", + "$value": "#fcedd9" + }, + "300": { + "$type": "color", + "$value": "#fae5ca" + }, + "400": { + "$type": "color", + "$value": "#f2cb99" + }, + "500": { + "$type": "color", + "$value": "#db8f2c" + }, + "600": { + "$type": "color", + "$value": "#613b0a" + } + }, + "Mango": { + "100": { + "$type": "color", + "$value": "#fff9ec" + }, + "200": { + "$type": "color", + "$value": "#fcf1d7" + }, + "300": { + "$type": "color", + "$value": "#fae9c3" + }, + "400": { + "$type": "color", + "$value": "#f5d68e" + }, + "500": { + "$type": "color", + "$value": "#e0a416" + }, + "600": { + "$type": "color", + "$value": "#5c4102" + } + }, + "Lemon": { + "100": { + "$type": "color", + "$value": "#fffbe8" + }, + "200": { + "$type": "color", + "$value": "#fcf5cf" + }, + "300": { + "$type": "color", + "$value": "#faefb9" + }, + "400": { + "$type": "color", + "$value": "#f5e282" + }, + "500": { + "$type": "color", + "$value": "#e0bb00" + }, + "600": { + "$type": "color", + "$value": "#574800" + } + }, + "Olive": { + "100": { + "$type": "color", + "$value": "#f9fae6" + }, + "200": { + "$type": "color", + "$value": "#f6f7d0" + }, + "300": { + "$type": "color", + "$value": "#f0f2b3" + }, + "400": { + "$type": "color", + "$value": "#dbde83" + }, + "500": { + "$type": "color", + "$value": "#adb204" + }, + "600": { + "$type": "color", + "$value": "#4a4c03" + } + }, + "Lime": { + "100": { + "$type": "color", + "$value": "#f6f9e6" + }, + "200": { + "$type": "color", + "$value": "#eef5ce" + }, + "300": { + "$type": "color", + "$value": "#e7f0bb" + }, + "400": { + "$type": "color", + "$value": "#cfdb91" + }, + "500": { + "$type": "color", + "$value": "#92a822" + }, + "600": { + "$type": "color", + "$value": "#414d05" + } + }, + "Grass": { + "100": { + "$type": "color", + "$value": "#f4faeb" + }, + "200": { + "$type": "color", + "$value": "#e9f5d7" + }, + "300": { + "$type": "color", + "$value": "#def0c5" + }, + "400": { + "$type": "color", + "$value": "#bfd998" + }, + "500": { + "$type": "color", + "$value": "#75a828" + }, + "600": { + "$type": "color", + "$value": "#334d0c" + } + }, + "Forest": { + "100": { + "$type": "color", + "$value": "#f1faf0" + }, + "200": { + "$type": "color", + "$value": "#e2f5df" + }, + "300": { + "$type": "color", + "$value": "#d7f0d3" + }, + "400": { + "$type": "color", + "$value": "#a8d6a1" + }, + "500": { + "$type": "color", + "$value": "#49a33b" + }, + "600": { + "$type": "color", + "$value": "#1e4f16" + } + }, + "Jade": { + "100": { + "$type": "color", + "$value": "#f0faf6" + }, + "200": { + "$type": "color", + "$value": "#dff5eb" + }, + "300": { + "$type": "color", + "$value": "#cef0e1" + }, + "400": { + "$type": "color", + "$value": "#90d1b5" + }, + "500": { + "$type": "color", + "$value": "#1c9963" + }, + "600": { + "$type": "color", + "$value": "#075231" + } + }, + "Aqua": { + "100": { + "$type": "color", + "$value": "#f0f9fa" + }, + "200": { + "$type": "color", + "$value": "#dff3f5" + }, + "300": { + "$type": "color", + "$value": "#ccecf0" + }, + "400": { + "$type": "color", + "$value": "#83ccd4" + }, + "500": { + "$type": "color", + "$value": "#008e9e" + }, + "600": { + "$type": "color", + "$value": "#004e57" + } + }, + "Azure": { + "100": { + "$type": "color", + "$value": "#f0f6fa" + }, + "200": { + "$type": "color", + "$value": "#e1eef7" + }, + "300": { + "$type": "color", + "$value": "#d3e6f5" + }, + "400": { + "$type": "color", + "$value": "#88c0eb" + }, + "500": { + "$type": "color", + "$value": "#0877cc" + }, + "600": { + "$type": "color", + "$value": "#154469" + } + }, + "Denim": { + "100": { + "$type": "color", + "$value": "#f0f3fa" + }, + "200": { + "$type": "color", + "$value": "#e3ebfa" + }, + "300": { + "$type": "color", + "$value": "#d7e2f7" + }, + "400": { + "$type": "color", + "$value": "#9ab6ed" + }, + "500": { + "$type": "color", + "$value": "#3267d1" + }, + "600": { + "$type": "color", + "$value": "#223c70" + } + }, + "Mauve": { + "100": { + "$type": "color", + "$value": "#f2f2fc" + }, + "200": { + "$type": "color", + "$value": "#e6e6fa" + }, + "300": { + "$type": "color", + "$value": "#dcdcf7" + }, + "400": { + "$type": "color", + "$value": "#aeaef5" + }, + "500": { + "$type": "color", + "$value": "#5555e0" + }, + "600": { + "$type": "color", + "$value": "#36366b" + } + }, + "Lavender": { + "100": { + "$type": "color", + "$value": "#f6f3fc" + }, + "200": { + "$type": "color", + "$value": "#ebe3fa" + }, + "300": { + "$type": "color", + "$value": "#e4daf7" + }, + "400": { + "$type": "color", + "$value": "#c1aaf0" + }, + "500": { + "$type": "color", + "$value": "#8153db" + }, + "600": { + "$type": "color", + "$value": "#462f75" + } + }, + "Lilac": { + "100": { + "$type": "color", + "$value": "#f7f0fa" + }, + "200": { + "$type": "color", + "$value": "#f0e1f7" + }, + "300": { + "$type": "color", + "$value": "#edd7f7" + }, + "400": { + "$type": "color", + "$value": "#d3a9e8" + }, + "500": { + "$type": "color", + "$value": "#9e4cc7" + }, + "600": { + "$type": "color", + "$value": "#562d6b" + } + }, + "Mallow": { + "100": { + "$type": "color", + "$value": "#faf0fa" + }, + "200": { + "$type": "color", + "$value": "#f5e1f4" + }, + "300": { + "$type": "color", + "$value": "#f5d7f4" + }, + "400": { + "$type": "color", + "$value": "#dea4dc" + }, + "500": { + "$type": "color", + "$value": "#b240af" + }, + "600": { + "$type": "color", + "$value": "#632861" + } + }, + "Camellia": { + "100": { + "$type": "color", + "$value": "#f9eff3" + }, + "200": { + "$type": "color", + "$value": "#f7e1eb" + }, + "300": { + "$type": "color", + "$value": "#f7d7e5" + }, + "400": { + "$type": "color", + "$value": "#e5a3c0" + }, + "500": { + "$type": "color", + "$value": "#c24279" + }, + "600": { + "$type": "color", + "$value": "#6e2343" + } + }, + "Smoke": { + "100": { + "$type": "color", + "$value": "#f5f5f5" + }, + "200": { + "$type": "color", + "$value": "#e8e8e8" + }, + "300": { + "$type": "color", + "$value": "#dedede" + }, + "400": { + "$type": "color", + "$value": "#b8b8b8" + }, + "500": { + "$type": "color", + "$value": "#6e6e6e" + }, + "600": { + "$type": "color", + "$value": "#404040" + } + }, + "Iron": { + "100": { + "$type": "color", + "$value": "#f2f4f7" + }, + "200": { + "$type": "color", + "$value": "#e6e9f0" + }, + "300": { + "$type": "color", + "$value": "#dadee5" + }, + "400": { + "$type": "color", + "$value": "#b0b5bf" + }, + "500": { + "$type": "color", + "$value": "#666f80" + }, + "600": { + "$type": "color", + "$value": "#394152" + } + } + }, + "Spacing": { + "0": { + "$type": "dimension", + "$value": "0px" + }, + "100": { + "$type": "dimension", + "$value": "4px" + }, + "200": { + "$type": "dimension", + "$value": "6px" + }, + "300": { + "$type": "dimension", + "$value": "8px" + }, + "400": { + "$type": "dimension", + "$value": "12px" + }, + "500": { + "$type": "dimension", + "$value": "16px" + }, + "600": { + "$type": "dimension", + "$value": "20px" + }, + "1000": { + "$type": "dimension", + "$value": "1000px" + } + }, + "Border-Radius": { + "0": { + "$type": "dimension", + "$value": "0px" + }, + "100": { + "$type": "dimension", + "$value": "4px" + }, + "200": { + "$type": "dimension", + "$value": "6px" + }, + "300": { + "$type": "dimension", + "$value": "8px" + }, + "400": { + "$type": "dimension", + "$value": "12px" + }, + "500": { + "$type": "dimension", + "$value": "16px" + }, + "600": { + "$type": "dimension", + "$value": "20px" + }, + "1000": { + "$type": "dimension", + "$value": "1000px" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json new file mode 100644 index 0000000000..99d266c008 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json @@ -0,0 +1,1039 @@ +{ + "Text": { + "primary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "inverse": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "on-fill": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "theme": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "action": { + "$type": "color", + "$value": "{Blue.500}" + }, + "action-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-hover": { + "$type": "color", + "$value": "{Red.400}" + }, + "purple": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Icon": { + "primary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "purple-thick": { + "$type": "color", + "$value": "#ffffff" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "#ffffff" + } + }, + "Border": { + "grey-primary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "grey-primary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "grey-secondary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "grey-secondary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "grey-tertiary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "grey-tertiary-hover": { + "$type": "color", + "$value": "{Neutral.700}" + }, + "grey-quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "grey-quaternary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.400}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Fill": { + "primary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "primary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "secondary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary-hover": { + "$type": "color", + "$value": "{Neutral.500}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "quaternary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "primary-alpha-5": { + "$type": "color", + "$value": "{Neutral.alpha-grey-100-05}", + "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." + }, + "primary-alpha-5-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-100-10}" + }, + "primary-alpha-80": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-80}" + }, + "primary-alpha-80-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-70}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "white-alpha": { + "$type": "color", + "$value": "{Neutral.alpha-white-20}" + }, + "white-alpha-hover": { + "$type": "color", + "$value": "{Neutral.alpha-white-30}" + }, + "black": { + "$type": "color", + "$value": "{Neutral.black}" + }, + "theme-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "theme-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.400}" + }, + "theme-select": { + "$type": "color", + "$value": "{Blue.alpha-blue-500-15}" + }, + "info-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "info-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-light": { + "$type": "color", + "$value": "{Green.100}" + }, + "success-light-hover": { + "$type": "color", + "$value": "{Green.200}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-light": { + "$type": "color", + "$value": "{Orange.100}" + }, + "warning-light-hover": { + "$type": "color", + "$value": "{Orange.200}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-light": { + "$type": "color", + "$value": "{Red.100}" + }, + "error-light-hover": { + "$type": "color", + "$value": "{Red.200}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.500}" + }, + "error-select": { + "$type": "color", + "$value": "{Red.alpha-red-500-10}" + }, + "purple-light": { + "$type": "color", + "$value": "{Purple.100}" + }, + "purple-light-hover": { + "$type": "color", + "$value": "{Purple.200}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + } + }, + "Surface": { + "primary": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "overlay": { + "$type": "color", + "$value": "{Neutral.alpha-black-60}" + } + }, + "Background": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.700}" + } + }, + "Badge_Color": { + "Rose": { + "rose-light-1": { + "$type": "color", + "$value": "#fcf2f2" + }, + "rose-light-2": { + "$type": "color", + "$value": "#fae3e3" + }, + "rose-light-3": { + "$type": "color", + "$value": "#fad9d9" + }, + "rose-thick-1": { + "$type": "color", + "$value": "#edadad" + }, + "rose-thick-2": { + "$type": "color", + "$value": "#cc4e4e" + }, + "rose-thick-3": { + "$type": "color", + "$value": "#702828" + } + }, + "Papaya": { + "papaya-light-1": { + "$type": "color", + "$value": "#fcf4f0" + }, + "papaya-light-2": { + "$type": "color", + "$value": "#fae8de" + }, + "papaya-light-3": { + "$type": "color", + "$value": "#fadfd2" + }, + "papaya-thick-1": { + "$type": "color", + "$value": "#f0bda3" + }, + "papaya-thick-2": { + "$type": "color", + "$value": "#d67240" + }, + "papaya-thick-3": { + "$type": "color", + "$value": "#6b3215" + } + }, + "Tangerine": { + "tangerine-light-1": { + "$type": "color", + "$value": "#fff7ed" + }, + "tangerine-light-2": { + "$type": "color", + "$value": "#fcedd9" + }, + "tangerine-light-3": { + "$type": "color", + "$value": "#fae5ca" + }, + "tangerine-thick-1": { + "$type": "color", + "$value": "#f2cb99" + }, + "tangerine-thick-2": { + "$type": "color", + "$value": "#db8f2c" + }, + "tangerine-thick-3": { + "$type": "color", + "$value": "#613b0a" + } + }, + "Mango": { + "mango-light-1": { + "$type": "color", + "$value": "#fff9ec" + }, + "mango-light-2": { + "$type": "color", + "$value": "#fcf1d7" + }, + "mango-light-3": { + "$type": "color", + "$value": "#fae9c3" + }, + "mango-thick-1": { + "$type": "color", + "$value": "#f5d68e" + }, + "mango-thick-2": { + "$type": "color", + "$value": "#e0a416" + }, + "mango-thick-3": { + "$type": "color", + "$value": "#5c4102" + } + }, + "Lemon": { + "lemon-light-1": { + "$type": "color", + "$value": "#fffbe8" + }, + "lemon-light-2": { + "$type": "color", + "$value": "#fcf5cf" + }, + "lemon-light-3": { + "$type": "color", + "$value": "#faefb9" + }, + "lemon-thick-1": { + "$type": "color", + "$value": "#f5e282" + }, + "lemon-thick-2": { + "$type": "color", + "$value": "#e0bb00" + }, + "lemon-thick-3": { + "$type": "color", + "$value": "#574800" + } + }, + "Olive": { + "olive-light-1": { + "$type": "color", + "$value": "#f9fae6" + }, + "olive-light-2": { + "$type": "color", + "$value": "#f6f7d0" + }, + "olive-light-3": { + "$type": "color", + "$value": "#f0f2b3" + }, + "olive-thick-1": { + "$type": "color", + "$value": "#dbde83" + }, + "olive-thick-2": { + "$type": "color", + "$value": "#adb204" + }, + "olive-thick-3": { + "$type": "color", + "$value": "#4a4c03" + } + }, + "Lime": { + "lime-light-1": { + "$type": "color", + "$value": "#f6f9e6" + }, + "lime-light-2": { + "$type": "color", + "$value": "#eef5ce" + }, + "lime-light-3": { + "$type": "color", + "$value": "#e7f0bb" + }, + "lime-thick-1": { + "$type": "color", + "$value": "#cfdb91" + }, + "lime-thick-2": { + "$type": "color", + "$value": "#92a822" + }, + "lime-thick-3": { + "$type": "color", + "$value": "#414d05" + } + }, + "Grass": { + "grass-light-1": { + "$type": "color", + "$value": "#f4faeb" + }, + "grass-light-2": { + "$type": "color", + "$value": "#e9f5d7" + }, + "grass-light-3": { + "$type": "color", + "$value": "#def0c5" + }, + "grass-thick-1": { + "$type": "color", + "$value": "#bfd998" + }, + "grass-thick-2": { + "$type": "color", + "$value": "#75a828" + }, + "grass-thick-3": { + "$type": "color", + "$value": "#334d0c" + } + }, + "Forest": { + "forest-light-1": { + "$type": "color", + "$value": "#f1faf0" + }, + "forest-light-2": { + "$type": "color", + "$value": "#e2f5df" + }, + "forest-light-3": { + "$type": "color", + "$value": "#d7f0d3" + }, + "forest-thick-1": { + "$type": "color", + "$value": "#a8d6a1" + }, + "forest-thick-2": { + "$type": "color", + "$value": "#49a33b" + }, + "forest-thick-3": { + "$type": "color", + "$value": "#1e4f16" + } + }, + "Jade": { + "jade-light-1": { + "$type": "color", + "$value": "#f0faf6" + }, + "jade-light-2": { + "$type": "color", + "$value": "#dff5eb" + }, + "jade-light-3": { + "$type": "color", + "$value": "#cef0e1" + }, + "jade-thick-1": { + "$type": "color", + "$value": "#90d1b5" + }, + "jade-thick-2": { + "$type": "color", + "$value": "#1c9963" + }, + "jade-thick-3": { + "$type": "color", + "$value": "#075231" + } + }, + "Aqua": { + "aqua-light-1": { + "$type": "color", + "$value": "#f0f9fa" + }, + "aqua-light-2": { + "$type": "color", + "$value": "#dff3f5" + }, + "aqua-light-3": { + "$type": "color", + "$value": "#ccecf0" + }, + "aqua-thick-1": { + "$type": "color", + "$value": "#83ccd4" + }, + "aqua-thick-2": { + "$type": "color", + "$value": "#008e9e" + }, + "aqua-thick-3": { + "$type": "color", + "$value": "#004e57" + } + }, + "Azure": { + "azure-light-1": { + "$type": "color", + "$value": "#f0f6fa" + }, + "azure-light-2": { + "$type": "color", + "$value": "#e1eef7" + }, + "azure-light-3": { + "$type": "color", + "$value": "#d3e6f5" + }, + "azure-thick-1": { + "$type": "color", + "$value": "#88c0eb" + }, + "azure-thick-2": { + "$type": "color", + "$value": "#0877cc" + }, + "azure-thick-3": { + "$type": "color", + "$value": "#154469" + } + }, + "Denim": { + "denim-light-1": { + "$type": "color", + "$value": "#f0f3fa" + }, + "denim-light-2": { + "$type": "color", + "$value": "#e3ebfa" + }, + "denim-light-3": { + "$type": "color", + "$value": "#d7e2f7" + }, + "denim-thick-1": { + "$type": "color", + "$value": "#9ab6ed" + }, + "denim-thick-2": { + "$type": "color", + "$value": "#3267d1" + }, + "denim-thick-3": { + "$type": "color", + "$value": "#223c70" + } + }, + "Mauve": { + "mauve-light-1": { + "$type": "color", + "$value": "#f2f2fc" + }, + "mauve-thick-2": { + "$type": "color", + "$value": "#5555e0" + }, + "mauve-thick-3": { + "$type": "color", + "$value": "#36366b" + }, + "mauve-thick-1": { + "$type": "color", + "$value": "#aeaef5" + } + }, + "Lavender": { + "lavender-light-1": { + "$type": "color", + "$value": "#f6f3fc" + }, + "lavender-light-2": { + "$type": "color", + "$value": "#ebe3fa" + }, + "lavender-light-3": { + "$type": "color", + "$value": "#e4daf7" + }, + "lavender-thick-1": { + "$type": "color", + "$value": "#c1aaf0" + }, + "lavender-thick-2": { + "$type": "color", + "$value": "#8153db" + }, + "lavender-thick-3": { + "$type": "color", + "$value": "#462f75" + } + }, + "Lilac": { + "liliac-light-1": { + "$type": "color", + "$value": "#f7f0fa" + }, + "liliac-light-2": { + "$type": "color", + "$value": "#f0e1f7" + }, + "liliac-light-3": { + "$type": "color", + "$value": "#edd7f7" + }, + "liliac-thick-1": { + "$type": "color", + "$value": "#d3a9e8" + }, + "liliac-thick-2": { + "$type": "color", + "$value": "#9e4cc7" + }, + "liliac-thick-3": { + "$type": "color", + "$value": "#562d6b" + } + }, + "Mallow": { + "mallow-light-1": { + "$type": "color", + "$value": "#faf0fa" + }, + "mallow-light-2": { + "$type": "color", + "$value": "#f5e1f4" + }, + "mallow-light-3": { + "$type": "color", + "$value": "#f5d7f4" + }, + "mallow-thick-1": { + "$type": "color", + "$value": "#dea4dc" + }, + "mallow-thick-2": { + "$type": "color", + "$value": "#b240af" + }, + "mallow-thick-3": { + "$type": "color", + "$value": "#632861" + } + }, + "Camellia": { + "camellia-light-1": { + "$type": "color", + "$value": "#f9eff3" + }, + "camellia-light-2": { + "$type": "color", + "$value": "#f7e1eb" + }, + "camellia-light-3": { + "$type": "color", + "$value": "#f7d7e5" + }, + "camellia-thick-1": { + "$type": "color", + "$value": "#e5a3c0" + }, + "camellia-thick-2": { + "$type": "color", + "$value": "#c24279" + }, + "camellia-thick-3": { + "$type": "color", + "$value": "#6e2343" + } + }, + "Smoke": { + "smoke-light-1": { + "$type": "color", + "$value": "#f5f5f5" + }, + "smoke-light-2": { + "$type": "color", + "$value": "#e8e8e8" + }, + "smoke-light-3": { + "$type": "color", + "$value": "#dedede" + }, + "smoke-thick-1": { + "$type": "color", + "$value": "#b8b8b8" + }, + "smoke-thick-2": { + "$type": "color", + "$value": "#6e6e6e" + }, + "smoke-thick-3": { + "$type": "color", + "$value": "#404040" + } + }, + "Iron": { + "icon-light-1": { + "$type": "color", + "$value": "#f2f4f7" + }, + "icon-light-2": { + "$type": "color", + "$value": "#e6e9f0" + }, + "icon-light-3": { + "$type": "color", + "$value": "#dadee5" + }, + "icon-thick-1": { + "$type": "color", + "$value": "#b0b5bf" + }, + "icon-thick-2": { + "$type": "color", + "$value": "#666f80" + }, + "icon-thick-3": { + "$type": "color", + "$value": "#394152" + } + } + }, + "Shadow": { + "sm": { + "$type": "dimension", + "$value": "0px" + }, + "md": { + "$type": "dimension", + "$value": "0px" + } + }, + "Brand": { + "Skyline": { + "$type": "color", + "$value": "#00b5ff" + }, + "Aqua": { + "$type": "color", + "$value": "#00c8ff" + }, + "Violet": { + "$type": "color", + "$value": "#9327ff" + }, + "Amethyst": { + "$type": "color", + "$value": "#8427e0" + }, + "Berry": { + "$type": "color", + "$value": "#e3006d" + }, + "Coral": { + "$type": "color", + "$value": "#fb006d" + }, + "Golden": { + "$type": "color", + "$value": "#f7931e" + }, + "Amber": { + "$type": "color", + "$value": "#ffbd00" + }, + "Lemon": { + "$type": "color", + "$value": "#ffce00" + } + }, + "Other_Colors": { + "text-highlight": { + "$type": "color", + "$value": "{Blue.200}" + } + }, + "Spacing": { + "spacing-0": { + "$type": "dimension", + "$value": "{Spacing.0}" + }, + "spacing-xs": { + "$type": "dimension", + "$value": "{Spacing.100}" + }, + "spacing-s": { + "$type": "dimension", + "$value": "{Spacing.200}" + }, + "spacing-m": { + "$type": "dimension", + "$value": "{Spacing.300}" + }, + "spacing-l": { + "$type": "dimension", + "$value": "{Spacing.400}" + }, + "spacing-xl": { + "$type": "dimension", + "$value": "{Spacing.500}" + }, + "spacing-xxl": { + "$type": "dimension", + "$value": "{Spacing.600}" + }, + "spacing-full": { + "$type": "dimension", + "$value": "{Spacing.1000}" + } + }, + "Border_Radius": { + "border-radius-0": { + "$type": "dimension", + "$value": "{Border-Radius.0}" + }, + "border-radius-xs": { + "$type": "dimension", + "$value": "{Border-Radius.100}" + }, + "border-radius-s": { + "$type": "dimension", + "$value": "{Border-Radius.200}" + }, + "border-radius-m": { + "$type": "dimension", + "$value": "{Border-Radius.300}" + }, + "border-radius-l": { + "$type": "dimension", + "$value": "{Border-Radius.400}" + }, + "border-radius-xl": { + "$type": "dimension", + "$value": "{Border-Radius.500}" + }, + "border-radius-xxl": { + "$type": "dimension", + "$value": "{Border-Radius.600}" + }, + "border-radius-full": { + "$type": "dimension", + "$value": "{Border-Radius.1000}" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json new file mode 100644 index 0000000000..4e6b0543dc --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json @@ -0,0 +1,1039 @@ +{ + "Text": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "inverse": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "on-fill": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "theme": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "action": { + "$type": "color", + "$value": "{Blue.500}" + }, + "action-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "purple": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Icon": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Border": { + "grey-primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "grey-primary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "grey-secondary": { + "$type": "color", + "$value": "{Neutral.800}" + }, + "grey-secondary-hover": { + "$type": "color", + "$value": "{Neutral.700}" + }, + "grey-tertiary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "grey-tertiary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "grey-quaternary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "grey-quaternary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + } + }, + "Fill": { + "primary": { + "$type": "color", + "$value": "{Neutral.1000}" + }, + "primary-hover": { + "$type": "color", + "$value": "{Neutral.900}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.600}" + }, + "secondary-hover": { + "$type": "color", + "$value": "{Neutral.500}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.300}" + }, + "tertiary-hover": { + "$type": "color", + "$value": "{Neutral.400}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "quaternary-hover": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "transparent": { + "$type": "color", + "$value": "{Neutral.alpha-white-0}" + }, + "primary-alpha-5": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-05}", + "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." + }, + "primary-alpha-5-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-10}" + }, + "primary-alpha-80": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-80}" + }, + "primary-alpha-80-hover": { + "$type": "color", + "$value": "{Neutral.alpha-grey-1000-70}" + }, + "white": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "white-alpha": { + "$type": "color", + "$value": "{Neutral.alpha-white-20}" + }, + "white-alpha-hover": { + "$type": "color", + "$value": "{Neutral.alpha-white-30}" + }, + "black": { + "$type": "color", + "$value": "{Neutral.black}" + }, + "theme-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "theme-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "theme-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "theme-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "theme-select": { + "$type": "color", + "$value": "{Blue.alpha-blue-500-15}" + }, + "info-light": { + "$type": "color", + "$value": "{Blue.100}" + }, + "info-light-hover": { + "$type": "color", + "$value": "{Blue.200}" + }, + "info-thick": { + "$type": "color", + "$value": "{Blue.500}" + }, + "info-thick-hover": { + "$type": "color", + "$value": "{Blue.600}" + }, + "success-light": { + "$type": "color", + "$value": "{Green.100}" + }, + "success-light-hover": { + "$type": "color", + "$value": "{Green.200}" + }, + "success-thick": { + "$type": "color", + "$value": "{Green.600}" + }, + "success-thick-hover": { + "$type": "color", + "$value": "{Green.700}" + }, + "warning-light": { + "$type": "color", + "$value": "{Orange.100}" + }, + "warning-light-hover": { + "$type": "color", + "$value": "{Orange.200}" + }, + "warning-thick": { + "$type": "color", + "$value": "{Orange.600}" + }, + "warning-thick-hover": { + "$type": "color", + "$value": "{Orange.700}" + }, + "error-light": { + "$type": "color", + "$value": "{Red.100}" + }, + "error-light-hover": { + "$type": "color", + "$value": "{Red.200}" + }, + "error-thick": { + "$type": "color", + "$value": "{Red.600}" + }, + "error-thick-hover": { + "$type": "color", + "$value": "{Red.700}" + }, + "error-select": { + "$type": "color", + "$value": "{Red.alpha-red-500-10}" + }, + "purple-light": { + "$type": "color", + "$value": "{Purple.100}" + }, + "purple-light-hover": { + "$type": "color", + "$value": "{Purple.200}" + }, + "purple-thick-hover": { + "$type": "color", + "$value": "{Purple.600}" + }, + "purple-thick": { + "$type": "color", + "$value": "{Purple.500}" + } + }, + "Surface": { + "primary": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "overlay": { + "$type": "color", + "$value": "{Neutral.alpha-black-60}" + } + }, + "Background": { + "primary": { + "$type": "color", + "$value": "{Neutral.white}" + }, + "secondary": { + "$type": "color", + "$value": "{Neutral.100}" + }, + "tertiary": { + "$type": "color", + "$value": "{Neutral.200}" + }, + "quaternary": { + "$type": "color", + "$value": "{Neutral.300}" + } + }, + "Badge_Color": { + "Rose": { + "rose-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Rose.100}" + }, + "rose-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Rose.200}" + }, + "rose-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Rose.300}" + }, + "rose-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Rose.400}" + }, + "rose-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Rose.500}" + }, + "rose-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Rose.600}" + } + }, + "Papaya": { + "papaya-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.100}" + }, + "papaya-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.200}" + }, + "papaya-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.300}" + }, + "papaya-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.400}" + }, + "papaya-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.500}" + }, + "papaya-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Papaya.600}" + } + }, + "Tangerine": { + "tangerine-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.100}" + }, + "tangerine-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.200}" + }, + "tangerine-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.300}" + }, + "tangerine-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.400}" + }, + "tangerine-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.500}" + }, + "tangerine-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Tangerine.600}" + } + }, + "Mango": { + "mango-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mango.100}" + }, + "mango-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Mango.200}" + }, + "mango-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Mango.300}" + }, + "mango-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mango.400}" + }, + "mango-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mango.500}" + }, + "mango-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mango.600}" + } + }, + "Lemon": { + "lemon-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.100}" + }, + "lemon-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.200}" + }, + "lemon-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.300}" + }, + "lemon-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.400}" + }, + "lemon-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.500}" + }, + "lemon-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lemon.600}" + } + }, + "Olive": { + "olive-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Olive.100}" + }, + "olive-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Olive.200}" + }, + "olive-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Olive.300}" + }, + "olive-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Olive.400}" + }, + "olive-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Olive.500}" + }, + "olive-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Olive.600}" + } + }, + "Lime": { + "lime-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lime.100}" + }, + "lime-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lime.200}" + }, + "lime-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lime.300}" + }, + "lime-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lime.400}" + }, + "lime-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lime.500}" + }, + "lime-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lime.600}" + } + }, + "Grass": { + "grass-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Grass.100}" + }, + "grass-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Grass.200}" + }, + "grass-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Grass.300}" + }, + "grass-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Grass.400}" + }, + "grass-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Grass.500}" + }, + "grass-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Grass.600}" + } + }, + "Forest": { + "forest-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Forest.100}" + }, + "forest-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Forest.200}" + }, + "forest-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Forest.300}" + }, + "forest-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Forest.400}" + }, + "forest-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Forest.500}" + }, + "forest-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Forest.600}" + } + }, + "Jade": { + "jade-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Jade.100}" + }, + "jade-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Jade.200}" + }, + "jade-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Jade.300}" + }, + "jade-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Jade.400}" + }, + "jade-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Jade.500}" + }, + "jade-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Jade.600}" + } + }, + "Aqua": { + "aqua-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.100}" + }, + "aqua-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.200}" + }, + "aqua-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.300}" + }, + "aqua-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.400}" + }, + "aqua-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.500}" + }, + "aqua-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Aqua.600}" + } + }, + "Azure": { + "azure-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Azure.100}" + }, + "azure-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Azure.200}" + }, + "azure-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Azure.300}" + }, + "azure-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Azure.400}" + }, + "azure-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Azure.500}" + }, + "azure-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Azure.600}" + } + }, + "Denim": { + "denim-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Denim.100}" + }, + "denim-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Denim.200}" + }, + "denim-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Denim.300}" + }, + "denim-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Denim.400}" + }, + "denim-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Denim.500}" + }, + "denim-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Denim.600}" + } + }, + "Mauve": { + "mauve-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.100}" + }, + "mauve-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.500}" + }, + "mauve-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.600}" + }, + "mauve-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mauve.400}" + } + }, + "Lavender": { + "lavender-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.100}" + }, + "lavender-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.200}" + }, + "lavender-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.300}" + }, + "lavender-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.400}" + }, + "lavender-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.500}" + }, + "lavender-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lavender.600}" + } + }, + "Lilac": { + "liliac-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.100}" + }, + "liliac-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.200}" + }, + "liliac-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.300}" + }, + "liliac-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.400}" + }, + "liliac-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.500}" + }, + "liliac-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Lilac.600}" + } + }, + "Mallow": { + "mallow-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.100}" + }, + "mallow-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.200}" + }, + "mallow-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.300}" + }, + "mallow-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.400}" + }, + "mallow-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.500}" + }, + "mallow-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Mallow.600}" + } + }, + "Camellia": { + "camellia-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.100}" + }, + "camellia-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.200}" + }, + "camellia-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.300}" + }, + "camellia-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.400}" + }, + "camellia-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.500}" + }, + "camellia-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Camellia.600}" + } + }, + "Smoke": { + "smoke-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.100}" + }, + "smoke-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.200}" + }, + "smoke-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.300}" + }, + "smoke-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.400}" + }, + "smoke-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.500}" + }, + "smoke-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Smoke.600}" + } + }, + "Iron": { + "icon-light-1": { + "$type": "color", + "$value": "{Subtle_Color.Iron.100}" + }, + "icon-light-2": { + "$type": "color", + "$value": "{Subtle_Color.Iron.200}" + }, + "icon-light-3": { + "$type": "color", + "$value": "{Subtle_Color.Iron.300}" + }, + "icon-thick-1": { + "$type": "color", + "$value": "{Subtle_Color.Iron.400}" + }, + "icon-thick-2": { + "$type": "color", + "$value": "{Subtle_Color.Iron.500}" + }, + "icon-thick-3": { + "$type": "color", + "$value": "{Subtle_Color.Iron.600}" + } + } + }, + "Shadow": { + "sm": { + "$type": "dimension", + "$value": "0px" + }, + "md": { + "$type": "dimension", + "$value": "0px" + } + }, + "Brand": { + "Skyline": { + "$type": "color", + "$value": "#00b5ff" + }, + "Aqua": { + "$type": "color", + "$value": "#00c8ff" + }, + "Violet": { + "$type": "color", + "$value": "#9327ff" + }, + "Amethyst": { + "$type": "color", + "$value": "#8427e0" + }, + "Berry": { + "$type": "color", + "$value": "#e3006d" + }, + "Coral": { + "$type": "color", + "$value": "#fb006d" + }, + "Golden": { + "$type": "color", + "$value": "#f7931e" + }, + "Amber": { + "$type": "color", + "$value": "#ffbd00" + }, + "Lemon": { + "$type": "color", + "$value": "#ffce00" + } + }, + "Other_Colors": { + "text-highlight": { + "$type": "color", + "$value": "{Blue.200}" + } + }, + "Spacing": { + "spacing-0": { + "$type": "dimension", + "$value": "{Spacing.0}" + }, + "spacing-xs": { + "$type": "dimension", + "$value": "{Spacing.100}" + }, + "spacing-s": { + "$type": "dimension", + "$value": "{Spacing.200}" + }, + "spacing-m": { + "$type": "dimension", + "$value": "{Spacing.300}" + }, + "spacing-l": { + "$type": "dimension", + "$value": "{Spacing.400}" + }, + "spacing-xl": { + "$type": "dimension", + "$value": "{Spacing.500}" + }, + "spacing-xxl": { + "$type": "dimension", + "$value": "{Spacing.600}" + }, + "spacing-full": { + "$type": "dimension", + "$value": "{Spacing.1000}" + } + }, + "Border_Radius": { + "border-radius-0": { + "$type": "dimension", + "$value": "{Border-Radius.0}" + }, + "border-radius-xs": { + "$type": "dimension", + "$value": "{Border-Radius.100}" + }, + "border-radius-s": { + "$type": "dimension", + "$value": "{Border-Radius.200}" + }, + "border-radius-m": { + "$type": "dimension", + "$value": "{Border-Radius.300}" + }, + "border-radius-l": { + "$type": "dimension", + "$value": "{Border-Radius.400}" + }, + "border-radius-xl": { + "$type": "dimension", + "$value": "{Border-Radius.500}" + }, + "border-radius-xxl": { + "$type": "dimension", + "$value": "{Border-Radius.600}" + }, + "border-radius-full": { + "$type": "dimension", + "$value": "{Border-Radius.1000}" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart new file mode 100644 index 0000000000..9c9c8a45ff --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -0,0 +1,269 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; + +void main() { + generatePrimitive(); + generateSemantic(); +} + +void generatePrimitive() { + // 1. Load the JSON file. + final jsonString = + File('script/Primitive.Mode 1.tokens.json').readAsStringSync(); + final jsonData = jsonDecode(jsonString) as Map; + + // 2. Prepare the output code. + final buffer = StringBuffer(); + + buffer.writeln(''' +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: ${DateTime.now().toIso8601String()} +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:flutter/material.dart'; + +class AppFlowyPrimitiveTokens { + AppFlowyPrimitiveTokens._();'''); + + // 3. Process each color category. + jsonData.forEach((categoryName, categoryData) { + if (categoryData is Map) { + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map && + tokenData['\$type'] == 'color') { + final colorValue = tokenData['\$value'] as String; + final dartColorValue = convertColor(colorValue); + final dartTokenName = + '${categoryName}_$tokenName'.replaceAll('-', '_').toCamelCase(); + + buffer.writeln(''' + + /// $colorValue + static Color get $dartTokenName => Color(0x$dartColorValue);'''); + } + }); + } + }); + + buffer.writeln('}'); + + // 4. Write the output to a Dart file. + final outputFile = File('lib/src/theme/data/appflowy_default/primitive.dart'); + outputFile.writeAsStringSync(buffer.toString()); + + print('Successfully generated ${outputFile.path}'); +} + +void generateSemantic() { + // 1. Load the JSON file. + final lightJsonString = + File('script/Semantic.Light Mode.tokens.json').readAsStringSync(); + final darkJsonString = + File('script/Semantic.Dark Mode.tokens.json').readAsStringSync(); + final lightJsonData = jsonDecode(lightJsonString) as Map; + final darkJsonData = jsonDecode(darkJsonString) as Map; + + // 2. Prepare the output code. + final buffer = StringBuffer(); + + buffer.writeln(''' +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// +// AUTO-GENERATED - DO NOT EDIT DIRECTLY +// +// This file is auto-generated by the generate_theme.dart script +// Generation time: ${DateTime.now().toIso8601String()} +// +// To modify these colors, edit the source JSON files and run the script: +// +// dart run script/generate_theme.dart +// +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flutter/material.dart'; + +import '../builder.dart'; +import 'primitive.dart'; + +class AppFlowyThemeData implements AppFlowyBaseThemeData { + const AppFlowyThemeData._({ + required this.textStyle, + required this.textColorScheme, + required this.borderColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.iconColorScheme, + required this.backgroundColorScheme, + required this.otherColorsColorScheme, + }); +'''); + + // 3. Process light mode semantic tokens + void writeThemeFactory(String brightness, Map jsonData) { + buffer.writeln(''' + factory AppFlowyThemeData.$brightness() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = themeBuilder.buildBorderRadius(); + final spacing = themeBuilder.buildSpacing(); + final shadow = themeBuilder.buildShadow(Brightness.$brightness);'''); + + jsonData.forEach((categoryName, categoryData) { + if (categoryData is Map) { + final hasNonColorType = categoryData.values.any( + (element) => + element is Map && element['\$type'] != 'color', + ); + if (hasNonColorType) { + return; + } + + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); + + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map && + tokenData['\$type'] == 'color') { + final semanticTokenName = + tokenName.replaceAll('-', '_').toCamelCase(); + + final value = tokenData['\$value'] as String; + final String colorOrPrimitiveToken; + if (value.isColor) { + colorOrPrimitiveToken = 'Color(0x${convertColor(value)})'; + } else { + final primitiveToken = value + .replaceAll('{', '') + .replaceAll('}', '') + .replaceAll('.', '_') + .replaceAll('-', '_') + .toCamelCase(); + colorOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; + } + + buffer.writeln(' $semanticTokenName: $colorOrPrimitiveToken,'); + } + }); + buffer.writeln(' );'); + } + }); + + buffer.writeln(); + buffer.writeln(''' + return AppFlowyThemeData._( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + }'''); + } + + writeThemeFactory('light', lightJsonData); + buffer.writeln(); + writeThemeFactory('dark', darkJsonData); + + buffer.writeln(''' + + static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); + + @override + final AppFlowyBaseTextStyle textStyle; + + @override + final AppFlowyTextColorScheme textColorScheme; + + @override + final AppFlowyBorderColorScheme borderColorScheme; + + @override + final AppFlowyFillColorScheme fillColorScheme; + + @override + final AppFlowySurfaceColorScheme surfaceColorScheme; + + @override + final AppFlowyBorderRadius borderRadius; + + @override + final AppFlowySpacing spacing; + + @override + final AppFlowyShadow shadow; + + @override + final AppFlowyBrandColorScheme brandColorScheme; + + @override + final AppFlowyIconColorScheme iconColorScheme; + + @override + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + @override + final AppFlowyOtherColorsColorScheme otherColorsColorScheme;'''); + buffer.writeln('}'); + + // 4. Write the output to a Dart file. + final outputFile = File('lib/src/theme/data/appflowy_default/semantic.dart'); + outputFile.writeAsStringSync(buffer.toString()); + + print('Successfully generated ${outputFile.path}'); +} + +String convertColor(String hexColor) { + String color = hexColor.toUpperCase().replaceAll('#', ''); + if (color.length == 6) { + color = 'FF$color'; // Add missing alpha channel + } else if (color.length == 8) { + color = color.substring(6) + color.substring(0, 6); // Rearrange to ARGB + } + return color; +} + +extension on String { + String toCamelCase() { + return split('_').mapIndexed((index, part) { + if (index == 0) { + return part.toLowerCase(); + } else { + return part[0].toUpperCase() + part.substring(1).toLowerCase(); + } + }).join(); + } + + String toCapitalize() { + if (isEmpty) { + return this; + } + return '${this[0].toUpperCase()}${substring(1)}'; + } + + bool get isColor => + startsWith('#') || + (startsWith('0x') && length == 10) || + (startsWith('0xFF') && length == 12); +} From 28e89beb432605d01c46db9829c866c056bf32a7 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:14:38 +0800 Subject: [PATCH 28/74] feat: appflowy theme lerp (#7771) * feat: implement appflowy theme lerping * refactor: use theme builder and adjust script * chore: rename theme data * chore: add doc comments * chore: rename function * chore: don't use theme extension * chore: use the animated appflowy theme widget * chore: clean up inherited theme --- .../lib/startup/tasks/app_widget.dart | 47 ++++---- .../appearance/mobile_appearance.dart | 17 +-- .../appflowy_ui/example/lib/main.dart | 12 +- .../src/component/textfield/textfield.dart | 4 +- .../lib/src/theme/appflowy_theme.dart | 105 ++++++++++++++++-- .../data/appflowy_default/primitive.dart | 2 +- .../theme/data/appflowy_default/semantic.dart | 81 +++----------- .../src/theme/data/custom/custom_theme.dart | 23 ++++ .../theme/data/{builder.dart => shared.dart} | 10 +- .../lib/src/theme/definition/base_theme.dart | 33 ------ .../color_scheme/background_color_scheme.dart | 12 ++ .../color_scheme/border_color_scheme.dart | 36 ++++++ .../color_scheme/brand_color_scheme.dart | 17 +++ .../color_scheme/fill_color_scheme.dart | 59 ++++++++++ .../color_scheme/icon_color_scheme.dart | 16 +++ .../color_scheme/other_color_scheme.dart | 9 ++ .../color_scheme/surface_color_scheme.dart | 10 ++ .../color_scheme/text_color_scheme.dart | 28 +++++ .../lib/src/theme/definition/theme_data.dart | 86 ++++++++++++++ .../appflowy_ui/lib/src/theme/theme.dart | 2 +- .../appflowy_ui/script/generate_theme.dart | 78 +++---------- 21 files changed, 467 insertions(+), 220 deletions(-) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart rename frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/{builder.dart => shared.dart} (92%) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 0ef21267aa..98b76802d4 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -234,27 +234,34 @@ class _ApplicationWidgetState extends State { supportedLocales: context.supportedLocales, locale: state.locale, routerConfig: routerConfig, - builder: (context, child) => AppFlowyTheme( - data: Theme.of(context).brightness == Brightness.light - ? AppFlowyThemeData.light() - : AppFlowyThemeData.dark(), - child: MediaQuery( - // use the 1.0 as the textScaleFactor to avoid the text size - // affected by the system setting. - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(state.textScaleFactor), + builder: (context, child) { + final themeBuilder = AppFlowyDefaultTheme(); + final brightness = Theme.of(context).brightness; + + return AnimatedAppFlowyTheme( + data: brightness == Brightness.light + ? themeBuilder.light() + : themeBuilder.dark(), + child: MediaQuery( + // use the 1.0 as the textScaleFactor to avoid the text size + // affected by the system setting. + data: MediaQuery.of(context).copyWith( + textScaler: + TextScaler.linear(state.textScaleFactor), + ), + child: overlayManagerBuilder( + context, + !UniversalPlatform.isMobile && + FeatureFlag.search.isOn + ? CommandPalette( + notifier: _commandPaletteNotifier, + child: child, + ) + : child, + ), ), - child: overlayManagerBuilder( - context, - !UniversalPlatform.isMobile && FeatureFlag.search.isOn - ? CommandPalette( - notifier: _commandPaletteNotifier, - child: child, - ) - : child, - ), - ), - ), + ); + }, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index eda3153459..46eddd53ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -28,13 +28,12 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, ); + final isLight = brightness == Brightness.light; final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - final theme = brightness == Brightness.light - ? appTheme.lightTheme - : appTheme.darkTheme; + final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; - final colorTheme = brightness == Brightness.light + final colorTheme = isLight ? ColorScheme( brightness: brightness, primary: _primaryColor, @@ -71,13 +70,9 @@ class MobileAppearance extends BaseAppearance { onSurface: const Color(0xffC5C6C7), // text/body color surfaceContainerHighest: theme.sidebarBg, ); - final hintColor = brightness == Brightness.light - ? const Color(0x991F2329) - : _hintColorInDarkMode; - final onBackground = - brightness == Brightness.light ? _onBackgroundColor : Colors.white; - final background = - brightness == Brightness.light ? Colors.white : const Color(0xff121212); + final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode; + final onBackground = isLight ? _onBackgroundColor : Colors.white; + final background = isLight ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart index c02001745d..0d23746ebd 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart @@ -26,16 +26,20 @@ class MyApp extends StatelessWidget { return ValueListenableBuilder( valueListenable: themeMode, builder: (context, themeMode, child) { + final themeBuilder = AppFlowyDefaultTheme(); final themeData = themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); - return AppFlowyTheme( + + return AnimatedAppFlowyTheme( data: themeMode == ThemeMode.light - ? AppFlowyThemeData.light() - : AppFlowyThemeData.dark(), + ? themeBuilder.light() + : themeBuilder.dark(), child: MaterialApp( debugShowCheckedModeBanner: false, title: 'AppFlowy UI Example', - theme: themeData.copyWith(visualDensity: VisualDensity.standard), + theme: themeData.copyWith( + visualDensity: VisualDensity.standard, + ), home: const MyHomePage( title: 'AppFlowy UI', ), diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart index 9e61b71709..3f5ad4cfed 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart @@ -233,7 +233,7 @@ enum AFTextFieldSize { m, l; - EdgeInsetsGeometry contentPadding(AppFlowyBaseThemeData theme) { + EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { return EdgeInsets.symmetric( vertical: switch (this) { AFTextFieldSize.m => theme.spacing.s, @@ -243,7 +243,7 @@ enum AFTextFieldSize { ); } - BorderRadius borderRadius(AppFlowyBaseThemeData theme) { + BorderRadius borderRadius(AppFlowyThemeData theme) { return BorderRadius.circular( switch (this) { AFTextFieldSize.m => theme.borderRadius.m, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart index 8a99b737ec..26e45ca8f1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -1,5 +1,6 @@ -import 'package:appflowy_ui/src/theme/definition/base_theme.dart'; -import 'package:flutter/widgets.dart'; +import 'package:appflowy_ui/src/theme/theme.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; class AppFlowyTheme extends StatelessWidget { const AppFlowyTheme({ @@ -8,10 +9,10 @@ class AppFlowyTheme extends StatelessWidget { required this.child, }); - final AppFlowyBaseThemeData data; + final AppFlowyThemeData data; final Widget child; - static AppFlowyBaseThemeData of(BuildContext context, {bool listen = true}) { + static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { final provider = maybeOf(context, listen: listen); if (provider == null) { throw FlutterError( @@ -26,26 +27,26 @@ class AppFlowyTheme extends StatelessWidget { return provider; } - static AppFlowyBaseThemeData? maybeOf( + static AppFlowyThemeData? maybeOf( BuildContext context, { bool listen = true, }) { if (listen) { return context .dependOnInheritedWidgetOfExactType() - ?.theme; + ?.themeData; } final provider = context .getElementForInheritedWidgetOfExactType() ?.widget; - return (provider as AppFlowyInheritedTheme?)?.theme; + return (provider as AppFlowyInheritedTheme?)?.themeData; } @override Widget build(BuildContext context) { return AppFlowyInheritedTheme( - theme: data, + themeData: data, child: child, ); } @@ -54,18 +55,98 @@ class AppFlowyTheme extends StatelessWidget { class AppFlowyInheritedTheme extends InheritedTheme { const AppFlowyInheritedTheme({ super.key, - required this.theme, + required this.themeData, required super.child, }); - final AppFlowyBaseThemeData theme; + final AppFlowyThemeData themeData; @override Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: theme, child: child); + return AppFlowyTheme(data: themeData, child: child); } @override bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - theme != oldWidget.theme; + themeData != oldWidget.themeData; +} + +/// An interpolation between two [AppFlowyThemeData]s. +/// +/// This class specializes the interpolation of [Tween] to +/// call the [AppFlowyThemeData.lerp] method. +/// +/// See [Tween] for a discussion on how to use interpolation objects. +class AppFlowyThemeDataTween extends Tween { + /// Creates a [AppFlowyThemeData] tween. + /// + /// The [begin] and [end] properties must be non-null before the tween is + /// first used, but the arguments can be null if the values are going to be + /// filled in later. + AppFlowyThemeDataTween({super.begin, super.end}); + + @override + AppFlowyThemeData lerp(double t) => AppFlowyThemeData.lerp(begin!, end!, t); +} + +class AnimatedAppFlowyTheme extends ImplicitlyAnimatedWidget { + /// Creates an animated theme. + /// + /// By default, the theme transition uses a linear curve. + const AnimatedAppFlowyTheme({ + super.key, + required this.data, + super.curve, + super.duration = kThemeAnimationDuration, + super.onEnd, + required this.child, + }); + + /// Specifies the color and typography values for descendant widgets. + final AppFlowyThemeData data; + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + + @override + AnimatedWidgetBaseState createState() => + _AnimatedThemeState(); +} + +class _AnimatedThemeState + extends AnimatedWidgetBaseState { + AppFlowyThemeDataTween? data; + + @override + void forEachTween(TweenVisitor visitor) { + data = visitor( + data, + widget.data, + (dynamic value) => + AppFlowyThemeDataTween(begin: value as AppFlowyThemeData), + )! as AppFlowyThemeDataTween; + } + + @override + Widget build(BuildContext context) { + return AppFlowyTheme( + data: data!.evaluate(animation), + child: widget.child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add( + DiagnosticsProperty( + 'data', + data, + showName: false, + defaultValue: null, + ), + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart index 1eee06953f..790db31660 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-15T23:05:39.278903 +// Generation time: 2025-04-16T22:13:33.297893 // // To modify these colors, edit the source JSON files and run the script: // diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index 08206a6572..03e80da962 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-15T23:05:39.288085 +// Generation time: 2025-04-16T22:13:33.307397 // // To modify these colors, edit the source JSON files and run the script: // @@ -12,30 +12,16 @@ import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; -import '../builder.dart'; +import '../shared.dart'; import 'primitive.dart'; -class AppFlowyThemeData implements AppFlowyBaseThemeData { - const AppFlowyThemeData._({ - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorScheme, - required this.backgroundColorScheme, - required this.otherColorsColorScheme, - }); - - factory AppFlowyThemeData.light() { +class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { + @override + AppFlowyThemeData light() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.light); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.light); final textColorScheme = AppFlowyTextColorScheme( primary: AppFlowyPrimitiveTokens.neutral1000, @@ -168,7 +154,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { textHighlight: AppFlowyPrimitiveTokens.blue200, ); - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -184,11 +170,12 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { ); } - factory AppFlowyThemeData.dark() { + @override + AppFlowyThemeData dark() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.dark); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark); final textColorScheme = AppFlowyTextColorScheme( primary: AppFlowyPrimitiveTokens.neutral200, @@ -321,7 +308,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { textHighlight: AppFlowyPrimitiveTokens.blue200, ); - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -336,42 +323,4 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { shadow: shadow, ); } - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorScheme iconColorScheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - @override - final AppFlowyOtherColorsColorScheme otherColorsColorScheme; } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart new file mode 100644 index 0000000000..6ef43076c5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart @@ -0,0 +1,23 @@ +import 'package:appflowy_ui/appflowy_ui.dart'; + +class CustomTheme implements AppFlowyThemeBuilder { + const CustomTheme({ + required this.lightThemeJson, + required this.darkThemeJson, + }); + + final Map lightThemeJson; + final Map darkThemeJson; + + @override + AppFlowyThemeData light() { + // TODO: implement light + throw UnimplementedError(); + } + + @override + AppFlowyThemeData dark() { + // TODO: implement dark + throw UnimplementedError(); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart similarity index 92% rename from frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart rename to frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart index a4f83109cb..c9c3c3adb0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/builder.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart @@ -21,10 +21,10 @@ class AppFlowyBorderRadiusConstant { static const double radius600 = 20; } -class AppFlowyThemeBuilder { - const AppFlowyThemeBuilder(); +class AppFlowySharedTokens { + const AppFlowySharedTokens(); - AppFlowyBorderRadius buildBorderRadius() { + static AppFlowyBorderRadius buildBorderRadius() { return AppFlowyBorderRadius( xs: AppFlowyBorderRadiusConstant.radius100, s: AppFlowyBorderRadiusConstant.radius200, @@ -35,7 +35,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowySpacing buildSpacing() { + static AppFlowySpacing buildSpacing() { return AppFlowySpacing( xs: AppFlowySpacingConstant.spacing100, s: AppFlowySpacingConstant.spacing200, @@ -46,7 +46,7 @@ class AppFlowyThemeBuilder { ); } - AppFlowyShadow buildShadow( + static AppFlowyShadow buildShadow( Brightness brightness, ) { return switch (brightness) { diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart deleted file mode 100644 index ca06c8da1b..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/base_theme.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'border_radius/border_radius.dart'; -import 'color_scheme/color_scheme.dart'; -import 'shadow/shadow.dart'; -import 'spacing/spacing.dart'; -import 'text_style/text_style.dart'; - -abstract class AppFlowyBaseThemeData { - const AppFlowyBaseThemeData(); - - AppFlowyTextColorScheme get textColorScheme; - - AppFlowyBaseTextStyle get textStyle; - - AppFlowyIconColorScheme get iconColorScheme; - - AppFlowyBorderColorScheme get borderColorScheme; - - AppFlowyBackgroundColorScheme get backgroundColorScheme; - - AppFlowyFillColorScheme get fillColorScheme; - - AppFlowySurfaceColorScheme get surfaceColorScheme; - - AppFlowyBorderRadius get borderRadius; - - AppFlowySpacing get spacing; - - AppFlowyShadow get shadow; - - AppFlowyBrandColorScheme get brandColorScheme; - - AppFlowyOtherColorsColorScheme get otherColorsColorScheme; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart index 547c7f1635..c7324c34fe 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart @@ -12,4 +12,16 @@ class AppFlowyBackgroundColorScheme { final Color secondary; final Color tertiary; final Color quaternary; + + AppFlowyBackgroundColorScheme lerp( + AppFlowyBackgroundColorScheme other, + double t, + ) { + return AppFlowyBackgroundColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart index d1618b6cff..28eee5b145 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart @@ -46,4 +46,40 @@ class AppFlowyBorderColorScheme { final Color errorThickHover; final Color purpleThick; final Color purpleThickHover; + + AppFlowyBorderColorScheme lerp( + AppFlowyBorderColorScheme other, + double t, + ) { + return AppFlowyBorderColorScheme( + greyPrimary: Color.lerp(greyPrimary, other.greyPrimary, t)!, + greyPrimaryHover: + Color.lerp(greyPrimaryHover, other.greyPrimaryHover, t)!, + greySecondary: Color.lerp(greySecondary, other.greySecondary, t)!, + greySecondaryHover: + Color.lerp(greySecondaryHover, other.greySecondaryHover, t)!, + greyTertiary: Color.lerp(greyTertiary, other.greyTertiary, t)!, + greyTertiaryHover: + Color.lerp(greyTertiaryHover, other.greyTertiaryHover, t)!, + greyQuaternary: Color.lerp(greyQuaternary, other.greyQuaternary, t)!, + greyQuaternaryHover: + Color.lerp(greyQuaternaryHover, other.greyQuaternaryHover, t)!, + transparent: Color.lerp(transparent, other.transparent, t)!, + themeThick: Color.lerp(themeThick, other.themeThick, t)!, + themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, + infoThick: Color.lerp(infoThick, other.infoThick, t)!, + infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, + successThick: Color.lerp(successThick, other.successThick, t)!, + successThickHover: + Color.lerp(successThickHover, other.successThickHover, t)!, + warningThick: Color.lerp(warningThick, other.warningThick, t)!, + warningThickHover: + Color.lerp(warningThickHover, other.warningThickHover, t)!, + errorThick: Color.lerp(errorThick, other.errorThick, t)!, + errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart index 8374d2bbfd..4140f6924a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart @@ -22,4 +22,21 @@ class AppFlowyBrandColorScheme { final Color golden; final Color amber; final Color lemon; + + AppFlowyBrandColorScheme lerp( + AppFlowyBrandColorScheme other, + double t, + ) { + return AppFlowyBrandColorScheme( + skyline: Color.lerp(skyline, other.skyline, t)!, + aqua: Color.lerp(aqua, other.aqua, t)!, + violet: Color.lerp(violet, other.violet, t)!, + amethyst: Color.lerp(amethyst, other.amethyst, t)!, + berry: Color.lerp(berry, other.berry, t)!, + coral: Color.lerp(coral, other.coral, t)!, + golden: Color.lerp(golden, other.golden, t)!, + amber: Color.lerp(amber, other.amber, t)!, + lemon: Color.lerp(lemon, other.lemon, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart index 8616bde4eb..3faac64dfc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart @@ -90,4 +90,63 @@ class AppFlowyFillColorScheme { final Color purpleLightHover; final Color purpleThick; final Color purpleThickHover; + + AppFlowyFillColorScheme lerp( + AppFlowyFillColorScheme other, + double t, + ) { + return AppFlowyFillColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + tertiaryHover: Color.lerp(tertiaryHover, other.tertiaryHover, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + quaternaryHover: Color.lerp(quaternaryHover, other.quaternaryHover, t)!, + transparent: Color.lerp(transparent, other.transparent, t)!, + primaryAlpha5: Color.lerp(primaryAlpha5, other.primaryAlpha5, t)!, + primaryAlpha5Hover: + Color.lerp(primaryAlpha5Hover, other.primaryAlpha5Hover, t)!, + primaryAlpha80: Color.lerp(primaryAlpha80, other.primaryAlpha80, t)!, + primaryAlpha80Hover: + Color.lerp(primaryAlpha80Hover, other.primaryAlpha80Hover, t)!, + white: Color.lerp(white, other.white, t)!, + whiteAlpha: Color.lerp(whiteAlpha, other.whiteAlpha, t)!, + whiteAlphaHover: Color.lerp(whiteAlphaHover, other.whiteAlphaHover, t)!, + black: Color.lerp(black, other.black, t)!, + themeLight: Color.lerp(themeLight, other.themeLight, t)!, + themeLightHover: Color.lerp(themeLightHover, other.themeLightHover, t)!, + themeThick: Color.lerp(themeThick, other.themeThick, t)!, + themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, + themeSelect: Color.lerp(themeSelect, other.themeSelect, t)!, + infoLight: Color.lerp(infoLight, other.infoLight, t)!, + infoLightHover: Color.lerp(infoLightHover, other.infoLightHover, t)!, + infoThick: Color.lerp(infoThick, other.infoThick, t)!, + infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, + successLight: Color.lerp(successLight, other.successLight, t)!, + successLightHover: + Color.lerp(successLightHover, other.successLightHover, t)!, + successThick: Color.lerp(successThick, other.successThick, t)!, + successThickHover: + Color.lerp(successThickHover, other.successThickHover, t)!, + warningLight: Color.lerp(warningLight, other.warningLight, t)!, + warningLightHover: + Color.lerp(warningLightHover, other.warningLightHover, t)!, + warningThick: Color.lerp(warningThick, other.warningThick, t)!, + warningThickHover: + Color.lerp(warningThickHover, other.warningThickHover, t)!, + errorLight: Color.lerp(errorLight, other.errorLight, t)!, + errorLightHover: Color.lerp(errorLightHover, other.errorLightHover, t)!, + errorThick: Color.lerp(errorThick, other.errorThick, t)!, + errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, + errorSelect: Color.lerp(errorSelect, other.errorSelect, t)!, + purpleLight: Color.lerp(purpleLight, other.purpleLight, t)!, + purpleLightHover: + Color.lerp(purpleLightHover, other.purpleLightHover, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart index 245a02aadb..efe59b8b99 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart @@ -18,4 +18,20 @@ class AppFlowyIconColorScheme { final Color white; final Color purpleThick; final Color purpleThickHover; + + AppFlowyIconColorScheme lerp( + AppFlowyIconColorScheme other, + double t, + ) { + return AppFlowyIconColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + white: Color.lerp(white, other.white, t)!, + purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, + purpleThickHover: + Color.lerp(purpleThickHover, other.purpleThickHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart index ed9b94695c..9bb21e54e6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart @@ -6,4 +6,13 @@ class AppFlowyOtherColorsColorScheme { }); final Color textHighlight; + + AppFlowyOtherColorsColorScheme lerp( + AppFlowyOtherColorsColorScheme other, + double t, + ) { + return AppFlowyOtherColorsColorScheme( + textHighlight: Color.lerp(textHighlight, other.textHighlight, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart index 8fdc21adef..67be450a04 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart @@ -8,4 +8,14 @@ class AppFlowySurfaceColorScheme { final Color primary; final Color overlay; + + AppFlowySurfaceColorScheme lerp( + AppFlowySurfaceColorScheme other, + double t, + ) { + return AppFlowySurfaceColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + overlay: Color.lerp(overlay, other.overlay, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart index 486378643f..17e1f057ce 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart @@ -44,4 +44,32 @@ class AppFlowyTextColorScheme { final Color errorHover; final Color purple; final Color purpleHover; + + AppFlowyTextColorScheme lerp( + AppFlowyTextColorScheme other, + double t, + ) { + return AppFlowyTextColorScheme( + primary: Color.lerp(primary, other.primary, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + tertiary: Color.lerp(tertiary, other.tertiary, t)!, + quaternary: Color.lerp(quaternary, other.quaternary, t)!, + inverse: Color.lerp(inverse, other.inverse, t)!, + onFill: Color.lerp(onFill, other.onFill, t)!, + theme: Color.lerp(theme, other.theme, t)!, + themeHover: Color.lerp(themeHover, other.themeHover, t)!, + action: Color.lerp(action, other.action, t)!, + actionHover: Color.lerp(actionHover, other.actionHover, t)!, + info: Color.lerp(info, other.info, t)!, + infoHover: Color.lerp(infoHover, other.infoHover, t)!, + success: Color.lerp(success, other.success, t)!, + successHover: Color.lerp(successHover, other.successHover, t)!, + warning: Color.lerp(warning, other.warning, t)!, + warningHover: Color.lerp(warningHover, other.warningHover, t)!, + error: Color.lerp(error, other.error, t)!, + errorHover: Color.lerp(errorHover, other.errorHover, t)!, + purple: Color.lerp(purple, other.purple, t)!, + purpleHover: Color.lerp(purpleHover, other.purpleHover, t)!, + ); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart new file mode 100644 index 0000000000..515e6b2ecf --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart @@ -0,0 +1,86 @@ +import 'border_radius/border_radius.dart'; +import 'color_scheme/color_scheme.dart'; +import 'shadow/shadow.dart'; +import 'spacing/spacing.dart'; +import 'text_style/text_style.dart'; + +/// [AppFlowyThemeData] defines the structure of the design system, and contains +/// the data that all child widgets will have access to. +class AppFlowyThemeData { + const AppFlowyThemeData({ + required this.textColorScheme, + required this.textStyle, + required this.iconColorScheme, + required this.borderColorScheme, + required this.backgroundColorScheme, + required this.fillColorScheme, + required this.surfaceColorScheme, + required this.borderRadius, + required this.spacing, + required this.shadow, + required this.brandColorScheme, + required this.otherColorsColorScheme, + }); + + final AppFlowyTextColorScheme textColorScheme; + + final AppFlowyBaseTextStyle textStyle; + + final AppFlowyIconColorScheme iconColorScheme; + + final AppFlowyBorderColorScheme borderColorScheme; + + final AppFlowyBackgroundColorScheme backgroundColorScheme; + + final AppFlowyFillColorScheme fillColorScheme; + + final AppFlowySurfaceColorScheme surfaceColorScheme; + + final AppFlowyBorderRadius borderRadius; + + final AppFlowySpacing spacing; + + final AppFlowyShadow shadow; + + final AppFlowyBrandColorScheme brandColorScheme; + + final AppFlowyOtherColorsColorScheme otherColorsColorScheme; + + static AppFlowyThemeData lerp( + AppFlowyThemeData begin, + AppFlowyThemeData end, + double t, + ) { + return AppFlowyThemeData( + textColorScheme: begin.textColorScheme.lerp(end.textColorScheme, t), + textStyle: end.textStyle, + iconColorScheme: begin.iconColorScheme.lerp(end.iconColorScheme, t), + borderColorScheme: begin.borderColorScheme.lerp(end.borderColorScheme, t), + backgroundColorScheme: + begin.backgroundColorScheme.lerp(end.backgroundColorScheme, t), + fillColorScheme: begin.fillColorScheme.lerp(end.fillColorScheme, t), + surfaceColorScheme: + begin.surfaceColorScheme.lerp(end.surfaceColorScheme, t), + borderRadius: end.borderRadius, + spacing: end.spacing, + shadow: end.shadow, + brandColorScheme: begin.brandColorScheme.lerp(end.brandColorScheme, t), + otherColorsColorScheme: + begin.otherColorsColorScheme.lerp(end.otherColorsColorScheme, t), + ); + } +} + +/// [AppFlowyThemeBuilder] is used to build the light and dark themes. Extend +/// this class to create a built-in theme, or use the [CustomTheme] class to +/// create a custom theme from JSON data. +/// +/// See also: +/// +/// - [AppFlowyThemeData] for the main theme data class. +abstract class AppFlowyThemeBuilder { + const AppFlowyThemeBuilder(); + + AppFlowyThemeData light(); + AppFlowyThemeData dark(); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart index 5f9f66cd2d..000b7a0372 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart @@ -2,7 +2,7 @@ export 'appflowy_theme.dart'; export 'data/built_in_themes.dart'; export 'definition/border_radius/border_radius.dart'; export 'definition/color_scheme/color_scheme.dart'; -export 'definition/base_theme.dart'; +export 'definition/theme_data.dart'; export 'definition/spacing/spacing.dart'; export 'definition/shadow/shadow.dart'; export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart index 9c9c8a45ff..9d429f537e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print, depend_on_referenced_packages + import 'dart:convert'; import 'dart:io'; @@ -90,34 +92,20 @@ void generateSemantic() { import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; -import '../builder.dart'; +import '../shared.dart'; import 'primitive.dart'; -class AppFlowyThemeData implements AppFlowyBaseThemeData { - const AppFlowyThemeData._({ - required this.textStyle, - required this.textColorScheme, - required this.borderColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.iconColorScheme, - required this.backgroundColorScheme, - required this.otherColorsColorScheme, - }); -'''); +class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); // 3. Process light mode semantic tokens - void writeThemeFactory(String brightness, Map jsonData) { + void writeThemeData(String brightness, Map jsonData) { buffer.writeln(''' - factory AppFlowyThemeData.$brightness() { + @override + AppFlowyThemeData $brightness() { final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = themeBuilder.buildBorderRadius(); - final spacing = themeBuilder.buildSpacing(); - final shadow = themeBuilder.buildShadow(Brightness.$brightness);'''); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.$brightness);'''); jsonData.forEach((categoryName, categoryData) { if (categoryData is Map) { @@ -165,7 +153,7 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { buffer.writeln(); buffer.writeln(''' - return AppFlowyThemeData._( + return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, borderColorScheme: borderColorScheme, @@ -182,49 +170,9 @@ class AppFlowyThemeData implements AppFlowyBaseThemeData { }'''); } - writeThemeFactory('light', lightJsonData); + writeThemeData('light', lightJsonData); buffer.writeln(); - writeThemeFactory('dark', darkJsonData); - - buffer.writeln(''' - - static const AppFlowyThemeBuilder themeBuilder = AppFlowyThemeBuilder(); - - @override - final AppFlowyBaseTextStyle textStyle; - - @override - final AppFlowyTextColorScheme textColorScheme; - - @override - final AppFlowyBorderColorScheme borderColorScheme; - - @override - final AppFlowyFillColorScheme fillColorScheme; - - @override - final AppFlowySurfaceColorScheme surfaceColorScheme; - - @override - final AppFlowyBorderRadius borderRadius; - - @override - final AppFlowySpacing spacing; - - @override - final AppFlowyShadow shadow; - - @override - final AppFlowyBrandColorScheme brandColorScheme; - - @override - final AppFlowyIconColorScheme iconColorScheme; - - @override - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - @override - final AppFlowyOtherColorsColorScheme otherColorsColorScheme;'''); + writeThemeData('dark', darkJsonData); buffer.writeln('}'); // 4. Write the output to a Dart file. From edc5710e323c2985c5b46ca8f7a7060b2a9cac9e Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:00:51 +0800 Subject: [PATCH 29/74] chore: auth type and remove unused code --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +-- .../user_profile/user_profile_bloc.dart | 13 +- .../bottom_sheet/bottom_sheet_view_page.dart | 2 +- .../favorite/mobile_favorite_page.dart | 8 +- .../presentation/home/mobile_home_page.dart | 14 +- .../home/setting/settings_popup_menu.dart | 2 +- .../home/tab/mobile_space_tab.dart | 3 +- .../mobile_notifications_page.dart | 10 +- .../setting/user_session_setting_group.dart | 2 +- .../application/sync/database_sync_bloc.dart | 6 +- .../database/widgets/row/row_banner.dart | 2 +- .../document/application/document_bloc.dart | 2 +- .../document_collaborators_bloc.dart | 2 +- .../application/document_sync_bloc.dart | 2 +- .../editor_plugins/file/file_util.dart | 8 +- .../page_style/_page_style_cover_image.dart | 2 +- .../lib/plugins/shared/share/share_bloc.dart | 2 +- .../icon_emoji_picker/icon_uploader.dart | 2 +- .../lib/startup/tasks/generate_router.dart | 18 -- .../user/application/encrypt_secret_bloc.dart | 114 -------- .../application/password/password_bloc.dart | 2 +- .../lib/user/application/user_listener.dart | 27 +- .../lib/user/application/user_service.dart | 10 - .../lib/user/presentation/anon_user.dart | 5 +- .../helpers/handle_user_profile_result.dart | 6 +- .../lib/user/presentation/router.dart | 14 - .../screens/encrypt_secret_screen.dart | 130 --------- .../user/presentation/screens/screens.dart | 1 - .../sign_in_screen/sign_in_screen.dart | 7 +- .../presentation/screens/splash_screen.dart | 34 +-- .../workspace/application/home/home_bloc.dart | 17 +- .../application/home/home_setting_bloc.dart | 10 +- .../settings/ai/settings_ai_bloc.dart | 12 +- .../settings/settings_dialog_bloc.dart | 2 +- .../application/user/user_workspace_bloc.dart | 2 +- .../home/desktop_home_screen.dart | 16 +- .../home/menu/sidebar/sidebar.dart | 2 +- .../menu/view/view_more_action_button.dart | 2 +- .../pages/account/account_sign_in_out.dart | 4 +- .../settings/pages/settings_account_view.dart | 31 +- .../pages/settings_workspace_view.dart | 4 +- .../settings/settings_dialog.dart | 2 +- .../widgets/setting_third_party_login.dart | 9 +- .../settings/widgets/settings_menu.dart | 6 +- .../more_view_actions/more_view_actions.dart | 2 +- .../bloc_test/home_test/view_bloc_test.dart | 19 +- .../event-integration-test/src/lib.rs | 27 +- .../event-integration-test/src/user_event.rs | 4 +- .../tests/chat/chat_message_test.rs | 14 +- .../af_cloud_test/file_upload_test.rs | 1 + .../user/af_cloud_test/anon_user_test.rs | 2 +- .../tests/user/af_cloud_test/auth_test.rs | 27 -- .../user/af_cloud_test/workspace_test.rs | 16 +- .../user/local_test/user_profile_test.rs | 29 +- .../rust-lib/flowy-ai/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 10 +- .../rust-lib/flowy-core/src/server_layer.rs | 72 +++-- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 1 + .../flowy-error/src/impl_from/database.rs | 6 +- .../flowy-folder/src/entities/workspace.rs | 2 +- .../flowy-folder/src/event_handler.rs | 2 +- .../rust-lib/flowy-folder/src/event_map.rs | 2 +- frontend/rust-lib/flowy-folder/src/manager.rs | 8 +- .../flowy-server/src/af_cloud/define.rs | 25 +- .../src/af_cloud/impls/database.rs | 16 +- .../src/af_cloud/impls/document.rs | 6 +- .../flowy-server/src/af_cloud/impls/folder.rs | 12 +- .../af_cloud/impls/user/cloud_service_impl.rs | 67 +++-- .../src/af_cloud/impls/user/dto.rs | 30 +- .../flowy-server/src/af_cloud/impls/util.rs | 7 +- .../flowy-server/src/af_cloud/server.rs | 65 ++--- .../src/local_server/impls/chat.rs | 4 +- .../src/local_server/impls/user.rs | 10 +- .../flowy-server/src/local_server/server.rs | 6 +- .../flowy-server/tests/af_cloud_test/util.rs | 8 +- frontend/rust-lib/flowy-sqlite/src/schema.rs | 37 +-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 22 +- .../rust-lib/flowy-user-pub/src/entities.rs | 84 +----- .../flowy-user/src/entities/user_profile.rs | 62 +--- .../flowy-user/src/entities/workspace.rs | 48 +++- .../rust-lib/flowy-user/src/event_handler.rs | 145 +++------- frontend/rust-lib/flowy-user/src/event_map.rs | 14 +- .../rust-lib/flowy-user/src/notification.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 17 +- .../flowy-user/src/services/sqlite_sql/mod.rs | 1 + .../src/services/sqlite_sql/user_sql.rs | 41 +-- .../src/services/sqlite_sql/workspace_sql.rs | 129 +++++---- .../flowy-user/src/user_manager/manager.rs | 176 +++++------- .../src/user_manager/manager_history_user.rs | 2 +- .../user_manager/manager_user_awareness.rs | 4 +- .../user_manager/manager_user_encryption.rs | 35 --- .../user_manager/manager_user_workspace.rs | 270 +++++++++--------- 93 files changed, 802 insertions(+), 1407 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 4b7ed5d639..92e52a1a79 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 + saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca 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 1480cc02e9..0527316860 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 @@ -19,14 +19,13 @@ class UserProfileBloc extends Bloc { Future _initialize(Emitter emit) async { emit(const UserProfileState.loading()); - - final workspaceOrFailure = + final latestOrFailure = await FolderEventGetCurrentWorkspaceSetting().send(); final userOrFailure = await getIt().getUser(); - final workspaceSetting = workspaceOrFailure.fold( - (workspaceSettingPB) => workspaceSettingPB, + final latest = latestOrFailure.fold( + (latestPB) => latestPB, (error) => null, ); @@ -35,13 +34,13 @@ class UserProfileBloc extends Bloc { (error) => null, ); - if (workspaceSetting == null || userProfile == null) { + if (latest == null || userProfile == null) { return emit(const UserProfileState.workspaceFailure()); } emit( UserProfileState.success( - workspaceSettings: workspaceSetting, + workspaceSettings: latest, userProfile: userProfile, ), ); @@ -59,7 +58,7 @@ class UserProfileState with _$UserProfileState { const factory UserProfileState.loading() = _Loading; const factory UserProfileState.workspaceFailure() = _WorkspaceFailure; const factory UserProfileState.success({ - required WorkspaceSettingPB workspaceSettings, + required WorkspaceLatestPB workspaceSettings, required UserProfilePB userProfile, }) = _Success; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 9497774298..85a5e3cbfa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud if (userProfile == null || - userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + userProfile.authType != AuthenticatorPB.AppFlowyCloud) { return []; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index e6d2d895b1..0e7a7cb4c6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -31,9 +31,9 @@ class MobileFavoriteScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) { - return workspaceSettingPB as WorkspaceSettingPB?; + final latest = snapshots.data?[0].fold( + (latest) { + return latest as WorkspaceLatestPB?; }, (error) => null, ); @@ -46,7 +46,7 @@ class MobileFavoriteScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (latest == null || userProfile == null) { return const WorkspaceFailedScreen(); } 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 1ae5d881ce..fdea8322c3 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 @@ -44,9 +44,9 @@ class MobileHomeScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) { - return workspaceSettingPB as WorkspaceSettingPB?; + final workspaceLatest = snapshots.data?[0].fold( + (workspaceLatestPB) { + return workspaceLatestPB as WorkspaceLatestPB?; }, (error) => null, ); @@ -59,7 +59,7 @@ class MobileHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (workspaceLatest == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -78,7 +78,7 @@ class MobileHomeScreen extends StatelessWidget { value: userProfile, child: MobileHomePage( userProfile: userProfile, - workspaceSetting: workspaceSetting, + workspaceLatest: workspaceLatest, ), ), ), @@ -95,11 +95,11 @@ class MobileHomePage extends StatefulWidget { const MobileHomePage({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceLatest, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceLatest; @override State createState() => _MobileHomePageState(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index 521fca4fdf..11a82d2c7a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, 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 7ebfeefbbc..cc3f4c8c56 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 @@ -167,8 +167,7 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 20, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index a8055b8ba2..33c2eb3905 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -50,9 +50,9 @@ class _MobileNotificationsScreenState extends State orElse: () => const Center(child: CircularProgressIndicator.adaptive()), workspaceFailure: () => const WorkspaceFailedScreen(), - success: (workspaceSetting, userProfile) => + success: (workspaceLatest, userProfile) => _NotificationScreenContent( - workspaceSetting: workspaceSetting, + workspaceLatest: workspaceLatest, userProfile: userProfile, controller: controller, reminderBloc: reminderBloc, @@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State class _NotificationScreenContent extends StatelessWidget { const _NotificationScreenContent({ - required this.workspaceSetting, + required this.workspaceLatest, required this.userProfile, required this.controller, required this.reminderBloc, }); - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceLatest; final UserProfilePB userProfile; final TabController controller; final ReminderBloc reminderBloc; @@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget { ..add( SidebarSectionsEvent.initial( userProfile, - workspaceSetting.workspaceId, + workspaceLatest.workspaceId, ), ), child: BlocBuilder( 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 617de1db50..09f38223f4 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 @@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget { // delete account button // only show the delete account button in cloud mode - if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index ae0b9173c7..53dc2bd6d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -30,9 +30,9 @@ class DatabaseSyncBloc extends Bloc { .then((value) => value.fold((s) => s, (f) => null)); emit( state.copyWith( - shouldShowIndicator: userProfile?.authenticator == - AuthenticatorPB.AppFlowyCloud && - databaseId != null, + shouldShowIndicator: + userProfile?.authType == AuthenticatorPB.AppFlowyCloud && + databaseId != null, ), ); if (databaseId != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 8d64c537c3..5706167f1a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -69,7 +69,7 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.authenticator ?? AuthenticatorPB.Local) == + (widget.userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 010dae1f12..252742aa5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -101,7 +101,7 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local; + final type = userProfilePB?.authType ?? AuthenticatorPB.Local; return type == AuthenticatorPB.Local; } 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 74a6199b89..b593ccc6cc 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 @@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc emit( state.copyWith( shouldShowIndicator: - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, + userProfile?.authType == AuthenticatorPB.AppFlowyCloud, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 0fae90920d..1001aaef5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, + userProfile?.authType == AuthenticatorPB.AppFlowyCloud, ), ); _syncStateListener.start( 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 83debdd71b..d3bbbd27d5 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 @@ -184,8 +184,8 @@ Future insertLocalFile( final fileType = file.fileType.toMediaFileTypePB(); // Check upload type - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; String? path; String? errorMsg; @@ -229,8 +229,8 @@ Future insertLocalFiles( if (files.every((f) => f.path.isEmpty)) return; // Check upload type - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 27498cc65e..601ba8fa20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -226,7 +226,7 @@ class PageStyleCoverImage extends StatelessWidget { (f) => null, ); final isAppFlowyCloud = - userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud; + userProfile?.authType == AuthenticatorPB.AppFlowyCloud; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index a852fa5e38..c3f04a8837 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.authenticator == AuthenticatorPB.AppFlowyCloud, + (v) => v.authType == AuthenticatorPB.AppFlowyCloud, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index 2974156a2a..f39fdb01dc 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -294,7 +294,7 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == + final isLocalMode = (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index b326276c56..7f9a2df329 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -51,7 +51,6 @@ GoRouter generateRouter(Widget child) { // Routes in both desktop and mobile _signInScreenRoute(), _skipLogInScreenRoute(), - _encryptSecretScreenRoute(), _workspaceErrorScreenRoute(), // Desktop only if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(), @@ -471,23 +470,6 @@ GoRoute _workspaceErrorScreenRoute() { ); } -GoRoute _encryptSecretScreenRoute() { - return GoRoute( - path: EncryptSecretScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - return CustomTransitionPage( - child: EncryptSecretScreen( - user: args[EncryptSecretScreen.argUser], - key: args[EncryptSecretScreen.argKey], - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - GoRoute _skipLogInScreenRoute() { return GoRoute( path: SkipLogInScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart deleted file mode 100644 index 19b8101ae8..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart +++ /dev/null @@ -1,114 +0,0 @@ -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/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_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'auth/auth_service.dart'; - -part 'encrypt_secret_bloc.freezed.dart'; - -class EncryptSecretBloc extends Bloc { - EncryptSecretBloc({required this.user}) - : super(EncryptSecretState.initial()) { - _dispatch(); - } - - final UserProfilePB user; - - void _dispatch() { - on((event, emit) async { - await event.when( - setEncryptSecret: (secret) async { - if (isLoading()) { - return; - } - - final payload = UserSecretPB.create() - ..encryptionSecret = secret - ..encryptionSign = user.encryptionSign - ..encryptionType = user.encryptionType - ..userId = user.id; - final result = await UserEventSetEncryptionSecret(payload).send(); - if (!isClosed) { - add(EncryptSecretEvent.didFinishCheck(result)); - } - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - successOrFail: null, - ), - ); - }, - cancelInputSecret: () async { - await getIt().signOut(); - emit( - state.copyWith( - successOrFail: null, - isSignOut: true, - ), - ); - }, - didFinishCheck: (result) { - result.fold( - (unit) { - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - successOrFail: result, - ), - ); - }, - (err) { - emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.failure(err)), - successOrFail: result, - ), - ); - }, - ); - }, - ); - }); - } - - bool isLoading() { - final loadingState = state.loadingState; - if (loadingState != null) { - return loadingState.when( - loading: () => true, - finish: (_) => false, - idle: () => false, - ); - } - return false; - } -} - -@freezed -class EncryptSecretEvent with _$EncryptSecretEvent { - const factory EncryptSecretEvent.setEncryptSecret(String secret) = - _SetEncryptSecret; - const factory EncryptSecretEvent.didFinishCheck( - FlowyResult result, - ) = _DidFinishCheck; - const factory EncryptSecretEvent.cancelInputSecret() = _CancelInputSecret; -} - -@freezed -class EncryptSecretState with _$EncryptSecretState { - const factory EncryptSecretState({ - required FlowyResult? successOrFail, - required bool isSignOut, - LoadingState? loadingState, - }) = _EncryptSecretState; - - factory EncryptSecretState.initial() => const EncryptSecretState( - successOrFail: null, - isSignOut: false, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart index 34e8514e4c..d421440d08 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -46,7 +46,7 @@ class PasswordBloc extends Bloc { bool _isInitialized = false; Future _init() async { - if (userProfile.authenticator == AuthenticatorPB.Local) { + if (userProfile.authType == AuthenticatorPB.Local) { Log.debug('PasswordBloc: skip init because user is local authenticator'); return; } diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 36d6039d40..d3ebe0201b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -24,7 +24,7 @@ typedef DidUpdateUserWorkspacesCallback = void Function( ); typedef UserProfileNotifyValue = FlowyResult; typedef DidUpdateUserWorkspaceSetting = void Function( - UseAISettingPB settings, + WorkspaceSettingsPB settings, ); class UserListener { @@ -101,10 +101,10 @@ class UserListener { result.map( (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)), ); - case user.UserNotification.DidUpdateAISetting: + case user.UserNotification.DidUpdateWorkspaceSetting: result.map( - (r) => - onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)), + (r) => onUserWorkspaceSettingUpdated + ?.call(WorkspaceSettingsPB.fromBuffer(r)), ); break; default: @@ -113,22 +113,21 @@ class UserListener { } } -typedef WorkspaceSettingNotifyValue - = FlowyResult; +typedef WorkspaceLatestNotifyValue = FlowyResult; class FolderListener { FolderListener(); - final PublishNotifier _settingChangedNotifier = + final PublishNotifier _latestChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, + void Function(WorkspaceLatestNotifyValue)? onLatestUpdated, }) { - if (onSettingUpdated != null) { - _settingChangedNotifier.addPublishListener(onSettingUpdated); + if (onLatestUpdated != null) { + _latestChangedNotifier.addPublishListener(onLatestUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -146,9 +145,9 @@ class FolderListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier.value = - FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), - (error) => _settingChangedNotifier.value = FlowyResult.failure(error), + (payload) => _latestChangedNotifier.value = + FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)), + (error) => _latestChangedNotifier.value = FlowyResult.failure(error), ); break; default: @@ -158,6 +157,6 @@ class FolderListener { Future stop() async { await _listener?.stop(); - _settingChangedNotifier.dispose(); + _latestChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 4359a23753..18f89ebc14 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -40,8 +40,6 @@ class UserBackendService implements IUserBackendService { String? password, String? email, String? iconUrl, - String? openAIKey, - String? stabilityAiKey, }) { final payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -61,14 +59,6 @@ class UserBackendService implements IUserBackendService { payload.iconUrl = iconUrl; } - if (openAIKey != null) { - payload.openaiKey = openAIKey; - } - - if (stabilityAiKey != null) { - payload.stabilityAiKey = stabilityAiKey; - } - return UserEventUpdateUserProfile(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index a9b11cb42e..014c7caaf8 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,9 +74,8 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = - isSelected || user.authenticator != AuthenticatorPB.Local; - final desc = "${user.name}\t ${user.authenticator}\t"; + final isDisabled = isSelected || user.authType != AuthenticatorPB.Local; + final desc = "${user.name}\t ${user.authType}\t"; final child = SizedBox( height: 30, child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart index 9abd417df3..83007786f1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart @@ -12,11 +12,7 @@ void handleUserProfileResult( ) { userProfileResult.fold( (userProfile) { - if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { - authRouter.pushEncryptionScreen(context, userProfile); - } else { - authRouter.goHomeScreen(context, userProfile); - } + authRouter.goHomeScreen(context, userProfile); }, (error) { handleOpenWorkspaceError(context, error); diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 370d9c2062..f6f6ec3e3a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -61,20 +61,6 @@ class AuthRouter { ); } - void pushEncryptionScreen( - BuildContext context, - UserProfilePB userProfile, - ) { - // After log in,push EncryptionScreen on the top SignInScreen - context.push( - EncryptSecretScreen.routeName, - extra: { - EncryptSecretScreen.argUser: userProfile, - EncryptSecretScreen.argKey: ValueKey(userProfile.id), - }, - ); - } - Future pushWorkspaceErrorScreen( BuildContext context, UserFolderPB userFolder, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart deleted file mode 100644 index f0b79ed9d2..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/helpers/helpers.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.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:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/encrypt_secret_bloc.dart'; - -class EncryptSecretScreen extends StatefulWidget { - const EncryptSecretScreen({required this.user, super.key}); - - final UserProfilePB user; - - static const routeName = '/EncryptSecretScreen'; - - // arguments used in GoRouter - static const argUser = 'user'; - static const argKey = 'key'; - - @override - State createState() => _EncryptSecretScreenState(); -} - -class _EncryptSecretScreenState extends State { - final TextEditingController _textEditingController = TextEditingController(); - - @override - void dispose() { - _textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: BlocProvider( - create: (context) => EncryptSecretBloc(user: widget.user), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (previous, current) => - previous.isSignOut != current.isSignOut, - listener: (context, state) async { - if (state.isSignOut) { - await runAppFlowy(); - } - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous.successOrFail != current.successOrFail, - listener: (context, state) async { - await state.successOrFail?.fold( - (unit) async { - await runAppFlowy(); - }, - (error) { - handleOpenWorkspaceError(context, error); - }, - ); - }, - ), - ], - child: BlocBuilder( - builder: (context, state) { - final indicator = state.loadingState?.when( - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - finish: (result) => const SizedBox.shrink(), - idle: () => const SizedBox.shrink(), - ) ?? - const SizedBox.shrink(); - return Center( - child: SizedBox( - width: 300, - height: 160, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.6, - child: FlowyText.medium( - "${LocaleKeys.settings_menu_inputEncryptPrompt.tr()} ${widget.user.email}", - fontSize: 14, - maxLines: 10, - ), - ), - const VSpace(6), - SizedBox( - width: 300, - child: FlowyTextField( - controller: _textEditingController, - hintText: - LocaleKeys.settings_menu_inputTextFieldHint.tr(), - onChanged: (_) {}, - ), - ), - OkCancelButton( - alignment: MainAxisAlignment.end, - onOkPressed: () => - context.read().add( - EncryptSecretEvent.setEncryptSecret( - _textEditingController.text, - ), - ), - onCancelPressed: () => context - .read() - .add(const EncryptSecretEvent.cancelInputSecret()), - mode: TextButtonMode.normal, - ), - const VSpace(6), - indicator, - ], - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 088da38978..540da8c2b4 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -2,6 +2,5 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; export 'sign_up_screen.dart'; -export 'encrypt_secret_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index afae06d50a..b359b2e217 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -4,7 +4,6 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -34,11 +33,7 @@ class SignInScreen extends StatelessWidget { if (successOrFail != null) { successOrFail.fold( (userProfile) { - if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { - getIt().pushEncryptionScreen(context, userProfile); - } else { - getIt().goHomeScreen(context, userProfile); - } + getIt().goHomeScreen(context, userProfile); }, (error) { Log.error('Sign in error: $error'); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 71345aa8dd..4062cedf8e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -8,7 +8,6 @@ import 'package:appflowy/user/presentation/helpers/helpers.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -61,32 +60,15 @@ class SplashScreen extends StatelessWidget { BuildContext context, Authenticated authenticated, ) async { - final userProfile = authenticated.userProfile; - - /// After a user is authenticated, this function checks if encryption is required. - final result = await UserEventCheckEncryptionSign().send(); - await result.fold( - (check) async { - /// If encryption is needed, the user is navigated to the encryption screen. - /// Otherwise, it fetches the current workspace for the user and navigates them - if (check.requireSecret) { - getIt().pushEncryptionScreen(context, userProfile); - } else { - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (workspaceSetting) { - // After login, replace Splash screen by corresponding home screen - getIt().goHomeScreen( - context, - ); - }, - (error) => handleOpenWorkspaceError(context, error), - ); - } - }, - (err) { - Log.error(err); + final result = await FolderEventGetCurrentWorkspaceSetting().send(); + result.fold( + (workspaceSetting) { + // After login, replace Splash screen by corresponding home screen + getIt().goHomeScreen( + context, + ); }, + (error) => handleOpenWorkspaceError(context, error), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 1afc253ab7..531e797ff5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -3,14 +3,14 @@ import 'package:appflowy/workspace/application/view/view_ext.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' - show WorkspaceSettingPB; + show WorkspaceLatestPB; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { - HomeBloc(WorkspaceSettingPB workspaceSetting) + HomeBloc(WorkspaceLatestPB workspaceSetting) : _workspaceListener = FolderListener(), super(HomeState.initial(workspaceSetting)) { _dispatch(workspaceSetting); @@ -24,7 +24,7 @@ class HomeBloc extends Bloc { return super.close(); } - void _dispatch(WorkspaceSettingPB workspaceSetting) { + void _dispatch(WorkspaceLatestPB workspaceSetting) { on( (event, emit) async { await event.map( @@ -36,10 +36,9 @@ class HomeBloc extends Bloc { }); _workspaceListener.start( - onSettingUpdated: (result) { + onLatestUpdated: (result) { result.fold( - (setting) => - add(HomeEvent.didReceiveWorkspaceSetting(setting)), + (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)), (r) => Log.error(r), ); }, @@ -78,7 +77,7 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; const factory HomeEvent.didReceiveWorkspaceSetting( - WorkspaceSettingPB setting, + WorkspaceLatestPB setting, ) = _DidReceiveWorkspaceSetting; } @@ -86,11 +85,11 @@ class HomeEvent with _$HomeEvent { class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, ViewPB? latestView, }) = _HomeState; - factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( + factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState( isLoading: false, workspaceSetting: workspaceSetting, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart index 657f2592d7..cde67045b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart @@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_context.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceSettingPB; + show WorkspaceLatestPB; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +12,7 @@ part 'home_setting_bloc.freezed.dart'; class HomeSettingBloc extends Bloc { HomeSettingBloc( - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, double screenWidthPx, ) : _listener = FolderListener(), @@ -124,7 +124,7 @@ class HomeSettingEvent with _$HomeSettingEvent { _ShowEditPanel; const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel; const factory HomeSettingEvent.didReceiveWorkspaceSetting( - WorkspaceSettingPB setting, + WorkspaceLatestPB setting, ) = _DidReceiveWorkspaceSetting; const factory HomeSettingEvent.collapseMenu() = _CollapseMenu; const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) = @@ -139,7 +139,7 @@ class HomeSettingEvent with _$HomeSettingEvent { class HomeSettingState with _$HomeSettingState { const factory HomeSettingState({ required EditPanelContext? panelContext, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, required bool keepMenuCollapsed, @@ -150,7 +150,7 @@ class HomeSettingState with _$HomeSettingState { }) = _HomeSettingState; factory HomeSettingState.initial( - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, AppearanceSettingsState appearanceSettingsState, double screenWidthPx, ) { 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 4383e0dbef..0141283765 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 @@ -55,7 +55,7 @@ class SettingsAIBloc extends Bloc { onProfileUpdated: _onProfileUpdated, onUserWorkspaceSettingUpdated: (settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadAISetting(settings)); + add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); } }, ); @@ -85,7 +85,7 @@ class SettingsAIBloc extends Bloc { ), ).send(); }, - didLoadAISetting: (UseAISettingPB settings) { + didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) { emit( state.copyWith( aiSettings: settings, @@ -150,7 +150,7 @@ class SettingsAIBloc extends Bloc { UserEventGetWorkspaceSetting(payload).send().then((result) { result.fold((settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadAISetting(settings)); + add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); } }, (err) { Log.error(err); @@ -162,8 +162,8 @@ class SettingsAIBloc extends Bloc { @freezed class SettingsAIEvent with _$SettingsAIEvent { const factory SettingsAIEvent.started() = _Started; - const factory SettingsAIEvent.didLoadAISetting( - UseAISettingPB settings, + const factory SettingsAIEvent.didLoadWorkspaceSetting( + WorkspaceSettingsPB settings, ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; @@ -183,7 +183,7 @@ class SettingsAIEvent with _$SettingsAIEvent { class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, - UseAISettingPB? aiSettings, + WorkspaceSettingsPB? aiSettings, AvailableModelsPB? availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; 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 0578d9808b..66277cf30b 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 @@ -92,7 +92,7 @@ class SettingsDialogBloc ]) async { if ([ AuthenticatorPB.Local, - ].contains(userProfile.authenticator)) { + ].contains(userProfile.authType)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 7f32a86d1c..e6a8c4d921 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && + userProfile.authType == AuthenticatorPB.AppFlowyCloud && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' 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 a8d768aa79..619ee4e229 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 @@ -52,8 +52,8 @@ class DesktopHomeScreen extends StatelessWidget { return _buildLoading(); } - final workspaceSetting = snapshots.data?[0].fold( - (workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB, + final workspaceLatest = snapshots.data?[0].fold( + (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB, (error) => null, ); @@ -64,7 +64,7 @@ class DesktopHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceSetting == null || userProfile == null) { + if (workspaceLatest == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -86,11 +86,11 @@ class DesktopHomeScreen extends StatelessWidget { BlocProvider.value(value: getIt()), BlocProvider( create: (_) => - HomeBloc(workspaceSetting)..add(const HomeEvent.initial()), + HomeBloc(workspaceLatest)..add(const HomeEvent.initial()), ), BlocProvider( create: (_) => HomeSettingBloc( - workspaceSetting, + workspaceLatest, context.read(), context.widthPx, )..add(const HomeSettingEvent.initial()), @@ -137,7 +137,7 @@ class DesktopHomeScreen extends StatelessWidget { child: _buildBody( context, userProfile, - workspaceSetting, + workspaceLatest, ), ), ), @@ -157,7 +157,7 @@ class DesktopHomeScreen extends StatelessWidget { Widget _buildBody( BuildContext context, UserProfilePB userProfile, - WorkspaceSettingPB workspaceSetting, + WorkspaceLatestPB workspaceSetting, ) { final layout = HomeLayout(context); final homeStack = HomeStack( @@ -190,7 +190,7 @@ class DesktopHomeScreen extends StatelessWidget { BuildContext context, { required HomeLayout layout, required UserProfilePB userProfile, - required WorkspaceSettingPB workspaceSetting, + required WorkspaceLatestPB workspaceSetting, }) { final homeMenu = HomeSideBar( userProfile: userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index ea55c72f16..9c19184217 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -60,7 +60,7 @@ class HomeSideBar extends StatelessWidget { final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final WorkspaceLatestPB workspaceSetting; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index d792c54f04..576ff07cb4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + if (userProfile.authType != AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 89066ea649..78f1aaf16e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -79,9 +79,7 @@ class AccountSignInOutButton extends StatelessWidget { showConfirmDialog( context: context, title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - description: userProfile.encryptionType == EncryptionTypePB.Symmetric - ? LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr() - : LocaleKeys.settings_menu_logoutPrompt.tr(), + description: LocaleKeys.settings_menu_logoutPrompt.tr(), onConfirm: () async { await getIt().signOut(); onAction(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index e223b2d063..f1ecfda8ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + state.userProfile.authType != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ @@ -82,30 +82,30 @@ class _SettingsAccountViewState extends State { ), AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authenticator == - AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.authenticator == - AuthenticatorPB.Local, + onAction: + state.userProfile.authType == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: + state.userProfile.authType == AuthenticatorPB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.authenticator == AuthenticatorPB.Local) ...[ + state.userProfile.authType == AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authenticator == - AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.authenticator == - AuthenticatorPB.Local, + onAction: + state.userProfile.authType == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: + state.userProfile.authType == AuthenticatorPB.Local, ), ], ), @@ -120,8 +120,7 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 1cfc833398..a602849527 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.authenticator != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.authenticator != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), 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 512d673407..b17a9beb7e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -140,7 +140,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { + if (user.authType == AuthenticatorPB.AppFlowyCloud) { return SettingsAIView( key: ValueKey(workspaceId), userProfile: user, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index cf51d7a3e9..8a85377efe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -2,7 +2,6 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -64,12 +63,8 @@ class SettingThirdPartyLogin extends StatelessWidget { ) async { result.fold( (user) async { - if (user.encryptionType == EncryptionTypePB.Symmetric) { - getIt().pushEncryptionScreen(context, user); - } else { - didLogin(); - await runAppFlowy(); - } + didLogin(); + await runAppFlowy(); }, (error) => showSnapBar(context, error.msg), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index c9069b8be3..1a7144993f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -63,8 +63,7 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + userProfile.authType == AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, @@ -110,8 +109,7 @@ class SettingsMenu extends StatelessWidget { ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.authenticator == - AuthenticatorPB.AppFlowyCloud) + if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 90e47e7c19..3b81d2a041 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { + userProfile.authType == AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index d6d0351414..41865b7dd7 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -182,14 +182,14 @@ void main() { await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); - var workspaceSetting = + var workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( (result) => result.fold( (l) => l, (r) => throw Exception(), ), ); - workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id; + workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id; // ignore: unused_local_variable final documentBloc = DocumentBloc(documentId: document.id) @@ -198,14 +198,13 @@ void main() { ); await blocResponseFuture(); - workspaceSetting = - await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceSetting.latestView.id == document.id; + workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( + (result) => result.fold( + (l) => l, + (r) => throw Exception(), + ), + ); + workspaceLatest.latestView.id == document.id; }); test('create views', () async { diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 02efc0f75a..1b54087ee1 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -8,7 +8,6 @@ use collab_entity::CollabType; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_server::AppFlowyServer; use flowy_user::entities::AuthenticatorPB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; @@ -113,16 +112,25 @@ impl EventIntegrationTest { self.appflowy_core.config.application_path.clone() } - pub fn get_server(&self) -> Arc { - self.appflowy_core.server_provider.get_server().unwrap() - } - pub async fn wait_ws_connected(&self) { - if self.get_server().get_ws_state().is_connected() { + if self + .appflowy_core + .server_provider + .get_server() + .unwrap() + .get_ws_state() + .is_connected() + { return; } - let mut ws_state = self.get_server().subscribe_ws_state().unwrap(); + let mut ws_state = self + .appflowy_core + .server_provider + .get_server() + .unwrap() + .subscribe_ws_state() + .unwrap(); loop { select! { _ = sleep(Duration::from_secs(20)) => { @@ -144,9 +152,10 @@ impl EventIntegrationTest { oid: &str, collab_type: CollabType, ) -> Result, FlowyError> { - let server = self.server_provider.get_server().unwrap(); + let server = self.server_provider.get_server()?; + let workspace_id = self.get_current_workspace().await.id; - let oid = Uuid::from_str(oid).unwrap(); + let oid = Uuid::from_str(oid)?; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() 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 1b82d9b83c..2ec74bafa6 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -24,6 +24,7 @@ use flowy_user::entities::{ }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; +use flowy_user_pub::entities::AuthType; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; @@ -189,9 +190,10 @@ impl EventIntegrationTest { } } - pub async fn create_workspace(&self, name: &str) -> UserWorkspacePB { + pub async fn create_workspace(&self, name: &str, auth_type: AuthType) -> UserWorkspacePB { let payload = CreateWorkspacePB { name: name.to_string(), + auth_type: auth_type.into(), }; EventBuilder::new(self.clone()) .event(UserEvent::CreateWorkspace) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index a9c928db5b..aacba827c4 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -19,7 +19,12 @@ async fn af_cloud_create_chat_message_test() { let current_workspace = test.get_current_workspace().await; let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test.server_provider.get_server().unwrap().chat_service(); + let chat_service = test + .appflowy_core + .server_provider + .get_server() + .unwrap() + .chat_service(); for i in 0..10 { let _ = chat_service .create_question( @@ -74,7 +79,12 @@ async fn af_cloud_load_remote_system_message_test() { let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test.server_provider.get_server().unwrap().chat_service(); + let chat_service = test + .appflowy_core + .server_provider + .get_server() + .unwrap() + .chat_service(); for i in 0..10 { let _ = chat_service .create_question( 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 04798f044a..7d8ecc9680 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 @@ -66,6 +66,7 @@ async fn af_cloud_upload_big_file_test() { // download the file and then compare the data. let file_service = test + .appflowy_core .server_provider .get_server() .unwrap() diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 718bc1d9af..f74b202860 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.authenticator, AuthenticatorPB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthenticatorPB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs index 7b31babd0e..eaec8f7540 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs @@ -1,6 +1,5 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; -use flowy_user::entities::UpdateUserProfilePayloadPB; use crate::util::generate_test_email; @@ -13,29 +12,3 @@ async fn af_cloud_sign_up_test() { let user = test.af_cloud_sign_in_with_email(&email).await.unwrap(); assert_eq!(user.email, email); } - -#[tokio::test] -async fn af_cloud_update_user_metadata() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - let user = test.af_cloud_sign_up().await; - - let old_profile = test.get_user_profile().await.unwrap(); - assert_eq!(old_profile.openai_key, "".to_string()); - - test - .update_user_profile(UpdateUserProfilePayloadPB { - id: user.id, - openai_key: Some("new openai key".to_string()), - stability_ai_key: Some("new stability ai key".to_string()), - ..Default::default() - }) - .await; - - let new_profile = test.get_user_profile().await.unwrap(); - assert_eq!(new_profile.openai_key, "new openai key".to_string()); - assert_eq!( - new_profile.stability_ai_key, - "new stability ai key".to_string() - ); -} 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 56cf22a4da..366e43c157 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 @@ -1,15 +1,15 @@ +use crate::user::af_cloud_test::util::get_synced_workspaces; use collab::core::collab::DataSource::DocStateV1; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::Folder; use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; +use flowy_user_pub::entities::AuthType; use std::time::Duration; use tokio::task::LocalSet; use tokio::time::sleep; -use crate::user::af_cloud_test::util::get_synced_workspaces; - #[tokio::test] async fn af_cloud_workspace_delete() { use_localhost_af_cloud().await; @@ -18,7 +18,9 @@ async fn af_cloud_workspace_delete() { let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 1); - let created_workspace = test.create_workspace("my second workspace").await; + let created_workspace = test + .create_workspace("my second workspace", AuthType::AppFlowyCloud) + .await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); @@ -66,7 +68,9 @@ async fn af_cloud_create_workspace_test() { let first_workspace_id = workspaces[0].workspace_id.as_str(); assert_eq!(workspaces.len(), 1); - let created_workspace = test.create_workspace("my second workspace").await; + let created_workspace = test + .create_workspace("my second workspace", AuthType::AppFlowyCloud) + .await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; @@ -113,7 +117,9 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[2].name, "A"); assert_eq!(views[3].name, "B"); - let user_workspace = test.create_workspace("second workspace").await; + let user_workspace = test + .create_workspace("second workspace", AuthType::AppFlowyCloud) + .await; test.open_workspace(&user_workspace.workspace_id).await; let second_workspace = test.get_current_workspace().await; test.create_document("C").await; 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 00df14e8e1..46f8eca9c6 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 @@ -24,9 +24,7 @@ async fn anon_user_profile_get() { .await .parse::(); 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.authenticator, AuthenticatorPB::Local); + assert_eq!(user_profile.auth_type, AuthenticatorPB::Local); } #[tokio::test] @@ -50,31 +48,6 @@ async fn user_update_with_name() { assert_eq!(user_profile.name, new_name,); } -#[tokio::test] -async fn user_update_with_ai_key() { - let sdk = EventIntegrationTest::new().await; - let user = sdk.init_anon_user().await; - let openai_key = "openai_key".to_owned(); - let stability_ai_key = "stability_ai_key".to_owned(); - let request = UpdateUserProfilePayloadPB::new(user.id) - .openai_key(&openai_key) - .stability_ai_key(&stability_ai_key); - let _ = EventBuilder::new(sdk.clone()) - .event(UpdateUserProfile) - .payload(request) - .async_send() - .await; - - let user_profile = EventBuilder::new(sdk.clone()) - .event(GetUserProfile) - .async_send() - .await - .parse::(); - - assert_eq!(user_profile.openai_key, openai_key,); - assert_eq!(user_profile.stability_ai_key, stability_ai_key,); -} - #[tokio::test] async fn anon_user_update_with_email() { let sdk = EventIntegrationTest::new().await; diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 85f2dc8306..f85858b1c2 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -330,7 +330,7 @@ pub(crate) async fn update_chat_settings_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip_all, err)] +#[tracing::instrument(level = "debug", skip_all)] pub(crate) async fn get_local_ai_setting_handler( ai_manager: AFPluginState>, ) -> DataResult { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index d217f52785..7e6d477407 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -8,7 +8,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::LoginUserService; +use flowy_server::af_cloud::define::LoggedUser; use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -34,7 +34,7 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::{current_server_type, ServerProvider}; +use crate::server_layer::ServerProvider; use deps_resolve::reminder_deps::CollabInteractImpl; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -131,12 +131,10 @@ impl AppFlowyCore { store_preference.clone(), )); - let auth_type = current_server_type(); - debug!("🔥runtime:{}, server:{}", runtime, auth_type); + debug!("🔥runtime:{}", runtime); let server_provider = Arc::new(ServerProvider::new( config.clone(), - auth_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -327,7 +325,7 @@ impl ServerUserImpl { } #[async_trait] -impl LoginUserService for ServerUserImpl { +impl LoggedUser for ServerUserImpl { fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index ebb86c4417..4b1834c41c 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,11 +1,12 @@ use crate::AppFlowyCoreConfig; use af_plugin::manager::PluginManager; use arc_swap::{ArcSwap, ArcSwapOption}; +use dashmap::mapref::one::Ref; use dashmap::DashMap; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::{ - define::{AIUserServiceImpl, LoginUserService}, + define::{AIUserServiceImpl, LoggedUser}, AppFlowyCloudServer, }; use flowy_server::local_server::LocalServer; @@ -13,31 +14,53 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; +use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +use tracing::info; pub struct ServerProvider { config: AppFlowyCoreConfig, providers: DashMap>, auth_type: ArcSwap, - user: Arc, + logged_user: Arc, pub local_ai: Arc, pub uid: Arc>, pub user_enable_sync: Arc, pub encryption: Arc, } +// Our little guard wrapper: +pub struct ServerHandle<'a>(Ref<'a, AuthType, Arc>); + +impl<'a> Deref for ServerHandle<'a> { + type Target = dyn AppFlowyServer; + fn deref(&self) -> &Self::Target { + // `self.0.value()` is an `&Arc` + // so `&**` gives us a `&dyn AppFlowyServer` + &**self.0.value() + } +} + +/// Determine current server type from ENV +pub fn current_server_type() -> AuthType { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => AuthType::Local, + AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, + } +} + impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - initial_auth: AuthType, store_preferences: Weak, - user_service: impl LoginUserService + 'static, + user_service: impl LoggedUser + 'static, ) -> Self { - let user = Arc::new(user_service); + let initial_auth = current_server_type(); + let logged_user = Arc::new(user_service) as Arc; let auth_type = ArcSwap::from(Arc::new(initial_auth)); let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; - let ai_user = Arc::new(AIUserServiceImpl(user.clone())); + let ai_user = Arc::new(AIUserServiceImpl(Arc::downgrade(&logged_user))); let plugins = Arc::new(PluginManager::new()); let local_ai = Arc::new(LocalAIController::new( plugins, @@ -51,7 +74,7 @@ impl ServerProvider { encryption, user_enable_sync: Arc::new(AtomicBool::new(true)), auth_type, - user, + logged_user, uid: Default::default(), local_ai, } @@ -59,9 +82,16 @@ impl ServerProvider { pub fn set_auth_type(&self, new_auth_type: AuthType) { let old_type = self.get_auth_type(); + info!( + "ServerProvider: set auth type from {:?} to {:?}", + old_type, new_auth_type + ); + if old_type != new_auth_type { self.auth_type.store(Arc::new(new_auth_type)); - self.providers.remove(&old_type); + if let Some((auth_type, _)) = self.providers.remove(&old_type) { + info!("ServerProvider: remove old auth type: {:?}", auth_type); + } } } @@ -70,14 +100,17 @@ impl ServerProvider { } /// Lazily create or fetch an AppFlowyServer instance - pub fn get_server(&self) -> FlowyResult> { + pub fn get_server(&self) -> FlowyResult { let auth_type = self.get_auth_type(); - if let Some(entry) = self.providers.get(&auth_type) { - return Ok(entry.clone()); + if let Some(r) = self.providers.get(&auth_type) { + return Ok(ServerHandle(r)); } let server: Arc = match auth_type { - AuthType::Local => Arc::new(LocalServer::new(self.user.clone(), self.local_ai.clone())), + AuthType::Local => Arc::new(LocalServer::new( + self.logged_user.clone(), + self.local_ai.clone(), + )), AuthType::AppFlowyCloud => { let cfg = self .config @@ -89,20 +122,13 @@ impl ServerProvider { self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), - self.user.clone(), + Arc::downgrade(&self.logged_user), )) }, }; - self.providers.insert(auth_type, server.clone()); - Ok(server) - } -} - -/// Determine current server type from ENV -pub fn current_server_type() -> AuthType { - match AuthenticatorType::from_env() { - AuthenticatorType::Local => AuthType::Local, - AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, + self.providers.insert(auth_type, server); + let guard = self.providers.get(&auth_type).unwrap(); + Ok(ServerHandle(guard)) } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 3288252ad2..4112883e61 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -380,6 +380,9 @@ pub enum ErrorCode { #[error("Local AI disabled")] LocalAIDisabled = 130, + + #[error("User not login")] + UserNotLogin = 131, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 96b9d1c3cf..a9a2b6fa2b 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -161,6 +161,7 @@ impl FlowyError { static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); static_flowy_error!(local_ai_disabled, ErrorCode::LocalAIDisabled); + static_flowy_error!(user_not_login, ErrorCode::UserNotLogin); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-error/src/impl_from/database.rs b/frontend/rust-lib/flowy-error/src/impl_from/database.rs index 3a72a7cdf3..077ff2b708 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/database.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/database.rs @@ -1,8 +1,12 @@ use crate::FlowyError; +use flowy_sqlite::Error; impl std::convert::From for FlowyError { fn from(error: flowy_sqlite::Error) -> Self { - FlowyError::internal().with_context(error) + match error { + Error::NotFound => FlowyError::record_not_found(), + _ => FlowyError::internal().with_context(error), + } } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 21ff046226..72e50562f3 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -134,7 +134,7 @@ impl TryInto for GetWorkspaceViewPB { } #[derive(Default, ProtoBuf, Debug, Clone)] -pub struct WorkspaceSettingPB { +pub struct WorkspaceLatestPB { #[pb(index = 1)] pub workspace_id: String, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index c20eb8a7ad..6889b7ebe6 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -84,7 +84,7 @@ pub(crate) async fn read_private_views_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; let setting = folder.get_workspace_setting_pb().await?; data_result_ok(setting) diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index abd74bd338..19953aad1b 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -65,7 +65,7 @@ pub enum FolderEvent { CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace - #[event(output = "WorkspaceSettingPB")] + #[event(output = "WorkspaceLatestPB")] GetCurrentWorkspaceSetting = 1, /// Return a list of workspaces that the current user can access. diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index ea89def872..e704be043d 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -3,7 +3,7 @@ use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, - ViewLayoutPB, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, + ViewLayoutPB, ViewPB, ViewSectionPB, WorkspaceLatestPB, WorkspacePB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -377,10 +377,10 @@ impl FolderManager { Ok(new_workspace) } - pub async fn get_workspace_setting_pb(&self) -> FlowyResult { + pub async fn get_workspace_setting_pb(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; - Ok(WorkspaceSettingPB { + Ok(WorkspaceLatestPB { workspace_id: workspace_id.to_string(), latest_view, }) @@ -1262,7 +1262,7 @@ impl FolderManager { } let workspace_id = self.user.workspace_id()?; - let setting = WorkspaceSettingPB { + let setting = WorkspaceLatestPB { workspace_id: workspace_id.to_string(), latest_view: view, }; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 0cebe3371f..a93066054d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -3,7 +3,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use uuid::Uuid; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; @@ -13,7 +13,7 @@ pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. #[async_trait] -pub trait LoginUserService: Send + Sync { +pub trait LoggedUser: Send + Sync { /// different user might return different workspace id. fn workspace_id(&self) -> FlowyResult; @@ -24,27 +24,36 @@ pub trait LoginUserService: Send + Sync { fn application_root_dir(&self) -> Result; } -pub struct AIUserServiceImpl(pub Arc); +pub struct AIUserServiceImpl(pub Weak); + +impl AIUserServiceImpl { + fn logged_user(&self) -> FlowyResult> { + self + .0 + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("User is not logged in")) + } +} #[async_trait] impl AIUserService for AIUserServiceImpl { fn user_id(&self) -> Result { - self.0.user_id() + self.logged_user()?.user_id() } async fn is_local_model(&self) -> FlowyResult { - self.0.is_local_mode().await + self.logged_user()?.is_local_mode().await } fn workspace_id(&self) -> Result { - self.0.workspace_id() + self.logged_user()?.workspace_id() } fn sqlite_connection(&self, uid: i64) -> Result { - self.0.get_sqlite_db(uid) + self.logged_user()?.get_sqlite_db(uid) } fn application_root_dir(&self) -> Result { - self.0.application_root_dir() + self.logged_user()?.application_root_dir() } } 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 d6a22a2f73..f29a7f89ad 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 @@ -1,5 +1,5 @@ #![allow(unused_variables)] -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ @@ -17,13 +17,13 @@ use flowy_database_pub::cloud::{ use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use serde_json::{Map, Value}; -use std::sync::Arc; +use std::sync::Weak; use tracing::{error, instrument}; use uuid::Uuid; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] @@ -40,7 +40,6 @@ where workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -50,7 +49,7 @@ where Ok(data) => { check_request_workspace_id_is_match( workspace_id, - &cloned_user, + &self.logged_user, format!("get database object: {}:{}", object_id, collab_type), )?; Ok(Some(data.encode_collab)) @@ -95,14 +94,17 @@ where workspace_id: &Uuid, ) -> Result { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let client = try_get_client?; let params = object_ids .into_iter() .map(|object_id| QueryCollab::new(object_id, object_ty)) .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")?; + check_request_workspace_id_is_match( + workspace_id, + &self.logged_user, + "batch get database object", + )?; Ok( results .0 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 ae67fc6d26..1e000d5971 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,17 +9,17 @@ use collab_entity::CollabType; use flowy_document_pub::cloud::*; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use std::sync::Arc; +use std::sync::Weak; use tracing::instrument; use uuid::Uuid; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] 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 116651a734..e6408bc24c 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 @@ -10,7 +10,7 @@ use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; use serde_json::to_vec; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::Weak; use tracing::{instrument, trace}; use uuid::Uuid; @@ -22,13 +22,13 @@ use flowy_folder_pub::cloud::{ use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl { pub inner: T, - pub logged_user: Arc, + pub logged_user: Weak, } #[async_trait] @@ -91,7 +91,6 @@ where ) -> Result, FlowyError> { let uid = *uid; let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*workspace_id, CollabType::Folder), @@ -103,7 +102,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder data")?; + check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder data")?; let folder = Folder::from_collab_doc_state( uid, CollabOrigin::Empty, @@ -131,7 +130,6 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.inner.try_get_client(); - let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, collab_type), @@ -143,7 +141,7 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get folder doc state")?; + check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder doc state")?; Ok(doc_state) } 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 0ac4555d6e..8309e4c65f 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 @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use anyhow::anyhow; use arc_swap::ArcSwapOption; @@ -31,7 +31,7 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use uuid::Uuid; -use crate::af_cloud::define::{LoginUserService, USER_SIGN_IN_URL}; +use crate::af_cloud::define::{LoggedUser, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; @@ -44,19 +44,19 @@ use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_st pub(crate) struct AFCloudUserAuthServiceImpl { server: T, user_change_recv: ArcSwapOption>, - user: Arc, + logged_user: Weak, } impl AFCloudUserAuthServiceImpl { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver, - user: Arc, + logged_user: Weak, ) -> Self { Self { server, user_change_recv: ArcSwapOption::new(Some(Arc::new(user_change_recv))), - user, + logged_user, } } } @@ -168,11 +168,7 @@ where Ok(url) } - async fn update_user( - &self, - _credential: UserCredentials, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; client @@ -187,8 +183,11 @@ where _credential: UserCredentials, ) -> Result { let try_get_client = self.server.try_get_client(); - let cloned_user = self.user.clone(); - let expected_workspace_id = cloned_user.workspace_id()?; + let expected_workspace_id = self + .logged_user + .upgrade() + .ok_or_else(FlowyError::user_not_login)? + .workspace_id()?; let client = try_get_client?; let profile = client.get_profile().await?; let token = client.get_token()?; @@ -196,7 +195,11 @@ where // 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")?; + check_request_workspace_id_is_match( + &expected_workspace_id, + &self.logged_user, + "get user profile", + )?; Ok(profile) } @@ -233,19 +236,17 @@ where async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let 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()); let client = try_get_client?; client .patch_workspace(PatchWorkspaceParam { workspace_id, - workspace_name: owned_workspace_name, - workspace_icon: owned_workspace_icon, + workspace_name: new_workspace_name, + workspace_icon: new_workspace_icon, }) .await?; Ok(()) @@ -363,7 +364,7 @@ where object_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); - let cloned_user = self.user.clone(); + let cloned_user = self.logged_user.clone(); let params = QueryCollabParams { workspace_id: *workspace_id, inner: QueryCollab::new(*object_id, CollabType::UserAwareness), @@ -435,9 +436,9 @@ where async fn subscribe_workspace( &self, - workspace_id: String, + workspace_id: Uuid, recurring_interval: RecurringInterval, - subscription_plan: SubscriptionPlan, + workspace_subscription_plan: SubscriptionPlan, success_url: String, ) -> Result { let try_get_client = self.server.try_get_client(); @@ -447,7 +448,7 @@ where .create_subscription( &workspace_id, recurring_interval, - subscription_plan, + workspace_subscription_plan, &success_url, ) .await?; @@ -490,11 +491,13 @@ where async fn get_workspace_subscription_one( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let workspace_subscriptions = client.get_workspace_subscriptions(&workspace_id).await?; + let workspace_subscriptions = client + .get_workspace_subscriptions(&workspace_id.to_string()) + .await?; Ok(workspace_subscriptions) } @@ -531,11 +534,13 @@ where async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let usage = client.get_workspace_usage_and_limit(&workspace_id).await?; + let usage = client + .get_workspace_usage_and_limit(&workspace_id.to_string()) + .await?; Ok(usage) } @@ -548,7 +553,7 @@ where async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { @@ -556,7 +561,7 @@ where let client = try_get_client?; client .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { - workspace_id, + workspace_id: workspace_id.to_string(), plan, recurring_interval, }) @@ -573,7 +578,7 @@ where async fn get_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); @@ -584,7 +589,7 @@ where async fn update_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, ) -> Result { trace!("Sync workspace settings: {:?}", workspace_settings); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index eb2bf26698..ba13a7fbca 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -4,20 +4,11 @@ use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFW use flowy_user_pub::entities::{ AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, - USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, + USER_METADATA_ICON_URL, }; -use crate::af_cloud::impls::user::util::encryption_type_from_profile; - pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUserParams { let mut user_metadata = UserMetaData::new(); - if let Some(openai_key) = update.openai_key { - user_metadata.insert(USER_METADATA_OPEN_AI_KEY, openai_key); - } - - if let Some(stability_ai_key) = update.stability_ai_key { - user_metadata.insert(USER_METADATA_STABILITY_AI_KEY, stability_ai_key); - } if let Some(icon_url) = update.icon_url { user_metadata.insert(USER_METADATA_ICON_URL, icon_url); @@ -35,19 +26,12 @@ pub fn user_profile_from_af_profile( token: String, profile: AFUserProfile, ) -> Result { - let encryption_type = encryption_type_from_profile(&profile); - let (icon_url, openai_key, stability_ai_key) = { + let icon_url = { profile .metadata .map(|m| { - ( - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - m.get(USER_METADATA_OPEN_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - m.get(USER_METADATA_STABILITY_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), - ) + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) }) .unwrap_or_default() }; @@ -57,13 +41,9 @@ pub fn user_profile_from_af_profile( name: profile.name.unwrap_or("".to_string()), token, icon_url: icon_url.unwrap_or_default(), - openai_key: openai_key.unwrap_or_default(), - stability_ai_key: stability_ai_key.unwrap_or_default(), - authenticator: AuthType::AppFlowyCloud, - encryption_type, + auth_type: AuthType::AppFlowyCloud, uid: profile.uid, updated_at: profile.updated_at, - ai_model: "".to_string(), }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index bedcc90ca0..300738c833 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,6 +1,6 @@ -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use flowy_error::{FlowyError, FlowyResult}; -use std::sync::Arc; +use std::sync::Weak; use tracing::warn; use uuid::Uuid; @@ -9,9 +9,10 @@ use uuid::Uuid; /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( expected_workspace_id: &Uuid, - user: &Arc, + user: &Weak, action: impl AsRef, ) -> FlowyResult<()> { + let user = user.upgrade().ok_or_else(FlowyError::user_not_login)?; let actual_workspace_id = user.workspace_id()?; if expected_workspace_id != &actual_workspace_id { warn!( diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index bb2d11cdbd..66abb32031 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,8 +1,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use std::time::Duration; -use crate::af_cloud::define::{AIUserServiceImpl, LoginUserService}; +use crate::af_cloud::define::{AIUserServiceImpl, LoggedUser}; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -53,7 +53,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc, pub device_id: String, ws_client: Arc, - logged_user: Arc, + logged_user: Weak, } impl AppFlowyCloudServer { @@ -62,7 +62,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - auth_user_service: Arc, + logged_user: Weak, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -91,8 +91,8 @@ impl AppFlowyCloudServer { ); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); - spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); + Self { config, client: api_client, @@ -100,16 +100,17 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - logged_user: auth_user_service, + logged_user, } } - fn get_client(&self) -> Option> { - if self.enable_sync.load(Ordering::SeqCst) { + fn get_server_impl(&self) -> AFServerImpl { + let client = if self.enable_sync.load(Ordering::SeqCst) { Some(self.client.clone()) } else { None - } + }; + AFServerImpl { client } } } @@ -165,9 +166,6 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn user_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; let mut user_change = self.ws_client.subscribe_user_changed(); let (tx, rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { @@ -185,59 +183,45 @@ impl AppFlowyServer for AppFlowyCloudServer { }); Arc::new(AFCloudUserAuthServiceImpl::new( - server, + self.get_server_impl(), rx, self.logged_user.clone(), )) } fn folder_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudFolderCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn database_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn database_ai_service(&self) -> Option> { - let server = AFServerImpl { - client: self.get_client(), - }; Some(Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), })) } fn document_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; Arc::new(AFCloudDocumentCloudServiceImpl { - inner: server, + inner: self.get_server_impl(), logged_user: self.logged_user.clone(), }) } fn chat_service(&self) -> Arc { - let server = AFServerImpl { - client: self.get_client(), - }; - Arc::new(AutoSyncChatService::new( - Arc::new(CloudChatServiceImpl { inner: server }), + Arc::new(CloudChatServiceImpl { + inner: self.get_server_impl(), + }), Arc::new(AIUserServiceImpl(self.logged_user.clone())), )) } @@ -269,21 +253,16 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn file_storage(&self) -> Option> { - let client = AFServerImpl { - client: self.get_client(), - }; Some(Arc::new(AFCloudFileStorageServiceImpl::new( - client, + self.get_server_impl(), self.config.maximum_upload_file_size_in_bytes, ))) } fn search_service(&self) -> Option> { - let server = AFServerImpl { - client: self.get_client(), - }; - - Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server })) + Some(Arc::new(AFCloudSearchCloudServiceImpl { + inner: self.get_server_impl(), + })) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 8e731edb84..f56a8d6e8b 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -1,4 +1,4 @@ -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use chrono::{TimeZone, Utc}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::CompletionStream; @@ -28,7 +28,7 @@ use tracing::trace; use uuid::Uuid; pub struct LocalChatServiceImpl { - pub user: Arc, + pub user: Arc, pub local_ai: Arc, } 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 706e7f0597..49db8e03a2 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 @@ -113,11 +113,7 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - async fn update_user( - &self, - _credential: UserCredentials, - _params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { + async fn update_user(&self, _params: UpdateUserProfileParams) -> Result<(), FlowyError> { Ok(()) } @@ -146,8 +142,8 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError> { Err( FlowyError::local_version_not_support() 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 719dd59c95..c81d526acb 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,7 @@ use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; -use crate::af_cloud::define::LoginUserService; +use crate::af_cloud::define::LoggedUser; use crate::local_server::impls::{ LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, @@ -17,13 +17,13 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(user: Arc, local_ai: Arc) -> Self { Self { user, local_ai, 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 a8dcdb507f..249ff9136d 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,7 @@ use flowy_error::{FlowyError, FlowyResult}; use uuid::Uuid; use crate::setup_log; -use flowy_server::af_cloud::define::LoginUserService; +use flowy_server::af_cloud::define::LoggedUser; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::DBConnection; @@ -30,19 +30,21 @@ pub fn get_af_cloud_config() -> Option { pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc { let fake_device_id = uuid::Uuid::new_v4().to_string(); + let logged_user = Arc::new(FakeServerUserImpl) as Arc; Arc::new(AppFlowyCloudServer::new( config, true, fake_device_id, Version::new(0, 5, 8), - Arc::new(FakeServerUserImpl), + // do nothing, just for test + Arc::downgrade(&logged_user), )) } struct FakeServerUserImpl; #[async_trait] -impl LoginUserService for FakeServerUserImpl { +impl LoggedUser for FakeServerUserImpl { fn workspace_id(&self) -> FlowyResult { todo!() } diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 91c9aa8162..27ccdc8f18 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -89,16 +89,11 @@ diesel::table! { user_table (id) { id -> Text, name -> Text, - workspace -> Text, icon_url -> Text, - openai_key -> Text, token -> Text, email -> Text, auth_type -> Integer, - encryption_type -> Text, - stability_ai_key -> Text, updated_at -> BigInt, - ai_model -> Text, } } @@ -112,6 +107,7 @@ diesel::table! { icon -> Text, member_count -> BigInt, role -> Nullable, + auth_type -> Integer, } } @@ -127,16 +123,25 @@ diesel::table! { } } +diesel::table! { + workspace_setting_table (id) { + id -> Text, + disable_search_indexing -> Bool, + ai_model -> Text, + } +} + diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dd69e4aa37..dee44fa5f3 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -164,11 +164,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result; /// Using the user's token to update the user information - async fn update_user( - &self, - credential: UserCredentials, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError>; + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError>; /// Get the user information using the user's token or uid /// return None if the user is not found @@ -187,8 +183,8 @@ pub trait UserCloudService: Send + Sync + 'static { async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + new_workspace_name: Option, + new_workspace_icon: Option, ) -> Result<(), FlowyError>; /// Deletes a workspace owned by the user. @@ -277,7 +273,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn subscribe_workspace( &self, - workspace_id: String, + workspace_id: Uuid, recurring_interval: RecurringInterval, workspace_subscription_plan: SubscriptionPlan, success_url: String, @@ -303,7 +299,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Get the workspace subscriptions for a workspace async fn get_workspace_subscription_one( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result, FlowyError> { Err(FlowyError::not_support()) } @@ -326,7 +322,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> Result { Err(FlowyError::not_support()) } @@ -337,7 +333,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { @@ -350,14 +346,14 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, ) -> Result { Err(FlowyError::not_support()) } async fn update_workspace_setting( &self, - workspace_id: &str, + workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, ) -> 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 d59f9cab47..7c8d775fa6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -10,8 +10,6 @@ use serde_json::Value; use serde_repr::*; use uuid::Uuid; -pub const USER_METADATA_OPEN_AI_KEY: &str = "openai_key"; -pub const USER_METADATA_STABILITY_AI_KEY: &str = "stability_ai_key"; pub const USER_METADATA_ICON_URL: &str = "icon_url"; pub const USER_METADATA_UPDATE_AT: &str = "updated_at"; @@ -171,21 +169,15 @@ impl UserWorkspace { } } -#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct UserProfile { - #[serde(rename = "id")] pub uid: i64, pub email: String, pub name: String, pub token: String, pub icon_url: String, - pub openai_key: String, - pub stability_ai_key: String, - pub authenticator: AuthType, - // If the encryption_sign is not empty, which means the user has enabled the encryption. - pub encryption_type: EncryptionType, + pub auth_type: AuthType, pub updated_at: i64, - pub ai_model: String, } #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] @@ -233,37 +225,23 @@ where { fn from(params: (&T, &AuthType)) -> Self { let (value, auth_type) = params; - let (icon_url, openai_key, stability_ai_key) = { - value - .metadata() - .as_ref() - .map(|m| { - ( - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - m.get(USER_METADATA_OPEN_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - m.get(USER_METADATA_STABILITY_AI_KEY) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default(), - ) - }) - .unwrap_or_default() - }; + let icon_url = value + .metadata() + .as_ref() + .map(|m| { + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default() + }) + .unwrap_or_default(); Self { uid: value.user_id(), email: value.user_email().unwrap_or_default(), name: value.user_name().to_owned(), token: value.user_token().unwrap_or_default(), icon_url, - openai_key, - authenticator: *auth_type, - encryption_type: value.encryption_type(), - stability_ai_key, + auth_type: *auth_type, updated_at: value.updated_at(), - ai_model: "".to_string(), } } } @@ -275,11 +253,7 @@ pub struct UpdateUserProfileParams { pub email: Option, pub password: Option, pub icon_url: Option, - pub openai_key: Option, - pub stability_ai_key: Option, - pub encryption_sign: Option, pub token: Option, - pub ai_model: Option, } impl UpdateUserProfileParams { @@ -314,40 +288,6 @@ impl UpdateUserProfileParams { self.icon_url = Some(icon_url.to_string()); self } - - pub fn with_openai_key(mut self, openai_key: &str) -> Self { - self.openai_key = Some(openai_key.to_owned()); - self - } - - pub fn with_stability_ai_key(mut self, stability_ai_key: &str) -> Self { - self.stability_ai_key = Some(stability_ai_key.to_owned()); - self - } - - pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self { - let sign = match encryption_type { - EncryptionType::NoEncryption => "".to_string(), - EncryptionType::SelfEncryption(sign) => sign, - }; - self.encryption_sign = Some(sign); - self - } - - pub fn with_ai_model(mut self, ai_model: &str) -> Self { - self.ai_model = Some(ai_model.to_owned()); - self - } - - pub fn is_empty(&self) -> bool { - self.name.is_none() - && self.email.is_none() - && self.password.is_none() - && self.icon_url.is_none() - && self.openai_key.is_none() - && self.encryption_sign.is_none() - && self.stability_ai_key.is_none() - } } #[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] 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 7e62958c68..54cb41db1e 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -4,11 +4,10 @@ use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey}; +use crate::entities::parser::{UserEmail, UserIcon, UserName}; use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; -use super::parser::UserStabilityAIKey; use super::AFRolePB; #[derive(Default, ProtoBuf)] @@ -41,22 +40,7 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub openai_key: String, - - #[pb(index = 7)] - pub authenticator: AuthenticatorPB, - - #[pb(index = 8)] - pub encryption_sign: String, - - #[pb(index = 9)] - pub encryption_type: EncryptionTypePB, - - #[pb(index = 10)] - pub stability_ai_key: String, - - #[pb(index = 11)] - pub ai_model: String, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -73,23 +57,13 @@ impl Default for EncryptionTypePB { impl From for UserProfilePB { fn from(user_profile: UserProfile) -> Self { - let (encryption_sign, encryption_ty) = match user_profile.encryption_type { - EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), - EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), - }; - let ai_model = user_profile.ai_model; Self { id: user_profile.uid, email: user_profile.email, name: user_profile.name, token: user_profile.token, icon_url: user_profile.icon_url, - openai_key: user_profile.openai_key, - authenticator: user_profile.authenticator.into(), - encryption_sign, - encryption_type: encryption_ty, - stability_ai_key: user_profile.stability_ai_key, - ai_model, + auth_type: user_profile.auth_type.into(), } } } @@ -110,12 +84,6 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 5, one_of)] pub icon_url: Option, - - #[pb(index = 6, one_of)] - pub openai_key: Option, - - #[pb(index = 7, one_of)] - pub stability_ai_key: Option, } impl UpdateUserProfilePayloadPB { @@ -145,16 +113,6 @@ impl UpdateUserProfilePayloadPB { self.icon_url = Some(icon_url.to_owned()); self } - - pub fn openai_key(mut self, openai_key: &str) -> Self { - self.openai_key = Some(openai_key.to_owned()); - self - } - - pub fn stability_ai_key(mut self, stability_ai_key: &str) -> Self { - self.stability_ai_key = Some(stability_ai_key.to_owned()); - self - } } impl TryInto for UpdateUserProfilePayloadPB { @@ -178,27 +136,13 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), }; - let openai_key = match self.openai_key { - None => None, - Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0), - }; - - let stability_ai_key = match self.stability_ai_key { - None => None, - Some(stability_ai_key) => Some(UserStabilityAIKey::parse(stability_ai_key)?.0), - }; - Ok(UpdateUserProfileParams { uid: self.id, name, email, password, icon_url, - openai_key, - encryption_sign: None, token: None, - stability_ai_key, - ai_model: None, }) } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 885ad6f3cf..26f848f3f5 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -5,9 +5,10 @@ use client_api::entity::billing_dto::{ use serde::{Deserialize, Serialize}; use validator::Validate; +use crate::services::sqlite_sql::workspace_setting_sql::WorkspaceSettingsTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; -use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember}; +use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] @@ -215,6 +216,34 @@ pub struct CreateWorkspacePB { #[pb(index = 1)] #[validate(custom(function = "required_not_empty_str"))] pub name: String, + + #[pb(index = 2)] + pub auth_type: AuthTypePB, +} + +#[derive(ProtoBuf_Enum, Default, Clone)] +pub enum AuthTypePB { + LocalAuthType = 0, + #[default] + CloudAuthType = 1, +} + +impl From for AuthTypePB { + fn from(value: AuthType) -> Self { + match value { + AuthType::Local => AuthTypePB::LocalAuthType, + AuthType::AppFlowyCloud => AuthTypePB::CloudAuthType, + } + } +} + +impl From for AuthType { + fn from(value: AuthTypePB) -> Self { + match value { + AuthTypePB::LocalAuthType => AuthType::Local, + AuthTypePB::CloudAuthType => AuthType::AppFlowyCloud, + } + } } #[derive(ProtoBuf, Default, Clone, Validate)] @@ -375,8 +404,8 @@ pub struct BillingPortalPB { pub url: String, } -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct UseAISettingPB { +#[derive(ProtoBuf, Default, Clone, Validate, Eq, PartialEq)] +pub struct WorkspaceSettingsPB { #[pb(index = 1)] pub disable_search_indexing: bool, @@ -384,8 +413,17 @@ pub struct UseAISettingPB { pub ai_model: String, } -impl From for UseAISettingPB { - fn from(value: AFWorkspaceSettings) -> Self { +impl From<&AFWorkspaceSettings> for WorkspaceSettingsPB { + fn from(value: &AFWorkspaceSettings) -> Self { + Self { + disable_search_indexing: value.disable_search_indexing, + ai_model: value.ai_model.clone(), + } + } +} + +impl From for WorkspaceSettingsPB { + fn from(value: WorkspaceSettingsTable) -> Self { Self { disable_search_indexing: value.disable_search_indexing, ai_model: value.ai_model, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 7a64c20e06..bb2a0b2c30 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,6 +1,5 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; @@ -17,6 +16,7 @@ use crate::services::cloud_config::{ get_cloud_config, get_or_create_cloud_config, save_cloud_config, }; use crate::services::data_import::prepare_import; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceChangeset; use crate::user_manager::UserManager; fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { @@ -45,7 +45,7 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_services.get_server_auth_type(); + let old_authenticator = manager.cloud_service.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await @@ -53,7 +53,7 @@ pub async fn sign_in_with_email_password_handler( Ok(token) => data_result_ok(token.into()), Err(err) => { manager - .cloud_services + .cloud_service .set_server_auth_type(&old_authenticator); return Err(err); }, @@ -78,11 +78,11 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_services.get_server_auth_type(); + let prev_auth_type = manager.cloud_service.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), Err(err) => { - manager.cloud_services.set_server_auth_type(&prev_auth_type); + manager.cloud_service.set_server_auth_type(&prev_auth_type); Err(err) }, } @@ -117,7 +117,7 @@ pub async fn get_user_profile_handler( // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.authenticator == AuthType::Local { + if user_profile.auth_type == AuthType::Local { user_profile.email = "".to_string(); } @@ -374,66 +374,6 @@ pub async fn sign_in_with_provider_handler( }) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn set_encrypt_secret_handler( - manager: AFPluginState>, - data: AFPluginData, - store_preferences: AFPluginState>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let store_preferences = upgrade_store_preferences(store_preferences)?; - let data = data.into_inner(); - match data.encryption_type { - EncryptionTypePB::NoEncryption => { - tracing::error!("Encryption type is NoEncryption, but set encrypt secret"); - }, - EncryptionTypePB::Symmetric => { - manager.check_encryption_sign_with_secret( - data.user_id, - &data.encryption_sign, - &data.encryption_secret, - )?; - - let config = UserCloudConfig::new(data.encryption_secret).with_enable_encrypt(true); - manager - .set_encrypt_secret( - data.user_id, - config.encrypt_secret.clone(), - EncryptionType::SelfEncryption(data.encryption_sign), - ) - .await?; - save_cloud_config(data.user_id, &store_preferences, &config)?; - }, - } - - manager.resume_sign_up().await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn check_encrypt_secret_handler( - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let uid = manager.get_session()?.user_id; - let profile = manager.get_user_profile_from_disk(uid).await?; - - let is_need_secret = match profile.encryption_type { - EncryptionType::NoEncryption => false, - EncryptionType::SelfEncryption(sign) => { - if sign.is_empty() { - false - } else { - manager.check_encryption_sign(uid, &sign).is_err() - } - }, - }; - - data_result_ok(UserEncryptionConfigurationPB { - require_secret: is_need_secret, - }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_cloud_config_handler( manager: AFPluginState>, @@ -449,40 +389,18 @@ pub async fn set_cloud_config_handler( if let Some(enable_sync) = update.enable_sync { manager - .cloud_services + .cloud_service .set_enable_sync(session.user_id, enable_sync); config.enable_sync = enable_sync; } - if let Some(enable_encrypt) = update.enable_encrypt { - debug_assert!(enable_encrypt, "Disable encryption is not supported"); - - if enable_encrypt { - tracing::info!("Enable encryption for user: {}", session.user_id); - config = config.with_enable_encrypt(enable_encrypt); - let encrypt_secret = config.encrypt_secret.clone(); - - // The encryption secret is generated when the user first enables encryption and will be - // used to validate the encryption secret is correct when the user logs in. - let encryption_sign = manager.generate_encryption_sign(session.user_id, &encrypt_secret)?; - let encryption_type = EncryptionType::SelfEncryption(encryption_sign); - manager - .set_encrypt_secret(session.user_id, encrypt_secret, encryption_type.clone()) - .await?; - - let params = - UpdateUserProfileParams::new(session.user_id).with_encryption_type(encryption_type); - manager.update_user_profile(params).await?; - } - } - save_cloud_config(session.user_id, &store_preferences, &config)?; let payload = CloudSettingPB { enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_services.service_url(), + server_url: manager.cloud_service.service_url(), }; send_notification( @@ -509,7 +427,7 @@ pub async fn get_cloud_config_handler( enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_services.service_url(), + server_url: manager.cloud_service.service_url(), }) } @@ -518,8 +436,10 @@ pub async fn get_all_workspace_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let uid = manager.get_session()?.user_id; - let user_workspaces = manager.get_all_user_workspaces(uid).await?; + let profile = manager.get_user_profile().await?; + let user_workspaces = manager + .get_all_user_workspaces(profile.uid, profile.auth_type) + .await?; data_result_ok(user_workspaces.into()) } @@ -542,7 +462,7 @@ pub async fn update_network_state_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let reachable = data.into_inner().ty.is_reachable(); - manager.cloud_services.set_network_reachable(reachable); + manager.cloud_service.set_network_reachable(reachable); manager .user_status_callback .read() @@ -687,8 +607,9 @@ pub async fn create_workspace_handler( manager: AFPluginState>, ) -> DataResult { let data = data.try_into_inner()?; + let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; - let new_workspace = manager.add_workspace(&data.name).await?; + let new_workspace = manager.create_workspace(&data.name, auth_type).await?; data_result_ok(new_workspace.into()) } @@ -712,9 +633,12 @@ pub async fn rename_workspace_handler( let params = rename_workspace_param.try_into_inner()?; let manager = upgrade_manager(manager)?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .patch_workspace(&workspace_id, Some(¶ms.new_name), None) - .await?; + let changeset = UserWorkspaceChangeset { + id: params.workspace_id, + name: Some(params.new_name), + icon: None, + }; + manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) } @@ -726,9 +650,12 @@ pub async fn change_workspace_icon_handler( let params = change_workspace_icon_param.try_into_inner()?; let manager = upgrade_manager(manager)?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .patch_workspace(&workspace_id, None, Some(¶ms.new_icon)) - .await?; + let changeset = UserWorkspaceChangeset { + id: workspace_id.to_string(), + name: None, + icon: Some(params.new_icon), + }; + manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) } @@ -825,9 +752,9 @@ pub async fn get_workspace_usage_handler( param: AFPluginData, manager: AFPluginState>, ) -> DataResult { - let workspace_id = param.into_inner().workspace_id; + let workspace_id = Uuid::from_str(¶m.into_inner().workspace_id)?; let manager = upgrade_manager(manager)?; - let workspace_usage = manager.get_workspace_usage(workspace_id).await?; + let workspace_usage = manager.get_workspace_usage(&workspace_id).await?; data_result_ok(WorkspaceUsagePB::from(workspace_usage)) } @@ -845,11 +772,12 @@ pub async fn update_workspace_subscription_payment_period_handler( params: AFPluginData, manager: AFPluginState>, ) -> FlowyResult<()> { + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let params = params.try_into_inner()?; let manager = upgrade_manager(manager)?; manager .update_workspace_subscription_payment_period( - params.workspace_id, + &workspace_id, params.plan.into(), params.recurring_interval.into(), ) @@ -884,7 +812,7 @@ pub async fn get_workspace_member_info( } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn update_workspace_setting( +pub async fn update_workspace_setting_handler( params: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { @@ -895,13 +823,14 @@ pub async fn update_workspace_setting( } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn get_workspace_setting( +pub async fn get_workspace_setting_handler( params: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let params = params.try_into_inner()?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let manager = upgrade_manager(manager)?; - let pb = manager.get_workspace_settings(¶ms.workspace_id).await?; + let pb = manager.get_workspace_settings(&workspace_id).await?; data_result_ok(pb) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 953011bc1c..03660ad0ff 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -35,8 +35,6 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetUserSetting, get_user_setting) .event(UserEvent::SetCloudConfig, set_cloud_config_handler) .event(UserEvent::GetCloudConfig, get_cloud_config_handler) - .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler) - .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler) .event(UserEvent::OauthSignIn, oauth_sign_in_handler) .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) @@ -77,8 +75,8 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::UpdateWorkspaceSubscriptionPaymentPeriod, update_workspace_subscription_payment_period_handler) .event(UserEvent::GetSubscriptionPlanDetails, get_subscription_plan_details_handler) // Workspace Setting - .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting) - .event(UserEvent::GetWorkspaceSetting, get_workspace_setting) + .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting_handler) + .event(UserEvent::GetWorkspaceSetting, get_workspace_setting_handler) .event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler) .event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler) } @@ -142,12 +140,6 @@ pub enum UserEvent { #[event(output = "CloudSettingPB")] GetCloudConfig = 14, - #[event(input = "UserSecretPB")] - SetEncryptionSecret = 15, - - #[event(output = "UserEncryptionConfigurationPB")] - CheckEncryptionSign = 16, - /// Return the all the workspaces of the user #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, @@ -257,7 +249,7 @@ pub enum UserEvent { #[event(input = "UpdateUserWorkspaceSettingPB")] UpdateWorkspaceSetting = 57, - #[event(input = "UserWorkspaceIdPB", output = "UseAISettingPB")] + #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSettingsPB")] GetWorkspaceSetting = 58, #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")] diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index a8bd91b55b..dd93593468 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -14,7 +14,7 @@ pub(crate) enum UserNotification { DidUpdateUserWorkspaces = 3, DidUpdateCloudConfig = 4, DidUpdateUserWorkspace = 5, - DidUpdateAISetting = 6, + DidUpdateWorkspaceSetting = 6, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index e324c2820f..f05c0bda95 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -7,20 +7,17 @@ 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; use flowy_sqlite::{ query_dsl::*, schema::{user_table, user_table::dsl}, DBConnection, Database, ExpressionMethods, }; -use flowy_user_pub::entities::{UserProfile, UserWorkspace}; - +use flowy_user_pub::entities::UserProfile; use lib_infra::file_util::{unzip_and_replace, zip_folder}; use tracing::{error, event, info, instrument}; use crate::services::sqlite_sql::user_sql::UserTable; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; @@ -143,18 +140,6 @@ impl UserDB { Ok(user.into()) } - pub fn get_user_workspace( - &self, - pool: &Arc, - uid: i64, - ) -> Result, FlowyError> { - let mut conn = pool.get()?; - let row = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .first::(&mut *conn)?; - Ok(Some(UserWorkspace::from(row))) - } - /// Open a collab db for the user. If the db is already opened, return the opened db. /// fn open_collab_db( diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs index 93e642f72e..635c79ba39 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod member_sql; pub(crate) mod user_sql; +pub(crate) mod workspace_setting_sql; pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index 1138efd092..3fbd4c5854 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -1,6 +1,5 @@ use diesel::RunQueryDsl; use flowy_error::FlowyError; -use std::str::FromStr; use flowy_user_pub::cloud::UserUpdate; use flowy_user_pub::entities::*; @@ -15,40 +14,26 @@ use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; pub struct UserTable { pub(crate) id: String, pub(crate) name: String, - #[deprecated( - note = "The workspace_id is deprecated, please use the [Session::UserWorkspace] instead" - )] - pub(crate) workspace: String, pub(crate) icon_url: String, - pub(crate) openai_key: String, pub(crate) token: String, pub(crate) email: String, pub(crate) auth_type: i32, - pub(crate) encryption_type: String, - pub(crate) stability_ai_key: String, pub(crate) updated_at: i64, - pub(crate) ai_model: String, } #[allow(deprecated)] impl From<(UserProfile, AuthType)> for UserTable { fn from(value: (UserProfile, AuthType)) -> Self { let (user_profile, auth_type) = value; - let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTable { id: user_profile.uid.to_string(), name: user_profile.name, #[allow(deprecated)] - workspace: "".to_string(), icon_url: user_profile.icon_url, - openai_key: user_profile.openai_key, token: user_profile.token, email: user_profile.email, auth_type: auth_type as i32, - encryption_type, - stability_ai_key: user_profile.stability_ai_key, updated_at: user_profile.updated_at, - ai_model: user_profile.ai_model, } } } @@ -61,12 +46,8 @@ impl From for UserProfile { name: table.name, token: table.token, icon_url: table.icon_url, - openai_key: table.openai_key, - authenticator: AuthType::from(table.auth_type), - encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), - stability_ai_key: table.stability_ai_key, + auth_type: AuthType::from(table.auth_type), updated_at: table.updated_at, - ai_model: table.ai_model, } } } @@ -75,50 +56,30 @@ impl From for UserProfile { #[diesel(table_name = user_table)] pub struct UserTableChangeset { pub id: String, - pub workspace: Option, // deprecated pub name: Option, pub email: Option, pub icon_url: Option, - pub openai_key: Option, - pub encryption_type: Option, pub token: Option, - pub stability_ai_key: Option, - pub ai_model: Option, } impl UserTableChangeset { pub fn new(params: UpdateUserProfileParams) -> Self { - let encryption_type = params.encryption_sign.map(|sign| { - let ty = EncryptionType::from_sign(&sign); - serde_json::to_string(&ty).unwrap_or_default() - }); UserTableChangeset { id: params.uid.to_string(), - workspace: None, name: params.name, email: params.email, icon_url: params.icon_url, - openai_key: params.openai_key, - encryption_type, token: params.token, - stability_ai_key: params.stability_ai_key, - ai_model: params.ai_model, } } pub fn from_user_profile(user_profile: UserProfile) -> Self { - let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTableChangeset { id: user_profile.uid.to_string(), - workspace: None, name: Some(user_profile.name), email: Some(user_profile.email), icon_url: Some(user_profile.icon_url), - openai_key: Some(user_profile.openai_key), - encryption_type: Some(encryption_type), token: Some(user_profile.token), - stability_ai_key: Some(user_profile.stability_ai_key), - ai_model: Some(user_profile.ai_model), } } } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index 8d5c1e8dc7..f197ca9738 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -4,8 +4,7 @@ use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use flowy_user_pub::entities::UserWorkspace; -use std::convert::TryFrom; +use flowy_user_pub::entities::{AuthType, UserWorkspace}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] @@ -18,9 +17,45 @@ pub struct UserWorkspaceTable { pub icon: String, pub member_count: i64, pub role: Option, + pub auth_type: i32, } -pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option { +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceChangeset { + pub id: String, + pub name: Option, + pub icon: Option, +} + +impl UserWorkspaceTable { + pub fn from_workspace( + uid: i64, + workspace: &UserWorkspace, + auth_type: AuthType, + ) -> Result { + if workspace.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if workspace.workspace_database_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: workspace.id.clone(), + name: workspace.name.clone(), + uid, + created_at: workspace.created_at.timestamp(), + database_storage_id: workspace.workspace_database_id.clone(), + icon: workspace.icon.clone(), + member_count: workspace.member_count, + role: workspace.role.clone().map(|v| v as i32), + auth_type: auth_type as i32, + }) + } +} + +pub fn select_user_workspace(workspace_id: &str, mut conn: DBConnection) -> Option { user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) .first::(&mut *conn) @@ -28,7 +63,7 @@ pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Opti .map(UserWorkspace::from) } -pub fn get_all_user_workspace_op( +pub fn select_all_user_workspace( user_id: i64, mut conn: DBConnection, ) -> Result, FlowyError> { @@ -38,81 +73,45 @@ pub fn get_all_user_workspace_op( Ok(rows.into_iter().map(UserWorkspace::from).collect()) } -/// Remove all existing workspaces for given user and insert the new ones. -/// -#[allow(dead_code)] -pub fn save_user_workspaces_op( - uid: i64, +pub fn update_user_workspace( mut conn: DBConnection, - user_workspaces: &[UserWorkspace], + changeset: UserWorkspaceChangeset, ) -> Result<(), FlowyError> { - conn.immediate_transaction(|conn| { - delete_existing_workspaces(uid, conn)?; - insert_or_update_workspaces_op(uid, user_workspaces, conn)?; - Ok(()) - }) -} + diesel::update(user_workspace_table::dsl::user_workspace_table) + .filter(user_workspace_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(&mut conn)?; -#[allow(dead_code)] -fn delete_existing_workspaces(uid: i64, conn: &mut SqliteConnection) -> Result<(), FlowyError> { - diesel::delete( - user_workspace_table::dsl::user_workspace_table.filter(user_workspace_table::uid.eq(uid)), - ) - .execute(conn)?; Ok(()) } -pub fn insert_or_update_workspaces_op( +pub fn upsert_user_workspace( uid: i64, - user_workspaces: &[UserWorkspace], + auth_type: AuthType, + user_workspace: UserWorkspace, conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { - for user_workspace in user_workspaces { - let new_record = UserWorkspaceTable::try_from((uid, user_workspace))?; + let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - )) - .execute(conn)?; - } + diesel::insert_into(user_workspace_table::table) + .values(new_record.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(new_record.name), + user_workspace_table::uid.eq(new_record.uid), + user_workspace_table::created_at.eq(new_record.created_at), + user_workspace_table::database_storage_id.eq(new_record.database_storage_id), + user_workspace_table::icon.eq(new_record.icon), + user_workspace_table::member_count.eq(new_record.member_count), + user_workspace_table::role.eq(new_record.role), + user_workspace_table::auth_type.eq(new_record.auth_type), + )) + .execute(conn)?; Ok(()) } -impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { - type Error = FlowyError; - - fn try_from(value: (i64, &UserWorkspace)) -> Result { - if value.1.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if value.1.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: value.1.id.clone(), - name: value.1.name.clone(), - uid: value.0, - created_at: value.1.created_at.timestamp(), - database_storage_id: value.1.workspace_database_id.clone(), - icon: value.1.icon.clone(), - member_count: value.1.member_count, - role: value.1.role.clone().map(|v| v as i32), - }) - } -} - impl From for UserWorkspace { fn from(value: UserWorkspaceTable) -> Self { Self { 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 8fb4009991..d7d7c8b68d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -39,17 +39,16 @@ use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; -use super::manager_user_workspace::save_user_workspace; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::user_manager::manager_user_encryption::validate_encryption_sign; +use crate::services::sqlite_sql::workspace_sql::upsert_user_workspace; use crate::user_manager::manager_user_workspace::save_all_user_workspaces; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; pub struct UserManager { - pub(crate) cloud_services: Arc, + pub(crate) cloud_service: Arc, pub(crate) store_preferences: Arc, pub(crate) user_awareness: Arc>>, pub(crate) user_status_callback: RwLock>, @@ -75,7 +74,7 @@ impl UserManager { let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { - cloud_services, + cloud_service: cloud_services, store_preferences, user_awareness: Default::default(), user_status_callback, @@ -89,7 +88,7 @@ impl UserManager { }); let weak_user_manager = Arc::downgrade(&user_manager); - if let Ok(user_service) = user_manager.cloud_services.get_user_service() { + if let Ok(user_service) = user_manager.cloud_service.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { tokio::spawn(async move { while let Some(update) = rx.recv().await { @@ -141,11 +140,11 @@ impl UserManager { // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.authenticator != AuthType::Local && user.authenticator != current_authenticator { + if user.auth_type != AuthType::Local && user.auth_type != current_authenticator { event!( tracing::Level::INFO, "Authenticator changed from {:?} to {:?}", - user.authenticator, + user.auth_type, current_authenticator ); self.sign_out().await?; @@ -157,7 +156,7 @@ impl UserManager { "init user session: {}:{}, authenticator: {:?}", user.uid, user.email, - user.authenticator, + user.auth_type, ); self.prepare_user(&session).await; @@ -166,21 +165,17 @@ impl UserManager { // Set the token if the current cloud service using token to authenticate // Currently, only the AppFlowy cloud using token to init the client api. // TODO(nathan): using trait to separate the init process for different cloud service - if user.authenticator.is_appflowy_cloud() { - if let Err(err) = self.cloud_services.set_token(&user.token) { + if user.auth_type.is_appflowy_cloud() { + if let Err(err) = self.cloud_service.set_token(&user.token) { error!("Set token failed: {}", err); } - if let Err(err) = self.cloud_services.set_ai_model(&user.ai_model) { - error!("Set ai model failed: {}", err); - } - // Subscribe the token state - let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_cloud_services = Arc::downgrade(&self.cloud_service); let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); let cloned_session = session.clone(); - if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { + if let Some(mut token_state_rx) = self.cloud_service.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; let local_token = user.token.clone(); @@ -273,18 +268,16 @@ impl UserManager { self.set_first_time_installed_version(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); // Init the user awareness. here we ignore the error - let _ = self - .initial_user_awareness(&session, &user.authenticator) - .await; + let _ = self.initial_user_awareness(&session, &user.auth_type).await; user_status_callback .did_init( user.uid, - &user.authenticator, + &user.auth_type, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, - &user.authenticator, + &user.auth_type, ) .await?; } else { @@ -349,12 +342,12 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - authenticator: AuthType, + auth_type: AuthType, ) -> Result { - self.cloud_services.set_server_auth_type(&authenticator); + self.cloud_service.set_server_auth_type(&auth_type); let response: AuthResponse = self - .cloud_services + .cloud_service .get_user_service()? .sign_in(BoxAny::new(params)) .await?; @@ -362,13 +355,11 @@ impl UserManager { self.prepare_user(&session).await; let latest_workspace = response.latest_workspace.clone(); - let user_profile = UserProfile::from((&response, &authenticator)); - self - .save_auth_data(&response, &authenticator, &session) - .await?; + let user_profile = UserProfile::from((&response, &auth_type)); + self.save_auth_data(&response, auth_type, &session).await?; let _ = self - .initial_user_awareness(&session, &user_profile.authenticator) + .initial_user_awareness(&session, &user_profile.auth_type) .await; self .user_status_callback @@ -378,7 +369,7 @@ impl UserManager { user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, - &authenticator, + &auth_type, ) .await?; send_auth_state_notification(AuthStateChangedPB { @@ -403,22 +394,13 @@ impl UserManager { ) -> Result { // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_services.set_server_auth_type(&auth_type); - let auth_service = self.cloud_services.get_user_service()?; + self.cloud_service.set_server_auth_type(&auth_type); + let auth_service = self.cloud_service.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); - if new_user_profile.encryption_type.require_encrypt_secret() { - self.auth_process.lock().await.replace(UserAuthProcess { - user_profile: new_user_profile.clone(), - migration_user, - response, - authenticator: auth_type, - }); - } else { - self - .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) - .await?; - } + self + .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) + .await?; Ok(new_user_profile) } @@ -455,10 +437,10 @@ impl UserManager { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, auth_type, &new_session) + .save_auth_data(&response, *auth_type, &new_session) .await?; let _ = self - .initial_user_awareness(&new_session, &new_user_profile.authenticator) + .initial_user_awareness(&new_session, &new_user_profile.auth_type) .await; self .user_status_callback @@ -514,7 +496,7 @@ impl UserManager { pub async fn sign_out(&self) -> Result<(), FlowyError> { if let Ok(session) = self.get_session() { sign_out( - &self.cloud_services, + &self.cloud_service, &session, &self.authenticate_user, self.db_connection(session.user_id)?, @@ -527,7 +509,7 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self))] pub async fn delete_account(&self) -> Result<(), FlowyError> { self - .cloud_services + .cloud_service .get_user_service()? .delete_account() .await?; @@ -553,10 +535,7 @@ impl UserManager { changeset, )?; - let profile = self.get_user_profile_from_disk(session.user_id).await?; - self - .update_user(session.user_id, profile.token, params) - .await?; + self.update_user(params).await?; Ok(()) } @@ -580,6 +559,12 @@ impl UserManager { .backup(session.user_id, &session.user_workspace.id); } + pub async fn get_user_profile(&self) -> FlowyResult { + let uid = self.get_session()?.user_id; + let profile = self.get_user_profile_from_disk(uid).await?; + Ok(profile) + } + /// Fetches the user profile for the given user ID. pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { select_user_profile(uid, self.db_connection(uid)?) @@ -588,7 +573,7 @@ impl UserManager { #[tracing::instrument(level = "info", skip_all, err)] pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile - if old_user_profile.authenticator.is_local() { + if old_user_profile.auth_type.is_local() { return Ok(()); } @@ -601,7 +586,7 @@ impl UserManager { let uid = old_user_profile.uid; let result: Result = self - .cloud_services + .cloud_service .get_user_service()? .get_user_profile(UserCredentials::from_uid(uid)) .await; @@ -610,7 +595,6 @@ impl UserManager { Ok(new_user_profile) => { // If the user profile is updated, save the new user profile if new_user_profile.updated_at > old_user_profile.updated_at { - validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); // Save the new user profile let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change( @@ -669,19 +653,11 @@ impl UserManager { Ok(None) } - async fn update_user( - &self, - uid: i64, - token: String, - params: UpdateUserProfileParams, - ) -> Result<(), FlowyError> { - let server = self.cloud_services.get_user_service()?; - tokio::spawn(async move { - let credentials = UserCredentials::new(Some(token), Some(uid), None); - server.update_user(credentials, params).await - }) - .await - .map_err(internal_error)??; + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + let server = self.cloud_service.get_user_service()?; + tokio::spawn(async move { server.update_user(params).await }) + .await + .map_err(internal_error)??; Ok(()) } @@ -702,7 +678,7 @@ impl UserManager { } pub async fn receive_realtime_event(&self, json: Value) { - if let Ok(user_service) = self.cloud_services.get_user_service() { + if let Ok(user_service) = self.cloud_service.get_user_service() { user_service.receive_realtime_event(json) } } @@ -712,9 +688,9 @@ impl UserManager { authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_services.set_server_auth_type(authenticator); + self.cloud_service.set_server_auth_type(authenticator); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; Ok(url) } @@ -725,9 +701,9 @@ impl UserManager { password: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) } @@ -738,9 +714,9 @@ impl UserManager { redirect_to: &str, ) -> Result<(), FlowyError> { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) .await?; @@ -753,9 +729,9 @@ impl UserManager { passcode: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) } @@ -765,9 +741,9 @@ impl UserManager { oauth_provider: &str, ) -> Result { self - .cloud_services + .cloud_service .set_server_auth_type(&AuthType::AppFlowyCloud); - let auth_service = self.cloud_services.get_user_service()?; + let auth_service = self.cloud_service.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) .await?; @@ -778,27 +754,32 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - authenticator: &AuthType, + auth_type: AuthType, session: &Session, ) -> Result<(), FlowyError> { - let user_profile = UserProfile::from((response, authenticator)); + let user_profile = UserProfile::from((response, &auth_type)); let uid = user_profile.uid; - if authenticator.is_local() { + if auth_type.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); self.set_anon_user(session); } - save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; + save_all_user_workspaces( + uid, + self.db_connection(uid)?, + auth_type, + response.user_workspaces(), + )?; info!( "Save new user profile to disk, authenticator: {:?}", - authenticator + auth_type ); self .authenticate_user .set_session(Some(session.clone().into()))?; self - .save_user(uid, (user_profile, *authenticator).into()) + .save_user(uid, (user_profile, auth_type).into()) .await?; Ok(()) } @@ -807,11 +788,6 @@ impl UserManager { let session = self.get_session()?; if session.user_id == user_update.uid { debug!("Receive user update: {:?}", user_update); - let user_profile = self.get_user_profile_from_disk(user_update.uid).await?; - if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) { - return Ok(()); - } - // Save the user profile change upsert_user_profile_change( user_update.uid, @@ -827,27 +803,29 @@ impl UserManager { &self, old_user: &AnonUser, _new_user_session: &Session, - authenticator: &AuthType, + auth_type: &AuthType, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - if authenticator == &AuthType::AppFlowyCloud { + if auth_type == &AuthType::AppFlowyCloud { self .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) .await?; } // Save the old user workspace setting. - save_user_workspace( + let mut conn = self + .authenticate_user + .database + .get_connection(old_user.session.user_id)?; + upsert_user_workspace( old_user.session.user_id, - self - .authenticate_user - .database - .get_connection(old_user.session.user_id)?, - &old_user.session.user_workspace.clone(), + *auth_type, + old_user.session.user_workspace.clone(), + &mut conn, )?; Ok(()) } @@ -927,7 +905,7 @@ pub(crate) fn run_collab_data_migration( let migrations = collab_migration_list(); match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( migrations, - &user.authenticator, + &user.auth_type, app_version, ) { Ok(applied_migrations) => { 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 ddb73667c8..7c5330149e 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 @@ -24,7 +24,7 @@ impl UserManager { .await .ok()?; - if user_profile.authenticator.is_local() { + if user_profile.auth_type.is_local() { Some(AnonUser { session }) } else { None 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 7de09995a0..afdfd218ef 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 @@ -215,7 +215,7 @@ impl UserManager { 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 cloud_services = self.cloud_service.clone(); let authenticate_user = self.authenticate_user.clone(); let is_loading_awareness = self.is_loading_awareness.clone(); @@ -376,7 +376,7 @@ impl UserManager { if !is_loading { let user_profile = self.get_user_profile_from_disk(session.user_id).await?; self - .initial_user_awareness(&session, &user_profile.authenticator) + .initial_user_awareness(&session, &user_profile.auth_type) .await?; } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs index 2bfba3422e..1462d1f019 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs @@ -1,31 +1,9 @@ -use crate::entities::{AuthStateChangedPB, AuthStatePB}; -use crate::notification::send_auth_state_notification; use crate::services::cloud_config::get_encrypt_secret; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{ - EncryptionType, UpdateUserProfileParams, UserCredentials, UserProfile, -}; use lib_infra::encryption::{decrypt_text, encrypt_text}; impl UserManager { - pub async fn set_encrypt_secret( - &self, - uid: i64, - secret: String, - encryption_type: EncryptionType, - ) -> FlowyResult<()> { - let params = UpdateUserProfileParams::new(uid).with_encryption_type(encryption_type); - self - .cloud_services - .get_user_service()? - .update_user(UserCredentials::from_uid(uid), params.clone()) - .await?; - self.cloud_services.set_encrypt_secret(secret); - - Ok(()) - } - pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult { let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?; Ok(encrypt_sign) @@ -63,16 +41,3 @@ impl UserManager { } } } - -pub(crate) fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { - // If the local user profile's encryption sign is not equal to the user update's encryption sign, - // which means the user enable encryption in another device, we should logout the current user. - let is_valid = user_profile.encryption_type.sign() == encryption_sign; - if !is_valid { - send_auth_state_notification(AuthStateChangedPB { - state: AuthStatePB::InvalidAuth, - message: "Encryption configuration was changed".to_string(), - }); - } - is_valid -} 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 b5ae54309b..3144b27213 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 @@ -2,7 +2,6 @@ use chrono::{Duration, NaiveDateTime, Utc}; use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlanDetail}; use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; -use std::convert::TryFrom; use std::str::FromStr; use std::sync::Arc; @@ -12,15 +11,14 @@ use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderD use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::entities::{ - Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, - WorkspaceMember, + AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use tracing::{error, info, instrument, trace, warn}; use uuid::Uuid; use crate::entities::{ RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, - UpdateUserWorkspaceSettingPB, UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB, + UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, }; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; @@ -31,12 +29,15 @@ use crate::services::data_import::{ use crate::services::sqlite_sql::member_sql::{ select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, }; -use crate::services::sqlite_sql::user_sql::UserTableChangeset; -use crate::services::sqlite_sql::workspace_sql::{ - get_all_user_workspace_op, get_user_workspace_op, insert_or_update_workspaces_op, - UserWorkspaceTable, +use crate::services::sqlite_sql::workspace_setting_sql::{ + select_workspace_setting, update_workspace_setting, upsert_workspace_setting, + WorkspaceSettingsChangeset, WorkspaceSettingsTable, }; -use crate::user_manager::{upsert_user_profile_change, UserManager}; +use crate::services::sqlite_sql::workspace_sql::{ + select_all_user_workspace, select_user_workspace, update_user_workspace, upsert_user_workspace, + UserWorkspaceChangeset, UserWorkspaceTable, +}; +use crate::user_manager::UserManager; use flowy_user_pub::session::Session; impl UserManager { @@ -119,12 +120,12 @@ impl UserManager { let user_id = current_session.user_id; let weak_user_collab_db = Arc::downgrade(&user_collab_db); - let weak_user_cloud_service = self.cloud_services.get_user_service()?; + let weak_user_cloud_service = self.cloud_service.get_user_service()?; match upload_collab_objects_data( user_id, weak_user_collab_db, ¤t_session.user_workspace.workspace_id()?, - &user.authenticator, + &user.auth_type, collab_data, weak_user_cloud_service, ) @@ -164,7 +165,7 @@ impl UserManager { pub async fn open_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("open workspace: {}", workspace_id); let user_workspace = self - .cloud_services + .cloud_service .get_user_service()? .open_workspace(workspace_id) .await?; @@ -177,7 +178,7 @@ impl UserManager { 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) + .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.auth_type) .await { error!( @@ -190,7 +191,7 @@ impl UserManager { .user_status_callback .read() .await - .open_workspace(uid, &user_workspace, &user_profile.authenticator) + .open_workspace(uid, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); @@ -200,9 +201,13 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn add_workspace(&self, workspace_name: &str) -> FlowyResult { + pub async fn create_workspace( + &self, + workspace_name: &str, + auth_type: AuthType, + ) -> FlowyResult { let new_workspace = self - .cloud_services + .cloud_service .get_user_service()? .create_workspace(workspace_name) .await?; @@ -215,44 +220,29 @@ impl UserManager { // save the workspace to sqlite db let uid = self.user_id()?; let mut conn = self.db_connection(uid)?; - insert_or_update_workspaces_op(uid, &[new_workspace.clone()], &mut conn)?; + upsert_user_workspace(uid, auth_type, new_workspace.clone(), &mut conn)?; Ok(new_workspace) } pub async fn patch_workspace( &self, workspace_id: &Uuid, - new_workspace_name: Option<&str>, - new_workspace_icon: Option<&str>, + changeset: UserWorkspaceChangeset, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? - .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) + .patch_workspace(workspace_id, changeset.name.clone(), changeset.icon.clone()) .await?; // save the icon and name to sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - let mut user_workspace = match self.get_user_workspace(uid, workspace_id) { - Some(user_workspace) => user_workspace, - None => { - return Err(FlowyError::record_not_found().with_context(format!( - "Expected to find user workspace with id: {}, but not found", - workspace_id - ))); - }, - }; - - if let Some(new_workspace_name) = new_workspace_name { - user_workspace.name = new_workspace_name.to_string(); - } - if let Some(new_workspace_icon) = new_workspace_icon { - user_workspace.icon = new_workspace_icon.to_string(); - } - - let _ = save_user_workspace(uid, conn, &user_workspace); + update_user_workspace(conn, changeset)?; + let user_workspace = self + .get_user_workspace(uid, workspace_id) + .ok_or_else(FlowyError::record_not_found)?; let payload: UserWorkspacePB = user_workspace.clone().into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) @@ -265,7 +255,7 @@ impl UserManager { pub async fn leave_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("leave workspace: {}", workspace_id); self - .cloud_services + .cloud_service .get_user_service()? .leave_workspace(workspace_id) .await?; @@ -285,7 +275,7 @@ impl UserManager { pub async fn delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { info!("delete workspace: {}", workspace_id); self - .cloud_services + .cloud_service .get_user_service()? .delete_workspace(workspace_id) .await?; @@ -308,7 +298,7 @@ impl UserManager { role: Role, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .invite_workspace_member(invitee_email, workspace_id, role) .await?; @@ -318,7 +308,7 @@ impl UserManager { pub async fn list_pending_workspace_invitations(&self) -> FlowyResult> { let status = Some(WorkspaceInvitationStatus::Pending); let invitations = self - .cloud_services + .cloud_service .get_user_service()? .list_workspace_invitations(status) .await?; @@ -327,7 +317,7 @@ impl UserManager { pub async fn accept_workspace_invitation(&self, invite_id: String) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .accept_workspace_invitations(invite_id) .await?; @@ -340,7 +330,7 @@ impl UserManager { workspace_id: Uuid, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .remove_workspace_member(user_email, workspace_id) .await?; @@ -352,7 +342,7 @@ impl UserManager { workspace_id: Uuid, ) -> FlowyResult> { let members = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_members(workspace_id) .await?; @@ -365,7 +355,7 @@ impl UserManager { uid: i64, ) -> FlowyResult { let member = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_member(workspace_id, uid) .await?; @@ -379,7 +369,7 @@ impl UserManager { role: Role, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .update_workspace_member(user_email, workspace_id, role) .await?; @@ -388,19 +378,23 @@ impl UserManager { pub fn get_user_workspace(&self, uid: i64, workspace_id: &Uuid) -> Option { let conn = self.db_connection(uid).ok()?; - get_user_workspace_op(workspace_id.to_string().as_str(), conn) + select_user_workspace(workspace_id.to_string().as_str(), conn) } - pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { + pub async fn get_all_user_workspaces( + &self, + uid: i64, + auth_type: AuthType, + ) -> FlowyResult> { let conn = self.db_connection(uid)?; - let workspaces = get_all_user_workspace_op(uid, conn)?; + let workspaces = select_all_user_workspace(uid, conn)?; - if let Ok(service) = self.cloud_services.get_user_service() { + if let Ok(service) = self.cloud_service.get_user_service() { if let Ok(pool) = self.db_pool(uid) { tokio::spawn(async move { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { - let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces); + let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) .payload(repeated_workspace_pbs) @@ -418,11 +412,12 @@ impl UserManager { &self, workspace_subscription: SubscribeWorkspacePB, ) -> FlowyResult { + let workspace_id = Uuid::from_str(&workspace_subscription.workspace_id)?; let payment_link = self - .cloud_services + .cloud_service .get_user_service()? .subscribe_workspace( - workspace_subscription.workspace_id, + workspace_id, workspace_subscription.recurring_interval.into(), workspace_subscription.workspace_subscription_plan.into(), workspace_subscription.success_url, @@ -437,10 +432,11 @@ impl UserManager { &self, workspace_id: String, ) -> FlowyResult { + let workspace_id = Uuid::from_str(&workspace_id)?; let subscriptions = self - .cloud_services + .cloud_service .get_user_service()? - .get_workspace_subscription_one(workspace_id.clone()) + .get_workspace_subscription_one(&workspace_id) .await?; Ok(WorkspaceSubscriptionInfoPB::from(subscriptions)) @@ -454,7 +450,7 @@ impl UserManager { reason: Option, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .cancel_workspace_subscription(workspace_id, plan, reason) .await?; @@ -464,12 +460,12 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn update_workspace_subscription_payment_period( &self, - workspace_id: String, + workspace_id: &Uuid, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> FlowyResult<()> { self - .cloud_services + .cloud_service .get_user_service()? .update_workspace_subscription_payment_period(workspace_id, plan, recurring_interval) .await?; @@ -479,7 +475,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_subscription_plan_details(&self) -> FlowyResult> { let plan_details = self - .cloud_services + .cloud_service .get_user_service()? .get_subscription_plan_details() .await?; @@ -489,10 +485,10 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_workspace_usage( &self, - workspace_id: String, + workspace_id: &Uuid, ) -> FlowyResult { let workspace_usage = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_usage(workspace_id) .await?; @@ -518,7 +514,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_billing_portal_url(&self) -> FlowyResult { let url = self - .cloud_services + .cloud_service .get_user_service()? .get_billing_portal_url() .await?; @@ -529,39 +525,92 @@ impl UserManager { &self, updated_settings: UpdateUserWorkspaceSettingPB, ) -> FlowyResult<()> { - let ai_model = updated_settings.ai_model.clone(); - let workspace_id = updated_settings.workspace_id.clone(); - let cloud_service = self.cloud_services.get_user_service()?; + let workspace_id = Uuid::from_str(&updated_settings.workspace_id)?; + let cloud_service = self.cloud_service.get_user_service()?; let settings = cloud_service - .update_workspace_setting(&workspace_id, updated_settings.into()) + .update_workspace_setting(&workspace_id, updated_settings.clone().into()) .await?; - let pb = UseAISettingPB::from(settings); + let changeset = WorkspaceSettingsChangeset { + id: workspace_id.to_string(), + disable_search_indexing: updated_settings.disable_search_indexing, + ai_model: updated_settings.ai_model.clone(), + }; + let uid = self.user_id()?; - send_notification(&uid.to_string(), UserNotification::DidUpdateAISetting) - .payload(pb) - .send(); + let mut conn = self.db_connection(uid)?; + update_workspace_setting(&mut conn, changeset)?; - if let Some(ai_model) = &ai_model { - if let Err(err) = self.cloud_services.set_ai_model(ai_model) { - error!("Set ai model failed: {}", err); - } - - let conn = self.db_connection(uid)?; - let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model); - upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; - } + let pb = WorkspaceSettingsPB::from(&settings); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(pb) + .send(); Ok(()) } - pub async fn get_workspace_settings(&self, workspace_id: &str) -> FlowyResult { - let cloud_service = self.cloud_services.get_user_service()?; - let settings = cloud_service.get_workspace_setting(workspace_id).await?; + pub async fn get_workspace_settings( + &self, + workspace_id: &Uuid, + ) -> FlowyResult { let uid = self.user_id()?; - let conn = self.db_connection(uid)?; - let params = UpdateUserProfileParams::new(uid).with_ai_model(&settings.ai_model); - upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; - Ok(UseAISettingPB::from(settings)) + let mut conn = self.db_connection(uid)?; + match select_workspace_setting(&mut conn, &workspace_id.to_string()) { + Ok(workspace_settings) => { + trace!("workspace settings found in local db"); + let pb = WorkspaceSettingsPB::from(workspace_settings); + + let old_pb = pb.clone(); + let workspace_id = *workspace_id; + let pool = self.db_pool(uid)?; + let cloud_service = self.cloud_service.clone(); + tokio::spawn(async move { + let cloud_service = cloud_service.get_user_service()?; + let settings = cloud_service.get_workspace_setting(&workspace_id).await?; + let new_pb = WorkspaceSettingsPB::from(&settings); + if new_pb != old_pb { + trace!("workspace settings updated"); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(new_pb) + .send(); + + if let Ok(mut conn) = pool.get() { + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + } + } + + Ok::<_, FlowyError>(()) + }); + Ok(pb) + }, + Err(err) => { + if err.is_record_not_found() { + trace!("No workspace settings found, fetch from remote"); + let settings = self + .cloud_service + .get_user_service()? + .get_workspace_setting(&workspace_id) + .await?; + let pb = WorkspaceSettingsPB::from(&settings); + let mut conn = self.db_connection(uid)?; + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + Ok::<_, FlowyError>(pb.clone()) + } else { + Err(err) + } + }, + } } pub async fn get_workspace_member_info( @@ -600,7 +649,7 @@ impl UserManager { ) -> FlowyResult { trace!("get workspace member info from remote: {}", workspace_id); let member = self - .cloud_services + .cloud_service .get_user_service()? .get_workspace_member_info(workspace_id, uid) .await?; @@ -629,7 +678,7 @@ impl UserManager { let plans = PeriodicallyCheckBillingState::new( workspace_id, success.plan.map(SubscriptionPlan::from), - Arc::downgrade(&self.cloud_services), + Arc::downgrade(&self.cloud_service), Arc::downgrade(&self.authenticate_user), ) .start() @@ -645,56 +694,21 @@ impl UserManager { } } -/// This method is used to save one user workspace to the SQLite database -/// -/// If the workspace is already persisted in the database, it will be overridden. -/// -/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user. -/// -pub fn save_user_workspace( - uid: i64, - mut conn: DBConnection, - user_workspace: &UserWorkspace, -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?; - let affected_rows = diesel::update( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(&user_workspace.id)), - ) - .set(( - user_workspace_table::name.eq(&user_workspace.name), - user_workspace_table::created_at.eq(&user_workspace.created_at), - user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), - user_workspace_table::icon.eq(&user_workspace.icon), - user_workspace_table::member_count.eq(&user_workspace.member_count), - )) - .execute(conn)?; - - if affected_rows == 0 { - diesel::insert_into(user_workspace_table::table) - .values(user_workspace) - .execute(conn)?; - } - - Ok::<(), FlowyError>(()) - }) -} - /// This method is used to save the user workspaces (plural) to the SQLite database /// /// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. /// -/// Consider using [save_user_workspace] if you only need to save a single workspace. +/// Consider using [upsert_user_workspace] if you only need to save a single workspace. /// pub fn save_all_user_workspaces( uid: i64, mut conn: DBConnection, + auth_type: AuthType, user_workspaces: &[UserWorkspace], ) -> FlowyResult<()> { let user_workspaces = user_workspaces .iter() - .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace))) + .map(|user_workspace| UserWorkspaceTable::from_workspace(uid, user_workspace, auth_type)) .collect::, _>>()?; conn.immediate_transaction(|conn| { From 3a05a4851fa83c8517e9f3ab29408a3c947975d6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:07:42 +0800 Subject: [PATCH 30/74] chore: clippy --- .../down.sql | 3 + .../up.sql | 24 ++++++ .../sqlite_sql/workspace_setting_sql.rs | 72 ++++++++++++++++ .../user_manager/manager_user_workspace.rs | 84 ++++++++++--------- 4 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql create mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql new file mode 100644 index 0000000000..50602eb129 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_workspace_table +DROP COLUMN auth_type; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql new file mode 100644 index 0000000000..b77372ad63 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql @@ -0,0 +1,24 @@ +-- Your SQL goes here +ALTER TABLE user_workspace_table + ADD COLUMN auth_type INTEGER NOT NULL DEFAULT 1; + +-- 2. Back‑fill from user_table.auth_type +UPDATE user_workspace_table +SET auth_type = (SELECT ut.auth_type + FROM user_table ut + WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) +WHERE EXISTS (SELECT 1 + FROM user_table ut + WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)); + +ALTER TABLE user_table DROP COLUMN stability_ai_key; +ALTER TABLE user_table DROP COLUMN openai_key; +ALTER TABLE user_table DROP COLUMN workspace; +ALTER TABLE user_table DROP COLUMN encryption_type; +ALTER TABLE user_table DROP COLUMN ai_model; + +CREATE TABLE workspace_setting_table ( + id TEXT PRIMARY KEY NOT NULL , + disable_search_indexing BOOLEAN DEFAULT FALSE NOT NULL , + ai_model TEXT DEFAULT "" NOT NULL +); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs new file mode 100644 index 0000000000..dcd87a36ee --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs @@ -0,0 +1,72 @@ +use client_api::entity::AFWorkspaceSettings; +use flowy_error::FlowyError; +use flowy_sqlite::schema::workspace_setting_table; +use flowy_sqlite::schema::workspace_setting_table::dsl; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use uuid::Uuid; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = workspace_setting_table)] +pub struct WorkspaceSettingsTable { + pub id: String, + pub disable_search_indexing: bool, + pub ai_model: String, +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = workspace_setting_table)] +pub struct WorkspaceSettingsChangeset { + pub id: String, + pub disable_search_indexing: Option, + pub ai_model: Option, +} + +impl WorkspaceSettingsTable { + pub fn from_workspace_settings(workspace_id: &Uuid, settings: &AFWorkspaceSettings) -> Self { + Self { + id: workspace_id.to_string(), + disable_search_indexing: settings.disable_search_indexing, + ai_model: settings.ai_model.clone(), + } + } +} + +pub fn update_workspace_setting( + conn: &mut DBConnection, + changeset: WorkspaceSettingsChangeset, +) -> Result<(), FlowyError> { + diesel::update(dsl::workspace_setting_table) + .filter(workspace_setting_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(conn)?; + Ok(()) +} + +/// Upserts a workspace setting into the database. +pub fn upsert_workspace_setting( + conn: &mut DBConnection, + settings: WorkspaceSettingsTable, +) -> Result<(), FlowyError> { + diesel::insert_into(dsl::workspace_setting_table) + .values(settings.clone()) + .on_conflict(workspace_setting_table::id) + .do_update() + .set(( + workspace_setting_table::disable_search_indexing.eq(settings.disable_search_indexing), + workspace_setting_table::ai_model.eq(settings.ai_model), + )) + .execute(conn)?; + Ok(()) +} + +/// Selects a workspace setting by id from the database. +pub fn select_workspace_setting( + conn: &mut DBConnection, + id: &str, +) -> Result { + let setting = dsl::workspace_setting_table + .filter(workspace_setting_table::id.eq(id)) + .first::(conn)?; + Ok(setting) +} 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 3144b27213..ea99a01f3a 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 @@ -5,17 +5,6 @@ use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; use std::str::FromStr; use std::sync::Arc; -use collab_integrate::CollabKVDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; -use flowy_user_pub::entities::{ - AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, -}; -use tracing::{error, info, instrument, trace, warn}; -use uuid::Uuid; - use crate::entities::{ RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, @@ -38,7 +27,18 @@ use crate::services::sqlite_sql::workspace_sql::{ UserWorkspaceChangeset, UserWorkspaceTable, }; use crate::user_manager::UserManager; +use collab_integrate::CollabKVDB; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_user_pub::cloud::UserCloudServiceProvider; +use flowy_user_pub::entities::{ + AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +}; use flowy_user_pub::session::Session; +use tracing::{error, info, instrument, trace, warn}; +use uuid::Uuid; impl UserManager { /// Import appflowy data from the given path. @@ -561,51 +561,29 @@ impl UserManager { Ok(workspace_settings) => { trace!("workspace settings found in local db"); let pb = WorkspaceSettingsPB::from(workspace_settings); - let old_pb = pb.clone(); let workspace_id = *workspace_id; + + // Spawn a task to sync remote settings using the helper let pool = self.db_pool(uid)?; let cloud_service = self.cloud_service.clone(); tokio::spawn(async move { - let cloud_service = cloud_service.get_user_service()?; - let settings = cloud_service.get_workspace_setting(&workspace_id).await?; - let new_pb = WorkspaceSettingsPB::from(&settings); - if new_pb != old_pb { - trace!("workspace settings updated"); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(new_pb) - .send(); - - if let Ok(mut conn) = pool.get() { - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), - )?; - } - } - - Ok::<_, FlowyError>(()) + let _ = sync_workspace_settings(cloud_service, workspace_id, old_pb, uid, pool).await; }); Ok(pb) }, Err(err) => { if err.is_record_not_found() { trace!("No workspace settings found, fetch from remote"); - let settings = self - .cloud_service - .get_user_service()? - .get_workspace_setting(&workspace_id) - .await?; + let service = self.cloud_service.get_user_service()?; + let settings = service.get_workspace_setting(&workspace_id).await?; let pb = WorkspaceSettingsPB::from(&settings); let mut conn = self.db_connection(uid)?; upsert_workspace_setting( &mut conn, WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), )?; - Ok::<_, FlowyError>(pb.clone()) + Ok(pb) } else { Err(err) } @@ -777,3 +755,31 @@ fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { None => false, } } + +async fn sync_workspace_settings( + cloud_service: Arc, + workspace_id: Uuid, + old_pb: WorkspaceSettingsPB, + uid: i64, + pool: Arc, +) -> FlowyResult<()> { + let service = cloud_service.get_user_service()?; + let settings = service.get_workspace_setting(&workspace_id).await?; + let new_pb = WorkspaceSettingsPB::from(&settings); + if new_pb != old_pb { + trace!("workspace settings updated"); + send_notification( + &uid.to_string(), + UserNotification::DidUpdateWorkspaceSetting, + ) + .payload(new_pb) + .send(); + if let Ok(mut conn) = pool.get() { + upsert_workspace_setting( + &mut conn, + WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + )?; + } + } + Ok(()) +} From e851fba71b28c38b599ef1ef0a4d96339f97c349 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 14:21:22 +0800 Subject: [PATCH 31/74] chore: clippy --- .../flowy-user/src/user_manager/manager_user_workspace.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ea99a01f3a..ac15324c00 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 @@ -576,12 +576,12 @@ impl UserManager { if err.is_record_not_found() { trace!("No workspace settings found, fetch from remote"); let service = self.cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(&workspace_id).await?; + let settings = service.get_workspace_setting(workspace_id).await?; let pb = WorkspaceSettingsPB::from(&settings); let mut conn = self.db_connection(uid)?; upsert_workspace_setting( &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), + WorkspaceSettingsTable::from_workspace_settings(workspace_id, &settings), )?; Ok(pb) } else { From 84952b905694863bd4f300f47792a87f05371dee Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:24:32 +0800 Subject: [PATCH 32/74] chore: adjust generate theme script to import subtle colors (#7784) --- .../data/appflowy_default/primitive.dart | 362 +++++++++++++++++- .../theme/data/appflowy_default/semantic.dart | 2 +- .../appflowy_ui/script/generate_theme.dart | 211 ++++++---- 3 files changed, 509 insertions(+), 66 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart index 790db31660..2bd6d619d8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-16T22:13:33.297893 +// Generation time: 2025-04-19T13:45:56.076897 // // To modify these colors, edit the source JSON files and run the script: // @@ -295,4 +295,364 @@ class AppFlowyPrimitiveTokens { /// #7f6200 static Color get yellow1000 => Color(0xFF7F6200); + + /// #fcf2f2 + static Color get subtleColorRose100 => Color(0xFFFCF2F2); + + /// #fae3e3 + static Color get subtleColorRose200 => Color(0xFFFAE3E3); + + /// #fad9d9 + static Color get subtleColorRose300 => Color(0xFFFAD9D9); + + /// #edadad + static Color get subtleColorRose400 => Color(0xFFEDADAD); + + /// #cc4e4e + static Color get subtleColorRose500 => Color(0xFFCC4E4E); + + /// #702828 + static Color get subtleColorRose600 => Color(0xFF702828); + + /// #fcf4f0 + static Color get subtleColorPapaya100 => Color(0xFFFCF4F0); + + /// #fae8de + static Color get subtleColorPapaya200 => Color(0xFFFAE8DE); + + /// #fadfd2 + static Color get subtleColorPapaya300 => Color(0xFFFADFD2); + + /// #f0bda3 + static Color get subtleColorPapaya400 => Color(0xFFF0BDA3); + + /// #d67240 + static Color get subtleColorPapaya500 => Color(0xFFD67240); + + /// #6b3215 + static Color get subtleColorPapaya600 => Color(0xFF6B3215); + + /// #fff7ed + static Color get subtleColorTangerine100 => Color(0xFFFFF7ED); + + /// #fcedd9 + static Color get subtleColorTangerine200 => Color(0xFFFCEDD9); + + /// #fae5ca + static Color get subtleColorTangerine300 => Color(0xFFFAE5CA); + + /// #f2cb99 + static Color get subtleColorTangerine400 => Color(0xFFF2CB99); + + /// #db8f2c + static Color get subtleColorTangerine500 => Color(0xFFDB8F2C); + + /// #613b0a + static Color get subtleColorTangerine600 => Color(0xFF613B0A); + + /// #fff9ec + static Color get subtleColorMango100 => Color(0xFFFFF9EC); + + /// #fcf1d7 + static Color get subtleColorMango200 => Color(0xFFFCF1D7); + + /// #fae9c3 + static Color get subtleColorMango300 => Color(0xFFFAE9C3); + + /// #f5d68e + static Color get subtleColorMango400 => Color(0xFFF5D68E); + + /// #e0a416 + static Color get subtleColorMango500 => Color(0xFFE0A416); + + /// #5c4102 + static Color get subtleColorMango600 => Color(0xFF5C4102); + + /// #fffbe8 + static Color get subtleColorLemon100 => Color(0xFFFFFBE8); + + /// #fcf5cf + static Color get subtleColorLemon200 => Color(0xFFFCF5CF); + + /// #faefb9 + static Color get subtleColorLemon300 => Color(0xFFFAEFB9); + + /// #f5e282 + static Color get subtleColorLemon400 => Color(0xFFF5E282); + + /// #e0bb00 + static Color get subtleColorLemon500 => Color(0xFFE0BB00); + + /// #574800 + static Color get subtleColorLemon600 => Color(0xFF574800); + + /// #f9fae6 + static Color get subtleColorOlive100 => Color(0xFFF9FAE6); + + /// #f6f7d0 + static Color get subtleColorOlive200 => Color(0xFFF6F7D0); + + /// #f0f2b3 + static Color get subtleColorOlive300 => Color(0xFFF0F2B3); + + /// #dbde83 + static Color get subtleColorOlive400 => Color(0xFFDBDE83); + + /// #adb204 + static Color get subtleColorOlive500 => Color(0xFFADB204); + + /// #4a4c03 + static Color get subtleColorOlive600 => Color(0xFF4A4C03); + + /// #f6f9e6 + static Color get subtleColorLime100 => Color(0xFFF6F9E6); + + /// #eef5ce + static Color get subtleColorLime200 => Color(0xFFEEF5CE); + + /// #e7f0bb + static Color get subtleColorLime300 => Color(0xFFE7F0BB); + + /// #cfdb91 + static Color get subtleColorLime400 => Color(0xFFCFDB91); + + /// #92a822 + static Color get subtleColorLime500 => Color(0xFF92A822); + + /// #414d05 + static Color get subtleColorLime600 => Color(0xFF414D05); + + /// #f4faeb + static Color get subtleColorGrass100 => Color(0xFFF4FAEB); + + /// #e9f5d7 + static Color get subtleColorGrass200 => Color(0xFFE9F5D7); + + /// #def0c5 + static Color get subtleColorGrass300 => Color(0xFFDEF0C5); + + /// #bfd998 + static Color get subtleColorGrass400 => Color(0xFFBFD998); + + /// #75a828 + static Color get subtleColorGrass500 => Color(0xFF75A828); + + /// #334d0c + static Color get subtleColorGrass600 => Color(0xFF334D0C); + + /// #f1faf0 + static Color get subtleColorForest100 => Color(0xFFF1FAF0); + + /// #e2f5df + static Color get subtleColorForest200 => Color(0xFFE2F5DF); + + /// #d7f0d3 + static Color get subtleColorForest300 => Color(0xFFD7F0D3); + + /// #a8d6a1 + static Color get subtleColorForest400 => Color(0xFFA8D6A1); + + /// #49a33b + static Color get subtleColorForest500 => Color(0xFF49A33B); + + /// #1e4f16 + static Color get subtleColorForest600 => Color(0xFF1E4F16); + + /// #f0faf6 + static Color get subtleColorJade100 => Color(0xFFF0FAF6); + + /// #dff5eb + static Color get subtleColorJade200 => Color(0xFFDFF5EB); + + /// #cef0e1 + static Color get subtleColorJade300 => Color(0xFFCEF0E1); + + /// #90d1b5 + static Color get subtleColorJade400 => Color(0xFF90D1B5); + + /// #1c9963 + static Color get subtleColorJade500 => Color(0xFF1C9963); + + /// #075231 + static Color get subtleColorJade600 => Color(0xFF075231); + + /// #f0f9fa + static Color get subtleColorAqua100 => Color(0xFFF0F9FA); + + /// #dff3f5 + static Color get subtleColorAqua200 => Color(0xFFDFF3F5); + + /// #ccecf0 + static Color get subtleColorAqua300 => Color(0xFFCCECF0); + + /// #83ccd4 + static Color get subtleColorAqua400 => Color(0xFF83CCD4); + + /// #008e9e + static Color get subtleColorAqua500 => Color(0xFF008E9E); + + /// #004e57 + static Color get subtleColorAqua600 => Color(0xFF004E57); + + /// #f0f6fa + static Color get subtleColorAzure100 => Color(0xFFF0F6FA); + + /// #e1eef7 + static Color get subtleColorAzure200 => Color(0xFFE1EEF7); + + /// #d3e6f5 + static Color get subtleColorAzure300 => Color(0xFFD3E6F5); + + /// #88c0eb + static Color get subtleColorAzure400 => Color(0xFF88C0EB); + + /// #0877cc + static Color get subtleColorAzure500 => Color(0xFF0877CC); + + /// #154469 + static Color get subtleColorAzure600 => Color(0xFF154469); + + /// #f0f3fa + static Color get subtleColorDenim100 => Color(0xFFF0F3FA); + + /// #e3ebfa + static Color get subtleColorDenim200 => Color(0xFFE3EBFA); + + /// #d7e2f7 + static Color get subtleColorDenim300 => Color(0xFFD7E2F7); + + /// #9ab6ed + static Color get subtleColorDenim400 => Color(0xFF9AB6ED); + + /// #3267d1 + static Color get subtleColorDenim500 => Color(0xFF3267D1); + + /// #223c70 + static Color get subtleColorDenim600 => Color(0xFF223C70); + + /// #f2f2fc + static Color get subtleColorMauve100 => Color(0xFFF2F2FC); + + /// #e6e6fa + static Color get subtleColorMauve200 => Color(0xFFE6E6FA); + + /// #dcdcf7 + static Color get subtleColorMauve300 => Color(0xFFDCDCF7); + + /// #aeaef5 + static Color get subtleColorMauve400 => Color(0xFFAEAEF5); + + /// #5555e0 + static Color get subtleColorMauve500 => Color(0xFF5555E0); + + /// #36366b + static Color get subtleColorMauve600 => Color(0xFF36366B); + + /// #f6f3fc + static Color get subtleColorLavender100 => Color(0xFFF6F3FC); + + /// #ebe3fa + static Color get subtleColorLavender200 => Color(0xFFEBE3FA); + + /// #e4daf7 + static Color get subtleColorLavender300 => Color(0xFFE4DAF7); + + /// #c1aaf0 + static Color get subtleColorLavender400 => Color(0xFFC1AAF0); + + /// #8153db + static Color get subtleColorLavender500 => Color(0xFF8153DB); + + /// #462f75 + static Color get subtleColorLavender600 => Color(0xFF462F75); + + /// #f7f0fa + static Color get subtleColorLilac100 => Color(0xFFF7F0FA); + + /// #f0e1f7 + static Color get subtleColorLilac200 => Color(0xFFF0E1F7); + + /// #edd7f7 + static Color get subtleColorLilac300 => Color(0xFFEDD7F7); + + /// #d3a9e8 + static Color get subtleColorLilac400 => Color(0xFFD3A9E8); + + /// #9e4cc7 + static Color get subtleColorLilac500 => Color(0xFF9E4CC7); + + /// #562d6b + static Color get subtleColorLilac600 => Color(0xFF562D6B); + + /// #faf0fa + static Color get subtleColorMallow100 => Color(0xFFFAF0FA); + + /// #f5e1f4 + static Color get subtleColorMallow200 => Color(0xFFF5E1F4); + + /// #f5d7f4 + static Color get subtleColorMallow300 => Color(0xFFF5D7F4); + + /// #dea4dc + static Color get subtleColorMallow400 => Color(0xFFDEA4DC); + + /// #b240af + static Color get subtleColorMallow500 => Color(0xFFB240AF); + + /// #632861 + static Color get subtleColorMallow600 => Color(0xFF632861); + + /// #f9eff3 + static Color get subtleColorCamellia100 => Color(0xFFF9EFF3); + + /// #f7e1eb + static Color get subtleColorCamellia200 => Color(0xFFF7E1EB); + + /// #f7d7e5 + static Color get subtleColorCamellia300 => Color(0xFFF7D7E5); + + /// #e5a3c0 + static Color get subtleColorCamellia400 => Color(0xFFE5A3C0); + + /// #c24279 + static Color get subtleColorCamellia500 => Color(0xFFC24279); + + /// #6e2343 + static Color get subtleColorCamellia600 => Color(0xFF6E2343); + + /// #f5f5f5 + static Color get subtleColorSmoke100 => Color(0xFFF5F5F5); + + /// #e8e8e8 + static Color get subtleColorSmoke200 => Color(0xFFE8E8E8); + + /// #dedede + static Color get subtleColorSmoke300 => Color(0xFFDEDEDE); + + /// #b8b8b8 + static Color get subtleColorSmoke400 => Color(0xFFB8B8B8); + + /// #6e6e6e + static Color get subtleColorSmoke500 => Color(0xFF6E6E6E); + + /// #404040 + static Color get subtleColorSmoke600 => Color(0xFF404040); + + /// #f2f4f7 + static Color get subtleColorIron100 => Color(0xFFF2F4F7); + + /// #e6e9f0 + static Color get subtleColorIron200 => Color(0xFFE6E9F0); + + /// #dadee5 + static Color get subtleColorIron300 => Color(0xFFDADEE5); + + /// #b0b5bf + static Color get subtleColorIron400 => Color(0xFFB0B5BF); + + /// #666f80 + static Color get subtleColorIron500 => Color(0xFF666F80); + + /// #394152 + static Color get subtleColorIron600 => Color(0xFF394152); } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index 03e80da962..fe774d3561 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -3,7 +3,7 @@ // AUTO-GENERATED - DO NOT EDIT DIRECTLY // // This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-16T22:13:33.307397 +// Generation time: 2025-04-19T13:45:56.089922 // // To modify these colors, edit the source JSON files and run the script: // diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart index 9d429f537e..bddcdb4eae 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart @@ -38,22 +38,13 @@ class AppFlowyPrimitiveTokens { // 3. Process each color category. jsonData.forEach((categoryName, categoryData) { - if (categoryData is Map) { - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map && - tokenData['\$type'] == 'color') { - final colorValue = tokenData['\$value'] as String; - final dartColorValue = convertColor(colorValue); - final dartTokenName = - '${categoryName}_$tokenName'.replaceAll('-', '_').toCamelCase(); - - buffer.writeln(''' - - /// $colorValue - static Color get $dartTokenName => Color(0x$dartColorValue);'''); - } - }); - } + categoryData.forEach((tokenName, tokenData) { + processPrimitiveTokenData( + buffer, + tokenData, + '${categoryName}_$tokenName', + ); + }); }); buffer.writeln('}'); @@ -65,6 +56,32 @@ class AppFlowyPrimitiveTokens { print('Successfully generated ${outputFile.path}'); } +void processPrimitiveTokenData( + StringBuffer buffer, + Map tokenData, + final String currentTokenName, +) { + if (tokenData + case { + r'$type': 'color', + r'$value': final String colorValue, + }) { + final dartColorValue = convertColor(colorValue); + final dartTokenName = currentTokenName.replaceAll('-', '_').toCamelCase(); + + buffer.writeln(''' + + /// $colorValue + static Color get $dartTokenName => Color(0x$dartColorValue);'''); + } else { + tokenData.forEach((key, value) { + if (value is Map) { + processPrimitiveTokenData(buffer, value, '${currentTokenName}_$key'); + } + }); + } +} + void generateSemantic() { // 1. Load the JSON file. final lightJsonString = @@ -98,61 +115,39 @@ import 'primitive.dart'; class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); // 3. Process light mode semantic tokens - void writeThemeData(String brightness, Map jsonData) { - buffer.writeln(''' + buffer.writeln(''' @override - AppFlowyThemeData $brightness() { + AppFlowyThemeData light() { final textStyle = AppFlowyBaseTextStyle(); final borderRadius = AppFlowySharedTokens.buildBorderRadius(); final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.$brightness);'''); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);'''); - jsonData.forEach((categoryName, categoryData) { - if (categoryData is Map) { - final hasNonColorType = categoryData.values.any( - (element) => - element is Map && element['\$type'] != 'color', - ); - if (hasNonColorType) { - return; - } + lightJsonData.forEach((categoryName, categoryData) { + if ([ + 'Spacing', + 'Border_Radius', + 'Shadow', + 'Badge_Color', + ].contains(categoryName)) { + return; + } - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map && - tokenData['\$type'] == 'color') { - final semanticTokenName = - tokenName.replaceAll('-', '_').toCamelCase(); - - final value = tokenData['\$value'] as String; - final String colorOrPrimitiveToken; - if (value.isColor) { - colorOrPrimitiveToken = 'Color(0x${convertColor(value)})'; - } else { - final primitiveToken = value - .replaceAll('{', '') - .replaceAll('}', '') - .replaceAll('.', '_') - .replaceAll('-', '_') - .toCamelCase(); - colorOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; - } - - buffer.writeln(' $semanticTokenName: $colorOrPrimitiveToken,'); - } - }); - buffer.writeln(' );'); - } + categoryData.forEach((tokenName, tokenData) { + processSemanticTokenData(buffer, tokenData, tokenName); }); + buffer.writeln(' );'); + }); - buffer.writeln(); - buffer.writeln(''' + buffer.writeln(); + buffer.writeln(''' return AppFlowyThemeData( textStyle: textStyle, textColorScheme: textColorScheme, @@ -168,11 +163,61 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); shadow: shadow, ); }'''); - } - writeThemeData('light', lightJsonData); buffer.writeln(); - writeThemeData('dark', darkJsonData); + + buffer.writeln(''' + @override + AppFlowyThemeData dark() { + final textStyle = AppFlowyBaseTextStyle(); + final borderRadius = AppFlowySharedTokens.buildBorderRadius(); + final spacing = AppFlowySharedTokens.buildSpacing(); + final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);'''); + + darkJsonData.forEach((categoryName, categoryData) { + if ([ + 'Spacing', + 'Border_Radius', + 'Shadow', + 'Badge_Color', + ].contains(categoryName)) { + return; + } + + final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); + final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; + + buffer + ..writeln() + ..writeln(' final $fullCategoryName = $className('); + + categoryData.forEach((tokenName, tokenData) { + if (tokenData is Map) { + processSemanticTokenData(buffer, tokenData, tokenName); + } + }); + buffer.writeln(' );'); + }); + + buffer.writeln(); + + buffer.writeln(''' + return AppFlowyThemeData( + textStyle: textStyle, + textColorScheme: textColorScheme, + borderColorScheme: borderColorScheme, + fillColorScheme: fillColorScheme, + surfaceColorScheme: surfaceColorScheme, + backgroundColorScheme: backgroundColorScheme, + iconColorScheme: iconColorScheme, + brandColorScheme: brandColorScheme, + otherColorsColorScheme: otherColorsColorScheme, + borderRadius: borderRadius, + spacing: spacing, + shadow: shadow, + ); + }'''); + buffer.writeln('}'); // 4. Write the output to a Dart file. @@ -182,6 +227,44 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); print('Successfully generated ${outputFile.path}'); } +void processSemanticTokenData( + StringBuffer buffer, + Map json, + final String currentTokenName, +) { + if (json + case { + r'$type': 'color', + r'$value': final String value, + }) { + final semanticTokenName = + currentTokenName.replaceAll('-', '_').toCamelCase(); + + final String colorValueOrPrimitiveToken; + if (value.isColor) { + colorValueOrPrimitiveToken = 'Color(0x${convertColor(value)})'; + } else { + final primitiveToken = value + .replaceAll(RegExp(r'\{|\}'), '') + .replaceAll(RegExp(r'\.|-'), '_') + .toCamelCase(); + colorValueOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; + } + + buffer.writeln(' $semanticTokenName: $colorValueOrPrimitiveToken,'); + } else { + json.forEach((key, value) { + if (value is Map) { + processSemanticTokenData( + buffer, + value, + '${currentTokenName}_$key', + ); + } + }); + } +} + String convertColor(String hexColor) { String color = hexColor.toUpperCase().replaceAll('#', ''); if (color.length == 6) { From 6dac45172e2e1e53ceacbcb82b4dea174f3022f3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 15:34:06 +0800 Subject: [PATCH 33/74] chore: auth type --- .../home/mobile_home_page_header.dart | 1 + .../lib/user/application/user_service.dart | 9 +- .../application/user/user_workspace_bloc.dart | 53 +++++++++--- .../workspace/_sidebar_workspace_menu.dart | 9 +- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++----- .../src/folder_event.rs | 16 +++- .../event-integration-test/src/user_event.rs | 13 +-- .../user/af_cloud_test/workspace_test.rs | 42 ++++++++-- .../flowy-core/src/user_state_callback.rs | 15 ++-- .../flowy-user/src/entities/user_profile.rs | 45 +++++++--- .../flowy-user/src/entities/workspace.rs | 32 ++++++- .../rust-lib/flowy-user/src/event_handler.rs | 29 ++++++- frontend/rust-lib/flowy-user/src/event_map.rs | 13 +-- .../src/services/sqlite_sql/workspace_sql.rs | 10 ++- .../flowy-user/src/user_manager/manager.rs | 1 - .../user_manager/manager_user_workspace.rs | 84 ++++++++++++++----- 16 files changed, 308 insertions(+), 110 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 97cc243c9e..4a1df63740 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -194,6 +194,7 @@ class _MobileWorkspace extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, + workspace.authType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 18f89ebc14..5e163b4e62 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -121,8 +121,13 @@ class UserBackendService implements IUserBackendService { }); } - Future> openWorkspace(String workspaceId) { - final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId; + Future> openWorkspace( + String workspaceId, + AuthTypePB authType, + ) { + final payload = OpenUserWorkspacePB() + ..workspaceId = workspaceId + ..authType = authType; return UserEventOpenWorkspace(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index e6a8c4d921..7e341f4f1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -52,7 +52,10 @@ class UserWorkspaceBloc extends Bloc { ); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); - await _userService.openWorkspace(currentWorkspace.workspaceId); + await _userService.openWorkspace( + currentWorkspace.workspaceId, + currentWorkspace.authType, + ); } emit( @@ -86,7 +89,12 @@ class UserWorkspaceBloc extends Bloc { Log.info( 'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}', ); - add(OpenWorkspace(currentWorkspace.workspaceId)); + add( + OpenWorkspace( + currentWorkspace.workspaceId, + currentWorkspace.authType, + ), + ); } }, createWorkspace: (name) async { @@ -118,7 +126,12 @@ class UserWorkspaceBloc extends Bloc { result ..onSuccess((s) { Log.info('create workspace success: $s'); - add(OpenWorkspace(s.workspaceId)); + add( + OpenWorkspace( + s.workspaceId, + s.authType, + ), + ); }) ..onFailure((f) { Log.error('create workspace error: $f'); @@ -171,7 +184,12 @@ class UserWorkspaceBloc extends Bloc { Log.info('delete workspace success: $workspaceId'); // if the current workspace is deleted, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }) ..onFailure((f) { @@ -179,7 +197,12 @@ class UserWorkspaceBloc extends Bloc { // if the workspace is deleted but return an error, we need to // open the first workspace if (!containsDeletedWorkspace) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }); emit( @@ -193,7 +216,7 @@ class UserWorkspaceBloc extends Bloc { ), ); }, - openWorkspace: (workspaceId) async { + openWorkspace: (workspaceId, authType) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -203,7 +226,10 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.openWorkspace(workspaceId); + final result = await _userService.openWorkspace( + workspaceId, + authType, + ); final currentWorkspace = result.fold( (s) => state.workspaces.firstWhereOrNull( (e) => e.workspaceId == workspaceId, @@ -337,7 +363,12 @@ class UserWorkspaceBloc extends Bloc { Log.info('leave workspace success: $workspaceId'); // if leaving the current workspace, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add(OpenWorkspace(workspaces.first.workspaceId)); + add( + OpenWorkspace( + workspaces.first.workspaceId, + workspaces.first.authType, + ), + ); } }) ..onFailure((f) { @@ -445,8 +476,10 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent { CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; - const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = - OpenWorkspace; + const factory UserWorkspaceEvent.openWorkspace( + String workspaceId, + AuthTypePB authType, + ) = OpenWorkspace; const factory UserWorkspaceEvent.renameWorkspace( String workspaceId, String name, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index ff393a8305..daf332cc15 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -306,9 +306,12 @@ class _WorkspaceInfo extends StatelessWidget { // Persist and close other tabs when switching workspace, restore tabs for new workspace getIt().add(TabsEvent.switchWorkspace(workspace.workspaceId)); - context - .read() - .add(UserWorkspaceEvent.openWorkspace(workspace.workspaceId)); + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + workspace.authType, + ), + ); PopoverContainer.of(context).closeAll(); } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index 30ee626f09..b4a1a3d20d 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d + flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 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 2e1b1cc417..345c1e58e0 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -11,8 +11,8 @@ use flowy_folder_pub::entities::PublishPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, - RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, WorkspaceMemberInvitationPB, - WorkspaceMemberPB, + RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, UserWorkspaceIdPB, UserWorkspacePB, + WorkspaceMemberInvitationPB, WorkspaceMemberPB, }; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; @@ -112,6 +112,18 @@ impl EventIntegrationTest { .parse::() } + pub async fn get_user_workspace(&self, workspace_id: &str) -> UserWorkspacePB { + let payload = UserWorkspaceIdPB { + workspace_id: workspace_id.to_string(), + }; + EventBuilder::new(self.clone()) + .event(UserEvent::GetUserWorkspace) + .payload(payload) + .async_send() + .await + .parse::() + } + pub fn get_folder_search_handler(&self) -> &Arc { self .appflowy_core 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 2ec74bafa6..5c9be65660 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,10 +17,10 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, - OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, - SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, - UserWorkspaceIdPB, UserWorkspacePB, + AuthTypePB, AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, + ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, + RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, + UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -280,9 +280,10 @@ impl EventIntegrationTest { .await; } - pub async fn open_workspace(&self, workspace_id: &str) { - let payload = UserWorkspaceIdPB { + pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) { + let payload = OpenUserWorkspacePB { workspace_id: workspace_id.to_string(), + auth_type, }; EventBuilder::new(self.clone()) .event(UserEvent::OpenWorkspace) 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 366e43c157..ea04d922fc 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 @@ -89,7 +89,9 @@ async fn af_cloud_create_workspace_test() { } { // after opening new workspace - test.open_workspace(&created_workspace.workspace_id).await; + test + .open_workspace(&created_workspace.workspace_id, created_workspace.auth_type) + .await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); let views = test.folder_read_current_workspace_views().await; @@ -110,6 +112,7 @@ async fn af_cloud_open_workspace_test() { test.create_document("A").await; test.create_document("B").await; let first_workspace = test.get_current_workspace().await; + let first_workspace = test.get_user_workspace(&first_workspace.id).await; let views = test.get_all_workspace_views().await; assert_eq!(views.len(), 4); assert_eq!(views[0].name, default_document_name); @@ -120,8 +123,11 @@ async fn af_cloud_open_workspace_test() { let user_workspace = test .create_workspace("second workspace", AuthType::AppFlowyCloud) .await; - test.open_workspace(&user_workspace.workspace_id).await; + test + .open_workspace(&user_workspace.workspace_id, user_workspace.auth_type) + .await; let second_workspace = test.get_current_workspace().await; + let second_workspace = test.get_user_workspace(&second_workspace.id).await; test.create_document("C").await; test.create_document("D").await; @@ -135,13 +141,23 @@ async fn af_cloud_open_workspace_test() { // simulate open workspace and check if the views are correct for i in 0..10 { if i % 2 == 0 { - test.open_workspace(&first_workspace.id).await; + test + .open_workspace( + &first_workspace.workspace_id, + first_workspace.auth_type.clone(), + ) + .await; sleep(Duration::from_millis(300)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) .await; } else { - test.open_workspace(&second_workspace.id).await; + test + .open_workspace( + &second_workspace.workspace_id, + second_workspace.auth_type.clone(), + ) + .await; sleep(Duration::from_millis(200)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) @@ -149,14 +165,24 @@ async fn af_cloud_open_workspace_test() { } } - test.open_workspace(&first_workspace.id).await; + test + .open_workspace( + &first_workspace.workspace_id, + first_workspace.auth_type.clone(), + ) + .await; let views_1 = test.get_all_workspace_views().await; assert_eq!(views_1[0].name, default_document_name); assert_eq!(views_1[1].name, "Shared"); assert_eq!(views_1[2].name, "A"); assert_eq!(views_1[3].name, "B"); - test.open_workspace(&second_workspace.id).await; + test + .open_workspace( + &second_workspace.workspace_id, + second_workspace.auth_type.clone(), + ) + .await; let views_2 = test.get_all_workspace_views().await; assert_eq!(views_2[0].name, default_document_name); assert_eq!(views_2[1].name, "Shared"); @@ -212,7 +238,9 @@ async fn af_cloud_different_open_same_workspace_test() { for i in 0..30 { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; - client.open_workspace(iter_workspace_id).await; + client + .open_workspace(iter_workspace_id, all_workspaces[index].auth_type.clone()) + .await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; assert_eq!(views.len(), 2); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index ff1c6b6699..6002746603 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -49,11 +49,10 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn did_init( &self, user_id: i64, - auth_type: &AuthType, cloud_config: &Option, user_workspace: &UserWorkspace, _device_id: &str, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; self.server_provider.set_auth_type(*auth_type); @@ -81,7 +80,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator == &AuthType::Local) + .initialize(user_id, auth_type == &AuthType::Local) .await?; self.document_manager.initialize(user_id).await?; @@ -95,7 +94,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_id: i64, user_workspace: &UserWorkspace, device_id: &str, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { event!( tracing::Level::TRACE, @@ -103,6 +102,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); + self.server_provider.set_auth_type(*auth_type); self .folder_manager @@ -110,7 +110,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, authenticator.is_local()) + .initialize(user_id, auth_type.is_local()) .await?; self.document_manager.initialize(user_id).await?; @@ -207,15 +207,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { &self, user_id: i64, user_workspace: &UserWorkspace, - authenticator: &AuthType, + auth_type: &AuthType, ) -> FlowyResult<()> { + self.server_provider.set_auth_type(*auth_type); self .folder_manager .initialize_with_workspace_id(user_id) .await?; self .database_manager - .initialize(user_id, authenticator.is_local()) + .initialize(user_id, auth_type.is_local()) .await?; self.document_manager.initialize(user_id).await?; self.ai_manager.initialize(&user_workspace.id).await?; 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 54cb41db1e..f7a78a8f7d 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,15 +1,14 @@ +use super::AFRolePB; +use crate::entities::parser::{UserEmail, UserIcon, UserName}; +use crate::entities::{AuthTypePB, AuthenticatorPB}; +use crate::errors::ErrorCode; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; -use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::AuthenticatorPB; -use crate::errors::ErrorCode; - -use super::AFRolePB; - #[derive(Default, ProtoBuf)] pub struct UserTokenPB { #[pb(index = 1)] @@ -153,10 +152,14 @@ pub struct RepeatedUserWorkspacePB { pub items: Vec, } -impl From> for RepeatedUserWorkspacePB { - fn from(workspaces: Vec) -> Self { +impl From<(AuthType, Vec)> for RepeatedUserWorkspacePB { + fn from(value: (AuthType, Vec)) -> Self { + let (auth_type, workspaces) = value; Self { - items: workspaces.into_iter().map(UserWorkspacePB::from).collect(), + items: workspaces + .into_iter() + .map(|w| UserWorkspacePB::from((auth_type, w))) + .collect(), } } } @@ -181,17 +184,35 @@ pub struct UserWorkspacePB { #[pb(index = 6, one_of)] pub role: Option, + + #[pb(index = 7)] + pub auth_type: AuthTypePB, } -impl From for UserWorkspacePB { - fn from(value: UserWorkspace) -> Self { +impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { + fn from(value: (AuthType, UserWorkspace)) -> Self { + Self { + workspace_id: value.1.id, + name: value.1.name, + created_at_timestamp: value.1.created_at.timestamp(), + icon: value.1.icon, + member_count: value.1.member_count, + role: value.1.role.map(AFRolePB::from), + auth_type: AuthTypePB::from(value.0), + } + } +} + +impl From for UserWorkspacePB { + fn from(value: UserWorkspaceTable) -> Self { Self { workspace_id: value.id, name: value.name, - created_at_timestamp: value.created_at.timestamp(), + created_at_timestamp: value.created_at, icon: value.icon, member_count: value.member_count, role: value.role.map(AFRolePB::from), + auth_type: AuthTypePB::from(value.auth_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 26f848f3f5..d277fb4614 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -155,6 +155,17 @@ pub enum AFRolePB { Guest = 2, } +impl From for AFRolePB { + fn from(value: i32) -> Self { + match value { + 0 => AFRolePB::Owner, + 1 => AFRolePB::Member, + 2 => AFRolePB::Guest, + _ => AFRolePB::Guest, + } + } +} + impl From for Role { fn from(value: AFRolePB) -> Self { match value { @@ -182,6 +193,16 @@ pub struct UserWorkspaceIdPB { pub workspace_id: String, } +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct OpenUserWorkspacePB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub workspace_id: String, + + #[pb(index = 2)] + pub auth_type: AuthTypePB, +} + #[derive(ProtoBuf, Default, Clone, Validate)] pub struct CancelWorkspaceSubscriptionPB { #[pb(index = 1)] @@ -221,12 +242,21 @@ pub struct CreateWorkspacePB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Default, Clone)] +#[derive(ProtoBuf_Enum, Default, Debug, Clone)] pub enum AuthTypePB { LocalAuthType = 0, #[default] CloudAuthType = 1, } +impl From for AuthTypePB { + fn from(value: i32) -> Self { + match value { + 0 => AuthTypePB::LocalAuthType, + 1 => AuthTypePB::CloudAuthType, + _ => AuthTypePB::CloudAuthType, + } + } +} impl From for AuthTypePB { fn from(value: AuthType) -> Self { diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bb2a0b2c30..4cd49649ce 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -440,21 +440,42 @@ pub async fn get_all_workspace_handler( let user_workspaces = manager .get_all_user_workspaces(profile.uid, profile.auth_type) .await?; - data_result_ok(user_workspaces.into()) + + data_result_ok(RepeatedUserWorkspacePB::from(( + profile.auth_type, + user_workspaces, + ))) } #[tracing::instrument(level = "info", skip(data, manager), err)] pub async fn open_workspace_handler( - data: AFPluginData, + data: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager.open_workspace(&workspace_id).await?; + manager + .open_workspace(&workspace_id, AuthType::from(params.auth_type)) + .await?; Ok(()) } +#[tracing::instrument(level = "info", skip(data, manager), err)] +pub async fn get_user_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let params = data.try_into_inner()?; + let workspace_id = Uuid::from_str(¶ms.workspace_id)?; + let uid = manager.user_id()?; + let user_workspace = manager + .get_user_workspace_from_db(uid, &workspace_id) + .ok_or_else(FlowyError::record_not_found)?; + data_result_ok(UserWorkspacePB::from(user_workspace)) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn update_network_state_handler( data: AFPluginData, @@ -610,7 +631,7 @@ pub async fn create_workspace_handler( let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; let new_workspace = manager.create_workspace(&data.name, auth_type).await?; - data_result_ok(new_workspace.into()) + data_result_ok(UserWorkspacePB::from((auth_type, new_workspace))) } #[tracing::instrument(level = "debug", skip_all, err)] diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 03660ad0ff..1ae414afa7 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -39,6 +39,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) + .event(UserEvent::GetUserWorkspace, get_user_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::OpenAnonUser, open_anon_user_handler) .event(UserEvent::GetAnonUser, get_anon_user_handler) @@ -144,9 +145,12 @@ pub enum UserEvent { #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, - #[event(input = "UserWorkspaceIdPB")] + #[event(input = "OpenUserWorkspacePB")] OpenWorkspace = 21, + #[event(input = "UserWorkspaceIdPB", output = "UserWorkspacePB")] + GetUserWorkspace = 22, + #[event(input = "NetworkStatePB")] UpdateNetworkState = 24, @@ -281,11 +285,10 @@ pub trait UserStatusCallback: Send + Sync + 'static { async fn did_init( &self, _user_id: i64, - _user_authenticator: &AuthType, _cloud_config: &Option, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -295,7 +298,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } @@ -318,7 +321,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { &self, _user_id: i64, _user_workspace: &UserWorkspace, - _authenticator: &AuthType, + _auth_type: &AuthType, ) -> FlowyResult<()> { Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index f197ca9738..7fc3b00157 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -1,5 +1,5 @@ use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; +use diesel::RunQueryDsl; use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; @@ -55,12 +55,14 @@ impl UserWorkspaceTable { } } -pub fn select_user_workspace(workspace_id: &str, mut conn: DBConnection) -> Option { +pub fn select_user_workspace( + workspace_id: &str, + mut conn: DBConnection, +) -> Option { user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) .first::(&mut *conn) .ok() - .map(UserWorkspace::from) } pub fn select_all_user_workspace( @@ -89,7 +91,7 @@ pub fn upsert_user_workspace( uid: i64, auth_type: AuthType, user_workspace: UserWorkspace, - conn: &mut SqliteConnection, + conn: &mut DBConnection, ) -> Result<(), FlowyError> { let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; 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 d7d7c8b68d..14aaebe86c 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -273,7 +273,6 @@ impl UserManager { user_status_callback .did_init( user.uid, - &user.auth_type, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, 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 ac15324c00..b24764cc6b 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 @@ -32,7 +32,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; -use flowy_user_pub::cloud::UserCloudServiceProvider; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; @@ -162,13 +162,32 @@ impl UserManager { } #[instrument(skip(self), err)] - pub async fn open_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { - info!("open workspace: {}", workspace_id); - let user_workspace = self - .cloud_service - .get_user_service()? - .open_workspace(workspace_id) - .await?; + pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { + info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + let uid = self.user_id()?; + let conn = self.db_connection(self.user_id()?)?; + let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { + None => { + sync_workspace( + workspace_id, + self.cloud_service.get_user_service()?, + uid, + auth_type, + self.db_pool(uid)?, + ) + .await? + }, + Some(row) => { + let user_workspace = UserWorkspace::from(row); + let workspace_id = *workspace_id; + let user_service = self.cloud_service.get_user_service()?; + let pool = self.db_pool(uid)?; + tokio::spawn(async move { + let _ = sync_workspace(&workspace_id, user_service, uid, auth_type, pool).await; + }); + user_workspace + }, + }; self .authenticate_user @@ -176,6 +195,15 @@ impl UserManager { let uid = self.user_id()?; let user_profile = self.get_user_profile_from_disk(uid).await?; + if let Err(err) = self + .user_status_callback + .read() + .await + .open_workspace(uid, &user_workspace, &user_profile.auth_type) + .await + { + error!("Open workspace failed: {:?}", err); + } if let Err(err) = self .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.auth_type) @@ -187,16 +215,6 @@ impl UserManager { ); } - if let Err(err) = self - .user_status_callback - .read() - .await - .open_workspace(uid, &user_workspace, &user_profile.auth_type) - .await - { - error!("Open workspace failed: {:?}", err); - } - Ok(()) } @@ -240,10 +258,11 @@ impl UserManager { let conn = self.db_connection(uid)?; update_user_workspace(conn, changeset)?; - let user_workspace = self - .get_user_workspace(uid, workspace_id) + let row = self + .get_user_workspace_from_db(uid, workspace_id) .ok_or_else(FlowyError::record_not_found)?; - let payload: UserWorkspacePB = user_workspace.clone().into(); + + let payload = UserWorkspacePB::from(row); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) .send(); @@ -376,7 +395,11 @@ impl UserManager { Ok(()) } - pub fn get_user_workspace(&self, uid: i64, workspace_id: &Uuid) -> Option { + pub fn get_user_workspace_from_db( + &self, + uid: i64, + workspace_id: &Uuid, + ) -> Option { let conn = self.db_connection(uid).ok()?; select_user_workspace(workspace_id.to_string().as_str(), conn) } @@ -395,7 +418,8 @@ impl UserManager { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); - let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); + let repeated_workspace_pbs = + RepeatedUserWorkspacePB::from((auth_type, new_user_workspaces)); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) .payload(repeated_workspace_pbs) .send(); @@ -783,3 +807,17 @@ async fn sync_workspace_settings( } Ok(()) } + +async fn sync_workspace( + workspace_id: &Uuid, + user_service: Arc, + uid: i64, + auth_type: AuthType, + pool: Arc, +) -> FlowyResult { + let user_workspace = user_service.open_workspace(workspace_id).await?; + if let Ok(mut conn) = pool.get() { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), &mut conn)?; + } + Ok(user_workspace) +} From 102087537a4a057170d3d040ac2dc98016fafee5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 15:50:42 +0800 Subject: [PATCH 34/74] chore: fmt --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 27ccdc8f18..28a83ed449 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); From 81f63bebe60fcf5a581ff6c49db71230d71e941f Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 21:03:50 +0800 Subject: [PATCH 35/74] chore: sql --- frontend/rust-lib/Cargo.lock | 5 +- .../src/local_server/impls/user.rs | 35 ++-- .../flowy-server/src/local_server/server.rs | 4 +- frontend/rust-lib/flowy-user-pub/Cargo.toml | 4 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 8 +- .../rust-lib/flowy-user-pub/src/entities.rs | 2 +- frontend/rust-lib/flowy-user-pub/src/lib.rs | 1 + .../rust-lib/flowy-user-pub/src/sql/mod.rs | 1 + .../flowy-user-pub/src/sql/workspace_sql.rs | 188 ++++++++++++++++++ frontend/rust-lib/flowy-user/Cargo.toml | 1 - .../src/services/sqlite_sql/workspace_sql.rs | 64 +++++- .../flowy-user/src/user_manager/manager.rs | 8 +- .../user_manager/manager_user_workspace.rs | 93 +-------- 13 files changed, 294 insertions(+), 120 deletions(-) create mode 100644 frontend/rust-lib/flowy-user-pub/src/sql/mod.rs create mode 100644 frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a703fb1e05..5beac0b3a6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -3086,7 +3086,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "nanoid", "protobuf", "quickcheck", "quickcheck_macros", @@ -3110,16 +3109,16 @@ dependencies = [ name = "flowy-user-pub" version = "0.1.0" dependencies = [ - "anyhow", - "arc-swap", "base64 0.21.5", "chrono", "client-api", "collab", "collab-entity", "collab-folder", + "diesel", "flowy-error", "flowy-folder-pub", + "flowy-sqlite", "lib-infra", "serde", "serde_json", 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 49db8e03a2..8189ec140c 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 @@ -1,13 +1,17 @@ #![allow(unused_variables)] + use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; use lazy_static::lazy_static; +use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; +use crate::af_cloud::define::LoggedUser; +use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; @@ -16,14 +20,14 @@ use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use lib_infra::util::timestamp; -use crate::local_server::uid::UserIDGenerator; - lazy_static! { - //FIXME: seriously, userID generation should work using lock-free algorithm static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserServiceImpl; +pub(crate) struct LocalServerUserServiceImpl { + pub user: Arc, +} + #[async_trait] impl UserCloudService for LocalServerUserServiceImpl { async fn sign_up(&self, params: BoxAny) -> Result { @@ -128,7 +132,13 @@ impl UserCloudService for LocalServerUserServiceImpl { ) } - async fn get_all_workspace(&self, _uid: i64) -> Result, FlowyError> { + async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { + let conn = self.user.get_sqlite_db(uid)?; + + select_all_user_workspaces(&conn).await.map_err(|e| { + FlowyError::internal().with_context(format!("Failed to get all workspaces: {}", e)) + })?; + Ok(vec![]) } @@ -145,17 +155,11 @@ impl UserCloudService for LocalServerUserServiceImpl { new_workspace_name: Option, new_workspace_icon: Option, ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) + Ok(()) } async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support multiple workspaces"), - ) + Ok(()) } async fn get_user_awareness_doc_state( @@ -188,10 +192,7 @@ impl UserCloudService for LocalServerUserServiceImpl { workspace_id: &Uuid, objects: Vec, ) -> Result<(), FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support batch create collab object"), - ) + Ok(()) } } 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 c81d526acb..8ce0d86221 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -41,7 +41,9 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { - Arc::new(LocalServerUserServiceImpl) + Arc::new(LocalServerUserServiceImpl { + user: self.user.clone(), + }) } fn folder_service(&self) -> Arc { diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index 80d087e88e..cb6b9b0738 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -15,7 +15,6 @@ collab-entity = { workspace = true } serde_json.workspace = true serde_repr.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } -anyhow.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.14" flowy-folder-pub.workspace = true @@ -23,4 +22,5 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } -arc-swap = "1.7.1" +flowy-sqlite.workspace = true +diesel.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index dee44fa5f3..00eaef8917 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -293,7 +293,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_subscriptions( &self, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } /// Get the workspace subscriptions for a workspace @@ -301,7 +301,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn cancel_workspace_subscription( @@ -310,14 +310,14 @@ pub trait UserCloudService: Send + Sync + 'static { plan: SubscriptionPlan, reason: Option, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } async fn get_workspace_plan( &self, workspace_id: Uuid, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn get_workspace_usage( diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 7c8d775fa6..6be5a9f64d 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -164,7 +164,7 @@ impl UserWorkspace { workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), member_count: 1, - role: None, + role: Some(Role::Owner), } } } diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index 2e51ecc626..d820e8cd34 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,6 +1,7 @@ pub mod cloud; pub mod entities; pub mod session; +mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs new file mode 100644 index 0000000000..d5a7aaf317 --- /dev/null +++ b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs @@ -0,0 +1 @@ +pub mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs new file mode 100644 index 0000000000..ecd242c240 --- /dev/null +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -0,0 +1,188 @@ +use crate::entities::{AuthType, UserWorkspace}; +use chrono::{TimeZone, Utc}; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use tracing::{info, trace, warn}; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceTable { + pub id: String, + pub name: String, + pub uid: i64, + pub created_at: i64, + pub database_storage_id: String, + pub icon: String, + pub member_count: i64, + pub role: Option, + pub auth_type: i32, +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceChangeset { + pub id: String, + pub name: Option, + pub icon: Option, +} + +impl UserWorkspaceTable { + pub fn from_workspace( + uid: i64, + workspace: &UserWorkspace, + auth_type: AuthType, + ) -> Result { + if workspace.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if workspace.workspace_database_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: workspace.id.clone(), + name: workspace.name.clone(), + uid, + created_at: workspace.created_at.timestamp(), + database_storage_id: workspace.workspace_database_id.clone(), + icon: workspace.icon.clone(), + member_count: workspace.member_count, + role: workspace.role.clone().map(|v| v as i32), + auth_type: auth_type as i32, + }) + } +} + +pub fn select_user_workspace( + workspace_id: &str, + mut conn: DBConnection, +) -> Option { + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(workspace_id)) + .first::(&mut *conn) + .ok() +} + +pub fn select_all_user_workspace( + user_id: i64, + mut conn: DBConnection, +) -> Result, FlowyError> { + let rows = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(user_id)) + .load::(&mut *conn)?; + Ok(rows.into_iter().map(UserWorkspace::from).collect()) +} + +pub fn update_user_workspace( + mut conn: DBConnection, + changeset: UserWorkspaceChangeset, +) -> Result<(), FlowyError> { + diesel::update(user_workspace_table::dsl::user_workspace_table) + .filter(user_workspace_table::id.eq(changeset.id.clone())) + .set(changeset) + .execute(&mut conn)?; + + Ok(()) +} + +pub fn upsert_user_workspace( + uid: i64, + auth_type: AuthType, + user_workspace: UserWorkspace, + conn: &mut SqliteConnection, +) -> Result<(), FlowyError> { + let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; + diesel::insert_into(user_workspace_table::table) + .values(new_record.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(new_record.name), + user_workspace_table::uid.eq(new_record.uid), + user_workspace_table::created_at.eq(new_record.created_at), + user_workspace_table::database_storage_id.eq(new_record.database_storage_id), + user_workspace_table::icon.eq(new_record.icon), + user_workspace_table::member_count.eq(new_record.member_count), + user_workspace_table::role.eq(new_record.role), + user_workspace_table::auth_type.eq(new_record.auth_type), + )) + .execute(conn)?; + + Ok(()) +} + +pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::(rows_affected) + })?; + + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); + } + Ok(()) +} + +impl From for UserWorkspace { + fn from(value: UserWorkspaceTable) -> Self { + Self { + id: value.id, + name: value.name, + created_at: Utc + .timestamp_opt(value.created_at, 0) + .single() + .unwrap_or_default(), + workspace_database_id: value.database_storage_id, + icon: value.icon, + member_count: value.member_count, + role: value.role.map(|v| v.into()), + } + } +} + +/// Delete all user workspaces for the given user and auth type. +pub fn delete_user_all_workspace( + uid: i64, + auth_type: AuthType, + conn: &mut SqliteConnection, +) -> FlowyResult<()> { + let n = diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + ) + .execute(conn)?; + info!( + "Delete {} workspaces for user {} and auth type {:?}", + n, uid, auth_type + ); + Ok(()) +} + +/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. +pub fn delete_all_then_insert_user_workspaces( + uid: i64, + mut conn: DBConnection, + auth_type: AuthType, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + delete_user_all_workspace(uid, auth_type, conn)?; + + info!( + "Insert {} workspaces for user {} and auth type {:?}", + user_workspaces.len(), + uid, + auth_type + ); + for user_workspace in user_workspaces { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; + } + Ok::<(), FlowyError>(()) + }) +} diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 4d021161ac..65be4cc3f9 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -48,7 +48,6 @@ validator = { workspace = true, features = ["derive"] } rayon = "1.10.0" [dev-dependencies] -nanoid = "0.4.0" fake = "2.0.0" rand = "0.8.4" quickcheck = "1.0.3" diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index 7fc3b00157..46d5d74e35 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -1,10 +1,11 @@ use chrono::{TimeZone, Utc}; -use diesel::RunQueryDsl; -use flowy_error::FlowyError; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; use flowy_sqlite::{query_dsl::*, ExpressionMethods}; use flowy_user_pub::entities::{AuthType, UserWorkspace}; +use tracing::{info, trace, warn}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] @@ -91,10 +92,9 @@ pub fn upsert_user_workspace( uid: i64, auth_type: AuthType, user_workspace: UserWorkspace, - conn: &mut DBConnection, + conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) .values(new_record.clone()) .on_conflict(user_workspace_table::id) @@ -114,6 +114,20 @@ pub fn upsert_user_workspace( Ok(()) } +pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::(rows_affected) + })?; + + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); + } + Ok(()) +} + impl From for UserWorkspace { fn from(value: UserWorkspaceTable) -> Self { Self { @@ -130,3 +144,45 @@ impl From for UserWorkspace { } } } + +/// Delete all user workspaces for the given user and auth type. +pub fn delete_user_all_workspace( + uid: i64, + auth_type: AuthType, + conn: &mut SqliteConnection, +) -> FlowyResult<()> { + let n = diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + ) + .execute(conn)?; + info!( + "Delete {} workspaces for user {} and auth type {:?}", + n, uid, auth_type + ); + Ok(()) +} + +/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. +pub fn delete_all_then_insert_user_workspaces( + uid: i64, + mut conn: DBConnection, + auth_type: AuthType, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + delete_user_all_workspace(uid, auth_type, conn)?; + + info!( + "Insert {} workspaces for user {} and auth type {:?}", + user_workspaces.len(), + uid, + auth_type + ); + for user_workspace in user_workspaces { + upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; + } + Ok::<(), FlowyError>(()) + }) +} 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 14aaebe86c..8d75a2c150 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -41,8 +41,9 @@ use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::services::sqlite_sql::workspace_sql::upsert_user_workspace; -use crate::user_manager::manager_user_workspace::save_all_user_workspaces; +use crate::services::sqlite_sql::workspace_sql::{ + delete_all_then_insert_user_workspaces, upsert_user_workspace, +}; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; @@ -758,12 +759,13 @@ impl UserManager { ) -> Result<(), FlowyError> { let user_profile = UserProfile::from((response, &auth_type)); let uid = user_profile.uid; + if auth_type.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); self.set_anon_user(session); } - save_all_user_workspaces( + delete_all_then_insert_user_workspaces( uid, self.db_connection(uid)?, auth_type, 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 b24764cc6b..1eaab813e4 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 @@ -23,21 +23,21 @@ use crate::services::sqlite_sql::workspace_setting_sql::{ WorkspaceSettingsChangeset, WorkspaceSettingsTable, }; use crate::services::sqlite_sql::workspace_sql::{ - select_all_user_workspace, select_user_workspace, update_user_workspace, upsert_user_workspace, - UserWorkspaceChangeset, UserWorkspaceTable, + delete_all_then_insert_user_workspaces, delete_user_workspace, select_all_user_workspace, + select_user_workspace, update_user_workspace, upsert_user_workspace, UserWorkspaceChangeset, + UserWorkspaceTable, }; use crate::user_manager::UserManager; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::{query_dsl::*, ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_sqlite::{ConnectionPool, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use flowy_user_pub::session::Session; -use tracing::{error, info, instrument, trace, warn}; +use tracing::{error, info, instrument, trace}; use uuid::Uuid; impl UserManager { @@ -282,7 +282,7 @@ impl UserManager { // delete workspace from local sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id.to_string().as_str())?; + delete_user_workspace(conn, workspace_id.to_string().as_str())?; self .user_workspace_service @@ -300,7 +300,7 @@ impl UserManager { .await?; let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspaces(conn, workspace_id.to_string().as_str())?; + delete_user_workspace(conn, workspace_id.to_string().as_str())?; self .user_workspace_service @@ -417,7 +417,8 @@ impl UserManager { tokio::spawn(async move { if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { if let Ok(conn) = pool.get() { - let _ = save_all_user_workspaces(uid, conn, auth_type, &new_user_workspaces); + let _ = + delete_all_then_insert_user_workspaces(uid, conn, auth_type, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from((auth_type, new_user_workspaces)); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) @@ -696,82 +697,6 @@ impl UserManager { } } -/// This method is used to save the user workspaces (plural) to the SQLite database -/// -/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. -/// -/// Consider using [upsert_user_workspace] if you only need to save a single workspace. -/// -pub fn save_all_user_workspaces( - uid: i64, - mut conn: DBConnection, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], -) -> FlowyResult<()> { - let user_workspaces = user_workspaces - .iter() - .map(|user_workspace| UserWorkspaceTable::from_workspace(uid, user_workspace, auth_type)) - .collect::, _>>()?; - - conn.immediate_transaction(|conn| { - let existing_ids = user_workspace_table::dsl::user_workspace_table - .select(user_workspace_table::id) - .load::(conn)?; - let new_ids: Vec = user_workspaces.iter().map(|w| w.id.clone()).collect(); - let ids_to_delete: Vec = existing_ids - .into_iter() - .filter(|id| !new_ids.contains(id)) - .collect(); - - // insert or update the user workspaces - for user_workspace in &user_workspaces { - let affected_rows = diesel::update( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(&user_workspace.id)), - ) - .set(( - user_workspace_table::name.eq(&user_workspace.name), - user_workspace_table::created_at.eq(&user_workspace.created_at), - user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), - user_workspace_table::icon.eq(&user_workspace.icon), - user_workspace_table::member_count.eq(&user_workspace.member_count), - user_workspace_table::role.eq(&user_workspace.role), - )) - .execute(conn)?; - - if affected_rows == 0 { - diesel::insert_into(user_workspace_table::table) - .values(user_workspace) - .execute(conn)?; - } - } - - // delete the user workspaces that are not in the new list - if !ids_to_delete.is_empty() { - diesel::delete( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq_any(ids_to_delete)), - ) - .execute(conn)?; - } - - Ok::<(), FlowyError>(()) - }) -} - -pub fn delete_user_workspaces(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::(rows_affected) - })?; - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { let current_time: NaiveDateTime = Utc::now().naive_utc(); match current_time.checked_sub_signed(Duration::minutes(minutes)) { From 72fc0cce075fdd9183d1bc0376d77849ef8b1239 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 22:18:15 +0800 Subject: [PATCH 36/74] chore: move sql --- frontend/rust-lib/Cargo.lock | 1 - frontend/rust-lib/Cargo.toml | 1 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 18 ++ .../flowy-core/src/user_state_callback.rs | 34 +++- .../rust-lib/flowy-database2/src/manager.rs | 22 +- .../rust-lib/flowy-document/src/manager.rs | 15 +- frontend/rust-lib/flowy-folder/src/manager.rs | 8 +- .../src/local_server/impls/user.rs | 9 +- frontend/rust-lib/flowy-sqlite/Cargo.toml | 2 +- .../rust-lib/flowy-storage/src/manager.rs | 8 + frontend/rust-lib/flowy-user-pub/Cargo.toml | 3 +- frontend/rust-lib/flowy-user-pub/src/lib.rs | 2 +- .../src/sql}/member_sql.rs | 4 +- .../rust-lib/flowy-user-pub/src/sql/mod.rs | 10 +- .../src/sql}/user_sql.rs | 25 ++- .../src/sql}/workspace_setting_sql.rs | 2 +- .../flowy-user-pub/src/sql/workspace_sql.rs | 5 +- .../flowy-user/src/entities/user_profile.rs | 2 +- .../flowy-user/src/entities/workspace.rs | 2 +- .../rust-lib/flowy-user/src/event_handler.rs | 17 +- .../data_import/appflowy_data_import.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 19 +- .../rust-lib/flowy-user/src/services/mod.rs | 1 - .../flowy-user/src/services/sqlite_sql/mod.rs | 4 - .../src/services/sqlite_sql/workspace_sql.rs | 188 ------------------ .../flowy-user/src/user_manager/manager.rs | 19 +- .../user_manager/manager_user_workspace.rs | 16 +- 27 files changed, 148 insertions(+), 291 deletions(-) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/member_sql.rs (95%) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/user_sql.rs (81%) rename frontend/rust-lib/{flowy-user/src/services/sqlite_sql => flowy-user-pub/src/sql}/workspace_setting_sql.rs (97%) delete mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs delete mode 100644 frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 5beac0b3a6..e285ae83c8 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -3115,7 +3115,6 @@ dependencies = [ "collab", "collab-entity", "collab-folder", - "diesel", "flowy-error", "flowy-folder-pub", "flowy-sqlite", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ee99fcacf1..0112c862a8 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -77,6 +77,7 @@ diesel = { version = "2.1.0", features = [ "r2d2", "serde_json", ] } +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" futures = "0.3.31" diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 7602ff8040..399a8d2d5d 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -128,6 +128,24 @@ impl AIManager { Ok(()) } + #[instrument(skip_all, err)] + pub async fn initialize_after_open_workspace( + &self, + _workspace_id: &str, + ) -> Result<(), FlowyError> { + let local_ai = self.local_ai.clone(); + tokio::spawn(async move { + if let Err(err) = local_ai.destroy_plugin().await { + error!("Failed to destroy plugin: {}", err); + } + + if let Err(err) = local_ai.reload().await { + error!("[AI Manager] failed to reload local AI: {:?}", err); + } + }); + Ok(()) + } + pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { self.chats.entry(*chat_id).or_insert_with(|| { Arc::new(Chat::new( diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 6002746603..e85b773ec4 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -106,13 +106,16 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .folder_manager - .initialize_with_workspace_id(user_id) + .initialize_after_sign_in(user_id) .await?; self .database_manager - .initialize(user_id, auth_type.is_local()) + .initialize_after_sign_in(user_id, auth_type.is_local()) + .await?; + self + .document_manager + .initialize_after_sign_in(user_id) .await?; - self.document_manager.initialize(user_id).await?; let workspace_id = user_workspace.id.clone(); self.init_ai_component(workspace_id); @@ -171,7 +174,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .folder_manager - .initialize_with_new_user( + .initialize_after_sign_up( user_profile.uid, &user_profile.token, is_new_user, @@ -183,13 +186,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { self .database_manager - .initialize_with_new_user(user_profile.uid, auth_type.is_local()) + .initialize_after_sign_up(user_profile.uid, auth_type.is_local()) .await .context("DatabaseManager error")?; self .document_manager - .initialize_with_new_user(user_profile.uid) + .initialize_after_sign_up(user_profile.uid) .await .context("DocumentManager error")?; @@ -212,15 +215,24 @@ impl UserStatusCallback for UserStatusCallbackImpl { self.server_provider.set_auth_type(*auth_type); self .folder_manager - .initialize_with_workspace_id(user_id) + .initialize_after_open_workspace(user_id) .await?; self .database_manager - .initialize(user_id, auth_type.is_local()) + .initialize_after_open_workspace(user_id, auth_type.is_local()) .await?; - self.document_manager.initialize(user_id).await?; - self.ai_manager.initialize(&user_workspace.id).await?; - self.storage_manager.initialize(&user_workspace.id).await; + self + .document_manager + .initialize_after_open_workspace(user_id) + .await?; + self + .ai_manager + .initialize_after_open_workspace(&user_workspace.id) + .await?; + self + .storage_manager + .initialize_after_open_workspace(&user_workspace.id) + .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 49a946d108..6ae1e5cf15 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -134,12 +134,30 @@ impl DatabaseManager { } #[instrument( - name = "database_initialize_with_new_user", + name = "database_initialize_after_sign_up", level = "debug", skip_all, err )] - pub async fn initialize_with_new_user( + pub async fn initialize_after_sign_up( + &self, + user_id: i64, + is_local_user: bool, + ) -> FlowyResult<()> { + self.initialize(user_id, is_local_user).await?; + Ok(()) + } + + pub async fn initialize_after_open_workspace( + &self, + user_id: i64, + is_local_user: bool, + ) -> FlowyResult<()> { + self.initialize(user_id, is_local_user).await?; + Ok(()) + } + + pub async fn initialize_after_sign_in( &self, user_id: i64, is_local_user: bool, diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 704dcd0865..9c6a383bae 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -106,12 +106,23 @@ impl DocumentManager { } #[instrument( - name = "document_initialize_with_new_user", + name = "document_initialize_after_sign_up", level = "debug", skip_all, err )] - pub async fn initialize_with_new_user(&self, uid: i64) -> FlowyResult<()> { + pub async fn initialize_after_sign_up(&self, uid: i64) -> FlowyResult<()> { + self.initialize(uid).await?; + Ok(()) + } + + pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { + self.initialize(uid).await?; + Ok(()) + } + + #[instrument(level = "debug", skip_all, err)] + pub async fn initialize_after_sign_in(&self, uid: i64) -> FlowyResult<()> { self.initialize(uid).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index e704be043d..8662c1e061 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -263,7 +263,7 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. #[tracing::instrument(skip(self, user_id), err)] - pub async fn initialize_with_workspace_id(&self, user_id: i64) -> FlowyResult<()> { + pub async fn initialize_after_sign_in(&self, user_id: i64) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; let object_id = &workspace_id; @@ -312,10 +312,14 @@ impl FolderManager { Ok(()) } + pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { + self.initialize_after_sign_in(uid).await + } + /// Initialize the folder for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user. #[instrument(level = "info", skip_all, err)] - pub async fn initialize_with_new_user( + pub async fn initialize_after_sign_up( &self, user_id: i64, _token: &str, 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 8189ec140c..d378765ebb 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 @@ -15,6 +15,7 @@ use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::select_all_user_workspace; use flowy_user_pub::DEFAULT_USER_NAME; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -134,12 +135,8 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { let conn = self.user.get_sqlite_db(uid)?; - - select_all_user_workspaces(&conn).await.map_err(|e| { - FlowyError::internal().with_context(format!("Failed to get all workspaces: {}", e)) - })?; - - Ok(vec![]) + let workspaces = select_all_user_workspace(uid, conn)?; + Ok(workspaces) } async fn create_workspace(&self, _workspace_name: &str) -> Result { diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 0e85aebee5..345b05f903 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] diesel.workspace = true -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel_derives = { workspace = true, features = ["sqlite", "r2d2"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] } tracing.workspace = true serde.workspace = true diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index dc1bf053ea..619dc47f90 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -181,6 +181,14 @@ impl StorageManager { } } + pub async fn initialize_after_open_workspace(&self, workspace_id: &str) { + self.enable_storage_write_access(); + + if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { + error!("prepare {} upload task failed: {}", workspace_id, err); + } + } + pub fn update_network_reachable(&self, reachable: bool) { if reachable { self.uploader.resume(); diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index cb6b9b0738..f8a673e918 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -22,5 +22,4 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } -flowy-sqlite.workspace = true -diesel.workspace = true \ No newline at end of file +flowy-sqlite.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index d820e8cd34..773ae96a9a 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,7 +1,7 @@ pub mod cloud; pub mod entities; pub mod session; -mod sql; +pub mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs similarity index 95% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs index 70351ab105..bc73a8f34c 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs @@ -1,10 +1,8 @@ use diesel::{insert_into, RunQueryDsl}; use flowy_error::FlowyResult; - use flowy_sqlite::schema::workspace_members_table; - use flowy_sqlite::schema::workspace_members_table::dsl; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; #[derive(Queryable, Insertable, AsChangeset, Debug)] #[diesel(table_name = workspace_members_table)] diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs index d5a7aaf317..2a5f7bf891 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs @@ -1 +1,9 @@ -pub mod workspace_sql; +mod member_sql; +mod user_sql; +mod workspace_setting_sql; +mod workspace_sql; + +pub use member_sql::*; +pub use user_sql::*; +pub use workspace_setting_sql::*; +pub use workspace_sql::*; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs similarity index 81% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 3fbd4c5854..5a910888a8 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -1,12 +1,9 @@ -use diesel::RunQueryDsl; -use flowy_error::FlowyError; - -use flowy_user_pub::cloud::UserUpdate; -use flowy_user_pub::entities::*; - +use crate::cloud::UserUpdate; +use crate::entities::{AuthType, UpdateUserProfileParams, UserProfile}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; +use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; /// The order of the fields in the struct must be the same as the order of the fields in the table. /// Check out the [schema.rs] for table schema. #[derive(Clone, Default, Queryable, Identifiable, Insertable)] @@ -109,3 +106,17 @@ pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result FlowyResult<()> { + conn.immediate_transaction(|conn| { + // delete old user if exists + diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) + .execute(conn)?; + + let _ = diesel::insert_into(user_table::table) + .values(user) + .execute(conn)?; + Ok::<(), FlowyError>(()) + })?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs similarity index 97% rename from frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs rename to frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs index dcd87a36ee..667d1f0ca0 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_setting_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs @@ -3,7 +3,7 @@ use flowy_error::FlowyError; use flowy_sqlite::schema::workspace_setting_table; use flowy_sqlite::schema::workspace_setting_table::dsl; use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use flowy_sqlite::{prelude::*, ExpressionMethods}; use uuid::Uuid; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index ecd242c240..bcac009527 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -1,11 +1,10 @@ use crate::entities::{AuthType, UserWorkspace}; use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use tracing::{info, trace, warn}; +use flowy_sqlite::{prelude::*, ExpressionMethods, RunQueryDsl, SqliteConnection}; +use tracing::{info, warn}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] 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 f7a78a8f7d..17b951bae0 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -2,9 +2,9 @@ use super::AFRolePB; use crate::entities::parser::{UserEmail, UserIcon, UserName}; use crate::entities::{AuthTypePB, AuthenticatorPB}; use crate::errors::ErrorCode; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::UserWorkspaceTable; use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index d277fb4614..e178b724db 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -5,10 +5,10 @@ use client_api::entity::billing_dto::{ use serde::{Deserialize, Serialize}; use validator::Validate; -use crate::services::sqlite_sql::workspace_setting_sql::WorkspaceSettingsTable; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; +use flowy_user_pub::sql::WorkspaceSettingsTable; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 4cd49649ce..12dd1f3e93 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,6 +1,14 @@ +use crate::entities::*; +use crate::notification::{send_notification, UserNotification}; +use crate::services::cloud_config::{ + get_cloud_config, get_or_create_cloud_config, save_cloud_config, +}; +use crate::services::data_import::prepare_import; +use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; +use flowy_user_pub::sql::UserWorkspaceChangeset; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use serde_json::Value; @@ -10,15 +18,6 @@ use std::{convert::TryInto, sync::Arc}; use tracing::{event, trace}; use uuid::Uuid; -use crate::entities::*; -use crate::notification::{send_notification, UserNotification}; -use crate::services::cloud_config::{ - get_cloud_config, get_or_create_cloud_config, save_cloud_config, -}; -use crate::services::data_import::prepare_import; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceChangeset; -use crate::user_manager::UserManager; - fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { let manager = manager .upgrade() 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 106e911c1e..2db5f418de 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 @@ -3,7 +3,6 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::data_import::importer::load_collab_by_object_ids; use crate::services::db::UserDBPath; 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; @@ -37,6 +36,7 @@ use std::collections::{HashMap, HashSet}; use collab_document::blocks::TextDelta; use collab_document::document::Document; +use flowy_user_pub::sql::select_user_profile; use semver::Version; use serde_json::json; use std::ops::{Deref, DerefMut}; diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index f05c0bda95..138ad95819 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -8,17 +8,12 @@ use dashmap::mapref::entry::Entry; use dashmap::DashMap; use flowy_error::FlowyError; use flowy_sqlite::ConnectionPool; -use flowy_sqlite::{ - query_dsl::*, - schema::{user_table, user_table::dsl}, - DBConnection, Database, ExpressionMethods, -}; +use flowy_sqlite::{DBConnection, Database}; use flowy_user_pub::entities::UserProfile; +use flowy_user_pub::sql::select_user_profile; use lib_infra::file_util::{unzip_and_replace, zip_folder}; use tracing::{error, event, info, instrument}; -use crate::services::sqlite_sql::user_sql::UserTable; - pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; fn collab_db_path(&self, uid: i64) -> PathBuf; @@ -131,13 +126,9 @@ impl UserDB { pool: &Arc, uid: i64, ) -> Result { - let uid = uid.to_string(); - let mut conn = pool.get()?; - let user = dsl::user_table - .filter(user_table::id.eq(&uid)) - .first::(&mut *conn)?; - - Ok(user.into()) + let conn = pool.get()?; + let profile = select_user_profile(uid, conn)?; + Ok(profile) } /// Open a collab db for the user. If the db is already opened, return the opened db. diff --git a/frontend/rust-lib/flowy-user/src/services/mod.rs b/frontend/rust-lib/flowy-user/src/services/mod.rs index 66316fa01a..ab4b3bea37 100644 --- a/frontend/rust-lib/flowy-user/src/services/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/mod.rs @@ -5,4 +5,3 @@ pub mod collab_interact; pub mod data_import; pub mod db; pub mod entities; -pub mod sqlite_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs deleted file mode 100644 index 635c79ba39..0000000000 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod member_sql; -pub(crate) mod user_sql; -pub(crate) mod workspace_setting_sql; -pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs deleted file mode 100644 index 46d5d74e35..0000000000 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ /dev/null @@ -1,188 +0,0 @@ -use chrono::{TimeZone, Utc}; -use diesel::{RunQueryDsl, SqliteConnection}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use flowy_user_pub::entities::{AuthType, UserWorkspace}; -use tracing::{info, trace, warn}; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceTable { - pub id: String, - pub name: String, - pub uid: i64, - pub created_at: i64, - pub database_storage_id: String, - pub icon: String, - pub member_count: i64, - pub role: Option, - pub auth_type: i32, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceChangeset { - pub id: String, - pub name: Option, - pub icon: Option, -} - -impl UserWorkspaceTable { - pub fn from_workspace( - uid: i64, - workspace: &UserWorkspace, - auth_type: AuthType, - ) -> Result { - if workspace.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if workspace.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: workspace.id.clone(), - name: workspace.name.clone(), - uid, - created_at: workspace.created_at.timestamp(), - database_storage_id: workspace.workspace_database_id.clone(), - icon: workspace.icon.clone(), - member_count: workspace.member_count, - role: workspace.role.clone().map(|v| v as i32), - auth_type: auth_type as i32, - }) - } -} - -pub fn select_user_workspace( - workspace_id: &str, - mut conn: DBConnection, -) -> Option { - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn) - .ok() -} - -pub fn select_all_user_workspace( - user_id: i64, - mut conn: DBConnection, -) -> Result, FlowyError> { - let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(user_id)) - .load::(&mut *conn)?; - Ok(rows.into_iter().map(UserWorkspace::from).collect()) -} - -pub fn update_user_workspace( - mut conn: DBConnection, - changeset: UserWorkspaceChangeset, -) -> Result<(), FlowyError> { - diesel::update(user_workspace_table::dsl::user_workspace_table) - .filter(user_workspace_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(&mut conn)?; - - Ok(()) -} - -pub fn upsert_user_workspace( - uid: i64, - auth_type: AuthType, - user_workspace: UserWorkspace, - conn: &mut SqliteConnection, -) -> Result<(), FlowyError> { - let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - user_workspace_table::auth_type.eq(new_record.auth_type), - )) - .execute(conn)?; - - Ok(()) -} - -pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::(rows_affected) - })?; - - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - -impl From for UserWorkspace { - fn from(value: UserWorkspaceTable) -> Self { - Self { - id: value.id, - name: value.name, - created_at: Utc - .timestamp_opt(value.created_at, 0) - .single() - .unwrap_or_default(), - workspace_database_id: value.database_storage_id, - icon: value.icon, - member_count: value.member_count, - role: value.role.map(|v| v.into()), - } - } -} - -/// Delete all user workspaces for the given user and auth type. -pub fn delete_user_all_workspace( - uid: i64, - auth_type: AuthType, - conn: &mut SqliteConnection, -) -> FlowyResult<()> { - let n = diesel::delete( - user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::auth_type.eq(auth_type as i32)), - ) - .execute(conn)?; - info!( - "Delete {} workspaces for user {} and auth type {:?}", - n, uid, auth_type - ); - Ok(()) -} - -/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. -pub fn delete_all_then_insert_user_workspaces( - uid: i64, - mut conn: DBConnection, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - delete_user_all_workspace(uid, auth_type, conn)?; - - info!( - "Insert {} workspaces for user {} and auth type {:?}", - user_workspaces.len(), - uid, - auth_type - ); - for user_workspace in user_workspaces { - upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; - } - Ok::<(), FlowyError>(()) - }) -} 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 8d75a2c150..86a3dfabce 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -40,13 +40,10 @@ use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; -use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::services::sqlite_sql::workspace_sql::{ - delete_all_then_insert_user_workspaces, upsert_user_workspace, -}; use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; +use flowy_user_pub::sql::*; pub struct UserManager { pub(crate) cloud_service: Arc, @@ -662,18 +659,8 @@ impl UserManager { } async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { - let mut conn = self.db_connection(uid)?; - conn.immediate_transaction(|conn| { - // delete old user if exists - diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) - .execute(conn)?; - - let _ = diesel::insert_into(user_table::table) - .values(user) - .execute(conn)?; - Ok::<(), FlowyError>(()) - })?; - + let conn = self.db_connection(uid)?; + upsert_user(user, conn)?; Ok(()) } 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 1eaab813e4..cc884dc1d9 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 @@ -15,28 +15,18 @@ use crate::services::billing_check::PeriodicallyCheckBillingState; use crate::services::data_import::{ generate_import_data, upload_collab_objects_data, ImportedFolder, ImportedSource, }; -use crate::services::sqlite_sql::member_sql::{ - select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, -}; -use crate::services::sqlite_sql::workspace_setting_sql::{ - select_workspace_setting, update_workspace_setting, upsert_workspace_setting, - WorkspaceSettingsChangeset, WorkspaceSettingsTable, -}; -use crate::services::sqlite_sql::workspace_sql::{ - delete_all_then_insert_user_workspaces, delete_user_workspace, select_all_user_workspace, - select_user_workspace, update_user_workspace, upsert_user_workspace, UserWorkspaceChangeset, - UserWorkspaceTable, -}; + use crate::user_manager::UserManager; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::{ConnectionPool, DBConnection, ExpressionMethods}; +use flowy_sqlite::ConnectionPool; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{ AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use flowy_user_pub::session::Session; +use flowy_user_pub::sql::*; use tracing::{error, info, instrument, trace}; use uuid::Uuid; From d478ecfd416fb8d0c5414eb92188beab71c4c3b2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 19 Apr 2025 23:33:34 +0800 Subject: [PATCH 37/74] chore: remove unused code --- .../workspace_menu_bottom_sheet.dart | 1 + .../lib/startup/tasks/generate_router.dart | 12 - .../lib/user/application/user_service.dart | 20 +- .../helpers/handle_user_profile_result.dart | 21 -- .../user/presentation/helpers/helpers.dart | 1 - .../lib/user/presentation/router.dart | 4 - .../user/presentation/screens/screens.dart | 1 - .../presentation/screens/sign_up_screen.dart | 220 ------------------ .../application/user/user_workspace_bloc.dart | 13 +- .../application/workspace/workspace_bloc.dart | 4 +- .../workspace/_sidebar_workspace_menu.dart | 7 +- .../af_cloud/impls/user/cloud_service_impl.rs | 9 +- .../src/local_server/impls/user.rs | 34 +-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 6 +- .../rust-lib/flowy-user-pub/src/entities.rs | 34 --- .../flowy-user-pub/src/sql/workspace_sql.rs | 8 +- .../rust-lib/flowy-user/src/entities/auth.rs | 44 ---- .../rust-lib/flowy-user/src/event_handler.rs | 4 +- .../flowy-user/src/user_manager/manager.rs | 2 +- .../user_manager/manager_user_workspace.rs | 33 +-- 20 files changed, 69 insertions(+), 409 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart delete mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index ef7f4492a5..eff2b4f420 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,6 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, + AuthTypePB.CloudAuthType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 7f9a2df329..e64e0f98de 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -119,18 +119,6 @@ GoRouter generateRouter(Widget child) { ); }, ), - GoRoute( - path: SignUpScreen.routeName, - pageBuilder: (context, state) { - return CustomTransitionPage( - child: SignUpScreen( - router: getIt(), - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 5e163b4e62..3ec181e009 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -140,25 +140,13 @@ class UserBackendService implements IUserBackendService { }); } - Future> createWorkspace( - String name, - String desc, - ) { - final request = CreateWorkspacePayloadPB.create() - ..name = name - ..desc = desc; - return FolderEventCreateFolderWorkspace(request).send().then((result) { - return result.fold( - (workspace) => FlowyResult.success(workspace), - (error) => FlowyResult.failure(error), - ); - }); - } - Future> createUserWorkspace( String name, + AuthTypePB authType, ) { - final request = CreateWorkspacePB.create()..name = name; + final request = CreateWorkspacePB.create() + ..name = name + ..authType = authType; return UserEventCreateWorkspace(request).send(); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart deleted file mode 100644 index 83007786f1..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy/user/presentation/helpers/helpers.dart'; -import 'package:appflowy/user/presentation/presentation.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/material.dart'; - -void handleUserProfileResult( - FlowyResult userProfileResult, - BuildContext context, - AuthRouter authRouter, -) { - userProfileResult.fold( - (userProfile) { - authRouter.goHomeScreen(context, userProfile); - }, - (error) { - handleOpenWorkspaceError(context, error); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart index 084a360666..11f321232e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart @@ -1,2 +1 @@ export 'handle_open_workspace_error.dart'; -export 'handle_user_profile_result.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index f6f6ec3e3a..339c2f29f7 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -21,10 +21,6 @@ class AuthRouter { getIt().pushWorkspaceStartScreen(context, userProfile); } - void pushSignUpScreen(BuildContext context) { - context.push(SignUpScreen.routeName); - } - /// Navigates to the home screen based on the current workspace and platform. /// /// This function takes in a [BuildContext] and a [UserProfilePB] object to diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 540da8c2b4..2aeba87995 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -1,6 +1,5 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; -export 'sign_up_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart deleted file mode 100644 index 8aea8dde55..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/sign_up_bloc.dart'; -import 'package:appflowy/user/presentation/router.dart'; -import 'package:appflowy/user/presentation/widgets/widgets.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SignUpScreen extends StatelessWidget { - const SignUpScreen({ - super.key, - required this.router, - }); - - static const routeName = '/SignUpScreen'; - final AuthRouter router; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), - child: BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null) { - _handleSuccessOrFail(context, successOrFail); - } - }, - child: const Scaffold(body: SignUpForm()), - ), - ); - } - - void _handleSuccessOrFail( - BuildContext context, - FlowyResult result, - ) { - result.fold( - (user) => router.pushWorkspaceStartScreen(context, user), - (error) => showSnapBar(context, error.msg), - ); - } -} - -class SignUpForm extends StatelessWidget { - const SignUpForm({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Align( - child: AuthFormContainer( - children: [ - FlowyLogoTitle( - title: LocaleKeys.signUp_title.tr(), - logoSize: const Size(60, 60), - ), - const VSpace(30), - const EmailTextField(), - const VSpace(5), - const PasswordTextField(), - const VSpace(5), - const RepeatPasswordTextField(), - const VSpace(30), - const SignUpButton(), - const VSpace(10), - const SignUpPrompt(), - if (context.read().state.isSubmitting) ...[ - const SizedBox(height: 8), - const LinearProgressIndicator(), - ], - ], - ), - ); - } -} - -class SignUpPrompt extends StatelessWidget { - const SignUpPrompt({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText.medium( - LocaleKeys.signUp_alreadyHaveAnAccount.tr(), - color: Theme.of(context).hintColor, - ), - TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.bodyMedium, - ), - onPressed: () => Navigator.pop(context), - child: FlowyText.medium( - LocaleKeys.signIn_buttonText.tr(), - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ); - } -} - -class SignUpButton extends StatelessWidget { - const SignUpButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return RoundedTextButton( - title: LocaleKeys.signUp_getStartedText.tr(), - height: 48, - onPressed: () { - context - .read() - .add(const SignUpEvent.signUpWithUserEmailAndPassword()); - }, - ); - } -} - -class PasswordTextField extends StatelessWidget { - const PasswordTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.passwordError != current.passwordError, - builder: (context, state) { - return RoundedInputField( - obscureText: true, - obscureIcon: const FlowySvg(FlowySvgs.hide_m), - obscureHideIcon: const FlowySvg(FlowySvgs.show_m), - hintText: LocaleKeys.signUp_passwordHint.tr(), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.passwordError ?? '', - onChanged: (value) => context - .read() - .add(SignUpEvent.passwordChanged(value)), - ); - }, - ); - } -} - -class RepeatPasswordTextField extends StatelessWidget { - const RepeatPasswordTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.repeatPasswordError != current.repeatPasswordError, - builder: (context, state) { - return RoundedInputField( - obscureText: true, - obscureIcon: const FlowySvg(FlowySvgs.hide_m), - obscureHideIcon: const FlowySvg(FlowySvgs.show_m), - hintText: LocaleKeys.signUp_repeatPasswordHint.tr(), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.repeatPasswordError ?? '', - onChanged: (value) => context - .read() - .add(SignUpEvent.repeatPasswordChanged(value)), - ); - }, - ); - } -} - -class EmailTextField extends StatelessWidget { - const EmailTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.emailError != current.emailError, - builder: (context, state) { - return RoundedInputField( - hintText: LocaleKeys.signUp_emailHint.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: context.read().state.emailError ?? '', - onChanged: (value) => - context.read().add(SignUpEvent.emailChanged(value)), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 7e341f4f1f..44e9fa3f92 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -97,7 +97,7 @@ class UserWorkspaceBloc extends Bloc { ); } }, - createWorkspace: (name) async { + createWorkspace: (name, authType) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -107,7 +107,10 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.createUserWorkspace(name); + final result = await _userService.createUserWorkspace( + name, + authType, + ); final workspaces = result.fold( (s) => [...state.workspaces, s], (e) => state.workspaces, @@ -472,8 +475,10 @@ class UserWorkspaceBloc extends Bloc { class UserWorkspaceEvent with _$UserWorkspaceEvent { const factory UserWorkspaceEvent.initial() = Initial; const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; - const factory UserWorkspaceEvent.createWorkspace(String name) = - CreateWorkspace; + const factory UserWorkspaceEvent.createWorkspace( + String name, + AuthTypePB authType, + ) = CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; const factory UserWorkspaceEvent.openWorkspace( diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index d8d5db45b4..0db1ac9443 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -64,7 +65,8 @@ class WorkspaceBloc extends Bloc { String desc, Emitter emit, ) async { - final result = await userService.createWorkspace(name, desc); + final result = + await userService.createUserWorkspace(name, AuthTypePB.CloudAuthType); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index daf332cc15..24a122fee5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -386,7 +386,12 @@ class _CreateWorkspaceButton extends StatelessWidget { final workspaceBloc = context.read(); await CreateWorkspaceDialog( onConfirm: (name) { - workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); + workspaceBloc.add( + UserWorkspaceEvent.createWorkspace( + name, + AuthTypePB.CloudAuthType, + ), + ); }, ).show(context); } 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 8309e4c65f..e0f81a62e4 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 @@ -24,8 +24,8 @@ use tracing::{instrument, trace}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; use flowy_user_pub::entities::{ - AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserCredentials, UserProfile, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserProfile, UserWorkspace, + WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -178,10 +178,7 @@ where } #[instrument(level = "debug", skip_all)] - async fn get_user_profile( - &self, - _credential: UserCredentials, - ) -> Result { + async fn get_user_profile(&self, _uid: i64) -> Result { let try_get_client = self.server.try_get_client(); let expected_workspace_id = self .logged_user 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 d378765ebb..eec50b1480 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 @@ -15,7 +15,7 @@ use crate::local_server::uid::UserIDGenerator; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::select_all_user_workspace; +use flowy_user_pub::sql::{select_all_user_workspace, select_user_profile, select_user_workspace}; use flowy_user_pub::DEFAULT_USER_NAME; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; @@ -59,7 +59,7 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn sign_in(&self, params: BoxAny) -> Result { let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let user_workspace = make_user_workspace(); + let user_workspace = make_user_workspace("My Workspace"); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -122,15 +122,18 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } - async fn get_user_profile(&self, credential: UserCredentials) -> Result { - Err(FlowyError::local_version_not_support().with_context("Not support")) + async fn get_user_profile(&self, uid: i64) -> Result { + let conn = self.user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, conn)?; + Ok(profile) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { - Err( - FlowyError::local_version_not_support() - .with_context("local server doesn't support open workspace"), - ) + let uid = self.user.user_id()?; + let conn = self.user.get_sqlite_db(uid)?; + + let workspace = select_user_workspace(&workspace_id.to_string(), conn)?; + Ok(UserWorkspace::from(workspace)) } async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { @@ -139,11 +142,8 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(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"), - ) + async fn create_workspace(&self, workspace_name: &str) -> Result { + Ok(make_user_workspace(workspace_name)) } async fn patch_workspace( @@ -193,12 +193,12 @@ impl UserCloudService for LocalServerUserServiceImpl { } } -fn make_user_workspace() -> UserWorkspace { +fn make_user_workspace(name: &str) -> UserWorkspace { UserWorkspace { - id: uuid::Uuid::new_v4().to_string(), - name: "My Workspace".to_string(), + id: Uuid::new_v4().to_string(), + name: name.to_string(), created_at: Default::default(), - workspace_database_id: uuid::Uuid::new_v4().to_string(), + workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), member_count: 1, role: None, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 00eaef8917..0964d80472 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -20,8 +20,8 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, AuthType, Role, UpdateUserProfileParams, UserCredentials, UserProfile, - UserTokenState, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, UserTokenState, + UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -168,7 +168,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Get the user information using the user's token or uid /// return None if the user is not found - async fn get_user_profile(&self, credential: UserCredentials) -> Result; + async fn get_user_profile(&self, uid: i64) -> Result; async fn open_workspace(&self, workspace_id: &Uuid) -> Result; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 6be5a9f64d..0d91f84b0e 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -100,40 +100,6 @@ impl UserAuthResponse for AuthResponse { } } -#[derive(Clone, Debug)] -pub struct UserCredentials { - /// Currently, the token is only used when the [AuthType] is AppFlowyCloud - pub token: Option, - - /// The user id - pub uid: Option, - - /// The user id - pub uuid: Option, -} - -impl UserCredentials { - pub fn from_uid(uid: i64) -> Self { - Self { - token: None, - uid: Some(uid), - uuid: None, - } - } - - pub fn from_uuid(uuid: String) -> Self { - Self { - token: None, - uid: None, - uuid: Some(uuid), - } - } - - pub fn new(token: Option, uid: Option, uuid: Option) -> Self { - Self { token, uid, uuid } - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserWorkspace { pub id: String, diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index bcac009527..a1165c9621 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -58,11 +58,11 @@ impl UserWorkspaceTable { pub fn select_user_workspace( workspace_id: &str, mut conn: DBConnection, -) -> Option { - user_workspace_table::dsl::user_workspace_table +) -> FlowyResult { + let row = user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn) - .ok() + .first::(&mut *conn)?; + Ok(row) } pub fn select_all_user_workspace( diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 6a889359c1..ddc0dc29f9 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -259,50 +259,6 @@ impl Default for AuthenticatorPB { } } -#[derive(Debug, ProtoBuf, Default)] -pub struct UserCredentialsPB { - #[pb(index = 1, one_of)] - pub uid: Option, - - #[pb(index = 2, one_of)] - pub uuid: Option, - - #[pb(index = 3, one_of)] - pub token: Option, -} - -impl UserCredentialsPB { - pub fn from_uid(uid: i64) -> Self { - Self { - uid: Some(uid), - uuid: None, - token: None, - } - } - - pub fn from_token(token: &str) -> Self { - Self { - uid: None, - uuid: None, - token: Some(token.to_owned()), - } - } - - pub fn from_uuid(uuid: &str) -> Self { - Self { - uid: None, - uuid: Some(uuid.to_owned()), - token: None, - } - } -} - -impl From for UserCredentials { - fn from(value: UserCredentialsPB) -> Self { - Self::new(value.token, value.uid, value.uuid) - } -} - #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 12dd1f3e93..bb38a9e0a0 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -469,9 +469,7 @@ pub async fn get_user_workspace_handler( let params = data.try_into_inner()?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let uid = manager.user_id()?; - let user_workspace = manager - .get_user_workspace_from_db(uid, &workspace_id) - .ok_or_else(FlowyError::record_not_found)?; + let user_workspace = manager.get_user_workspace_from_db(uid, &workspace_id)?; data_result_ok(UserWorkspacePB::from(user_workspace)) } 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 86a3dfabce..6cfa8bf16f 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -585,7 +585,7 @@ impl UserManager { let result: Result = self .cloud_service .get_user_service()? - .get_user_profile(UserCredentials::from_uid(uid)) + .get_user_profile(uid) .await; match result { 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 cc884dc1d9..17565e83df 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,17 +157,21 @@ impl UserManager { let uid = self.user_id()?; let conn = self.db_connection(self.user_id()?)?; let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { - None => { - sync_workspace( - workspace_id, - self.cloud_service.get_user_service()?, - uid, - auth_type, - self.db_pool(uid)?, - ) - .await? + Err(err) => { + if err.is_record_not_found() { + sync_workspace( + workspace_id, + self.cloud_service.get_user_service()?, + uid, + auth_type, + self.db_pool(uid)?, + ) + .await? + } else { + return Err(err); + } }, - Some(row) => { + Ok(row) => { let user_workspace = UserWorkspace::from(row); let workspace_id = *workspace_id; let user_service = self.cloud_service.get_user_service()?; @@ -248,10 +252,7 @@ impl UserManager { let conn = self.db_connection(uid)?; update_user_workspace(conn, changeset)?; - let row = self - .get_user_workspace_from_db(uid, workspace_id) - .ok_or_else(FlowyError::record_not_found)?; - + let row = self.get_user_workspace_from_db(uid, workspace_id)?; let payload = UserWorkspacePB::from(row); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) @@ -389,8 +390,8 @@ impl UserManager { &self, uid: i64, workspace_id: &Uuid, - ) -> Option { - let conn = self.db_connection(uid).ok()?; + ) -> FlowyResult { + let conn = self.db_connection(uid)?; select_user_workspace(workspace_id.to_string().as_str(), conn) } From 2f5b494885904aac89cd173d0eb0978c7a0bd9b7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 00:28:15 +0800 Subject: [PATCH 38/74] chore: remove authticator pb --- .../bottom_sheet/bottom_sheet_view_page.dart | 3 +- .../home/setting/settings_popup_menu.dart | 2 +- .../home/tab/mobile_space_tab.dart | 2 +- .../workspace_menu_bottom_sheet.dart | 2 +- .../setting/user_session_setting_group.dart | 2 +- .../lib/plugins/ai_chat/chat_page.dart | 2 +- .../application/sync/database_sync_bloc.dart | 2 +- .../database/widgets/row/row_banner.dart | 5 +- .../document/application/document_bloc.dart | 4 +- .../document_collaborators_bloc.dart | 3 +- .../application/document_sync_bloc.dart | 3 +- .../editor_plugins/file/file_util.dart | 6 +-- .../page_style/_page_style_cover_image.dart | 3 +- .../lib/plugins/shared/share/share_bloc.dart | 2 +- .../icon_emoji_picker/icon_uploader.dart | 6 +-- .../lib/startup/deps_resolver.dart | 2 +- .../startup/tasks/appflowy_cloud_task.dart | 2 +- .../auth/af_cloud_auth_service.dart | 2 +- .../auth/af_cloud_mock_auth_service.dart | 6 +-- .../auth/backend_auth_service.dart | 7 +-- .../application/password/password_bloc.dart | 4 +- .../lib/user/presentation/anon_user.dart | 2 +- .../settings/settings_dialog_bloc.dart | 4 +- .../application/user/user_workspace_bloc.dart | 2 +- .../application/workspace/workspace_bloc.dart | 2 +- .../workspace/_sidebar_workspace_menu.dart | 2 +- .../menu/view/view_more_action_button.dart | 2 +- .../settings/pages/settings_account_view.dart | 28 +++++------ .../pages/settings_workspace_view.dart | 4 +- .../settings/settings_dialog.dart | 2 +- .../settings/widgets/settings_menu.dart | 4 +- .../more_view_actions/more_view_actions.dart | 2 +- .../event-integration-test/src/lib.rs | 4 +- .../event-integration-test/src/user_event.rs | 10 ++-- .../user/af_cloud_test/anon_user_test.rs | 4 +- .../tests/user/local_test/auth_test.rs | 8 ++-- .../user/local_test/user_profile_test.rs | 4 +- .../src/local_server/impls/user.rs | 24 ++++------ .../rust-lib/flowy-user-pub/src/entities.rs | 6 +-- .../flowy-user-pub/src/sql/workspace_sql.rs | 20 ++++---- .../rust-lib/flowy-user/src/entities/auth.rs | 47 ++++--------------- .../flowy-user/src/entities/user_profile.rs | 4 +- .../flowy-user/src/entities/workspace.rs | 25 ++++++---- .../user_manager/manager_user_workspace.rs | 22 ++++++--- 44 files changed, 134 insertions(+), 168 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 85a5e3cbfa..9706777df0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -202,8 +202,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { List _buildPublishActions(BuildContext context) { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud - if (userProfile == null || - userProfile.authType != AuthenticatorPB.AppFlowyCloud) { + if (userProfile == null || userProfile.authType != AuthTypePB.Server) { return []; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index 11a82d2c7a..bd41730934 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthTypePB.Server) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, 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 cc3f4c8c56..c89367f379 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 @@ -167,7 +167,7 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthTypePB.Server) Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 20, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index eff2b4f420..d306f48964 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.CloudAuthType, + AuthTypePB.Server, ), ); }, 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 09f38223f4..405fef0d1a 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 @@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget { // delete account button // only show the delete account button in cloud mode - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) ...[ + if (userProfile.authType == AuthTypePB.Server) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.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 c154b0e3a0..90085354db 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -48,7 +48,7 @@ class AIChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - // if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + // if (userProfile.authenticator != AuthTypePB.Server) { // return Center( // child: FlowyText( // LocaleKeys.chat_unsupportedCloudPrompt.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index 53dc2bd6d5..5116785c1f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud && + userProfile?.authType == AuthTypePB.Server && databaseId != null, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 5706167f1a..e2f470e0d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -21,8 +21,8 @@ import 'package:appflowy/shared/af_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -69,8 +69,7 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.authType ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + (widget.userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; @override void dispose() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 252742aa5e..ac03fe5308 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -101,8 +101,8 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.authType ?? AuthenticatorPB.Local; - return type == AuthenticatorPB.Local; + final type = userProfilePB?.authType ?? AuthTypePB.Local; + return type == AuthTypePB.Local; } @override 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 b593ccc6cc..682f600f0a 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 @@ -31,8 +31,7 @@ class DocumentCollaboratorsBloc final userProfile = result.fold((s) => s, (f) => null); emit( state.copyWith( - shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud, + shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 1001aaef5e..2ba50fc6c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -30,8 +30,7 @@ class DocumentSyncBloc extends Bloc { ); emit( state.copyWith( - shouldShowIndicator: - userProfile?.authType == AuthenticatorPB.AppFlowyCloud, + shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, ), ); _syncStateListener.start( 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 d3bbbd27d5..69791f78b7 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 @@ -14,8 +14,8 @@ import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:cross_file/cross_file.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_impl.dart'; @@ -185,7 +185,7 @@ Future insertLocalFile( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; String? path; String? errorMsg; @@ -230,7 +230,7 @@ Future insertLocalFiles( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 601ba8fa20..6136392884 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -225,8 +225,7 @@ class PageStyleCoverImage extends StatelessWidget { (s) => s, (f) => null, ); - final isAppFlowyCloud = - userProfile?.authType == AuthenticatorPB.AppFlowyCloud; + final isAppFlowyCloud = userProfile?.authType == AuthTypePB.Server; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index c3f04a8837..2356399b4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.authType == AuthenticatorPB.AppFlowyCloud, + (v) => v.authType == AuthTypePB.Server, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index f39fdb01dc..ff8e7b88ec 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -13,7 +13,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/util/default_extensions.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -294,8 +294,8 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = (userProfile?.authType ?? AuthenticatorPB.Local) == - AuthenticatorPB.Local; + final isLocalMode = + (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); } else { diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 621ba988cf..5a8c0fa651 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -102,7 +102,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { case AuthenticatorType.local: getIt.registerFactory( () => BackendAuthService( - AuthenticatorPB.Local, + AuthTypePB.Local, ), ); break; 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 184cc9dd09..362b27a85a 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -112,7 +112,7 @@ class AppFlowyCloudDeepLink { (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( - authenticator: AuthenticatorPB.AppFlowyCloud, + authenticator: AuthTypePB.Server, map: { AuthServiceMapKeys.signInURL: uri.toString(), AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 6d02f188c8..4f4cece9bb 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -18,7 +18,7 @@ class AppFlowyCloudAuthService implements AuthService { AppFlowyCloudAuthService(); final BackendAuthService _backendAuthService = BackendAuthService( - AuthenticatorPB.AppFlowyCloud, + AuthTypePB.Server, ); @override 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 d8cee89b59..8be71dc648 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.AppFlowyCloud); + BackendAuthService(AuthTypePB.Server); @override Future> signUp({ @@ -48,7 +48,7 @@ class AppFlowyCloudMockAuthService implements AuthService { Map params = const {}, }) async { final payload = SignInUrlPayloadPB.create() - ..authenticator = AuthenticatorPB.AppFlowyCloud + ..authenticator = AuthTypePB.Server // don't use nanoid here, the gotrue server will transform the email ..email = userEmail; @@ -58,7 +58,7 @@ class AppFlowyCloudMockAuthService implements AuthService { return getSignInURLResult.fold( (urlPB) async { final payload = OauthSignInPB( - authenticator: AuthenticatorPB.AppFlowyCloud, + authenticator: AuthTypePB.Server, map: { AuthServiceMapKeys.signInURL: urlPB.signInUrl, AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index f47fd5a4a6..cab8cd170c 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,7 +16,7 @@ import 'device_id.dart'; class BackendAuthService implements AuthService { BackendAuthService(this.authType); - final AuthenticatorPB authType; + final AuthTypePB authType; @override Future> @@ -71,7 +72,7 @@ class BackendAuthService implements AuthService { ..email = userEmail ..password = password // When sign up as guest, the auth type is always local. - ..authType = AuthenticatorPB.Local + ..authType = AuthTypePB.Local ..deviceId = await getDeviceId(); final response = await UserEventSignUp(request).send().then( (value) => value, @@ -82,7 +83,7 @@ class BackendAuthService implements AuthService { @override Future> signUpWithOAuth({ required String platform, - AuthenticatorPB authType = AuthenticatorPB.Local, + AuthTypePB authType = AuthTypePB.Local, Map params = const {}, }) async { return FlowyResult.failure( diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart index d421440d08..b85efe38ae 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -4,8 +4,8 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/password/password_http_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -46,7 +46,7 @@ class PasswordBloc extends Bloc { bool _isInitialized = false; Future _init() async { - if (userProfile.authType == AuthenticatorPB.Local) { + if (userProfile.authType == AuthTypePB.Local) { Log.debug('PasswordBloc: skip init because user is local authenticator'); return; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index 014c7caaf8..c8744fb304 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,7 +74,7 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.authType != AuthenticatorPB.Local; + final isDisabled = isSelected || user.authType != AuthTypePB.Local; final desc = "${user.name}\t ${user.authType}\t"; final child = SizedBox( height: 30, 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 66277cf30b..ce659d5262 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 @@ -2,8 +2,6 @@ import 'package:appflowy/user/application/user_listener.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/auth.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; @@ -91,7 +89,7 @@ class SettingsDialogBloc AFRolePB? currentWorkspaceMemberRole, ]) async { if ([ - AuthenticatorPB.Local, + AuthTypePB.Local, ].contains(userProfile.authType)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 44e9fa3f92..5ed56b890e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.authType == AuthenticatorPB.AppFlowyCloud && + userProfile.authType == AuthTypePB.Server && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index 0db1ac9443..ed06f16c8f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -66,7 +66,7 @@ class WorkspaceBloc extends Bloc { Emitter emit, ) async { final result = - await userService.createUserWorkspace(name, AuthTypePB.CloudAuthType); + await userService.createUserWorkspace(name, AuthTypePB.Server); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 24a122fee5..c9f464d43a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.CloudAuthType, + AuthTypePB.Local, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 576ff07cb4..7ccd03b4f4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.authType != AuthenticatorPB.AppFlowyCloud) { + if (userProfile.authType != AuthTypePB.Server) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index f1ecfda8ad..e242da473b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -7,8 +7,8 @@ import 'package:appflowy/workspace/presentation/settings/pages/account/account.d import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.authType != AuthenticatorPB.Local) ...[ + state.userProfile.authType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ @@ -82,30 +82,26 @@ class _SettingsAccountViewState extends State { ), AccountSignInOutSection( userProfile: state.userProfile, - onAction: - state.userProfile.authType == AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: - state.userProfile.authType == AuthenticatorPB.Local, + onAction: state.userProfile.authType == AuthTypePB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authType == AuthTypePB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.authType == AuthenticatorPB.Local) ...[ + state.userProfile.authType == AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: - state.userProfile.authType == AuthenticatorPB.Local - ? widget.didLogin - : widget.didLogout, - signIn: - state.userProfile.authType == AuthenticatorPB.Local, + onAction: state.userProfile.authType == AuthTypePB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authType == AuthTypePB.Local, ), ], ), @@ -120,7 +116,7 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (widget.userProfile.authType == AuthTypePB.Server) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index a602849527..9a17016e5f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.authType != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.authType != AuthenticatorPB.Local) ...[ + if (userProfile.authType != AuthTypePB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), 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 b17a9beb7e..e262a27cb6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -140,7 +140,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.authType == AuthenticatorPB.AppFlowyCloud) { + if (user.authType == AuthTypePB.Server) { return SettingsAIView( key: ValueKey(workspaceId), userProfile: user, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 1a7144993f..04a93656ca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -63,7 +63,7 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.authType == AuthenticatorPB.AppFlowyCloud) + userProfile.authType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, @@ -109,7 +109,7 @@ class SettingsMenu extends StatelessWidget { ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.authType == AuthenticatorPB.AppFlowyCloud) + if (userProfile.authType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 3b81d2a041..fe202e7590 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.authType == AuthenticatorPB.AppFlowyCloud) { + userProfile.authType == AuthTypePB.Server) { return const SizedBox.shrink(); } diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 1b54087ee1..ff0a3847df 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -8,7 +8,7 @@ use collab_entity::CollabType; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_user::entities::AuthenticatorPB; +use flowy_user::entities::AuthTypePB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; use nanoid::nanoid; @@ -59,7 +59,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(AtomicU8::new(AuthenticatorPB::Local as u8)); + let authenticator = Arc::new(AtomicU8::new(AuthTypePB::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 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 5c9be65660..c4e34f27f7 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,7 +17,7 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, + AuthTypePB, AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, @@ -65,7 +65,7 @@ impl EventIntegrationTest { email, name: "appflowy".to_string(), password: password.clone(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: uuid::Uuid::new_v4().to_string(), } .into_bytes() @@ -113,7 +113,7 @@ impl EventIntegrationTest { .await; } - pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { + pub fn set_auth_type(&self, auth_type: AuthTypePB) { self.authenticator.store(auth_type as u8, Ordering::Release); } @@ -140,7 +140,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthenticatorPB::AppFlowyCloud, + authenticator: AuthTypePB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +155,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthenticatorPB::AppFlowyCloud, + authenticator: AuthTypePB::AppFlowyCloud, }; let user_profile = EventBuilder::new(self.clone()) diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index f74b202860..1abdde509f 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -1,7 +1,7 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_user::entities::AuthenticatorPB; +use flowy_user::entities::AuthTypePB; use crate::util::unzip; @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.auth_type, AuthenticatorPB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthTypePB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 3c73fd01eb..138f6f0258 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -1,6 +1,6 @@ use event_integration_test::user_event::{login_password, unique_email}; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthenticatorPB, SignInPayloadPB, SignUpPayloadPB}; +use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -14,7 +14,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; @@ -40,7 +40,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; @@ -67,7 +67,7 @@ async fn sign_in_with_invalid_password() { email: unique_email(), password, name: "".to_string(), - auth_type: AuthenticatorPB::Local, + auth_type: AuthTypePB::Local, device_id: "".to_string(), }; 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 46f8eca9c6..47c2d53a6b 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 @@ -1,6 +1,6 @@ use crate::user::local_test::helper::*; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use nanoid::nanoid; #[tokio::test] @@ -24,7 +24,7 @@ async fn anon_user_profile_get() { .await .parse::(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.auth_type, AuthenticatorPB::Local); + assert_eq!(user_profile.auth_type, AuthTypePB::Local); } #[tokio::test] 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 eec50b1480..14d39690f9 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 @@ -35,7 +35,7 @@ impl UserCloudService for LocalServerUserServiceImpl { let params = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); let workspace_id = Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new_local(&workspace_id, uid); + let user_workspace = UserWorkspace::new_local(workspace_id, ""); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() } else { @@ -59,7 +59,9 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn sign_in(&self, params: BoxAny) -> Result { let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let user_workspace = make_user_workspace("My Workspace"); + + let workspace_id = Uuid::new_v4(); + let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), "My Workspace"); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -143,7 +145,11 @@ impl UserCloudService for LocalServerUserServiceImpl { } async fn create_workspace(&self, workspace_name: &str) -> Result { - Ok(make_user_workspace(workspace_name)) + let workspace_id = Uuid::new_v4(); + Ok(UserWorkspace::new_local( + workspace_id.to_string(), + workspace_name, + )) } async fn patch_workspace( @@ -192,15 +198,3 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } } - -fn make_user_workspace(name: &str) -> UserWorkspace { - UserWorkspace { - id: Uuid::new_v4().to_string(), - name: name.to_string(), - created_at: Default::default(), - workspace_database_id: Uuid::new_v4().to_string(), - icon: "".to_string(), - member_count: 1, - role: None, - } -} diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 0d91f84b0e..efceb8b5f6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -122,10 +122,10 @@ impl UserWorkspace { Ok(id) } - pub fn new_local(workspace_id: &str, _uid: i64) -> Self { + pub fn new_local(workspace_id: String, name: &str) -> Self { Self { - id: workspace_id.to_string(), - name: "".to_string(), + id: workspace_id, + name: name.to_string(), created_at: Utc::now(), workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index a1165c9621..0570020716 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -93,20 +93,20 @@ pub fn upsert_user_workspace( user_workspace: UserWorkspace, conn: &mut SqliteConnection, ) -> Result<(), FlowyError> { - let new_record = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; + let row = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; diesel::insert_into(user_workspace_table::table) - .values(new_record.clone()) + .values(row.clone()) .on_conflict(user_workspace_table::id) .do_update() .set(( - user_workspace_table::name.eq(new_record.name), - user_workspace_table::uid.eq(new_record.uid), - user_workspace_table::created_at.eq(new_record.created_at), - user_workspace_table::database_storage_id.eq(new_record.database_storage_id), - user_workspace_table::icon.eq(new_record.icon), - user_workspace_table::member_count.eq(new_record.member_count), - user_workspace_table::role.eq(new_record.role), - user_workspace_table::auth_type.eq(new_record.auth_type), + user_workspace_table::name.eq(row.name), + user_workspace_table::uid.eq(row.uid), + user_workspace_table::created_at.eq(row.created_at), + user_workspace_table::database_storage_id.eq(row.database_storage_id), + user_workspace_table::icon.eq(row.icon), + user_workspace_table::member_count.eq(row.member_count), + user_workspace_table::role.eq(row.role), + user_workspace_table::auth_type.eq(row.auth_type), )) .execute(conn)?; diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index ddc0dc29f9..a61ba5cc96 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; use std::convert::TryInto; +use crate::entities::parser::*; +use crate::entities::AuthTypePB; +use crate::errors::ErrorCode; use client_api::entity::GotrueTokenResponse; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; -use crate::entities::parser::*; -use crate::errors::ErrorCode; - #[derive(ProtoBuf, Default)] pub struct SignInPayloadPB { #[pb(index = 1)] @@ -20,7 +20,7 @@ pub struct SignInPayloadPB { pub name: String, #[pb(index = 4)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, #[pb(index = 5)] pub device_id: String, @@ -53,7 +53,7 @@ pub struct SignUpPayloadPB { pub password: String, #[pb(index = 4)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, #[pb(index = 5)] pub device_id: String, @@ -144,7 +144,7 @@ pub struct OauthSignInPB { pub map: HashMap, #[pb(index = 2)] - pub authenticator: AuthenticatorPB, + pub authenticator: AuthTypePB, } #[derive(ProtoBuf, Default)] @@ -153,7 +153,7 @@ pub struct SignInUrlPayloadPB { pub email: String, #[pb(index = 2)] - pub authenticator: AuthenticatorPB, + pub authenticator: AuthTypePB, } #[derive(ProtoBuf, Default)] @@ -228,41 +228,10 @@ pub struct OauthProviderDataPB { pub oauth_url: String, } -#[repr(u8)] -#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] -pub enum AuthenticatorPB { - Local = 0, - AppFlowyCloud = 2, -} - -impl From for AuthenticatorPB { - fn from(auth_type: AuthType) -> Self { - match auth_type { - AuthType::Local => AuthenticatorPB::Local, - AuthType::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, - } - } -} - -impl From for AuthType { - fn from(pb: AuthenticatorPB) -> Self { - match pb { - AuthenticatorPB::Local => AuthType::Local, - AuthenticatorPB::AppFlowyCloud => AuthType::AppFlowyCloud, - } - } -} - -impl Default for AuthenticatorPB { - fn default() -> Self { - Self::Local - } -} - #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, } #[derive(ProtoBuf, Debug, Default, 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 17b951bae0..5afc93850a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,6 +1,6 @@ use super::AFRolePB; use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::{AuthTypePB, AuthenticatorPB}; +use crate::entities::AuthTypePB; use crate::errors::ErrorCode; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; @@ -39,7 +39,7 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub auth_type: AuthenticatorPB, + pub auth_type: AuthTypePB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index e178b724db..ed8eb8aa76 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -242,18 +242,23 @@ pub struct CreateWorkspacePB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Default, Debug, Clone)] +#[derive(ProtoBuf_Enum, Default, Debug, Clone, Eq, PartialEq)] +#[repr(u8)] pub enum AuthTypePB { - LocalAuthType = 0, #[default] - CloudAuthType = 1, + Local = 0, + Server = 1, } + impl From for AuthTypePB { fn from(value: i32) -> Self { match value { - 0 => AuthTypePB::LocalAuthType, - 1 => AuthTypePB::CloudAuthType, - _ => AuthTypePB::CloudAuthType, + 0 => AuthTypePB::Local, + 1 => AuthTypePB::Server, + // For historical reasons, 2 also maps to Server. + // Check the AuthenticatorType in flowy-server-pub + 2 => AuthTypePB::Server, + _ => AuthTypePB::Server, } } } @@ -261,8 +266,8 @@ impl From for AuthTypePB { impl From for AuthTypePB { fn from(value: AuthType) -> Self { match value { - AuthType::Local => AuthTypePB::LocalAuthType, - AuthType::AppFlowyCloud => AuthTypePB::CloudAuthType, + AuthType::Local => AuthTypePB::Local, + AuthType::AppFlowyCloud => AuthTypePB::Server, } } } @@ -270,8 +275,8 @@ impl From for AuthTypePB { impl From for AuthType { fn from(value: AuthTypePB) -> Self { match value { - AuthTypePB::LocalAuthType => AuthType::Local, - AuthTypePB::CloudAuthType => AuthType::AppFlowyCloud, + AuthTypePB::Local => AuthType::Local, + AuthTypePB::Server => AuthType::AppFlowyCloud, } } } 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 17565e83df..6c786d9435 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 @@ -218,15 +218,23 @@ impl UserManager { workspace_name: &str, auth_type: AuthType, ) -> FlowyResult { - let new_workspace = self - .cloud_service - .get_user_service()? - .create_workspace(workspace_name) - .await?; + let new_workspace = match auth_type { + AuthType::Local => { + let workspace_id = Uuid::new_v4(); + UserWorkspace::new_local(workspace_id.to_string(), workspace_name) + }, + AuthType::AppFlowyCloud => { + self + .cloud_service + .get_user_service()? + .create_workspace(workspace_name) + .await? + }, + }; info!( - "new workspace: {}, name:{}", - new_workspace.id, new_workspace.name + "create workspace: {}, name:{}, auth_type: {}", + new_workspace.id, new_workspace.name, auth_type ); // save the workspace to sqlite db From 2c5f41b58062771135fa4dd77c6b3e8777c7c2db Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 10:53:21 +0800 Subject: [PATCH 39/74] fix: set auth type --- .../settings/settings_dialog_bloc.dart | 1 + .../workspace/_sidebar_workspace_menu.dart | 2 +- .../user/af_cloud_test/anon_user_test.rs | 2 +- .../rust-lib/flowy-core/src/server_layer.rs | 10 ++--- .../flowy-core/src/user_state_callback.rs | 6 --- .../src/local_server/impls/folder.rs | 2 +- .../src/local_server/impls/user.rs | 3 +- .../flowy-user-pub/src/sql/workspace_sql.rs | 1 - .../flowy-user/src/entities/workspace.rs | 3 -- .../rust-lib/flowy-user/src/event_handler.rs | 14 +----- .../flowy-user/src/user_manager/manager.rs | 45 ++++++------------- .../user_manager/manager_user_workspace.rs | 2 + 12 files changed, 29 insertions(+), 62 deletions(-) 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 ce659d5262..726e95bb9e 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 @@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_listener.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/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index c9f464d43a..097da2c2ae 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 1abdde509f..7f743b931c 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.auth_type, AuthTypePB::AppFlowyCloud); + assert_eq!(user.auth_type, AuthTypePB::Server); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 4b1834c41c..b666ab4749 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -82,12 +82,12 @@ impl ServerProvider { pub fn set_auth_type(&self, new_auth_type: AuthType) { let old_type = self.get_auth_type(); - info!( - "ServerProvider: set auth type from {:?} to {:?}", - old_type, new_auth_type - ); - if old_type != new_auth_type { + info!( + "ServerProvider: auth type from {:?} to {:?}", + old_type, new_auth_type + ); + self.auth_type.store(Arc::new(new_auth_type)); if let Some((auth_type, _)) = self.providers.remove(&old_type) { info!("ServerProvider: remove old auth type: {:?}", auth_type); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index e85b773ec4..f3eaa00543 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -55,7 +55,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { auth_type: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; - self.server_provider.set_auth_type(*auth_type); if let Some(cloud_config) = cloud_config { self @@ -102,8 +101,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); - self.server_provider.set_auth_type(*auth_type); - self .folder_manager .initialize_after_sign_in(user_id) @@ -130,8 +127,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { device_id: &str, auth_type: &AuthType, ) -> FlowyResult<()> { - self.server_provider.set_auth_type(*auth_type); - event!( tracing::Level::TRACE, "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", @@ -212,7 +207,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace: &UserWorkspace, auth_type: &AuthType, ) -> FlowyResult<()> { - self.server_provider.set_auth_type(*auth_type); self .folder_manager .initialize_after_open_workspace(user_id) 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 31a65e7fa9..72bab514ec 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 @@ -84,7 +84,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { workspace_id: &Uuid, view_ids: Vec, ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) + Ok(()) } async fn get_publish_info(&self, view_id: &Uuid) -> Result { 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 14d39690f9..e73943bbbe 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 @@ -48,7 +48,8 @@ impl UserCloudService for LocalServerUserServiceImpl { latest_workspace: user_workspace.clone(), user_workspaces: vec![user_workspace], is_new_user: true, - email: Some(params.email), + // Anon user doesn't have email + email: None, token: None, encryption_type: EncryptionType::NoEncryption, updated_at: timestamp(), diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index 0570020716..cb6d299039 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -172,7 +172,6 @@ pub fn delete_all_then_insert_user_workspaces( ) -> FlowyResult<()> { conn.immediate_transaction(|conn| { delete_user_all_workspace(uid, auth_type, conn)?; - info!( "Insert {} workspaces for user {} and auth type {:?}", user_workspaces.len(), diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index ed8eb8aa76..99544eede4 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -255,9 +255,6 @@ impl From for AuthTypePB { match value { 0 => AuthTypePB::Local, 1 => AuthTypePB::Server, - // For historical reasons, 2 also maps to Server. - // Check the AuthenticatorType in flowy-server-pub - 2 => AuthTypePB::Server, _ => AuthTypePB::Server, } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bb38a9e0a0..905eb595f1 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -44,18 +44,12 @@ pub async fn sign_in_with_email_password_handler( let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; - let old_authenticator = manager.cloud_service.get_server_auth_type(); match manager .sign_in_with_password(¶ms.email, ¶ms.password) .await { Ok(token) => data_result_ok(token.into()), - Err(err) => { - manager - .cloud_service - .set_server_auth_type(&old_authenticator); - return Err(err); - }, + Err(err) => Err(err), } } @@ -77,13 +71,9 @@ pub async fn sign_up( let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type; - let prev_auth_type = manager.cloud_service.get_server_auth_type(); match manager.sign_up(auth_type, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), - Err(err) => { - manager.cloud_service.set_server_auth_type(&prev_auth_type); - Err(err) - }, + Err(err) => Err(err), } } 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 6cfa8bf16f..9520489771 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -132,18 +132,19 @@ impl UserManager { if let Ok(session) = self.get_session() { let user = self.get_user_profile_from_disk(session.user_id).await?; + self.cloud_service.set_server_auth_type(&user.auth_type); // Get the current authenticator from the environment variable - let current_authenticator = current_authenticator(); + let env_auth_type = current_authenticator(); // If the current authenticator is different from the authenticator in the session and it's // not a local authenticator, we need to sign out the user. - if user.auth_type != AuthType::Local && user.auth_type != current_authenticator { + if user.auth_type != AuthType::Local && user.auth_type != env_auth_type { event!( tracing::Level::INFO, - "Authenticator changed from {:?} to {:?}", + "Auth type changed from {:?} to {:?}", user.auth_type, - current_authenticator + env_auth_type ); self.sign_out().await?; return Ok(()); @@ -151,7 +152,7 @@ impl UserManager { event!( tracing::Level::INFO, - "init user session: {}:{}, authenticator: {:?}", + "init user session: {}:{}, auth type: {:?}", user.uid, user.email, user.auth_type, @@ -389,9 +390,10 @@ impl UserManager { auth_type: AuthType, params: BoxAny, ) -> Result { + self.cloud_service.set_server_auth_type(&auth_type); + // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; - self.cloud_service.set_server_auth_type(&auth_type); let auth_service = self.cloud_service.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; let new_user_profile = UserProfile::from((&response, &auth_type)); @@ -401,28 +403,6 @@ impl UserManager { Ok(new_user_profile) } - #[tracing::instrument(level = "info", skip(self))] - pub async fn resume_sign_up(&self) -> Result<(), FlowyError> { - let UserAuthProcess { - user_profile, - migration_user, - response, - authenticator, - } = self - .auth_process - .lock() - .await - .clone() - .ok_or(FlowyError::new( - ErrorCode::Internal, - "No resumable sign up data", - ))?; - self - .continue_sign_up(&user_profile, migration_user, response, &authenticator) - .await?; - Ok(()) - } - #[tracing::instrument(level = "info", skip_all, err)] async fn continue_sign_up( &self, @@ -436,9 +416,7 @@ impl UserManager { self .save_auth_data(&response, *auth_type, &new_session) .await?; - let _ = self - .initial_user_awareness(&new_session, &new_user_profile.auth_type) - .await; + let _ = self.initial_user_awareness(&new_session, auth_type).await; self .user_status_callback .read() @@ -670,6 +648,7 @@ impl UserManager { } } + #[instrument(level = "info", skip_all)] pub(crate) async fn generate_sign_in_url_with_email( &self, authenticator: &AuthType, @@ -682,6 +661,7 @@ impl UserManager { Ok(url) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_password( &self, email: &str, @@ -695,6 +675,7 @@ impl UserManager { Ok(response) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_magic_link( &self, email: &str, @@ -710,6 +691,7 @@ impl UserManager { Ok(()) } + #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_passcode( &self, email: &str, @@ -723,6 +705,7 @@ impl UserManager { Ok(response) } + #[instrument(level = "info", skip_all)] pub(crate) async fn generate_oauth_url( &self, oauth_provider: &str, 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 6c786d9435..04e7e6b0d9 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 @@ -154,6 +154,8 @@ impl UserManager { #[instrument(skip(self), err)] pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + self.cloud_service.set_server_auth_type(&auth_type); + let uid = self.user_id()?; let conn = self.db_connection(self.user_id()?)?; let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { From ccd1f5f8e9742a86c023664edb2d17e848a1c6ef Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 12:47:55 +0800 Subject: [PATCH 40/74] chore: revert pod lock --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 92e52a1a79..4b7ed5d639 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -181,37 +181,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 - appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 + appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 - flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 - keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87 + saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca From bb5d36402aa1396d4b76429a7ddd4ec265cc5257 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 13:35:06 +0800 Subject: [PATCH 41/74] chore: fix test --- .../event-integration-test/src/user_event.rs | 12 ++++---- .../flowy-core/src/user_state_callback.rs | 16 +++++----- .../rust-lib/flowy-user/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-user/src/event_map.rs | 29 ++++++++++--------- .../flowy-user/src/user_manager/manager.rs | 12 +++----- .../user_manager/manager_user_workspace.rs | 6 ++-- .../flowy-user/src/user_manager/mod.rs | 1 - .../src/user_manager/user_login_state.rs | 11 ------- 8 files changed, 38 insertions(+), 51 deletions(-) delete mode 100644 frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs 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 c4e34f27f7..ab10bb7083 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,10 +17,10 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, - ImportAppFlowyDataPB, OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, - RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, - UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, + AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, + OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, + SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, + UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -140,7 +140,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthTypePB::AppFlowyCloud, + authenticator: AuthTypePB::Server, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +155,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthTypePB::AppFlowyCloud, + authenticator: AuthTypePB::Server, }; let user_profile = EventBuilder::new(self.clone()) diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index f3eaa00543..e43002d709 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -46,7 +46,7 @@ impl UserStatusCallbackImpl { #[async_trait] impl UserStatusCallback for UserStatusCallbackImpl { - async fn did_init( + async fn on_launch_if_authenticated( &self, user_id: i64, cloud_config: &Option, @@ -88,7 +88,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_sign_in( + async fn on_sign_in( &self, user_id: i64, user_workspace: &UserWorkspace, @@ -119,7 +119,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_sign_up( + async fn on_sign_up( &self, is_new_user: bool, user_profile: &UserProfile, @@ -196,12 +196,12 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - async fn did_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { + async fn on_token_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { self.folder_manager.clear(user_id).await; Ok(()) } - async fn open_workspace( + async fn on_workspace_opened( &self, user_id: i64, user_workspace: &UserWorkspace, @@ -230,13 +230,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { Ok(()) } - fn did_update_network(&self, reachable: bool) { + fn on_network_status_changed(&self, reachable: bool) { info!("Notify did update network: reachable: {}", reachable); self.collab_builder.update_network(reachable); self.storage_manager.update_network_reachable(reachable); } - fn did_update_plans(&self, plans: Vec) { + fn on_subscription_plans_updated(&self, plans: Vec) { let mut storage_plan_changed = false; for plan in &plans { match plan { @@ -249,7 +249,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } - fn did_update_storage_limitation(&self, can_write: bool) { + fn on_storage_permission_updated(&self, can_write: bool) { if can_write { self.storage_manager.enable_storage_write_access(); } else { diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 905eb595f1..b1ce8c5ec6 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -475,7 +475,7 @@ pub async fn update_network_state_handler( .user_status_callback .read() .await - .did_update_network(reachable); + .on_network_status_changed(reachable); Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 1ae414afa7..0b489e6f28 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -279,10 +279,9 @@ pub enum UserEvent { pub trait UserStatusCallback: Send + Sync + 'static { /// When the [AuthType] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn authenticator_did_changed(&self, _authenticator: AuthType) {} - /// This will be called after the application launches if the user is already signed in. - /// If the user is not signed in, this method will not be called - async fn did_init( + fn on_auth_type_changed(&self, _authenticator: AuthType) {} + /// Fires on app launch, but only if the user is already signed in. + async fn on_launch_if_authenticated( &self, _user_id: i64, _cloud_config: &Option, @@ -292,8 +291,8 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - /// Will be called after the user signed in. - async fn did_sign_in( + /// Fires right after the user successfully signs in. + async fn on_sign_in( &self, _user_id: i64, _user_workspace: &UserWorkspace, @@ -302,8 +301,9 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - /// Will be called after the user signed up. - async fn did_sign_up( + + /// Fires right after the user successfully signs up. + async fn on_sign_up( &self, _is_new_user: bool, _user_profile: &UserProfile, @@ -314,10 +314,13 @@ pub trait UserStatusCallback: Send + Sync + 'static { Ok(()) } - async fn did_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { + /// Fires when an authentication token has expired. + async fn on_token_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { Ok(()) } - async fn open_workspace( + + /// Fires when a workspace is opened by the user. + async fn on_workspace_opened( &self, _user_id: i64, _user_workspace: &UserWorkspace, @@ -325,9 +328,9 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } - fn did_update_network(&self, _reachable: bool) {} - fn did_update_plans(&self, _plans: Vec) {} - fn did_update_storage_limitation(&self, _can_write: bool) {} + fn on_network_status_changed(&self, _reachable: bool) {} + fn on_subscription_plans_updated(&self, _plans: Vec) {} + fn on_storage_permission_updated(&self, _can_write: bool) {} } /// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function 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 9520489771..f4c09d0af8 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,7 +1,7 @@ use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::{internal_error, ErrorCode, FlowyResult}; +use flowy_error::{internal_error, FlowyResult}; use arc_swap::ArcSwapOption; use collab::lock::RwLock; @@ -21,7 +21,6 @@ use serde_json::Value; use std::string::ToString; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; -use tokio::sync::Mutex; use tokio_stream::StreamExt; use tracing::{debug, error, event, info, instrument, warn}; use uuid::Uuid; @@ -40,7 +39,6 @@ use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; -use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; use flowy_user_pub::sql::*; @@ -53,7 +51,6 @@ pub struct UserManager { pub(crate) collab_builder: Weak, pub(crate) collab_interact: RwLock>, pub(crate) user_workspace_service: Arc, - auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, pub(crate) is_loading_awareness: Arc>, @@ -78,7 +75,6 @@ impl UserManager { user_status_callback, collab_builder, collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), - auth_process: Default::default(), authenticate_user, refresh_user_profile_since, user_workspace_service, @@ -270,7 +266,7 @@ impl UserManager { let _ = self.initial_user_awareness(&session, &user.auth_type).await; user_status_callback - .did_init( + .on_launch_if_authenticated( user.uid, &cloud_config, &session.user_workspace, @@ -363,7 +359,7 @@ impl UserManager { .user_status_callback .read() .await - .did_sign_in( + .on_sign_in( user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, @@ -421,7 +417,7 @@ impl UserManager { .user_status_callback .read() .await - .did_sign_up( + .on_sign_up( response.is_new_user, new_user_profile, &new_session.user_workspace, 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 04e7e6b0d9..20acb9866f 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 @@ -195,7 +195,7 @@ impl UserManager { .user_status_callback .read() .await - .open_workspace(uid, &user_workspace, &user_profile.auth_type) + .on_workspace_opened(uid, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); @@ -532,7 +532,7 @@ impl UserManager { .user_status_callback .read() .await - .did_update_storage_limitation(can_write); + .on_storage_permission_updated(can_write); Ok(workspace_usage) } @@ -693,7 +693,7 @@ impl UserManager { .user_status_callback .read() .await - .did_update_plans(plans); + .on_subscription_plans_updated(plans); Ok(()) } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs index 3ce66227c5..23c050c1f2 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs @@ -3,6 +3,5 @@ pub(crate) mod manager_history_user; pub(crate) mod manager_user_awareness; pub(crate) mod manager_user_encryption; pub(crate) mod manager_user_workspace; -mod user_login_state; pub use manager::*; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs deleted file mode 100644 index ed7fdacf8d..0000000000 --- a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::migrations::AnonUser; -use flowy_user_pub::entities::{AuthResponse, AuthType, UserProfile}; - -/// recording the intermediate state of the sign-in/sign-up process -#[derive(Clone)] -pub struct UserAuthProcess { - pub user_profile: UserProfile, - pub response: AuthResponse, - pub authenticator: AuthType, - pub migration_user: Option, -} From 747a63d452507c201d2d0007a8fd2868f8cce9fc Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 14:33:05 +0800 Subject: [PATCH 42/74] chore: create user workspace for anon user --- .../home/mobile_home_page_header.dart | 2 +- .../application/user/user_workspace_bloc.dart | 12 ++-- .../workspace/_sidebar_workspace_menu.dart | 2 +- .../user/af_cloud_test/workspace_test.rs | 23 +++++--- .../src/local_server/impls/user.rs | 4 +- .../up.sql | 4 +- frontend/rust-lib/flowy-sqlite/src/schema.rs | 26 ++++----- .../flowy-user-pub/src/sql/workspace_sql.rs | 12 ++-- .../flowy-user/src/entities/user_profile.rs | 6 +- .../src/migrations/anon_user_workspace.rs | 58 +++++++++++++++++++ .../src/migrations/doc_key_with_workspace.rs | 2 + .../src/migrations/document_empty_content.rs | 2 + .../flowy-user/src/migrations/migration.rs | 5 +- .../rust-lib/flowy-user/src/migrations/mod.rs | 1 + .../migrations/workspace_and_favorite_v1.rs | 2 + .../src/migrations/workspace_trash_v1.rs | 2 + .../flowy-user/src/user_manager/manager.rs | 2 + .../user_manager/manager_user_workspace.rs | 8 +-- 18 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 4a1df63740..113f12e543 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -194,7 +194,7 @@ class _MobileWorkspace extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.authType, + workspace.workspaceAuthType, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 5ed56b890e..0e0b912a08 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -54,7 +54,7 @@ class UserWorkspaceBloc extends Bloc { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); await _userService.openWorkspace( currentWorkspace.workspaceId, - currentWorkspace.authType, + currentWorkspace.workspaceAuthType, ); } @@ -92,7 +92,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( currentWorkspace.workspaceId, - currentWorkspace.authType, + currentWorkspace.workspaceAuthType, ), ); } @@ -132,7 +132,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( s.workspaceId, - s.authType, + s.workspaceAuthType, ), ); }) @@ -190,7 +190,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } @@ -203,7 +203,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } @@ -369,7 +369,7 @@ class UserWorkspaceBloc extends Bloc { add( OpenWorkspace( workspaces.first.workspaceId, - workspaces.first.authType, + workspaces.first.workspaceAuthType, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 097da2c2ae..4ff5ccbf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -309,7 +309,7 @@ class _WorkspaceInfo extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.authType, + workspace.workspaceAuthType, ), ); 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 ea04d922fc..3bb71ea0dc 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 @@ -90,7 +90,10 @@ async fn af_cloud_create_workspace_test() { { // after opening new workspace test - .open_workspace(&created_workspace.workspace_id, created_workspace.auth_type) + .open_workspace( + &created_workspace.workspace_id, + created_workspace.workspace_auth_type, + ) .await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); @@ -124,7 +127,10 @@ async fn af_cloud_open_workspace_test() { .create_workspace("second workspace", AuthType::AppFlowyCloud) .await; test - .open_workspace(&user_workspace.workspace_id, user_workspace.auth_type) + .open_workspace( + &user_workspace.workspace_id, + user_workspace.workspace_auth_type, + ) .await; let second_workspace = test.get_current_workspace().await; let second_workspace = test.get_user_workspace(&second_workspace.id).await; @@ -144,7 +150,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.auth_type.clone(), + first_workspace.workspace_auth_type.clone(), ) .await; sleep(Duration::from_millis(300)).await; @@ -155,7 +161,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.auth_type.clone(), + second_workspace.workspace_auth_type.clone(), ) .await; sleep(Duration::from_millis(200)).await; @@ -168,7 +174,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.auth_type.clone(), + first_workspace.workspace_auth_type.clone(), ) .await; let views_1 = test.get_all_workspace_views().await; @@ -180,7 +186,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.auth_type.clone(), + second_workspace.workspace_auth_type.clone(), ) .await; let views_2 = test.get_all_workspace_views().await; @@ -239,7 +245,10 @@ async fn af_cloud_different_open_same_workspace_test() { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; client - .open_workspace(iter_workspace_id, all_workspaces[index].auth_type.clone()) + .open_workspace( + iter_workspace_id, + all_workspaces[index].workspace_auth_type.clone(), + ) .await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; 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 e73943bbbe..512ad90a22 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 @@ -133,9 +133,9 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn open_workspace(&self, workspace_id: &Uuid) -> Result { let uid = self.user.user_id()?; - let conn = self.user.get_sqlite_db(uid)?; + let mut conn = self.user.get_sqlite_db(uid)?; - let workspace = select_user_workspace(&workspace_id.to_string(), conn)?; + let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; Ok(UserWorkspace::from(workspace)) } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql index b77372ad63..7d986e3e57 100644 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql @@ -1,10 +1,10 @@ -- Your SQL goes here ALTER TABLE user_workspace_table - ADD COLUMN auth_type INTEGER NOT NULL DEFAULT 1; + ADD COLUMN workspace_type INTEGER NOT NULL DEFAULT 1; -- 2. Back‑fill from user_table.auth_type UPDATE user_workspace_table -SET auth_type = (SELECT ut.auth_type +SET workspace_type = (SELECT ut.auth_type FROM user_table ut WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) WHERE EXISTS (SELECT 1 diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 28a83ed449..eb6b248ea6 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -107,7 +107,7 @@ diesel::table! { icon -> Text, member_count -> BigInt, role -> Nullable, - auth_type -> Integer, + workspace_type -> Integer, } } @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index cb6d299039..709e218514 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -17,7 +17,7 @@ pub struct UserWorkspaceTable { pub icon: String, pub member_count: i64, pub role: Option, - pub auth_type: i32, + pub workspace_type: i32, } #[derive(AsChangeset, Identifiable, Default, Debug)] @@ -50,18 +50,18 @@ impl UserWorkspaceTable { icon: workspace.icon.clone(), member_count: workspace.member_count, role: workspace.role.clone().map(|v| v as i32), - auth_type: auth_type as i32, + workspace_type: auth_type as i32, }) } } pub fn select_user_workspace( workspace_id: &str, - mut conn: DBConnection, + conn: &mut SqliteConnection, ) -> FlowyResult { let row = user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn)?; + .first::(conn)?; Ok(row) } @@ -106,7 +106,7 @@ pub fn upsert_user_workspace( user_workspace_table::icon.eq(row.icon), user_workspace_table::member_count.eq(row.member_count), user_workspace_table::role.eq(row.role), - user_workspace_table::auth_type.eq(row.auth_type), + user_workspace_table::workspace_type.eq(row.workspace_type), )) .execute(conn)?; @@ -153,7 +153,7 @@ pub fn delete_user_all_workspace( let n = diesel::delete( user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::auth_type.eq(auth_type as i32)), + .filter(user_workspace_table::workspace_type.eq(auth_type as i32)), ) .execute(conn)?; info!( 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 5afc93850a..6a95a89041 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -186,7 +186,7 @@ pub struct UserWorkspacePB { pub role: Option, #[pb(index = 7)] - pub auth_type: AuthTypePB, + pub workspace_auth_type: AuthTypePB, } impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { @@ -198,7 +198,7 @@ impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { icon: value.1.icon, member_count: value.1.member_count, role: value.1.role.map(AFRolePB::from), - auth_type: AuthTypePB::from(value.0), + workspace_auth_type: AuthTypePB::from(value.0), } } } @@ -212,7 +212,7 @@ impl From for UserWorkspacePB { icon: value.icon, member_count: value.member_count, role: value.role.map(AFRolePB::from), - auth_type: AuthTypePB::from(value.auth_type), + workspace_auth_type: AuthTypePB::from(value.workspace_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs new file mode 100644 index 0000000000..7c806d3aaf --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs @@ -0,0 +1,58 @@ +use diesel::SqliteConnection; +use semver::Version; +use std::sync::Arc; +use tracing::{info, instrument}; + +use collab_integrate::CollabKVDB; +use flowy_error::FlowyResult; +use flowy_user_pub::entities::AuthType; + +use crate::migrations::migration::UserDataMigration; +use flowy_user_pub::session::Session; +use flowy_user_pub::sql::{select_user_workspace, upsert_user_workspace}; + +pub struct AnonUserWorkspaceTableMigration; + +impl UserDataMigration for AnonUserWorkspaceTableMigration { + fn name(&self) -> &str { + "anon_user_workspace_table_migration" + } + + fn run_when( + &self, + first_installed_version: &Option, + _current_version: &Version, + ) -> bool { + match first_installed_version { + None => true, + Some(version) => version <= &Version::new(0, 8, 10), + } + } + + #[instrument(name = "AnonUserWorkspaceTableMigration", skip_all, err)] + fn run( + &self, + session: &Session, + _collab_db: &Arc, + auth_type: &AuthType, + db: &mut SqliteConnection, + ) -> FlowyResult<()> { + // For historical reason, anon user doesn't have a workspace in user_workspace_table. + // So we need to create a new entry for the anon user in the user_workspace_table. + if matches!(auth_type, AuthType::Local) { + let user_workspace = &session.user_workspace; + let result = select_user_workspace(&user_workspace.id, db); + if let Err(e) = result { + if e.is_record_not_found() { + info!( + "Anon user workspace not found in the database, creating a new entry for user_id: {}", + session.user_id + ); + upsert_user_workspace(session.user_id, *auth_type, user_workspace.clone(), db)?; + } + } + } + + Ok(()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 5a8bb4d516..84acc0b56a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_plugins::local_storage::kv::doc::migrate_old_keys; use collab_plugins::local_storage::kv::KVTransactionDB; +use diesel::SqliteConnection; use semver::Version; use tracing::{instrument, trace}; @@ -40,6 +41,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { trace!( "migrate key with workspace id:{}", 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 ab828e18d8..2e4581f7ec 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 @@ -6,6 +6,7 @@ use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::{Folder, View}; use collab_plugins::local_storage::kv::KVTransactionDB; +use diesel::SqliteConnection; use semver::Version; use tracing::{event, instrument}; @@ -42,6 +43,7 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { session: &Session, collab_db: &Arc, authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 1330d1995b..0f5c2c2624 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - authenticator: &AuthType, + auth_type: &AuthType, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -75,7 +75,7 @@ impl UserLocalDataMigration { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - migration.run(&self.session, &self.collab_db, authenticator)?; + migration.run(&self.session, &self.collab_db, auth_type, &mut conn)?; applied_migrations.push(migration.name().to_string()); save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); @@ -99,6 +99,7 @@ pub trait UserDataMigration { user: &Session, collab_db: &Arc, authenticator: &AuthType, + db: &mut SqliteConnection, ) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index c8d04edf66..3d87dc595f 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,6 +1,7 @@ use flowy_user_pub::session::Session; use std::sync::Arc; +pub mod anon_user_workspace; pub mod doc_key_with_workspace; pub mod document_empty_content; pub mod migration; 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 51894f9a04..5f14051e26 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 @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use diesel::SqliteConnection; use semver::Version; use tracing::instrument; @@ -40,6 +41,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( 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 70123edb2c..b5eeead8c6 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 @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use diesel::SqliteConnection; use semver::Version; use tracing::instrument; @@ -38,6 +39,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { session: &Session, collab_db: &Arc, _authenticator: &AuthType, + _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( 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 f4c09d0af8..4299f0823b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -38,6 +38,7 @@ use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; +use crate::migrations::anon_user_workspace::AnonUserWorkspaceTableMigration; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; @@ -849,6 +850,7 @@ fn collab_migration_list() -> Vec> { Box::new(FavoriteV1AndWorkspaceArrayMigration), Box::new(WorkspaceTrashMapToSectionMigration), Box::new(CollabDocKeyWithWorkspaceIdMigration), + Box::new(AnonUserWorkspaceTableMigration), ] } 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 20acb9866f..b68643c83b 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,8 +157,8 @@ impl UserManager { self.cloud_service.set_server_auth_type(&auth_type); let uid = self.user_id()?; - let conn = self.db_connection(self.user_id()?)?; - let user_workspace = match select_user_workspace(&workspace_id.to_string(), conn) { + let mut conn = self.db_connection(self.user_id()?)?; + let user_workspace = match select_user_workspace(&workspace_id.to_string(), &mut conn) { Err(err) => { if err.is_record_not_found() { sync_workspace( @@ -401,8 +401,8 @@ impl UserManager { uid: i64, workspace_id: &Uuid, ) -> FlowyResult { - let conn = self.db_connection(uid)?; - select_user_workspace(workspace_id.to_string().as_str(), conn) + let mut conn = self.db_connection(uid)?; + select_user_workspace(workspace_id.to_string().as_str(), &mut conn) } pub async fn get_all_user_workspaces( From fd581b44535894540c5e9e2a2f6240cb47cf0642 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 14:37:21 +0800 Subject: [PATCH 43/74] chore: clippy --- frontend/rust-lib/flowy-sqlite/src/schema.rs | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index eb6b248ea6..f91d187b75 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -132,16 +132,16 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, - chat_message_table, - chat_table, - collab_snapshot, - upload_file_part, - upload_file_table, - user_data_migration_records, - user_table, - user_workspace_table, - workspace_members_table, - workspace_setting_table, + af_collab_metadata, + chat_local_setting_table, + chat_message_table, + chat_table, + collab_snapshot, + upload_file_part, + upload_file_table, + user_data_migration_records, + user_table, + user_workspace_table, + workspace_members_table, + workspace_setting_table, ); From fa798f3ecd95a5f98d1e0891bb68abac00f7b6a2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 15:54:37 +0800 Subject: [PATCH 44/74] chore: workspace usage --- .../workspace/_sidebar_workspace_menu.dart | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 5 + .../flowy-server/src/af_cloud/define.rs | 4 + .../src/local_server/impls/chat.rs | 34 ++-- .../src/local_server/impls/folder.rs | 7 +- .../src/local_server/impls/user.rs | 153 ++++++++++++++++-- .../flowy-server/src/local_server/server.rs | 14 +- .../flowy-server/tests/af_cloud_test/util.rs | 10 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 21 +-- .../flowy-user-pub/src/sql/member_sql.rs | 18 ++- .../flowy-user-pub/src/sql/user_sql.rs | 18 ++- .../src/sql/workspace_setting_sql.rs | 8 +- .../data_import/appflowy_data_import.rs | 2 +- .../rust-lib/flowy-user/src/services/db.rs | 4 +- .../flowy-user/src/user_manager/manager.rs | 21 ++- .../user_manager/manager_user_workspace.rs | 4 +- 16 files changed, 241 insertions(+), 84 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 4ff5ccbf67..04eb701a66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Server, + AuthTypePB.Local, ), ); }, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 7e6d477407..893a010a18 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused_doc_comments)] use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use collab_plugins::CollabKVDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; @@ -342,6 +343,10 @@ impl LoggedUser for ServerUserImpl { self.upgrade_user()?.get_sqlite_connection(uid) } + fn get_collab_db(&self, uid: i64) -> Result, FlowyError> { + self.upgrade_user()?.get_collab_db(uid) + } + fn application_root_dir(&self) -> Result { Ok(PathBuf::from( self.upgrade_user()?.get_application_root_dir(), diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index a93066054d..65808e5b6b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,3 +1,4 @@ +use collab_plugins::CollabKVDB; use flowy_ai::ai_manager::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; @@ -21,6 +22,9 @@ pub trait LoggedUser: Send + Sync { async fn is_local_mode(&self) -> FlowyResult; fn get_sqlite_db(&self, uid: i64) -> Result; + + fn get_collab_db(&self, uid: i64) -> Result, FlowyError>; + fn application_root_dir(&self) -> Result; } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index f56a8d6e8b..3af87eb18e 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -28,14 +28,14 @@ use tracing::trace; use uuid::Uuid; pub struct LocalChatServiceImpl { - pub user: Arc, + pub logged_user: Arc, pub local_ai: Arc, } impl LocalChatServiceImpl { fn get_message_content(&self, message_id: i64) -> FlowyResult { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let content = select_message_content(db, message_id)?.ok_or_else(|| { FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) })?; @@ -43,8 +43,8 @@ impl LocalChatServiceImpl { } async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let conn = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let conn = self.logged_user.get_sqlite_db(uid)?; let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); upsert_chat_messages(conn, &[row])?; Ok(()) @@ -62,8 +62,8 @@ impl ChatCloudService for LocalChatServiceImpl { _name: &str, metadata: Value, ) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); upsert_chat(db, &row)?; Ok(()) @@ -139,8 +139,8 @@ impl ChatCloudService for LocalChatServiceImpl { chat_id: &Uuid, question_id: i64, ) -> Result { - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { None => Err(FlowyError::record_not_found()), @@ -156,8 +156,8 @@ impl ChatCloudService for LocalChatServiceImpl { limit: u64, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let result = select_chat_messages(db, &chat_id, limit, offset)?; let messages = result @@ -180,8 +180,8 @@ impl ChatCloudService for LocalChatServiceImpl { answer_message_id: i64, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? .map(chat_message_from_row) .ok_or_else(FlowyError::record_not_found)?; @@ -278,8 +278,8 @@ impl ChatCloudService for LocalChatServiceImpl { chat_id: &Uuid, ) -> Result { let chat_id = chat_id.to_string(); - let uid = self.user.user_id()?; - let db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let db = self.logged_user.get_sqlite_db(uid)?; let row = read_chat(db, &chat_id)?; let rag_ids = deserialize_rag_ids(&row.rag_ids); let metadata = deserialize_chat_metadata::(&row.metadata); @@ -298,8 +298,8 @@ impl ChatCloudService for LocalChatServiceImpl { id: &Uuid, s: UpdateChatParams, ) -> Result<(), FlowyError> { - let uid = self.user.user_id()?; - let mut db = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let mut db = self.logged_user.get_sqlite_db(uid)?; let changeset = ChatTableChangeset { chat_id: id.to_string(), name: s.name, 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 72bab514ec..1acb0846c9 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 @@ -1,5 +1,6 @@ #![allow(unused_variables)] +use crate::af_cloud::define::LoggedUser; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab_entity::CollabType; @@ -10,9 +11,13 @@ use flowy_folder_pub::cloud::{ }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerFolderCloudServiceImpl; +pub(crate) struct LocalServerFolderCloudServiceImpl { + #[allow(dead_code)] + pub logged_user: Arc, +} #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { 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 512ad90a22..bd6e930584 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 @@ -1,32 +1,38 @@ #![allow(unused_variables)] +use crate::af_cloud::define::LoggedUser; +use crate::local_server::uid::UserIDGenerator; use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; -use lazy_static::lazy_static; -use std::sync::Arc; -use tokio::sync::Mutex; -use uuid::Uuid; - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::uid::UserIDGenerator; +use flowy_ai_pub::cloud::billing_dto::WorkspaceUsageAndLimit; +use flowy_ai_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::{select_all_user_workspace, select_user_profile, select_user_workspace}; +use flowy_user_pub::sql::{ + select_all_user_workspace, select_user_profile, select_user_workspace, select_workspace_member, + select_workspace_setting, update_user_profile, update_workspace_setting, upsert_workspace_member, + upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, WorkspaceSettingsChangeset, + WorkspaceSettingsTable, +}; use flowy_user_pub::DEFAULT_USER_NAME; +use lazy_static::lazy_static; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; use lib_infra::util::timestamp; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; lazy_static! { static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } pub(crate) struct LocalServerUserServiceImpl { - pub user: Arc, + pub logged_user: Arc, } #[async_trait] @@ -121,26 +127,30 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - async fn update_user(&self, _params: UpdateUserProfileParams) -> Result<(), FlowyError> { + async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let changeset = UserTableChangeset::new(params); + update_user_profile(&mut conn, changeset)?; Ok(()) } async fn get_user_profile(&self, uid: i64) -> Result { - let conn = self.user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, conn)?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, &mut conn)?; Ok(profile) } async fn open_workspace(&self, workspace_id: &Uuid) -> Result { - let uid = self.user.user_id()?; - let mut conn = self.user.get_sqlite_db(uid)?; + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; Ok(UserWorkspace::from(workspace)) } async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { - let conn = self.user.get_sqlite_db(uid)?; + let conn = self.logged_user.get_sqlite_db(uid)?; let workspaces = select_all_user_workspace(uid, conn)?; Ok(workspaces) } @@ -198,4 +208,117 @@ impl UserCloudService for LocalServerUserServiceImpl { ) -> Result<(), FlowyError> { Ok(()) } + + async fn get_workspace_member_info( + &self, + workspace_id: &Uuid, + uid: i64, + ) -> Result { + // For local server, only current user is the member + let conn = self.logged_user.get_sqlite_db(uid)?; + let result = select_workspace_member(conn, &workspace_id.to_string(), uid); + + match result { + Ok(row) => Ok(WorkspaceMember::from(row)), + Err(err) => { + if err.is_record_not_found() { + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let profile = select_user_profile(uid, &mut conn)?; + let row = WorkspaceMemberTable { + email: profile.email.to_string(), + role: 0, + name: profile.name.to_string(), + avatar_url: Some(profile.icon_url), + uid, + workspace_id: workspace_id.to_string(), + updated_at: Default::default(), + }; + + let member = WorkspaceMember::from(row.clone()); + upsert_workspace_member(&mut conn, row)?; + Ok(member) + } else { + Err(err) + } + }, + } + } + + async fn get_workspace_usage( + &self, + workspace_id: &Uuid, + ) -> Result { + Ok(WorkspaceUsageAndLimit { + member_count: 1, + member_count_limit: 1, + storage_bytes: i64::MAX, + storage_bytes_limit: i64::MAX, + storage_bytes_unlimited: true, + single_upload_limit: i64::MAX, + single_upload_unlimited: true, + ai_responses_count: i64::MAX, + ai_responses_count_limit: i64::MAX, + ai_image_responses_count: i64::MAX, + ai_image_responses_count_limit: 0, + local_ai: true, + ai_responses_unlimited: true, + }) + } + + async fn get_workspace_setting( + &self, + workspace_id: &Uuid, + ) -> Result { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + + // By default, workspace setting is existed in local server + let result = select_workspace_setting(&mut conn, &workspace_id.to_string()); + match result { + Ok(row) => Ok(AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model, + }), + Err(err) => { + if err.is_record_not_found() { + let row = WorkspaceSettingsTable { + id: workspace_id.to_string(), + disable_search_indexing: false, + ai_model: "".to_string(), + }; + let setting = AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model.clone(), + }; + upsert_workspace_setting(&mut conn, row)?; + Ok(setting) + } else { + Err(err) + } + }, + } + } + + async fn update_workspace_setting( + &self, + workspace_id: &Uuid, + workspace_settings: AFWorkspaceSettingsChange, + ) -> Result { + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + + let changeset = WorkspaceSettingsChangeset { + id: workspace_id.to_string(), + disable_search_indexing: workspace_settings.disable_search_indexing, + ai_model: workspace_settings.ai_model, + }; + + update_workspace_setting(&mut conn, changeset)?; + let row = select_workspace_setting(&mut conn, &workspace_id.to_string())?; + + Ok(AFWorkspaceSettings { + disable_search_indexing: row.disable_search_indexing, + ai_model: row.ai_model, + }) + } } 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 8ce0d86221..8ddcb2d76c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -17,15 +17,15 @@ use flowy_user_pub::cloud::UserCloudService; use tokio::sync::mpsc; pub struct LocalServer { - user: Arc, + logged_user: Arc, local_ai: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(user: Arc, local_ai: Arc) -> Self { + pub fn new(logged_user: Arc, local_ai: Arc) -> Self { Self { - user, + logged_user, local_ai, stop_tx: Default::default(), } @@ -42,12 +42,14 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { Arc::new(LocalServerUserServiceImpl { - user: self.user.clone(), + logged_user: self.logged_user.clone(), }) } fn folder_service(&self) -> Arc { - Arc::new(LocalServerFolderCloudServiceImpl) + Arc::new(LocalServerFolderCloudServiceImpl { + logged_user: self.logged_user.clone(), + }) } fn database_service(&self) -> Arc { @@ -64,7 +66,7 @@ impl AppFlowyServer for LocalServer { fn chat_service(&self) -> Arc { Arc::new(LocalChatServiceImpl { - user: self.user.clone(), + logged_user: self.logged_user.clone(), local_ai: self.local_ai.clone(), }) } 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 249ff9136d..7e38f423cc 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 @@ -1,10 +1,10 @@ use client_api::ClientConfiguration; +use collab_plugins::CollabKVDB; +use flowy_error::{FlowyError, FlowyResult}; use semver::Version; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::Arc; - -use flowy_error::{FlowyError, FlowyResult}; +use std::sync::{Arc, Weak}; use uuid::Uuid; use crate::setup_log; @@ -61,6 +61,10 @@ impl LoggedUser for FakeServerUserImpl { todo!() } + fn get_collab_db(&self, _uid: i64) -> Result, FlowyError> { + todo!() + } + fn application_root_dir(&self) -> Result { todo!() } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 0964d80472..b15299b194 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -285,10 +285,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, workspace_id: &Uuid, uid: i64, - ) -> Result { - Err(FlowyError::not_support()) - } - + ) -> Result; /// Get all subscriptions for all workspaces for a user (email) async fn get_workspace_subscriptions( &self, @@ -323,9 +320,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_usage( &self, workspace_id: &Uuid, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; async fn get_billing_portal_url(&self) -> Result { Err(FlowyError::not_support()) @@ -337,27 +332,23 @@ pub trait UserCloudService: Send + Sync + 'static { plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } async fn get_subscription_plan_details(&self) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Ok(vec![]) } async fn get_workspace_setting( &self, workspace_id: &Uuid, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; async fn update_workspace_setting( &self, workspace_id: &Uuid, workspace_settings: AFWorkspaceSettingsChange, - ) -> Result { - Err(FlowyError::not_support()) - } + ) -> Result; } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs index bc73a8f34c..58ca65e732 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs @@ -1,10 +1,11 @@ +use crate::entities::{Role, WorkspaceMember}; use diesel::{insert_into, RunQueryDsl}; use flowy_error::FlowyResult; use flowy_sqlite::schema::workspace_members_table; use flowy_sqlite::schema::workspace_members_table::dsl; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; -#[derive(Queryable, Insertable, AsChangeset, Debug)] +#[derive(Queryable, Insertable, AsChangeset, Debug, Clone)] #[diesel(table_name = workspace_members_table)] #[diesel(primary_key(email, workspace_id))] pub struct WorkspaceMemberTable { @@ -17,8 +18,19 @@ pub struct WorkspaceMemberTable { pub updated_at: chrono::NaiveDateTime, } +impl From for WorkspaceMember { + fn from(value: WorkspaceMemberTable) -> Self { + Self { + email: value.email, + role: Role::from(value.role), + name: value.name, + avatar_url: value.avatar_url, + } + } +} + pub fn upsert_workspace_member>( - mut conn: DBConnection, + conn: &mut SqliteConnection, member: T, ) -> FlowyResult<()> { let member = member.into(); @@ -31,7 +43,7 @@ pub fn upsert_workspace_member>( )) .do_update() .set(&member) - .execute(&mut conn)?; + .execute(conn)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 5a910888a8..4cdf26520e 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -92,10 +92,24 @@ impl From for UserTableChangeset { } } -pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result { +pub fn update_user_profile( + conn: &mut SqliteConnection, + changeset: UserTableChangeset, +) -> Result<(), FlowyError> { + let user_id = changeset.id.clone(); + update(user_table::dsl::user_table.filter(user_table::id.eq(&user_id))) + .set(changeset) + .execute(conn)?; + Ok(()) +} + +pub fn select_user_profile( + uid: i64, + conn: &mut SqliteConnection, +) -> Result { let user: UserProfile = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) - .first::(&mut *conn) + .first::(conn) .map_err(|err| { FlowyError::record_not_found().with_context(format!( "Can't find the user profile for user id: {}, error: {:?}", diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs index 667d1f0ca0..7eeafaf1e4 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs @@ -45,7 +45,7 @@ pub fn update_workspace_setting( /// Upserts a workspace setting into the database. pub fn upsert_workspace_setting( - conn: &mut DBConnection, + conn: &mut SqliteConnection, settings: WorkspaceSettingsTable, ) -> Result<(), FlowyError> { diesel::insert_into(dsl::workspace_setting_table) @@ -62,11 +62,11 @@ pub fn upsert_workspace_setting( /// Selects a workspace setting by id from the database. pub fn select_workspace_setting( - conn: &mut DBConnection, - id: &str, + conn: &mut SqliteConnection, + workspace_id: &str, ) -> Result { let setting = dsl::workspace_setting_table - .filter(workspace_setting_table::id.eq(id)) + .filter(workspace_setting_table::id.eq(workspace_id)) .first::(conn)?; Ok(setting) } 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 2db5f418de..a4c7d3bd1d 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 @@ -103,7 +103,7 @@ pub(crate) fn prepare_import( ); let imported_user = select_user_profile( imported_session.user_id, - imported_sqlite_db.get_connection()?, + &mut *imported_sqlite_db.get_connection()?, )?; run_collab_data_migration( diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 138ad95819..3280370c88 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -126,8 +126,8 @@ impl UserDB { pool: &Arc, uid: i64, ) -> Result { - let conn = pool.get()?; - let profile = select_user_profile(uid, conn)?; + let mut conn = pool.get()?; + let profile = select_user_profile(uid, &mut conn)?; Ok(profile) } 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 4299f0823b..3b568f976e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,7 +1,7 @@ use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::{internal_error, FlowyResult}; +use flowy_error::FlowyResult; use arc_swap::ArcSwapOption; use collab::lock::RwLock; @@ -506,8 +506,12 @@ impl UserManager { self.db_connection(session.user_id)?, changeset, )?; + self + .cloud_service + .get_user_service()? + .update_user(params) + .await?; - self.update_user(params).await?; Ok(()) } @@ -539,7 +543,8 @@ impl UserManager { /// Fetches the user profile for the given user ID. pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { - select_user_profile(uid, self.db_connection(uid)?) + let mut conn = self.db_connection(uid)?; + select_user_profile(uid, &mut conn) } #[tracing::instrument(level = "info", skip_all, err)] @@ -625,14 +630,6 @@ impl UserManager { Ok(None) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { - let server = self.cloud_service.get_user_service()?; - tokio::spawn(async move { server.update_user(params).await }) - .await - .map_err(internal_error)??; - Ok(()) - } - async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { let conn = self.db_connection(uid)?; upsert_user(user, conn)?; @@ -816,7 +813,7 @@ pub fn upsert_user_profile_change( "Update user profile with changeset: {:?}", changeset ); - diesel_update_table!(user_table, changeset, &mut *conn); + update_user_profile(&mut conn, changeset)?; let user: UserProfile = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) .first::(&mut *conn)? 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 b68643c83b..425b2351b0 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 @@ -668,8 +668,8 @@ impl UserManager { updated_at: Utc::now().naive_utc(), }; - let db = self.authenticate_user.get_sqlite_connection(uid)?; - upsert_workspace_member(db, record)?; + let mut db = self.authenticate_user.get_sqlite_connection(uid)?; + upsert_workspace_member(&mut db, record)?; Ok(member) } From 791a79a2341b3744727e1a8378b97f2701a39902 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 17:29:27 +0800 Subject: [PATCH 45/74] chore: impl local unspport --- .../workspace/_sidebar_workspace_menu.dart | 2 +- frontend/rust-lib/Cargo.lock | 16 +++---- frontend/rust-lib/Cargo.toml | 16 +++---- .../src/local_server/impls/database.rs | 42 +++++++++-------- .../src/local_server/impls/folder.rs | 29 +++++++++++- .../flowy-server/src/local_server/mod.rs | 1 + .../flowy-server/src/local_server/server.rs | 4 +- .../flowy-server/src/local_server/util.rs | 45 +++++++++++++++++++ 8 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 frontend/rust-lib/flowy-server/src/local_server/util.rs diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 04eb701a66..4ff5ccbf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index dcac892c6a..4a3fae985a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "bytes", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "arc-swap", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-recursion", @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "async-stream", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1920e21f47e88a238e11356be0b3ef2f3acdc23e#1920e21f47e88a238e11356be0b3ef2f3acdc23e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 61259d1e7b..1561c7ea7d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -144,14 +144,14 @@ 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 = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1920e21f47e88a238e11356be0b3ef2f3acdc23e" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } # Working directory: frontend # To update the commit ID, run: 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 46b0cdd649..ad1184a09a 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,16 +1,18 @@ #![allow(unused_variables)] + +use crate::af_cloud::define::LoggedUser; +use crate::local_server::util::default_encode_collab_for_collab_type; use collab::entity::EncodedCollab; -use collab_database::database::default_database_data; -use collab_database::workspace_database::default_workspace_database_data; -use collab_document::document_data::default_document_collab_data; use collab_entity::CollabType; -use collab_user::core::default_user_awareness_data; use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; -use flowy_error::FlowyError; +use flowy_error::{ErrorCode, FlowyError}; use lib_infra::async_trait::async_trait; +use std::sync::Arc; use uuid::Uuid; -pub(crate) struct LocalServerDatabaseCloudServiceImpl(); +pub(crate) struct LocalServerDatabaseCloudServiceImpl { + pub logged_user: Arc, +} #[async_trait] impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { @@ -18,24 +20,20 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { &self, object_id: &Uuid, collab_type: CollabType, - workspace_id: &Uuid, + _workspace_id: &Uuid, // underscore to silence “unused” warning ) -> Result, FlowyError> { + let uid = self.logged_user.user_id()?; let object_id = object_id.to_string(); - match collab_type { - CollabType::Document => { - let encode_collab = default_document_collab_data(&object_id)?; - Ok(Some(encode_collab)) - }, - CollabType::Database => default_database_data(&object_id) - .await - .map(Some) - .map_err(Into::into), - CollabType::WorkspaceDatabase => Ok(Some(default_workspace_database_data(&object_id))), - CollabType::Folder => Ok(None), - CollabType::DatabaseRow => Ok(None), - CollabType::UserAwareness => Ok(Some(default_user_awareness_data(&object_id))), - CollabType::Unknown => Ok(None), - } + default_encode_collab_for_collab_type(uid, &object_id, collab_type) + .await + .map(Some) + .or_else(|err| { + if matches!(err.code, ErrorCode::NotSupportYet) { + Ok(None) + } else { + Err(err) + } + }) } async fn create_database_encode_collab( 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 1acb0846c9..7f4720de99 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 @@ -1,9 +1,14 @@ #![allow(unused_variables)] use crate::af_cloud::define::LoggedUser; +use crate::local_server::util::default_encode_collab_for_collab_type; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; +use collab::core::origin::CollabOrigin; +use collab::preclude::Collab; use collab_entity::CollabType; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, @@ -61,7 +66,29 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { collab_type: CollabType, object_id: &Uuid, ) -> Result, FlowyError> { - Err(FlowyError::local_version_not_support()) + let object_id = object_id.to_string(); + let workspace_id = workspace_id.to_string(); + let collab_db = self.logged_user.get_collab_db(uid)?.upgrade().unwrap(); + let read_txn = collab_db.read_txn(); + let is_exist = read_txn.is_exist(uid, &workspace_id.to_string(), &object_id.to_string()); + if is_exist { + // load doc + let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); + read_txn.load_doc(uid, &workspace_id, &object_id, collab.doc())?; + let data = collab.encode_collab_v1(|c| { + collab_type + .validate_require_data(c) + .map_err(|err| FlowyError::invalid_data().with_context(err))?; + Ok::<_, FlowyError>(()) + })?; + Ok(data.doc_state.to_vec()) + } else { + let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; + drop(read_txn); + + // create default folder doc + Err(FlowyError::local_version_not_support()) + } } async fn batch_create_folder_collab_objects( diff --git a/frontend/rust-lib/flowy-server/src/local_server/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/mod.rs index 6e67356fd9..2b9fe07250 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/mod.rs @@ -3,3 +3,4 @@ pub use server::*; pub mod impls; mod server; pub(crate) mod uid; +mod util; 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 8ddcb2d76c..9ce19f5df6 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -53,7 +53,9 @@ impl AppFlowyServer for LocalServer { } fn database_service(&self) -> Arc { - Arc::new(LocalServerDatabaseCloudServiceImpl()) + Arc::new(LocalServerDatabaseCloudServiceImpl { + logged_user: self.logged_user.clone(), + }) } fn database_ai_service(&self) -> Option> { diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs new file mode 100644 index 0000000000..bd00212afb --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/util.rs @@ -0,0 +1,45 @@ +use collab::core::origin::CollabOrigin; +use collab::entity::EncodedCollab; +use collab::preclude::Collab; +use collab_database::database::default_database_data; +use collab_database::workspace_database::default_workspace_database_data; +use collab_document::document_data::default_document_collab_data; +use collab_entity::CollabType; +use collab_user::core::default_user_awareness_data; +use flowy_error::{FlowyError, FlowyResult}; + +pub async fn default_encode_collab_for_collab_type( + _uid: i64, + object_id: &str, + collab_type: CollabType, +) -> FlowyResult { + match collab_type { + CollabType::Document => { + let encode_collab = default_document_collab_data(object_id)?; + Ok(encode_collab) + }, + CollabType::Database => default_database_data(object_id).await.map_err(Into::into), + CollabType::WorkspaceDatabase => Ok(default_workspace_database_data(object_id)), + CollabType::Folder => { + // let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); + // let workspace = Workspace::new(object_id.to_string(), "".to_string(), uid); + // let folder_data = FolderData::new(workspace); + // let folder = Folder::create(uid, collab, None, folder_data); + // let data = folder.encode_collab_v1(|c| { + // collab_type + // .validate_require_data(c) + // .map_err(|err| FlowyError::invalid_data().with_context(err))?; + // Ok::<_, FlowyError>(()) + // })?; + // Ok(data) + Err(FlowyError::not_support()) + }, + CollabType::DatabaseRow => Err(FlowyError::not_support()), + CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), + CollabType::Unknown => { + let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); + let data = collab.encode_collab_v1(|_| Ok::<_, FlowyError>(()))?; + Ok(data) + }, + } +} From 92d5690bba8e417ee11aa34c8a72b36886e80134 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 18:07:02 +0800 Subject: [PATCH 46/74] chore: pass folder init data --- frontend/rust-lib/Cargo.lock | 1 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 1 + .../flowy-core/src/user_state_callback.rs | 117 ++++++++++++------ frontend/rust-lib/flowy-folder/Cargo.toml | 1 + frontend/rust-lib/flowy-folder/src/manager.rs | 54 +++----- .../src/local_server/impls/folder.rs | 4 +- .../rust-lib/flowy-storage/src/manager.rs | 2 +- frontend/rust-lib/flowy-user/src/event_map.rs | 7 +- .../user_manager/manager_user_workspace.rs | 2 +- 10 files changed, 106 insertions(+), 85 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4a3fae985a..51a3f1a3b2 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2835,6 +2835,7 @@ dependencies = [ "flowy-notification", "flowy-search-pub", "flowy-sqlite", + "flowy-user-pub", "futures", "lazy_static", "lib-dispatch", diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 399a8d2d5d..9055341b99 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -131,7 +131,7 @@ impl AIManager { #[instrument(skip_all, err)] pub async fn initialize_after_open_workspace( &self, - _workspace_id: &str, + _workspace_id: &Uuid, ) -> Result<(), FlowyError> { let local_ai = self.local_ai.clone(); tokio::spawn(async move { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 893a010a18..c2800bd73b 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -253,6 +253,7 @@ impl AppFlowyCore { .await; let user_status_callback = UserStatusCallbackImpl { + user_manager: user_manager.clone(), collab_builder, folder_manager: folder_manager.clone(), database_manager: database_manager.clone(), diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index e43002d709..8db4e3f926 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -4,23 +4,27 @@ use anyhow::Context; use client_api::entity::billing_dto::SubscriptionPlan; use tracing::{error, event, info}; +use crate::server_layer::ServerProvider; use collab_entity::CollabType; use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use collab_plugins::local_storage::kv::doc::CollabKVAction; +use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; +use flowy_user::user_manager::UserManager; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::async_trait::async_trait; - -use crate::server_layer::ServerProvider; +use uuid::Uuid; pub(crate) struct UserStatusCallbackImpl { + pub(crate) user_manager: Arc, pub(crate) collab_builder: Arc, pub(crate) folder_manager: Arc, pub(crate) database_manager: Arc, @@ -42,6 +46,60 @@ impl UserStatusCallbackImpl { } }); } + + async fn folder_init_data_source( + &self, + user_id: i64, + workspace_id: &Uuid, + auth_type: &AuthType, + ) -> FlowyResult { + let is_exist = self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)?; + if is_exist { + Ok(FolderInitDataSource::LocalDisk { + create_if_not_exist: false, + }) + } else { + let data_source = match self + .folder_manager + .cloud_service + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) + .await + { + Ok(doc_state) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + }, + Err(err) => match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => { + return Err(err); + }, + }, + }; + Ok(data_source) + } + } + + fn is_object_exist_on_disk( + &self, + user_id: i64, + workspace_id: &Uuid, + object_id: &Uuid, + ) -> FlowyResult { + let db = self + .user_manager + .get_collab_db(user_id)? + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Collab db is not initialized"))?; + let read = db.read_txn(); + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); + Ok(read.is_exist(user_id, &workspace_id, &object_id)) + } } #[async_trait] @@ -101,9 +159,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); + let workspace_id = user_workspace.workspace_id()?; + let data_source = self + .folder_init_data_source(user_id, &workspace_id, auth_type) + .await?; self .folder_manager - .initialize_after_sign_in(user_id) + .initialize_after_sign_in(user_id, data_source) .await?; self .database_manager @@ -135,37 +197,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { device_id ); let workspace_id = user_workspace.workspace_id()?; - - // In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace - // is automatically created for them. However, for users who sign up through Supabase, the creation - // of the default workspace relies on the client-side operation. This means that the process - // for initializing a default workspace differs depending on the sign-up method used. - let data_source = match self - .folder_manager - .cloud_service - .get_folder_doc_state( - &workspace_id, - user_profile.uid, - CollabType::Folder, - &workspace_id, - ) - .await - { - Ok(doc_state) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }, - Err(err) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => { - return Err(err); - }, - }, - }; + let data_source = self + .folder_init_data_source(user_profile.uid, &workspace_id, auth_type) + .await?; self .folder_manager @@ -204,12 +238,17 @@ impl UserStatusCallback for UserStatusCallbackImpl { async fn on_workspace_opened( &self, user_id: i64, - user_workspace: &UserWorkspace, + workspace_id: &Uuid, + _user_workspace: &UserWorkspace, auth_type: &AuthType, ) -> FlowyResult<()> { + let data_source = self + .folder_init_data_source(user_id, workspace_id, auth_type) + .await?; + self .folder_manager - .initialize_after_open_workspace(user_id) + .initialize_after_open_workspace(user_id, data_source) .await?; self .database_manager @@ -221,11 +260,11 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .ai_manager - .initialize_after_open_workspace(&user_workspace.id) + .initialize_after_open_workspace(workspace_id) .await?; self .storage_manager - .initialize_after_open_workspace(&user_workspace.id) + .initialize_after_open_workspace(workspace_id) .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 998fcb84f5..13b19e48b8 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -14,6 +14,7 @@ collab-plugins = { workspace = true } collab-integrate = { workspace = true } flowy-folder-pub = { workspace = true } flowy-search-pub = { workspace = true } +flowy-user-pub = { workspace = true } flowy-sqlite = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 8662c1e061..fd12c15fba 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -262,16 +262,17 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. - #[tracing::instrument(skip(self, user_id), err)] - pub async fn initialize_after_sign_in(&self, user_id: i64) -> FlowyResult<()> { + #[tracing::instrument(skip_all, err)] + pub async fn initialize_after_sign_in( + &self, + user_id: i64, + data_source: FolderInitDataSource, + ) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; - let object_id = &workspace_id; - - let is_exist = self - .user - .is_folder_exist_on_disk(user_id, &workspace_id) - .unwrap_or(false); - if is_exist { + if let Err(err) = self.initialize(user_id, &workspace_id, data_source).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, @@ -281,39 +282,17 @@ 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(()) } - pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { - self.initialize_after_sign_in(uid).await + pub async fn initialize_after_open_workspace( + &self, + uid: i64, + data_source: FolderInitDataSource, + ) -> FlowyResult<()> { + self.initialize_after_sign_in(uid, data_source).await } /// Initialize the folder for the new user. @@ -2139,6 +2118,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde } #[allow(clippy::large_enum_variant)] +#[derive(Debug)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, 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 7f4720de99..483ca3d100 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 @@ -85,9 +85,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { } else { let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; drop(read_txn); - - // create default folder doc - Err(FlowyError::local_version_not_support()) + Ok(data.doc_state.to_vec()) } } diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index 619dc47f90..0dd729b087 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -181,7 +181,7 @@ impl StorageManager { } } - pub async fn initialize_after_open_workspace(&self, workspace_id: &str) { + pub async fn initialize_after_open_workspace(&self, workspace_id: &Uuid) { self.enable_storage_write_access(); if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 0b489e6f28..fbee0a96d9 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,13 +1,13 @@ use client_api::entity::billing_dto::SubscriptionPlan; -use std::sync::Weak; -use strum_macros::Display; - use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::async_trait::async_trait; +use std::sync::Weak; +use strum_macros::Display; +use uuid::Uuid; use crate::event_handler::*; use crate::user_manager::UserManager; @@ -323,6 +323,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { async fn on_workspace_opened( &self, _user_id: i64, + _workspace_id: &Uuid, _user_workspace: &UserWorkspace, _auth_type: &AuthType, ) -> FlowyResult<()> { 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 425b2351b0..34d0ece483 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 @@ -195,7 +195,7 @@ impl UserManager { .user_status_callback .read() .await - .on_workspace_opened(uid, &user_workspace, &user_profile.auth_type) + .on_workspace_opened(uid, workspace_id, &user_workspace, &user_profile.auth_type) .await { error!("Open workspace failed: {:?}", err); From 2ee786f3512a8d631ac084596fd1716dd5a147c0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 19:29:56 +0800 Subject: [PATCH 47/74] chore: update logs --- .../folder_deps/folder_deps_chat_impl.rs | 8 +-- .../folder_deps/folder_deps_database_impl.rs | 4 +- .../flowy-core/src/user_state_callback.rs | 56 ++++++++++--------- .../flowy-server/src/af_cloud/impls/chat.rs | 4 +- .../af_cloud/impls/user/cloud_service_impl.rs | 31 ++-------- .../src/local_server/impls/chat.rs | 6 +- .../src/local_server/impls/user.rs | 2 +- .../flowy-server/src/local_server/util.rs | 6 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 12 +--- .../user_manager/manager_user_workspace.rs | 4 +- 10 files changed, 55 insertions(+), 78 deletions(-) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs index fc7a861cb8..e2791827ee 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs @@ -31,7 +31,7 @@ impl FolderOperationHandler for ChatFolderOperation { } async fn duplicate_view(&self, _view_id: &Uuid) -> Result { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Duplicate view")) } async fn create_view_with_view_data( @@ -39,7 +39,7 @@ impl FolderOperationHandler for ChatFolderOperation { _user_id: i64, _params: CreateViewParams, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Can't create view")) } async fn create_default_view( @@ -65,7 +65,7 @@ impl FolderOperationHandler for ChatFolderOperation { _import_type: ImportType, _bytes: Vec, ) -> Result, FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("import from data")) } async fn import_from_file_path( @@ -74,6 +74,6 @@ impl FolderOperationHandler for ChatFolderOperation { _name: &str, _path: String, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("import file from path")) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs index d98e32f67d..edc40c6d5b 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs @@ -198,7 +198,9 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar, ViewLayoutPB::Grid => DatabaseLayoutPB::Grid, ViewLayoutPB::Document | ViewLayoutPB::Chat => { - return Err(FlowyError::not_support()); + return Err( + FlowyError::invalid_data().with_context("Can't handle document layout type"), + ); }, }; let name = params.name.to_string(); diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 8db4e3f926..3be1bf15ed 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -53,35 +53,17 @@ impl UserStatusCallbackImpl { workspace_id: &Uuid, auth_type: &AuthType, ) -> FlowyResult { - let is_exist = self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)?; - if is_exist { - Ok(FolderInitDataSource::LocalDisk { + if self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)? { + return Ok(FolderInitDataSource::LocalDisk { create_if_not_exist: false, - }) - } else { - let data_source = match self - .folder_manager - .cloud_service - .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) - .await - { - Ok(doc_state) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }, - Err(err) => match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => { - return Err(err); - }, - }, - }; - Ok(data_source) + }); } + let doc_state_result = self + .folder_manager + .cloud_service + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) + .await; + resolve_data_source(auth_type, doc_state_result) } fn is_object_exist_on_disk( @@ -296,3 +278,23 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } } + +fn resolve_data_source( + auth_type: &AuthType, + doc_state_result: Result, FlowyError>, +) -> FlowyResult { + match doc_state_result { + Ok(doc_state) => Ok(match auth_type { + AuthType::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + }), + Err(err) => match auth_type { + AuthType::Local => Ok(FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }), + AuthType::AppFlowyCloud => Err(err), + }, + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 14a26078f5..6086f7084b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -217,10 +217,10 @@ where chat_id: &Uuid, metadata: Option>, ) -> Result<(), FlowyError> { - return Err( + Err( FlowyError::not_support() .with_context("indexing file with appflowy cloud is not suppotred yet"), - ); + ) } async fn get_chat_settings( 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 e0f81a62e4..d6260d9e09 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 @@ -13,8 +13,8 @@ use client_api::entity::workspace_dto::{ WorkspaceMemberInvitation, }; use client_api::entity::{ - AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, - AuthProvider, CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, + AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, AuthProvider, + CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; @@ -341,18 +341,6 @@ where Ok(members) } - async fn get_workspace_member( - &self, - workspace_id: Uuid, - uid: i64, - ) -> Result { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let query = QueryWorkspaceMember { workspace_id, 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, @@ -452,7 +440,7 @@ where Ok(payment_link) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, @@ -464,17 +452,8 @@ where 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, - }) + + Ok(from_af_workspace_member(member)) } async fn get_workspace_subscriptions( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs index 3af87eb18e..845b6dec1c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs @@ -8,7 +8,7 @@ use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; use flowy_ai_pub::cloud::{ AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, - ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, + ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, DEFAULT_AI_MODEL_NAME, }; use flowy_ai_pub::persistence::{ deserialize_chat_metadata, deserialize_rag_ids, read_chat, @@ -313,11 +313,11 @@ impl ChatCloudService for LocalChatServiceImpl { } async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(ModelList { models: vec![] }) } async fn get_workspace_default_model(&self, _workspace_id: &Uuid) -> Result { - Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + Ok(DEFAULT_AI_MODEL_NAME.to_string()) } } 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 bd6e930584..0023defb41 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 @@ -209,7 +209,7 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs index bd00212afb..378ccee6a2 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/util.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/util.rs @@ -32,9 +32,11 @@ pub async fn default_encode_collab_for_collab_type( // Ok::<_, FlowyError>(()) // })?; // Ok(data) - Err(FlowyError::not_support()) + Err(FlowyError::not_support().with_context("Can not create default folder")) + }, + CollabType::DatabaseRow => { + Err(FlowyError::not_support().with_context("Can not create default database row")) }, - CollabType::DatabaseRow => Err(FlowyError::not_support()), CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), CollabType::Unknown => { let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index b15299b194..e58c626532 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -132,7 +132,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Delete an account and all the data associated with the account async fn delete_account(&self) -> Result<(), FlowyError> { - Err(FlowyError::not_support()) + Ok(()) } /// Generate a sign in url for the user with the given email @@ -234,14 +234,6 @@ pub trait UserCloudService: Send + Sync + 'static { Ok(vec![]) } - async fn get_workspace_member( - &self, - workspace_id: Uuid, - uid: i64, - ) -> Result { - Err(FlowyError::not_support()) - } - async fn get_user_awareness_doc_state( &self, uid: i64, @@ -281,7 +273,7 @@ pub trait UserCloudService: Send + Sync + 'static { Err(FlowyError::not_support()) } - async fn get_workspace_member_info( + async fn get_workspace_member( &self, workspace_id: &Uuid, uid: i64, 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 34d0ece483..f74cf45ef7 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 @@ -377,7 +377,7 @@ impl UserManager { let member = self .cloud_service .get_user_service()? - .get_workspace_member(workspace_id, uid) + .get_workspace_member(&workspace_id, uid) .await?; Ok(member) } @@ -655,7 +655,7 @@ impl UserManager { let member = self .cloud_service .get_user_service()? - .get_workspace_member_info(workspace_id, uid) + .get_workspace_member(workspace_id, uid) .await?; let record = WorkspaceMemberTable { From cf46213e0019b1b1228479598b1e46b5d7d1d71c Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:34:05 +0800 Subject: [PATCH 48/74] chore: Update frontend/rust-lib/flowy-folder/src/manager.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- frontend/rust-lib/flowy-folder/src/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index fd12c15fba..a12959b803 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -272,7 +272,7 @@ impl FolderManager { if let Err(err) = self.initialize(user_id, &workspace_id, data_source).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); + error!("initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", user_id, workspace_id, err); self .initialize( user_id, From c6010a6734a7b7c61d7ce306be96735f00a91f0b Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 20 Apr 2025 19:51:12 +0800 Subject: [PATCH 49/74] chore: fmt --- frontend/rust-lib/flowy-folder/src/manager.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index a12959b803..8e228191c4 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -272,7 +272,10 @@ impl FolderManager { if let Err(err) = self.initialize(user_id, &workspace_id, data_source).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 for user {} with workspace {} encountered error: {:?}, fallback local", user_id, workspace_id, err); + error!( + "initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", + user_id, workspace_id, err + ); self .initialize( user_id, From f8927b18430a9acc42746a3a7fa39e0ff1ab7bd6 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 21 Apr 2025 10:22:02 +0800 Subject: [PATCH 50/74] fix: crash when trying to delete emoji (#7787) * fix: emoji picker error on desktop * fix: test errors --- .../uncategorized/emoji_shortcut_test.dart | 68 +++++---- .../slash_menu_items/emoji_item.dart | 22 ++- .../plugins/emoji/emoji_actions_command.dart | 7 +- .../lib/plugins/emoji/emoji_handler.dart | 16 +- .../lib/plugins/emoji/emoji_menu.dart | 10 +- .../widgets/emoji_picker/emoji_menu_item.dart | 139 ------------------ .../widgets/emoji_picker/emoji_picker.dart | 1 - .../emoji_picker/emoji_shortcut_event.dart | 77 ++-------- 8 files changed, 85 insertions(+), 255 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index 1a4e57078f..d3226a3ad0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -1,44 +1,63 @@ import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/emoji/emoji_handler.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - -import '../../shared/keyboard.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Future prepare(WidgetTester tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.createNewPageWithNameUnderParent(); + await tester.editor.tapLineOfEditorAt(0); + } + // May be better to move this to an existing test but unsure what it fits with group('Keyboard shortcuts related to emojis', () { testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); + await prepare(tester); - final Finder editor = find.byType(AppFlowyEditor); - await tester.tap(editor); - await tester.pumpAndSettle(); + expect(find.byType(EmojiHandler), findsNothing); - expect(find.byType(EmojiSelectionMenu), findsNothing); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.keyE, - ], - tester: tester, + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyE, + isAltPressed: true, + isMetaPressed: Platform.isMacOS, + isControlPressed: !Platform.isMacOS, ); + await tester.pumpAndSettle(Duration(seconds: 1)); + expect(find.byType(EmojiHandler), findsOneWidget); - expect(find.byType(EmojiSelectionMenu), findsOneWidget); + /// press backspace to hide the emoji picker + await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); + expect(find.byType(EmojiHandler), findsNothing); + }); + + testWidgets('insert emoji by slash menu', (tester) async { + await prepare(tester); + await tester.editor.showSlashMenu(); + + /// show emoji picler + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_emoji.tr(), + offset: 100, + ); + await tester.pumpAndSettle(Duration(seconds: 1)); + expect(find.byType(EmojiHandler), findsOneWidget); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + final firstNode = + tester.editor.getCurrentEditorState().getNodeAtPath([0])!; + + /// except the emoji is in document + expect(firstNode.delta!.toPlainText().contains('😀'), true); }); }); @@ -47,10 +66,7 @@ void main() { WidgetTester tester, { String? search, }) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(); - await tester.editor.tapLineOfEditorAt(0); + await prepare(tester); await tester.ime.insertText(':${search ?? 'a'}'); await tester.pumpAndSettle(Duration(seconds: 1)); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart index df1c457cc2..890ba113cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart @@ -1,10 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; +import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; +import 'package:appflowy/plugins/emoji/emoji_menu.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:universal_platform/universal_platform.dart'; import 'slash_menu_item_builder.dart'; @@ -37,11 +40,16 @@ extension on EditorState { }) async { final container = Overlay.of(context); menuService.dismiss(); - showEmojiPickerMenu( - container, - this, - menuService.alignment, - menuService.offset, - ); + if (UniversalPlatform.isMobile || selection == null) { + return; + } + + final node = getNodeAtPath(selection!.end.path); + final delta = node?.delta; + if (node == null || delta == null || node.type == CodeBlockKeys.type) { + return; + } + emojiMenuService = EmojiMenu(editorState: this, overlay: container); + emojiMenuService?.show(''); } } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart index 9d386b36be..c116680c2e 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart @@ -18,7 +18,7 @@ CharacterShortcutEvent emojiCommand(BuildContext context) => }, handlerWithCharacter: (editorState, character) { emojiMenuService = EmojiMenu( - context: context, + overlay: Overlay.of(context), editorState: editorState, ); return emojiCommandHandler(editorState, context, character); @@ -40,10 +40,7 @@ Future emojiCommandHandler( final node = editorState.getNodeAtPath(selection.end.path); final delta = node?.delta; - if (node == null || - delta == null || - delta.isEmpty || - node.type == CodeBlockKeys.type) { + if (node == null || delta == null || node.type == CodeBlockKeys.type) { return false; } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart index 3ab578b961..b1b1e7cdbb 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart @@ -22,7 +22,6 @@ class EmojiHandler extends StatefulWidget { required this.onDismiss, required this.onSelectionUpdate, required this.onEmojiSelect, - this.startCharAmount = 1, this.cancelBySpaceHandler, this.initialSearchText = '', }); @@ -32,7 +31,6 @@ class EmojiHandler extends StatefulWidget { final VoidCallback onDismiss; final VoidCallback onSelectionUpdate; final SelectEmojiItemHandler onEmojiSelect; - final int startCharAmount; final String initialSearchText; final bool Function()? cancelBySpaceHandler; @@ -54,6 +52,8 @@ class _EmojiHandlerState extends State { defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, ); + int get startCharAmount => widget.initialSearchText.length; + set search(String search) { _search = search; _doSearch(); @@ -68,7 +68,8 @@ class _EmojiHandlerState extends State { (_) => focusNode.requestFocus(), ); - startOffset = (widget.editorState.selection?.endIndex ?? 0) - 1; + startOffset = + (widget.editorState.selection?.endIndex ?? 0) - startCharAmount; if (kCachedEmojiData != null) { loadEmojis(kCachedEmojiData!); @@ -194,7 +195,8 @@ class _EmojiHandlerState extends State { void _doSearch() { if (!loaded || !mounted) return; - if (_search.startsWith(' ') || _search.isEmpty) { + final enableEmptySearch = widget.initialSearchText.isEmpty; + if ((_search.startsWith(' ') || _search.isEmpty) && !enableEmptySearch) { widget.onDismiss.call(); return; } @@ -232,6 +234,10 @@ class _EmojiHandlerState extends State { widget.onDismiss.call(); } else if (event.logicalKey == LogicalKeyboardKey.backspace) { if (_search.isEmpty) { + if (widget.initialSearchText.isEmpty) { + widget.onDismiss.call(); + return KeyEventResult.handled; + } if (_canDeleteLastCharacter()) { widget.editorState.deleteBackward(); } else { @@ -276,7 +282,7 @@ class _EmojiHandlerState extends State { void onSelect(int index) { widget.onEmojiSelect.call( context, - (startOffset - widget.startCharAmount, startOffset + _search.length), + (startOffset - startCharAmount, startOffset + _search.length), emojiData.getEmojiById(searchedEmojis[index].id), ); widget.onDismiss.call(); diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart index 4aff4cf6cb..29f130d77d 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart @@ -12,21 +12,19 @@ abstract class EmojiMenuService { class EmojiMenu extends EmojiMenuService { EmojiMenu({ - required this.context, + required this.overlay, required this.editorState, - this.startCharAmount = 1, this.cancelBySpaceHandler, this.menuHeight = 400, this.menuWidth = 300, }); - final BuildContext context; final EditorState editorState; final double menuHeight; final double menuWidth; + final OverlayState overlay; final bool Function()? cancelBySpaceHandler; - final int startCharAmount; Offset _offset = Offset.zero; Alignment _alignment = Alignment.topLeft; OverlayEntry? _menuEntry; @@ -97,7 +95,6 @@ class EmojiMenu extends EmojiMenuService { menuService: this, onDismiss: dismiss, onSelectionUpdate: _onSelectionUpdate, - startCharAmount: startCharAmount, cancelBySpaceHandler: cancelBySpaceHandler, initialSearchText: initialCharacter, onEmojiSelect: ( @@ -132,8 +129,9 @@ class EmojiMenu extends EmojiMenuService { ), ); - Overlay.of(context).insert(_menuEntry!); + overlay.insert(_menuEntry!); + keepEditorFocusNotifier.increase(); editorState.service.keyboardService?.disable(showCursor: true); editorState.service.scrollService?.disable(); selectionService.currentSelection.addListener(_onSelectionChange); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart deleted file mode 100644 index 72aed27ad4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/decoration.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -SelectionMenuItem emojiMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_emoji.tr, - icon: (editorState, onSelected, style) => SelectableIconWidget( - icon: Icons.emoji_emotions_outlined, - isSelected: onSelected, - style: style, - ), - keywords: ['emoji'], - handler: (editorState, menuService, context) { - final container = Overlay.of(context); - menuService.dismiss(); - showEmojiPickerMenu( - container, - editorState, - menuService.alignment, - menuService.offset, - ); - }, -); - -void showEmojiPickerMenu( - OverlayState container, - EditorState editorState, - Alignment alignment, - Offset offset, -) { - (double? left, double? top, double? right, double? bottom) getPosition() { - double? left, top, right, bottom; - switch (alignment) { - case Alignment.topLeft: - left = offset.dx; - top = offset.dy; - break; - case Alignment.bottomLeft: - left = offset.dx; - bottom = offset.dy; - break; - case Alignment.topRight: - right = offset.dx; - top = offset.dy; - break; - case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; - break; - } - - return (left, top, right, bottom); - } - - final (left, top, right, bottom) = getPosition(); - - keepEditorFocusNotifier.increase(); - late OverlayEntry emojiPickerMenuEntry; - emojiPickerMenuEntry = FullScreenOverlayEntry( - left: left, - top: top, - bottom: bottom, - right: right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) => Material( - type: MaterialType.transparency, - child: Container( - width: 360, - height: 380, - padding: const EdgeInsets.all(4.0), - decoration: FlowyDecoration.decoration( - Theme.of(context).cardColor, - Theme.of(context).colorScheme.shadow, - ), - child: EmojiSelectionMenu( - onSubmitted: (emoji) { - editorState.insertTextAtCurrentSelection(emoji); - emojiPickerMenuEntry.remove(); - }, - onExit: () { - // close emoji panel - emojiPickerMenuEntry.remove(); - }, - ), - ), - ), - ).build(); - container.insert(emojiPickerMenuEntry); -} - -class EmojiSelectionMenu extends StatefulWidget { - const EmojiSelectionMenu({ - super.key, - required this.onSubmitted, - required this.onExit, - }); - - final void Function(String emoji) onSubmitted; - final void Function() onExit; - - @override - State createState() => _EmojiSelectionMenuState(); -} - -class _EmojiSelectionMenuState extends State { - @override - void initState() { - super.initState(); - HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); - } - - bool _handleGlobalKeyEvent(KeyEvent event) { - if (event.logicalKey == LogicalKeyboardKey.escape && - event is KeyDownEvent) { - //triggers on esc - widget.onExit(); - return true; - } - return false; - } - - @override - void deactivate() { - HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); - super.deactivate(); - } - - @override - Widget build(BuildContext context) { - return FlowyEmojiPicker( - onEmojiSelected: (r) => widget.onSubmitted(r.emoji), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart index a369cc6b87..6e1f6e239f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart @@ -1,4 +1,3 @@ -export 'emoji_menu_item.dart'; export 'emoji_shortcut_event.dart'; export 'src/emji_picker_config.dart'; export 'src/emoji_picker.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index 5bb4766353..6959f69788 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -1,5 +1,7 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; +import 'package:appflowy/plugins/emoji/emoji_menu.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( @@ -15,73 +17,16 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { if (selection == null) { return KeyEventResult.ignored; } - final context = editorState.getNodeAtPath(selection.start.path)?.context; - if (context == null) { + final node = editorState.getNodeAtPath(selection.start.path); + final context = node?.context; + if (node == null || + context == null || + node.delta == null || + node.type == CodeBlockKeys.type) { return KeyEventResult.ignored; } - final container = Overlay.of(context); - - Alignment alignment = Alignment.topLeft; - Offset offset = Offset.zero; - - final selectionService = editorState.service.selectionService; - final selectionRects = selectionService.selectionRects; - if (selectionRects.isEmpty) { - return KeyEventResult.ignored; - } - final rect = selectionRects.first; - - // Calculate the offset and alignment - // Don't like these values being hardcoded but unsure how to grab the - // values dynamically to match the /emoji command. - const menuHeight = 380.0; - const menuOffset = Offset(10, 10); // Tried (0, 10) but that looked off - - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = editorState.renderBox!.size.height; - final editorWidth = editorState.renderBox!.size.width; - - // show below default - alignment = Alignment.topLeft; - final bottomRight = rect.bottomRight; - final topRight = rect.topRight; - var newOffset = bottomRight + menuOffset; - offset = Offset( - newOffset.dx, - newOffset.dy, - ); - - // show above - if (newOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { - newOffset = topRight - menuOffset; - alignment = Alignment.bottomLeft; - - offset = Offset( - newOffset.dx, - editorHeight + editorOffset.dy - newOffset.dy, - ); - } - - // show on left - if (offset.dx - editorOffset.dx > editorWidth / 2) { - alignment = alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - offset = Offset( - editorWidth - offset.dx + editorOffset.dx, - offset.dy, - ); - } - - showEmojiPickerMenu( - container, - editorState, - alignment, - offset, - ); - + emojiMenuService = EmojiMenu(editorState: editorState, overlay: container); + emojiMenuService?.show(''); return KeyEventResult.handled; }; From 3451100b80f0200a622eace82bd79ff1c5652f02 Mon Sep 17 00:00:00 2001 From: David Woods Date: Sun, 20 Apr 2025 22:22:22 -0400 Subject: [PATCH 51/74] chore: bumped device_info_plus to 11.2.2 (#7782) * bumped device_info_plus to 11.2.2 -- this version avoids the crash that happens when getInfo() is called on Windows in a VM or with Hyper-V active * chore: bumped device_info_plus to 11.2.2 -- this version avoids the crash that happens when getInfo() is called on Windows in a VM or with Hyper-V active --- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c871a41f7e..1a393e1180 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -533,10 +533,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "11.3.0" device_info_plus_platform_interface: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e8042d6a57..1e92765ff6 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -177,7 +177,7 @@ dev_dependencies: dependency_overrides: http: ^1.0.0 - device_info_plus: ^10.1.0 + device_info_plus: ^11.2.2 url_protocol: git: From 04407fe8ff601b8e0f9516bf098f80f0f2b0ab31 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 21 Apr 2025 10:45:27 +0800 Subject: [PATCH 52/74] fix: issues with displaying mention text (#7773) * fix: some mention text can not display correctly * fix: remove the image widget from bookmark if the image url is null --- .../link_preview/custom_link_preview.dart | 30 +++++++++---------- .../link_parsers/default_parser.dart | 18 +++++++---- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index d7f3e26302..9be73fcc0b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -161,25 +161,13 @@ class CustomLinkPreviewWidget extends StatelessWidget { } Widget buildImage(BuildContext context) { + if (imageUrl?.isEmpty ?? true) { + return SizedBox.shrink(); + } final theme = AppFlowyTheme.of(context), fillScheme = theme.fillColorScheme, iconScheme = theme.iconColorScheme; final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; - Widget child; - if (imageUrl?.isNotEmpty ?? false) { - child = FlowyNetworkImage( - url: imageUrl!, - width: width, - ); - } else { - child = Center( - child: FlowySvg( - FlowySvgs.toolbar_link_earth_m, - color: iconScheme.secondary, - size: Size.square(30), - ), - ); - } return ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), @@ -188,7 +176,17 @@ class CustomLinkPreviewWidget extends StatelessWidget { child: Container( width: width, color: fillScheme.quaternary, - child: child, + child: FlowyNetworkImage( + url: imageUrl!, + width: width, + errorWidgetBuilder: (_, __, ___) => Center( + child: FlowySvg( + FlowySvgs.toolbar_link_earth_m, + color: iconScheme.secondary, + size: Size.square(30), + ), + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart index ab0b246743..7b52994654 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart @@ -3,6 +3,7 @@ import 'package:appflowy_backend/log.dart'; // ignore: depend_on_referenced_packages import 'package:html/parser.dart' as html_parser; import 'package:http/http.dart' as http; +import 'dart:convert'; abstract class LinkInfoParser { Future parse( @@ -38,12 +39,19 @@ class DefaultParser implements LinkInfoParser { if (code != 200 && isHome) { throw Exception('Http request error: $code'); } - // else if (!isHome && code == 403) { - // uri = Uri.parse('${uri.scheme}://${uri.host}/'); - // response = await http.get(uri).timeout(timeout); - // } - final document = html_parser.parse(response.body); + final contentType = response.headers['content-type']; + final charset = contentType?.split('charset=').lastOrNull; + String body = ''; + if (charset == null || + charset.toLowerCase() == 'latin-1' || + charset.toLowerCase() == 'iso-8859-1') { + body = latin1.decode(response.bodyBytes); + } else { + body = utf8.decode(response.bodyBytes, allowMalformed: true); + } + + final document = html_parser.parse(body); final siteName = document .querySelector('meta[property="og:site_name"]') From 13563825240acbbcba358eb730485ac14a50c9a9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 11:41:58 +0800 Subject: [PATCH 53/74] refactor: only notify when user workspaces were changed --- .../bottom_sheet/bottom_sheet_view_page.dart | 3 +- .../home/setting/settings_popup_menu.dart | 2 +- .../home/tab/mobile_space_tab.dart | 2 +- .../setting/user_session_setting_group.dart | 2 +- .../application/sync/database_sync_bloc.dart | 2 +- .../database/widgets/row/row_banner.dart | 3 +- .../document/application/document_bloc.dart | 2 +- .../document_collaborators_bloc.dart | 3 +- .../application/document_sync_bloc.dart | 3 +- .../editor_plugins/file/file_util.dart | 4 +- .../page_style/_page_style_cover_image.dart | 3 +- .../lib/plugins/shared/share/share_bloc.dart | 2 +- .../icon_emoji_picker/icon_uploader.dart | 4 +- .../application/password/password_bloc.dart | 2 +- .../lib/user/presentation/anon_user.dart | 4 +- .../settings/settings_dialog_bloc.dart | 2 +- .../application/user/user_workspace_bloc.dart | 2 +- .../menu/view/view_more_action_button.dart | 2 +- .../settings/pages/settings_account_view.dart | 18 +- .../pages/settings_workspace_view.dart | 4 +- .../settings/settings_dialog.dart | 2 +- .../settings/widgets/settings_menu.dart | 4 +- .../more_view_actions/more_view_actions.dart | 2 +- .../collab-integrate/src/collab_builder.rs | 7 +- .../src/folder_event.rs | 15 -- .../tests/folder/mod.rs | 1 - .../user/af_cloud_test/anon_user_test.rs | 2 +- .../user/af_cloud_test/workspace_test.rs | 36 ++++ .../user/local_test/user_profile_test.rs | 2 +- .../src/deps_resolve/cloud_service_impl.rs | 99 ++++------ .../rust-lib/flowy-document/src/manager.rs | 1 + .../rust-lib/flowy-folder-pub/src/cloud.rs | 16 -- .../flowy-folder/src/event_handler.rs | 22 --- .../rust-lib/flowy-folder/src/event_map.rs | 4 +- frontend/rust-lib/flowy-folder/src/manager.rs | 16 +- .../flowy-server/src/af_cloud/impls/folder.rs | 104 ++-------- .../af_cloud/impls/user/cloud_service_impl.rs | 61 +++--- .../src/af_cloud/impls/user/dto.rs | 2 + .../src/local_server/impls/folder.rs | 66 ++----- .../src/local_server/impls/user.rs | 16 +- .../flowy-server/src/local_server/server.rs | 10 +- frontend/rust-lib/flowy-server/src/server.rs | 4 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 3 +- .../rust-lib/flowy-user-pub/src/entities.rs | 4 +- .../flowy-user-pub/src/sql/user_sql.rs | 37 ++-- .../flowy-user-pub/src/sql/workspace_sql.rs | 182 +++++++++++++----- .../flowy-user/src/entities/user_profile.rs | 8 +- .../rust-lib/flowy-user/src/event_handler.rs | 21 +- .../data_import/appflowy_data_import.rs | 1 + .../rust-lib/flowy-user/src/services/db.rs | 3 +- .../flowy-user/src/user_manager/manager.rs | 56 +++--- .../src/user_manager/manager_history_user.rs | 4 +- .../user_manager/manager_user_awareness.rs | 6 +- .../user_manager/manager_user_workspace.rs | 105 ++++++---- 54 files changed, 497 insertions(+), 494 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 9706777df0..47ab37505e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -202,7 +202,8 @@ class MobileViewBottomSheetBody extends StatelessWidget { List _buildPublishActions(BuildContext context) { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud - if (userProfile == null || userProfile.authType != AuthTypePB.Server) { + if (userProfile == null || + userProfile.workspaceAuthType != AuthTypePB.Server) { return []; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart index bd41730934..659473a6b1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart @@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.authType == AuthTypePB.Server) ...[ + if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, 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 c89367f379..56f5f3e6ab 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 @@ -167,7 +167,7 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.authType == AuthTypePB.Server) + if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) Positioned( bottom: MediaQuery.of(context).padding.bottom + 16, left: 20, 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 405fef0d1a..5ca5525099 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 @@ -40,7 +40,7 @@ class UserSessionSettingGroup extends StatelessWidget { // delete account button // only show the delete account button in cloud mode - if (userProfile.authType == AuthTypePB.Server) ...[ + if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index 5116785c1f..351dea2cd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.authType == AuthTypePB.Server && + userProfile?.workspaceAuthType == AuthTypePB.Server && databaseId != null, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index e2f470e0d3..debbb467e7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -69,7 +69,8 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; + (widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) == + AuthTypePB.Local; @override void dispose() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index ac03fe5308..264ec4bb11 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -101,7 +101,7 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.authType ?? AuthTypePB.Local; + final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local; return type == AuthTypePB.Local; } 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 682f600f0a..a0678372cf 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 @@ -31,7 +31,8 @@ class DocumentCollaboratorsBloc final userProfile = result.fold((s) => s, (f) => null); emit( state.copyWith( - shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, + shouldShowIndicator: + userProfile?.workspaceAuthType == AuthTypePB.Server, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 2ba50fc6c4..7254539809 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -30,7 +30,8 @@ class DocumentSyncBloc extends Bloc { ); emit( state.copyWith( - shouldShowIndicator: userProfile?.authType == AuthTypePB.Server, + shouldShowIndicator: + userProfile?.workspaceAuthType == AuthTypePB.Server, ), ); _syncStateListener.start( 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 69791f78b7..8e6651ff73 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 @@ -185,7 +185,7 @@ Future insertLocalFile( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; + (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; String? path; String? errorMsg; @@ -230,7 +230,7 @@ Future insertLocalFiles( // Check upload type final isLocalMode = - (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; + (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 6136392884..45a23bc6ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -225,7 +225,8 @@ class PageStyleCoverImage extends StatelessWidget { (s) => s, (f) => null, ); - final isAppFlowyCloud = userProfile?.authType == AuthTypePB.Server; + final isAppFlowyCloud = + userProfile?.workspaceAuthType == AuthTypePB.Server; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index 2356399b4a..e683518526 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.authType == AuthTypePB.Server, + (v) => v.workspaceAuthType == AuthTypePB.Server, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index ff8e7b88ec..c303160ffe 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -294,8 +294,8 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = - (userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local; + final isLocalMode = (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == + AuthTypePB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); } else { diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart index b85efe38ae..80dd5ca3c9 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart @@ -46,7 +46,7 @@ class PasswordBloc extends Bloc { bool _isInitialized = false; Future _init() async { - if (userProfile.authType == AuthTypePB.Local) { + if (userProfile.workspaceAuthType == AuthTypePB.Local) { Log.debug('PasswordBloc: skip init because user is local authenticator'); return; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index c8744fb304..ddb1a07f96 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,8 +74,8 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.authType != AuthTypePB.Local; - final desc = "${user.name}\t ${user.authType}\t"; + final isDisabled = isSelected || user.workspaceAuthType != AuthTypePB.Local; + final desc = "${user.name}\t ${user.workspaceAuthType}\t"; final child = SizedBox( height: 30, child: FlowyButton( 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 726e95bb9e..83588f0079 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 @@ -91,7 +91,7 @@ class SettingsDialogBloc ]) async { if ([ AuthTypePB.Local, - ].contains(userProfile.authType)) { + ].contains(userProfile.workspaceAuthType)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 0e0b912a08..d14f258462 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.authType == AuthTypePB.Server && + userProfile.userAuthType == AuthTypePB.Server && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 7ccd03b4f4..5b531c2f28 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.authType != AuthTypePB.Server) { + if (userProfile.workspaceAuthType != AuthTypePB.Server) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index e242da473b..d7afb03e87 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.authType != AuthTypePB.Local) ...[ + state.userProfile.workspaceAuthType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.newSettings_myAccount_myAccount.tr(), children: [ @@ -82,26 +82,30 @@ class _SettingsAccountViewState extends State { ), AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authType == AuthTypePB.Local + onAction: state.userProfile.workspaceAuthType == + AuthTypePB.Local ? widget.didLogin : widget.didLogout, - signIn: state.userProfile.authType == AuthTypePB.Local, + signIn: state.userProfile.workspaceAuthType == + AuthTypePB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.authType == AuthTypePB.Local) ...[ + state.userProfile.workspaceAuthType == AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.authType == AuthTypePB.Local + onAction: state.userProfile.workspaceAuthType == + AuthTypePB.Local ? widget.didLogin : widget.didLogout, - signIn: state.userProfile.authType == AuthTypePB.Local, + signIn: state.userProfile.workspaceAuthType == + AuthTypePB.Local, ), ], ), @@ -116,7 +120,7 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.authType == AuthTypePB.Server) + if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 9a17016e5f..78ffd34eef 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.authType != AuthTypePB.Local) ...[ + if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.authType != AuthTypePB.Local) ...[ + if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), 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 e262a27cb6..cd33c62090 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -140,7 +140,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.authType == AuthTypePB.Server) { + if (user.workspaceAuthType == AuthTypePB.Server) { return SettingsAIView( key: ValueKey(workspaceId), userProfile: user, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 04a93656ca..979f19fbde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -63,7 +63,7 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.authType == AuthTypePB.Server) + userProfile.workspaceAuthType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, @@ -109,7 +109,7 @@ class SettingsMenu extends StatelessWidget { ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.authType == AuthTypePB.Server) + if (userProfile.workspaceAuthType == AuthTypePB.Server) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index fe202e7590..62b3ccc8f3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.authType == AuthTypePB.Server) { + userProfile.workspaceAuthType == AuthTypePB.Server) { return const SizedBox.shrink(); } diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 10149bf259..223ebacc91 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -359,7 +359,12 @@ impl AppFlowyCollabBuilder { { if let Some(collab_db) = collab_db.upgrade() { let write_txn = collab_db.write_txn(); - trace!("flush collab:{}-{}-{} to disk", uid, collab_type, object_id); + trace!( + "flush workspace: {} {}:collab:{} to disk", + workspace_id, + collab_type, + object_id + ); let collab: &Collab = collab.borrow(); let encode_collab = collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab))?; 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 345c1e58e0..26515ab5af 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -397,18 +397,3 @@ impl ViewTest { Self::new(sdk, ViewLayout::Calendar, data).await } } - -#[allow(dead_code)] -async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { - let request = CreateWorkspacePayloadPB { - name: name.to_owned(), - desc: desc.to_owned(), - }; - - EventBuilder::new(sdk.clone()) - .event(CreateFolderWorkspace) - .payload(request) - .async_send() - .await - .parse::() -} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs index 01d3a22023..c5566e1b80 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs @@ -1,4 +1,3 @@ mod local_test; - // #[cfg(feature = "supabase_cloud_test")] // mod supabase_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 7f743b931c..301b6e5a62 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.auth_type, AuthTypePB::Server); + assert_eq!(user.user_auth_type, AuthTypePB::Server); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); 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 3bb71ea0dc..625ceeb1b7 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 @@ -292,3 +292,39 @@ async fn af_cloud_different_open_same_workspace_test() { assert_eq!(views.len(), 2, "only get: {:?}", views); // Expecting two views. assert_eq!(views[0].name, "General"); } + +#[tokio::test] +async fn af_cloud_create_local_workspace_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + let _ = test.af_cloud_sign_up().await; + + let workspaces = test.get_all_workspaces().await.items; + assert_eq!(workspaces.len(), 1); + + let created_workspace = test + .create_workspace("my local workspace", AuthType::Local) + .await; + assert_eq!(created_workspace.name, "my local workspace"); + + let workspaces = test.get_all_workspaces().await.items; + assert_eq!(workspaces.len(), 2); + assert_eq!(workspaces[1].name, "my local workspace"); + + test + .open_workspace( + &created_workspace.workspace_id, + created_workspace.workspace_auth_type, + ) + .await; + + let views = test.get_all_views().await; + assert_eq!(views.len(), 2); + assert!(views + .iter() + .any(|view| view.parent_view_id == workspaces[1].workspace_id)); + + for view in views { + test.get_view(&view.id).await; + } +} 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 47c2d53a6b..438b120483 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 @@ -24,7 +24,7 @@ async fn anon_user_profile_get() { .await .parse::(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.auth_type, AuthTypePB::Local); + assert_eq!(user_profile.user_auth_type, AuthTypePB::Local); } #[tokio::test] diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 35300563d7..6c9581d4a7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -26,8 +26,7 @@ use flowy_document::deps::DocumentData; use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, FullSyncCollabParams, - Workspace, WorkspaceRecord, + FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, }; use flowy_folder_pub::entities::PublishPayload; use flowy_search_pub::cloud::SearchCloudService; @@ -230,43 +229,13 @@ impl UserCloudServiceProvider for ServerProvider { #[async_trait] impl FolderCloudService for ServerProvider { - async fn create_workspace(&self, uid: i64, name: &str) -> Result { - let server = self.get_server()?; - let name = name.to_string(); - server.folder_service().create_workspace(uid, &name).await - } - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - let server = self.get_server()?; - server.folder_service().open_workspace(workspace_id).await - } - - async fn get_all_workspace(&self) -> Result, FlowyError> { - let server = self.get_server()?; - server.folder_service().get_all_workspace().await - } - - async fn get_folder_data( - &self, - workspace_id: &Uuid, - uid: &i64, - ) -> Result, FlowyError> { - let server = self.get_server()?; - - server - .folder_service() - .get_folder_data(workspace_id, uid) - .await - } - async fn get_folder_snapshots( &self, workspace_id: &str, limit: usize, ) -> Result, FlowyError> { - let server = self.get_server()?; - - server + self + .get_server()? .folder_service() .get_folder_snapshots(workspace_id, limit) .await @@ -279,14 +248,25 @@ impl FolderCloudService for ServerProvider { collab_type: CollabType, object_id: &Uuid, ) -> Result, FlowyError> { - let server = self.get_server()?; - - server + self + .get_server()? .folder_service() .get_folder_doc_state(workspace_id, uid, collab_type, object_id) .await } + async fn full_sync_collab_object( + &self, + workspace_id: &Uuid, + params: FullSyncCollabParams, + ) -> Result<(), FlowyError> { + self + .get_server()? + .folder_service() + .full_sync_collab_object(workspace_id, params) + .await + } + async fn batch_create_folder_collab_objects( &self, workspace_id: &Uuid, @@ -312,9 +292,8 @@ impl FolderCloudService for ServerProvider { workspace_id: &Uuid, payload: Vec, ) -> Result<(), FlowyError> { - let server = self.get_server()?; - - server + self + .get_server()? .folder_service() .publish_view(workspace_id, payload) .await @@ -325,8 +304,8 @@ impl FolderCloudService for ServerProvider { workspace_id: &Uuid, view_ids: Vec, ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server + self + .get_server()? .folder_service() .unpublish_views(workspace_id, view_ids) .await @@ -343,8 +322,8 @@ impl FolderCloudService for ServerProvider { view_id: Uuid, new_name: String, ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server + self + .get_server()? .folder_service() .set_publish_name(workspace_id, view_id, new_name) .await @@ -355,21 +334,13 @@ impl FolderCloudService for ServerProvider { workspace_id: &Uuid, new_namespace: String, ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server + self + .get_server()? .folder_service() .set_publish_namespace(workspace_id, new_namespace) .await } - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { - let server = self.get_server()?; - server - .folder_service() - .get_publish_namespace(workspace_id) - .await - } - /// List all published views of the current workspace. async fn list_published_views( &self, @@ -413,6 +384,14 @@ impl FolderCloudService for ServerProvider { .await } + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { + let server = self.get_server()?; + server + .folder_service() + .get_publish_namespace(workspace_id) + .await + } + async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { self .get_server()? @@ -420,18 +399,6 @@ impl FolderCloudService for ServerProvider { .import_zip(file_path) .await } - - async fn full_sync_collab_object( - &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .full_sync_collab_object(workspace_id, params) - .await - } } #[async_trait] diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9c6a383bae..3cf4e290fd 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -159,6 +159,7 @@ impl DocumentManager { format!("document {} already exists", doc_id), )) } else { + info!("create document {}", doc_id); let encoded_collab = doc_state_from_document_data(doc_id, data).await?; self .persistence()? diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 52ed4b7314..05cc8f867d 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -11,22 +11,6 @@ use uuid::Uuid; /// [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 - async fn create_workspace(&self, uid: i64, name: &str) -> Result; - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; - - /// Returns all workspaces of the user. - /// Returns vec![] if the cloud service doesn't support multiple workspaces - async fn get_all_workspace(&self) -> Result, FlowyError>; - - async fn get_folder_data( - &self, - workspace_id: &Uuid, - uid: &i64, - ) -> Result, FlowyError>; - async fn get_folder_snapshots( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 6889b7ebe6..ec11d57517 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -18,28 +18,6 @@ fn upgrade_folder( Ok(folder) } -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn create_workspace_handler( - data: AFPluginData, - folder: AFPluginState>, -) -> DataResult { - let folder = upgrade_folder(folder)?; - let params: CreateWorkspaceParams = data.into_inner().try_into()?; - let workspace = folder.create_workspace(params).await?; - let views = folder - .get_views_belong_to(&workspace.id) - .await? - .into_iter() - .map(|view| view_pb_without_child_views(view.as_ref().clone())) - .collect::>(); - data_result_ok(WorkspacePB { - id: workspace.id, - name: workspace.name, - views, - create_time: workspace.created_at, - }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_all_workspace_handler( _data: AFPluginData, diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 19953aad1b..c857353c4b 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -11,7 +11,6 @@ use crate::manager::FolderManager; pub fn init(folder: Weak) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace - .event(FolderEvent::CreateFolderWorkspace, create_workspace_handler) .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) @@ -60,8 +59,7 @@ pub fn init(folder: Weak) -> AFPlugin { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum FolderEvent { - /// Create a new workspace - #[event(input = "CreateWorkspacePayloadPB", output = "WorkspacePB")] + /// Deprecated: Create a new workspace CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 8e228191c4..37533ae500 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,9 +1,9 @@ use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, - CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, - MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, - ViewLayoutPB, ViewPB, ViewSectionPB, WorkspaceLatestPB, WorkspacePB, + CreateViewParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, + RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewLayoutPB, ViewPB, + ViewSectionPB, WorkspaceLatestPB, WorkspacePB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -353,16 +353,6 @@ impl FolderManager { /// pub async fn clear(&self, _user_id: i64) {} - #[tracing::instrument(level = "info", skip_all, err)] - pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult { - let uid = self.user.user_id()?; - let new_workspace = self - .cloud_service - .create_workspace(uid, ¶ms.name) - .await?; - Ok(new_workspace) - } - pub async fn get_workspace_setting_pb(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; 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 e6408bc24c..578f2870c6 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 @@ -1,13 +1,9 @@ use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::{ - workspace_dto::CreateWorkspaceParam, CollabParams, PublishCollabItem, PublishCollabMetadata, - QueryCollab, QueryCollabParams, + CollabParams, PublishCollabItem, PublishCollabMetadata, QueryCollab, QueryCollabParams, }; use client_api::entity::{PatchPublishedCollab, PublishInfo}; -use collab::core::collab::DataSource; -use collab::core::origin::CollabOrigin; use collab_entity::CollabType; -use collab_folder::RepeatedViewIdentifier; use serde_json::to_vec; use std::path::PathBuf; use std::sync::Weak; @@ -16,8 +12,7 @@ use uuid::Uuid; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ - Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, FullSyncCollabParams, - Workspace, WorkspaceRecord, + FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; @@ -36,83 +31,6 @@ impl FolderCloudService for AFCloudFolderCloudServiceImpl where T: AFServer, { - 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(); - - 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), - }) - } - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client(); - let client = try_get_client?; - let _ = client.open_workspace(workspace_id).await?; - Ok(()) - } - - async fn get_all_workspace(&self) -> Result, FlowyError> { - let try_get_client = self.inner.try_get_client(); - - 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)] - async fn get_folder_data( - &self, - workspace_id: &Uuid, - uid: &i64, - ) -> Result, FlowyError> { - let uid = *uid; - let try_get_client = self.inner.try_get_client(); - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*workspace_id, 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, &self.logged_user, "get folder data")?; - let folder = Folder::from_collab_doc_state( - uid, - CollabOrigin::Empty, - DataSource::DocStateV1(doc_state), - &workspace_id.to_string(), - vec![], - )?; - Ok(folder.get_folder_data(&workspace_id.to_string())) - } - async fn get_folder_snapshots( &self, _workspace_id: &str, @@ -278,15 +196,6 @@ where Ok(()) } - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { - let namespace = self - .inner - .try_get_client()? - .get_workspace_publish_namespace(workspace_id) - .await?; - Ok(namespace) - } - async fn list_published_views( &self, workspace_id: &Uuid, @@ -337,6 +246,15 @@ where Ok(()) } + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { + let namespace = self + .inner + .try_get_client()? + .get_workspace_publish_namespace(workspace_id) + .await?; + Ok(namespace) + } + async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { let file_path = PathBuf::from(file_path); let client = self.inner.try_get_client()?; 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 d6260d9e09..7cc3f5d88c 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 @@ -21,16 +21,6 @@ use client_api::{Client, ClientConfiguration}; use collab_entity::{CollabObject, CollabType}; use tracing::{instrument, trace}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; -use flowy_user_pub::entities::{ - AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserProfile, UserWorkspace, - WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, -}; -use lib_infra::async_trait::async_trait; -use lib_infra::box_any::BoxAny; -use uuid::Uuid; - use crate::af_cloud::define::{LoggedUser, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, @@ -38,6 +28,16 @@ use crate::af_cloud::impls::user::dto::{ use crate::af_cloud::impls::user::util::encryption_type_from_profile; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::{AFCloudClient, AFServer}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; +use flowy_user_pub::entities::{ + AFCloudOAuthParams, AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, + UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +}; +use flowy_user_pub::sql::select_user_workspace; +use lib_infra::async_trait::async_trait; +use lib_infra::box_any::BoxAny; +use uuid::Uuid; use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_status}; @@ -178,25 +178,30 @@ where } #[instrument(level = "debug", skip_all)] - async fn get_user_profile(&self, _uid: i64) -> Result { - let try_get_client = self.server.try_get_client(); - let expected_workspace_id = self + async fn get_user_profile( + &self, + uid: i64, + workspace_id: &str, + ) -> Result { + let client = self.server.try_get_client()?; + let logged_user = self .logged_user .upgrade() - .ok_or_else(FlowyError::user_not_login)? - .workspace_id()?; - let client = try_get_client?; + .ok_or_else(FlowyError::user_not_login)?; + let profile = client.get_profile().await?; let token = client.get_token()?; - let profile = user_profile_from_af_profile(token, profile)?; + + let mut conn = logged_user.get_sqlite_db(uid)?; + let workspace_auth_type = select_user_workspace(workspace_id, &mut conn) + .map(|row| AuthType::from(row.workspace_type)) + .unwrap_or(AuthType::AppFlowyCloud); + let profile = user_profile_from_af_profile(token, profile, workspace_auth_type)?; // 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, - &self.logged_user, - "get user profile", - )?; + let workspace_id = Uuid::from_str(workspace_id)?; + check_request_workspace_id_is_match(&workspace_id, &self.logged_user, "get user profile")?; Ok(profile) } @@ -219,10 +224,10 @@ where } 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(); - let client = try_get_client?; - let new_workspace = client + let new_workspace = self + .server + .try_get_client()? .create_workspace(CreateWorkspaceParam { workspace_name: Some(workspace_name_owned), }) @@ -236,10 +241,10 @@ where new_workspace_name: Option, new_workspace_icon: Option, ) -> Result<(), FlowyError> { - let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_owned(); - let client = try_get_client?; - client + self + .server + .try_get_client()? .patch_workspace(PatchWorkspaceParam { workspace_id, workspace_name: new_workspace_name, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index ba13a7fbca..838e9dd6ca 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -25,6 +25,7 @@ pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUs pub fn user_profile_from_af_profile( token: String, profile: AFUserProfile, + workspace_auth_type: AuthType, ) -> Result { let icon_url = { profile @@ -44,6 +45,7 @@ pub fn user_profile_from_af_profile( auth_type: AuthType::AppFlowyCloud, uid: profile.uid, updated_at: profile.updated_at, + workspace_auth_type, }) } 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 483ca3d100..79b1d4be12 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 @@ -11,8 +11,7 @@ use collab_plugins::local_storage::kv::doc::CollabKVAction; use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ - gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, - FullSyncCollabParams, Workspace, WorkspaceRecord, + FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; @@ -26,31 +25,6 @@ pub(crate) struct LocalServerFolderCloudServiceImpl { #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { - async fn create_workspace(&self, uid: i64, name: &str) -> Result { - let name = name.to_string(); - Ok(Workspace::new( - gen_workspace_id().to_string(), - name.to_string(), - uid, - )) - } - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_all_workspace(&self) -> Result, FlowyError> { - Ok(vec![]) - } - - async fn get_folder_data( - &self, - workspace_id: &Uuid, - uid: &i64, - ) -> Result, FlowyError> { - Ok(None) - } - async fn get_folder_snapshots( &self, _workspace_id: &str, @@ -89,6 +63,14 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { } } + async fn full_sync_collab_object( + &self, + workspace_id: &Uuid, + params: FullSyncCollabParams, + ) -> Result<(), FlowyError> { + Ok(()) + } + async fn batch_create_folder_collab_objects( &self, workspace_id: &Uuid, @@ -121,18 +103,6 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { Err(FlowyError::local_version_not_support()) } - async fn set_publish_namespace( - &self, - workspace_id: &Uuid, - new_namespace: String, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { - Err(FlowyError::local_version_not_support()) - } - async fn set_publish_name( &self, workspace_id: &Uuid, @@ -142,6 +112,14 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { Err(FlowyError::local_version_not_support()) } + async fn set_publish_namespace( + &self, + workspace_id: &Uuid, + new_namespace: String, + ) -> Result<(), FlowyError> { + Err(FlowyError::local_version_not_support()) + } + async fn list_published_views( &self, workspace_id: &Uuid, @@ -168,15 +146,11 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { Err(FlowyError::local_version_not_support()) } - async fn import_zip(&self, _file_path: &str) -> Result<(), FlowyError> { + async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { Err(FlowyError::local_version_not_support()) } - async fn full_sync_collab_object( - &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - Ok(()) + async fn import_zip(&self, _file_path: &str) -> Result<(), FlowyError> { + Err(FlowyError::local_version_not_support()) } } 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 0023defb41..5a4caeb050 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 @@ -41,7 +41,7 @@ impl UserCloudService for LocalServerUserServiceImpl { let params = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); let workspace_id = Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new_local(workspace_id, ""); + let user_workspace = UserWorkspace::new_local(workspace_id, "My Workspace"); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() } else { @@ -135,9 +135,13 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } - async fn get_user_profile(&self, uid: i64) -> Result { + async fn get_user_profile( + &self, + uid: i64, + workspace_id: &str, + ) -> Result { let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, &mut conn)?; + let profile = select_user_profile(uid, workspace_id, &mut conn)?; Ok(profile) } @@ -150,8 +154,8 @@ impl UserCloudService for LocalServerUserServiceImpl { } async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { - let conn = self.logged_user.get_sqlite_db(uid)?; - let workspaces = select_all_user_workspace(uid, conn)?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let workspaces = select_all_user_workspace(uid, &mut conn)?; Ok(workspaces) } @@ -223,7 +227,7 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(err) => { if err.is_record_not_found() { let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, &mut conn)?; + let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn)?; let row = WorkspaceMemberTable { email: profile.email.to_string(), role: 0, 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 9ce19f5df6..8829ded3fc 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,19 +1,19 @@ -use flowy_search_pub::cloud::SearchCloudService; -use std::sync::Arc; - use crate::af_cloud::define::LoggedUser; use crate::local_server::impls::{ LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, }; use crate::AppFlowyServer; +use anyhow::Error; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::UserCloudService; +use std::sync::Arc; use tokio::sync::mpsc; pub struct LocalServer { @@ -40,6 +40,10 @@ impl LocalServer { } impl AppFlowyServer for LocalServer { + fn set_token(&self, _token: &str) -> Result<(), Error> { + Ok(()) + } + fn user_service(&self) -> Arc { Arc::new(LocalServerUserServiceImpl { logged_user: self.logged_user.clone(), diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 4c92fe28d2..2702b4f104 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -41,9 +41,7 @@ where /// and functionalities in AppFlowy. The methods provided ensure efficient, asynchronous operations /// for managing and accessing user data, folders, collaborative objects, and documents in a cloud environment. pub trait AppFlowyServer: Send + Sync + 'static { - fn set_token(&self, _token: &str) -> Result<(), Error> { - Ok(()) - } + fn set_token(&self, _token: &str) -> Result<(), Error>; fn set_ai_model(&self, _ai_model: &str) -> Result<(), Error> { Ok(()) diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index e58c626532..9b223178ff 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -168,7 +168,8 @@ pub trait UserCloudService: Send + Sync + 'static { /// Get the user information using the user's token or uid /// return None if the user is not found - async fn get_user_profile(&self, uid: i64) -> Result; + async fn get_user_profile(&self, uid: i64, workspace_id: &str) + -> Result; async fn open_workspace(&self, workspace_id: &Uuid) -> Result; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index efceb8b5f6..061bda56f5 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -143,6 +143,7 @@ pub struct UserProfile { pub token: String, pub icon_url: String, pub auth_type: AuthType, + pub workspace_auth_type: AuthType, pub updated_at: i64, } @@ -207,6 +208,7 @@ where token: value.user_token().unwrap_or_default(), icon_url, auth_type: *auth_type, + workspace_auth_type: *auth_type, updated_at: value.updated_at(), } } @@ -317,7 +319,7 @@ pub enum UserTokenState { } // Workspace Role -#[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] +#[derive(Clone, Copy, Debug, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] pub enum Role { Owner = 0, diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 4cdf26520e..16db46125f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -1,8 +1,10 @@ use crate::cloud::UserUpdate; use crate::entities::{AuthType, UpdateUserProfileParams, UserProfile}; +use crate::sql::select_user_workspace; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; +use tracing::trace; /// The order of the fields in the struct must be the same as the order of the fields in the table. /// Check out the [schema.rs] for table schema. @@ -35,20 +37,6 @@ impl From<(UserProfile, AuthType)> for UserTable { } } -impl From for UserProfile { - fn from(table: UserTable) -> Self { - UserProfile { - uid: table.id.parse::().unwrap_or(0), - email: table.email, - name: table.name, - token: table.token, - icon_url: table.icon_url, - auth_type: AuthType::from(table.auth_type), - updated_at: table.updated_at, - } - } -} - #[derive(AsChangeset, Identifiable, Default, Debug)] #[diesel(table_name = user_table)] pub struct UserTableChangeset { @@ -96,6 +84,7 @@ pub fn update_user_profile( conn: &mut SqliteConnection, changeset: UserTableChangeset, ) -> Result<(), FlowyError> { + trace!("update user profile: {:?}", changeset); let user_id = changeset.id.clone(); update(user_table::dsl::user_table.filter(user_table::id.eq(&user_id))) .set(changeset) @@ -105,9 +94,13 @@ pub fn update_user_profile( pub fn select_user_profile( uid: i64, + workspace_id: &str, conn: &mut SqliteConnection, ) -> Result { - let user: UserProfile = user_table::dsl::user_table + let workspace = select_user_workspace(workspace_id, conn)?; + let workspace_auth_type = AuthType::from(workspace.workspace_type); + + let row = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) .first::(conn) .map_err(|err| { @@ -115,8 +108,18 @@ pub fn select_user_profile( "Can't find the user profile for user id: {}, error: {:?}", uid, err )) - })? - .into(); + })?; + + let user = UserProfile { + uid: row.id.parse::().unwrap_or(0), + email: row.email, + name: row.name, + token: row.token, + icon_url: row.icon_url, + auth_type: AuthType::from(row.auth_type), + workspace_auth_type, + updated_at: row.updated_at, + }; Ok(user) } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index 709e218514..80c99eb7e6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -2,8 +2,10 @@ use crate::entities::{AuthType, UserWorkspace}; use chrono::{TimeZone, Utc}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::schema::user_workspace_table::dsl; use flowy_sqlite::DBConnection; use flowy_sqlite::{prelude::*, ExpressionMethods, RunQueryDsl, SqliteConnection}; +use std::collections::{HashMap, HashSet}; use tracing::{info, warn}; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] @@ -26,11 +28,43 @@ pub struct UserWorkspaceChangeset { pub id: String, pub name: Option, pub icon: Option, + pub role: Option, + pub member_count: Option, +} + +impl UserWorkspaceChangeset { + pub fn has_changes(&self) -> bool { + self.name.is_some() || self.icon.is_some() || self.role.is_some() || self.member_count.is_some() + } + pub fn from_version(old: &UserWorkspace, new: &UserWorkspace) -> Self { + let mut changeset = Self { + id: new.id.clone(), + name: None, + icon: None, + role: None, + member_count: None, + }; + + if old.name != new.name { + changeset.name = Some(new.name.clone()); + } + if old.icon != new.icon { + changeset.icon = Some(new.icon.clone()); + } + if old.role != new.role { + changeset.role = new.role.map(|v| v as i32); + } + if old.member_count != new.member_count { + changeset.member_count = Some(new.member_count); + } + + changeset + } } impl UserWorkspaceTable { pub fn from_workspace( - uid: i64, + uid_val: i64, workspace: &UserWorkspace, auth_type: AuthType, ) -> Result { @@ -44,12 +78,12 @@ impl UserWorkspaceTable { Ok(Self { id: workspace.id.clone(), name: workspace.name.clone(), - uid, + uid: uid_val, created_at: workspace.created_at.timestamp(), database_storage_id: workspace.workspace_database_id.clone(), icon: workspace.icon.clone(), member_count: workspace.member_count, - role: workspace.role.clone().map(|v| v as i32), + role: workspace.role.map(|v| v as i32), workspace_type: auth_type as i32, }) } @@ -59,19 +93,20 @@ pub fn select_user_workspace( workspace_id: &str, conn: &mut SqliteConnection, ) -> FlowyResult { - let row = user_workspace_table::dsl::user_workspace_table + let row = dsl::user_workspace_table .filter(user_workspace_table::id.eq(workspace_id)) .first::(conn)?; Ok(row) } pub fn select_all_user_workspace( - user_id: i64, - mut conn: DBConnection, + uid: i64, + conn: &mut SqliteConnection, ) -> Result, FlowyError> { let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(user_id)) - .load::(&mut *conn)?; + .filter(user_workspace_table::uid.eq(uid)) + .order(user_workspace_table::created_at.desc()) + .load::(conn)?; Ok(rows.into_iter().map(UserWorkspace::from).collect()) } @@ -87,32 +122,6 @@ pub fn update_user_workspace( Ok(()) } -pub fn upsert_user_workspace( - uid: i64, - auth_type: AuthType, - user_workspace: UserWorkspace, - conn: &mut SqliteConnection, -) -> Result<(), FlowyError> { - let row = UserWorkspaceTable::from_workspace(uid, &user_workspace, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(row.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(row.name), - user_workspace_table::uid.eq(row.uid), - user_workspace_table::created_at.eq(row.created_at), - user_workspace_table::database_storage_id.eq(row.database_storage_id), - user_workspace_table::icon.eq(row.icon), - user_workspace_table::member_count.eq(row.member_count), - user_workspace_table::role.eq(row.role), - user_workspace_table::workspace_type.eq(row.workspace_type), - )) - .execute(conn)?; - - Ok(()) -} - pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { let n = conn.immediate_transaction(|conn| { let rows_affected: usize = @@ -151,7 +160,7 @@ pub fn delete_user_all_workspace( conn: &mut SqliteConnection, ) -> FlowyResult<()> { let n = diesel::delete( - user_workspace_table::dsl::user_workspace_table + dsl::user_workspace_table .filter(user_workspace_table::uid.eq(uid)) .filter(user_workspace_table::workspace_type.eq(auth_type as i32)), ) @@ -163,24 +172,93 @@ pub fn delete_user_all_workspace( Ok(()) } -/// Delete all user workspaces for the given user and auth type, then insert the provided user workspaces. -pub fn delete_all_then_insert_user_workspaces( - uid: i64, - mut conn: DBConnection, +#[derive(Debug)] +pub enum WorkspaceChange { + Inserted(String), + Updated(String), +} + +pub fn upsert_user_workspace( + uid_val: i64, + auth_type: AuthType, + user_workspace: UserWorkspace, + conn: &mut SqliteConnection, +) -> Result { + let row = UserWorkspaceTable::from_workspace(uid_val, &user_workspace, auth_type)?; + let n = insert_into(user_workspace_table::table) + .values(row.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(row.name), + user_workspace_table::uid.eq(row.uid), + user_workspace_table::created_at.eq(row.created_at), + user_workspace_table::database_storage_id.eq(row.database_storage_id), + user_workspace_table::icon.eq(row.icon), + user_workspace_table::member_count.eq(row.member_count), + user_workspace_table::role.eq(row.role), + )) + .execute(conn)?; + + Ok(n) +} + +pub fn sync_user_workspaces_with_diff( + uid_val: i64, auth_type: AuthType, user_workspaces: &[UserWorkspace], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - delete_user_all_workspace(uid, auth_type, conn)?; - info!( - "Insert {} workspaces for user {} and auth type {:?}", - user_workspaces.len(), - uid, - auth_type - ); - for user_workspace in user_workspaces { - upsert_user_workspace(uid, auth_type, user_workspace.clone(), conn)?; + conn: &mut SqliteConnection, +) -> FlowyResult> { + let diff = conn.immediate_transaction(|conn| { + // 1) Load all existing workspaces into a map + let existing_rows: Vec = dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid_val)) + .filter(user_workspace_table::workspace_type.eq(auth_type as i32)) + .load(conn)?; + let mut existing_map: HashMap = existing_rows + .into_iter() + .map(|r| (r.id.clone(), r)) + .collect(); + + // 2) Build incoming ID set and delete any stale ones + let incoming_ids: HashSet = user_workspaces.iter().map(|uw| uw.id.clone()).collect(); + let to_delete: Vec = existing_map + .keys() + .filter(|id| !incoming_ids.contains(*id)) + .cloned() + .collect(); + + if !to_delete.is_empty() { + diesel::delete(dsl::user_workspace_table.filter(user_workspace_table::id.eq_any(&to_delete))) + .execute(conn)?; } - Ok::<(), FlowyError>(()) - }) + + // 3) For each incoming workspace, either INSERT or UPDATE if changed + let mut diffs = Vec::new(); + for uw in user_workspaces { + match existing_map.remove(&uw.id) { + None => { + // new workspace → insert + let new_row = UserWorkspaceTable::from_workspace(uid_val, uw, auth_type)?; + diesel::insert_into(user_workspace_table::table) + .values(new_row) + .execute(conn)?; + diffs.push(WorkspaceChange::Inserted(uw.id.clone())); + }, + + Some(old) => { + let changes = UserWorkspaceChangeset::from_version(&UserWorkspace::from(old), uw); + if changes.has_changes() { + diesel::update(dsl::user_workspace_table.find(&uw.id)) + .set(&changes) + .execute(conn)?; + diffs.push(WorkspaceChange::Updated(uw.id.clone())); + } + }, + } + } + + Ok::<_, FlowyError>(diffs) + })?; + Ok(diff) } 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 6a95a89041..a117d0839e 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -39,7 +39,10 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub auth_type: AuthTypePB, + pub user_auth_type: AuthTypePB, + + #[pb(index = 7)] + pub workspace_auth_type: AuthTypePB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -62,7 +65,8 @@ impl From for UserProfilePB { name: user_profile.name, token: user_profile.token, icon_url: user_profile.icon_url, - auth_type: user_profile.auth_type.into(), + user_auth_type: user_profile.auth_type.into(), + workspace_auth_type: user_profile.workspace_auth_type.into(), } } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index b1ce8c5ec6..bf865f24f0 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -91,16 +91,22 @@ pub async fn get_user_profile_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let uid = manager.get_session()?.user_id; - let mut user_profile = manager.get_user_profile_from_disk(uid).await?; + let session = manager.get_session()?; + + let mut user_profile = manager + .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .await?; let weak_manager = Arc::downgrade(&manager); let cloned_user_profile = user_profile.clone(); + let workspace_id = session.user_workspace.id.clone(); // Refresh the user profile in the background tokio::spawn(async move { if let Some(manager) = weak_manager.upgrade() { - let _ = manager.refresh_user_profile(&cloned_user_profile).await; + let _ = manager + .refresh_user_profile(&cloned_user_profile, &workspace_id) + .await; } }); @@ -425,7 +431,10 @@ pub async fn get_all_workspace_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let profile = manager.get_user_profile().await?; + let session = manager.get_session()?; + let profile = manager + .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .await?; let user_workspaces = manager .get_all_user_workspaces(profile.uid, profile.auth_type) .await?; @@ -645,6 +654,8 @@ pub async fn rename_workspace_handler( id: params.workspace_id, name: Some(params.new_name), icon: None, + role: None, + member_count: None, }; manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) @@ -662,6 +673,8 @@ pub async fn change_workspace_icon_handler( id: workspace_id.to_string(), name: None, icon: Some(params.new_icon), + role: None, + member_count: None, }; manager.patch_workspace(&workspace_id, changeset).await?; Ok(()) 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 a4c7d3bd1d..316311b6e2 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 @@ -103,6 +103,7 @@ pub(crate) fn prepare_import( ); let imported_user = select_user_profile( imported_session.user_id, + &imported_session.user_workspace.id, &mut *imported_sqlite_db.get_connection()?, )?; diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 3280370c88..15126558d7 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -125,9 +125,10 @@ impl UserDB { &self, pool: &Arc, uid: i64, + workspace_id: &str, ) -> Result { let mut conn = pool.get()?; - let profile = select_user_profile(uid, &mut conn)?; + let profile = select_user_profile(uid, workspace_id, &mut conn)?; Ok(profile) } 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 3b568f976e..b3691a331e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -128,7 +128,9 @@ impl UserManager { *self.collab_interact.write().await = Arc::new(collab_interact); if let Ok(session) = self.get_session() { - let user = self.get_user_profile_from_disk(session.user_id).await?; + let user = self + .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .await?; self.cloud_service.set_server_auth_type(&user.auth_type); // Get the current authenticator from the environment variable @@ -175,6 +177,7 @@ impl UserManager { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; let local_token = user.token.clone(); + let workspace_id = session.user_workspace.id.clone(); tokio::spawn(async move { while let Some(token_state) = token_state_rx.next().await { debug!("Token state changed: {:?}", token_state); @@ -184,7 +187,7 @@ impl UserManager { if new_token != local_token { if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { // Save the new token - if let Err(err) = save_user_token(user_uid, conn, new_token) { + if let Err(err) = save_user_token(user_uid, &workspace_id, conn, new_token) { error!("Save user token failed: {}", err); } } @@ -503,6 +506,7 @@ impl UserManager { let session = self.get_session()?; upsert_user_profile_change( session.user_id, + &session.user_workspace.id, self.db_connection(session.user_id)?, changeset, )?; @@ -535,20 +539,22 @@ impl UserManager { .backup(session.user_id, &session.user_workspace.id); } - pub async fn get_user_profile(&self) -> FlowyResult { - let uid = self.get_session()?.user_id; - let profile = self.get_user_profile_from_disk(uid).await?; - Ok(profile) - } - /// Fetches the user profile for the given user ID. - pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { + pub async fn get_user_profile_from_disk( + &self, + uid: i64, + workspace_id: &str, + ) -> Result { let mut conn = self.db_connection(uid)?; - select_user_profile(uid, &mut conn) + select_user_profile(uid, workspace_id, &mut conn) } #[tracing::instrument(level = "info", skip_all, err)] - pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { + pub async fn refresh_user_profile( + &self, + old_user_profile: &UserProfile, + workspace_id: &str, + ) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile if old_user_profile.auth_type.is_local() { return Ok(()); @@ -565,7 +571,7 @@ impl UserManager { let result: Result = self .cloud_service .get_user_service()? - .get_user_profile(uid) + .get_user_profile(uid, workspace_id) .await; match result { @@ -576,6 +582,7 @@ impl UserManager { let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change( uid, + workspace_id, self.authenticate_user.database.get_connection(uid)?, changeset, ); @@ -729,12 +736,8 @@ impl UserManager { self.set_anon_user(session); } - delete_all_then_insert_user_workspaces( - uid, - self.db_connection(uid)?, - auth_type, - response.user_workspaces(), - )?; + let mut conn = self.db_connection(uid)?; + sync_user_workspaces_with_diff(uid, auth_type, response.user_workspaces(), &mut conn)?; info!( "Save new user profile to disk, authenticator: {:?}", auth_type @@ -756,6 +759,7 @@ impl UserManager { // Save the user profile change upsert_user_profile_change( user_update.uid, + &session.user_workspace.id, self.db_connection(user_update.uid)?, UserTableChangeset::from(user_update), )?; @@ -805,6 +809,7 @@ fn current_authenticator() -> AuthType { pub fn upsert_user_profile_change( uid: i64, + workspace_id: &str, mut conn: DBConnection, changeset: UserTableChangeset, ) -> FlowyResult<()> { @@ -814,10 +819,7 @@ pub fn upsert_user_profile_change( changeset ); update_user_profile(&mut conn, changeset)?; - let user: UserProfile = user_table::dsl::user_table - .filter(user_table::id.eq(&uid.to_string())) - .first::(&mut *conn)? - .into(); + let user = select_user_profile(uid, workspace_id, &mut conn)?; send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile) .payload(UserProfilePB::from(user)) .send(); @@ -825,10 +827,15 @@ pub fn upsert_user_profile_change( } #[instrument(level = "info", skip_all, err)] -fn save_user_token(uid: i64, conn: DBConnection, token: String) -> FlowyResult<()> { +fn save_user_token( + uid: i64, + workspace_id: &str, + conn: DBConnection, + token: String, +) -> FlowyResult<()> { let params = UpdateUserProfileParams::new(uid).with_token(token); let changeset = UserTableChangeset::new(params); - upsert_user_profile_change(uid, conn, changeset) + upsert_user_profile_change(uid, workspace_id, conn, changeset) } #[instrument(level = "info", skip_all, err)] @@ -886,6 +893,7 @@ pub(crate) fn run_collab_data_migration( } } +#[instrument(level = "info", skip_all, err)] pub async fn sign_out( cloud_services: &Arc, session: &Session, 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 7c5330149e..a7191f0509 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 @@ -20,7 +20,7 @@ impl UserManager { let session = self.get_session().ok()?; let user_profile = self - .get_user_profile_from_disk(session.user_id) + .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) .await .ok()?; @@ -48,7 +48,7 @@ impl UserManager { "Anon user not found", ))?; let profile = self - .get_user_profile_from_disk(anon_session.user_id) + .get_user_profile_from_disk(anon_session.user_id, &anon_session.user_workspace.id) .await?; Ok(UserProfilePB::from(profile)) } 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 afdfd218ef..47826054bf 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 @@ -374,9 +374,11 @@ impl UserManager { .unwrap_or(false); if !is_loading { - let user_profile = self.get_user_profile_from_disk(session.user_id).await?; + let user_profile = self + .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .await?; self - .initial_user_awareness(&session, &user_profile.auth_type) + .initial_user_awareness(&session, &user_profile.workspace_auth_type) .await?; } 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 f74cf45ef7..c641fde831 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 @@ -101,7 +101,7 @@ impl UserManager { collab_data: ImportedCollabData, ) -> Result<(), FlowyError> { let user = self - .get_user_profile_from_disk(current_session.user_id) + .get_user_profile_from_disk(current_session.user_id, ¤t_session.user_workspace.id) .await?; let user_collab_db = self .get_collab_db(current_session.user_id)? @@ -115,7 +115,7 @@ impl UserManager { user_id, weak_user_collab_db, ¤t_session.user_workspace.workspace_id()?, - &user.auth_type, + &user.workspace_auth_type, collab_data, weak_user_cloud_service, ) @@ -154,11 +154,19 @@ impl UserManager { #[instrument(skip(self), err)] pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + let workspace_id_str = workspace_id.to_string(); self.cloud_service.set_server_auth_type(&auth_type); let uid = self.user_id()?; + let profile = self + .get_user_profile_from_disk(uid, &workspace_id_str) + .await?; + if let Err(err) = self.cloud_service.set_token(&profile.token) { + error!("Set token failed: {}", err); + } + let mut conn = self.db_connection(self.user_id()?)?; - let user_workspace = match select_user_workspace(&workspace_id.to_string(), &mut conn) { + let user_workspace = match select_user_workspace(&workspace_id_str, &mut conn) { Err(err) => { if err.is_record_not_found() { sync_workspace( @@ -190,19 +198,18 @@ impl UserManager { .set_user_workspace(user_workspace.clone())?; let uid = self.user_id()?; - let user_profile = self.get_user_profile_from_disk(uid).await?; if let Err(err) = self .user_status_callback .read() .await - .on_workspace_opened(uid, workspace_id, &user_workspace, &user_profile.auth_type) + .on_workspace_opened(uid, workspace_id, &user_workspace, &auth_type) .await { error!("Open workspace failed: {:?}", err); } if let Err(err) = self - .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.auth_type) + .initial_user_awareness(self.get_session()?.as_ref(), &auth_type) .await { error!( @@ -220,19 +227,13 @@ impl UserManager { workspace_name: &str, auth_type: AuthType, ) -> FlowyResult { - let new_workspace = match auth_type { - AuthType::Local => { - let workspace_id = Uuid::new_v4(); - UserWorkspace::new_local(workspace_id.to_string(), workspace_name) - }, - AuthType::AppFlowyCloud => { - self - .cloud_service - .get_user_service()? - .create_workspace(workspace_name) - .await? - }, - }; + self.cloud_service.set_server_auth_type(&auth_type); + + let new_workspace = self + .cloud_service + .get_user_service()? + .create_workspace(workspace_name) + .await?; info!( "create workspace: {}, name:{}, auth_type: {}", @@ -410,27 +411,59 @@ impl UserManager { uid: i64, auth_type: AuthType, ) -> FlowyResult> { - let conn = self.db_connection(uid)?; - let workspaces = select_all_user_workspace(uid, conn)?; + // 1) Load & return the local copy immediately + let mut conn = self.db_connection(uid)?; + let local_workspaces = select_all_user_workspace(uid, &mut conn)?; - if let Ok(service) = self.cloud_service.get_user_service() { - if let Ok(pool) = self.db_pool(uid) { - tokio::spawn(async move { - if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { - if let Ok(conn) = pool.get() { - let _ = - delete_all_then_insert_user_workspaces(uid, conn, auth_type, &new_user_workspaces); - let repeated_workspace_pbs = - RepeatedUserWorkspacePB::from((auth_type, new_user_workspaces)); + // 2) If both cloud service and pool are available, fire off a background sync + if let (Ok(service), Ok(pool)) = (self.cloud_service.get_user_service(), self.db_pool(uid)) { + // capture only what we need + let auth_copy = auth_type; + + tokio::spawn(async move { + // fetch remote list + let new_ws = match service.get_all_workspace(uid).await { + Ok(ws) => ws, + Err(e) => { + trace!("failed to fetch remote workspaces for {}: {:?}", uid, e); + return; + }, + }; + + // get a pooled DB connection + let mut conn = match pool.get() { + Ok(c) => c, + Err(e) => { + trace!("failed to get DB connection for {}: {:?}", uid, e); + return; + }, + }; + + // sync + diff + match sync_user_workspaces_with_diff(uid, auth_copy, &new_ws, &mut conn) { + Ok(changes) if !changes.is_empty() => { + info!( + "synced {} workspaces for user {} and auth type {:?}. changes: {:?}", + changes.len(), + uid, + auth_copy, + changes + ); + // only send notification if there were real changes + if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) { + let repeated_pb = RepeatedUserWorkspacePB::from((auth_copy, updated_list)); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) - .payload(repeated_workspace_pbs) + .payload(repeated_pb) .send(); } - } - }); - } + }, + Ok(_) => trace!("no workspaces updated for {}", uid), + Err(e) => trace!("sync error for {}: {:?}", uid, e), + } + }); } - Ok(workspaces) + + Ok(local_workspaces) } #[instrument(level = "info", skip(self), err)] @@ -660,7 +693,7 @@ impl UserManager { let record = WorkspaceMemberTable { email: member.email.clone(), - role: member.role.clone().into(), + role: member.role.into(), name: member.name.clone(), avatar_url: member.avatar_url.clone(), uid, From 7885cb80f4056513765da58a70d188f6b5afa0ed Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 12:03:01 +0800 Subject: [PATCH 54/74] chore: fix rust test --- .../tests/folder/local_test/test.rs | 46 ------------------- .../tests/user/af_cloud_test/util.rs | 11 +++-- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs index 09af815d65..2297324c53 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs @@ -4,23 +4,6 @@ use flowy_folder::entities::icon::{UpdateViewIconPayloadPB, ViewIconPB, ViewIcon use flowy_folder::entities::*; use flowy_user::errors::ErrorCode; -#[tokio::test] -async fn create_workspace_event_test() { - let test = EventIntegrationTest::new_anon().await; - let request = CreateWorkspacePayloadPB { - name: "my second workspace".to_owned(), - desc: "".to_owned(), - }; - let view_pb = EventBuilder::new(test) - .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) - .payload(request) - .async_send() - .await - .parse::(); - - assert_eq!(view_pb.parent_view_id, "my second workspace".to_owned()); -} - // #[tokio::test] // async fn open_workspace_event_test() { // let test = EventIntegrationTest::new_with_guest_user().await; @@ -464,35 +447,6 @@ async fn move_view_event_after_delete_view_test2() { assert_eq!(views[3].name, "My 1-5 view"); } -#[tokio::test] -async fn create_parent_view_with_invalid_name() { - for (name, code) in invalid_workspace_name_test_case() { - let sdk = EventIntegrationTest::new().await; - let request = CreateWorkspacePayloadPB { - name, - desc: "".to_owned(), - }; - assert_eq!( - EventBuilder::new(sdk) - .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) - .payload(request) - .async_send() - .await - .error() - .unwrap() - .code, - code - ) - } -} - -fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { - vec![ - ("".to_owned(), ErrorCode::WorkspaceNameInvalid), - ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong), - ] -} - #[tokio::test] async fn move_view_across_parent_test() { let test = EventIntegrationTest::new_anon().await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs index 9830656bb3..0caa9a6227 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs @@ -12,7 +12,7 @@ pub async fn get_synced_workspaces( test: &EventIntegrationTest, user_id: i64, ) -> Vec { - let _workspaces = test.get_all_workspaces().await.items; + let workspaces = test.get_all_workspaces().await.items; let sub_id = user_id.to_string(); let rx = test .notification_sender @@ -20,8 +20,9 @@ pub async fn get_synced_workspaces( &sub_id, UserNotification::DidUpdateUserWorkspaces as i32, ); - receive_with_timeout(rx, Duration::from_secs(60)) - .await - .unwrap() - .items + if let Some(result) = receive_with_timeout(rx, Duration::from_secs(10)).await { + result.items + } else { + workspaces + } } From 2cf96a2ce33a49667510ae2cb75b933852b50b3d Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 13:07:50 +0800 Subject: [PATCH 55/74] chore: fix rust test --- .../rust-lib/flowy-document/src/manager.rs | 1 - .../flowy-user-pub/src/sql/user_sql.rs | 44 +++++++++++++++---- .../data_import/appflowy_data_import.rs | 20 ++++++--- .../flowy-user/src/user_manager/manager.rs | 6 +-- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 3cf4e290fd..9c6a383bae 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -159,7 +159,6 @@ impl DocumentManager { format!("document {} already exists", doc_id), )) } else { - info!("create document {}", doc_id); let encoded_collab = doc_state_from_document_data(doc_id, data).await?; self .persistence()? diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 16db46125f..384b66d51b 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -4,7 +4,7 @@ use crate::sql::select_user_workspace; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use tracing::trace; +use tracing::{trace, warn}; /// The order of the fields in the struct must be the same as the order of the fields in the table. /// Check out the [schema.rs] for table schema. @@ -92,14 +92,7 @@ pub fn update_user_profile( Ok(()) } -pub fn select_user_profile( - uid: i64, - workspace_id: &str, - conn: &mut SqliteConnection, -) -> Result { - let workspace = select_user_workspace(workspace_id, conn)?; - let workspace_auth_type = AuthType::from(workspace.workspace_type); - +fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result { let row = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) .first::(conn) @@ -109,6 +102,17 @@ pub fn select_user_profile( uid, err )) })?; + Ok(row) +} + +pub fn select_user_profile( + uid: i64, + workspace_id: &str, + conn: &mut SqliteConnection, +) -> Result { + let workspace = select_user_workspace(workspace_id, conn)?; + let workspace_auth_type = AuthType::from(workspace.workspace_type); + let row = select_user_table_row(uid, conn)?; let user = UserProfile { uid: row.id.parse::().unwrap_or(0), @@ -124,6 +128,28 @@ pub fn select_user_profile( Ok(user) } +pub fn select_workspace_auth_type( + uid: i64, + workspace_id: &str, + conn: &mut SqliteConnection, +) -> Result { + match select_user_workspace(workspace_id, conn) { + Ok(workspace) => Ok(AuthType::from(workspace.workspace_type)), + Err(err) => { + if err.is_record_not_found() { + let row = select_user_table_row(uid, conn)?; + warn!( + "user user auth type:{} as workspace auth type", + row.auth_type + ); + Ok(AuthType::from(row.auth_type)) + } else { + Err(err) + } + }, + } +} + pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> { conn.immediate_transaction(|conn| { // delete old user if exists 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 316311b6e2..20b0c26368 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 @@ -36,7 +36,7 @@ use std::collections::{HashMap, HashSet}; use collab_document::blocks::TextDelta; use collab_document::document::Document; -use flowy_user_pub::sql::select_user_profile; +use flowy_user_pub::sql::{select_user_profile, select_workspace_auth_type}; use semver::Version; use serde_json::json; use std::ops::{Deref, DerefMut}; @@ -101,15 +101,25 @@ pub(crate) fn prepare_import( CollabKVDB::open(collab_db_path) .map_err(|err| anyhow!("[AppflowyData]: open import collab db failed: {:?}", err))?, ); - let imported_user = select_user_profile( + + let mut conn = imported_sqlite_db.get_connection()?; + let imported_workspace_auth_type = select_user_profile( imported_session.user_id, &imported_session.user_workspace.id, - &mut *imported_sqlite_db.get_connection()?, - )?; + &mut conn, + ) + .map(|v| v.workspace_auth_type) + .or_else(|_| { + select_workspace_auth_type( + imported_session.user_id, + &imported_session.user_workspace.id, + &mut conn, + ) + })?; run_collab_data_migration( &imported_session, - &imported_user, + &imported_workspace_auth_type, imported_collab_db.clone(), imported_sqlite_db.get_pool(), other_store_preferences.clone(), 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 b3691a331e..b9af5c330d 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -253,7 +253,7 @@ impl UserManager { (Ok(collab_db), Ok(sqlite_pool)) => { run_collab_data_migration( &session, - &user, + &user.auth_type, collab_db, sqlite_pool, self.store_preferences.clone(), @@ -869,7 +869,7 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc) { pub(crate) fn run_collab_data_migration( session: &Session, - user: &UserProfile, + auth_type: &AuthType, collab_db: Arc, sqlite_pool: Arc, kv: Arc, @@ -878,7 +878,7 @@ pub(crate) fn run_collab_data_migration( let migrations = collab_migration_list(); match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( migrations, - &user.auth_type, + auth_type, app_version, ) { Ok(applied_migrations) => { From 10a536b1a2e3100fa3da99ab05851702f701a3c0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 16:37:02 +0800 Subject: [PATCH 56/74] fix: loading exception when switching workspace --- frontend/appflowy_flutter/lib/shared/loading.dart | 6 +++++- .../menu/sidebar/workspace/sidebar_workspace.dart | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/shared/loading.dart b/frontend/appflowy_flutter/lib/shared/loading.dart index f8d1c6fc86..3958601ed8 100644 --- a/frontend/appflowy_flutter/lib/shared/loading.dart +++ b/frontend/appflowy_flutter/lib/shared/loading.dart @@ -40,7 +40,11 @@ class Loading { void stop() { if (loadingContext != null) { - Navigator.of(loadingContext!).pop(); + if (loadingContext!.mounted) { + if (Navigator.canPop(loadingContext!)) { + Navigator.of(loadingContext!).pop(); + } + } loadingContext = null; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 50ea9d83c7..07a5aeec4e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -32,7 +32,10 @@ class _SidebarWorkspaceState extends State { @override void dispose() { onHover.dispose(); - + if (loadingIndicator != null) { + loadingIndicator?.stop(); + loadingIndicator = null; + } super.dispose(); } @@ -100,8 +103,10 @@ class _SidebarWorkspaceState extends State { if (isLoading) { loadingIndicator ??= Loading(context)..start(); return; - } else { - loadingIndicator?.stop(); + } else if (loadingIndicator != null) { + if (mounted) { + loadingIndicator?.stop(); + } loadingIndicator = null; } From 8cd0442a1f7f53b270e60e73b0e9e7b5569557e6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 16:47:27 +0800 Subject: [PATCH 57/74] fix: left side bar does not reflect right workspace content --- .../home/menu/sidebar/sidebar.dart | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 9c19184217..d05200cabf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -176,35 +176,43 @@ class HomeSideBar extends StatelessWidget { listener: _onNotificationAction, ), BlocListener( + listenWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId || + current.actionResult?.actionType == + UserWorkspaceActionType.create || + current.actionResult?.actionType == + UserWorkspaceActionType.delete || + current.actionResult?.actionType == + UserWorkspaceActionType.open, listener: (context, state) { - final actionType = state.actionResult?.actionType; + final workspaceId = state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId; - if (actionType == UserWorkspaceActionType.create || - actionType == UserWorkspaceActionType.delete || - actionType == UserWorkspaceActionType.open) { - if (context.read().state.spaces.isEmpty) { - context.read().add( - SidebarSectionsEvent.reload( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - ), - ); - } else { - context.read().add( - SpaceEvent.reset( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - true, - ), - ); - } + // Reset all workspace-specific data + getIt().reset(); - context - .read() - .add(const FavoriteEvent.fetchFavorites()); - } + // Always reload sidebar sections to ensure fresh data + context.read().add( + SidebarSectionsEvent.reload( + userProfile, + workspaceId, + ), + ); + + // Reset space data with the current workspace + context.read().add( + SpaceEvent.reset( + userProfile, + workspaceId, + true, + ), + ); + + // Fetch updated favorites + context + .read() + .add(const FavoriteEvent.fetchFavorites()); }, ), ], From 4634b51edf1c43a9e1f0c331551d0e410a1ee077 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 16:47:40 +0800 Subject: [PATCH 58/74] chore: open workspace with workspace auth type --- .../workspace_menu_bottom_sheet.dart | 2 +- .../lib/user/application/user_service.dart | 2 +- .../workspace/_sidebar_workspace_menu.dart | 2 +- frontend/appflowy_flutter/macos/Podfile.lock | 2 +- .../event-integration-test/src/user_event.rs | 2 +- .../af_cloud/impls/user/cloud_service_impl.rs | 1 + .../rust-lib/flowy-user-pub/src/entities.rs | 7 +++++ .../rust-lib/flowy-user-pub/src/session.rs | 3 +- .../flowy-user-pub/src/sql/workspace_sql.rs | 1 + .../flowy-user/src/entities/user_profile.rs | 28 ++++++++----------- .../flowy-user/src/entities/workspace.rs | 2 +- .../rust-lib/flowy-user/src/event_handler.rs | 9 ++---- .../user_manager/manager_user_workspace.rs | 2 +- 13 files changed, 33 insertions(+), 30 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index d306f48964..9743da966e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Server, + AuthTypePB.Local, ), ); }, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 3ec181e009..918350d4af 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -127,7 +127,7 @@ class UserBackendService implements IUserBackendService { ) { final payload = OpenUserWorkspacePB() ..workspaceId = workspaceId - ..authType = authType; + ..workspaceAuthType = authType; return UserEventOpenWorkspace(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 4ff5ccbf67..04eb701a66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Server, + AuthTypePB.Local, ), ); }, diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..f4bedfef79 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -150,7 +150,7 @@ SPEC CHECKSUMS: bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 + device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 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 ab10bb7083..821c3c9a1d 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -283,7 +283,7 @@ impl EventIntegrationTest { pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) { let payload = OpenUserWorkspacePB { workspace_id: workspace_id.to_string(), - auth_type, + workspace_auth_type: auth_type, }; EventBuilder::new(self.clone()) .event(UserEvent::OpenWorkspace) 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 7cc3f5d88c..4e46f310c5 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 @@ -656,6 +656,7 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace { icon: af_workspace.icon, member_count: af_workspace.member_count.unwrap_or(0), role: af_workspace.role.map(|r| r.into()), + workspace_type: AuthType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 061bda56f5..a870b9c0b0 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -114,6 +114,12 @@ pub struct UserWorkspace { pub member_count: i64, #[serde(default)] pub role: Option, + #[serde(default = "default_workspace_type")] + pub workspace_type: AuthType, +} + +fn default_workspace_type() -> AuthType { + AuthType::AppFlowyCloud } impl UserWorkspace { @@ -131,6 +137,7 @@ impl UserWorkspace { icon: "".to_string(), member_count: 1, role: Some(Role::Owner), + workspace_type: AuthType::Local, } } } diff --git a/frontend/rust-lib/flowy-user-pub/src/session.rs b/frontend/rust-lib/flowy-user-pub/src/session.rs index 4c2668477a..83a5670ddb 100644 --- a/frontend/rust-lib/flowy-user-pub/src/session.rs +++ b/frontend/rust-lib/flowy-user-pub/src/session.rs @@ -1,4 +1,4 @@ -use crate::entities::{UserAuthResponse, UserWorkspace}; +use crate::entities::{AuthType, UserAuthResponse, UserWorkspace}; use base64::engine::general_purpose::STANDARD; use base64::Engine; use chrono::Utc; @@ -77,6 +77,7 @@ impl<'de> Visitor<'de> for SessionVisitor { icon: "".to_owned(), member_count: 1, role: None, + workspace_type: AuthType::Local, }) } } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs index 80c99eb7e6..27f63e3de6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs @@ -149,6 +149,7 @@ impl From for UserWorkspace { icon: value.icon, member_count: value.member_count, role: value.role.map(|v| v.into()), + workspace_type: AuthType::from(value.workspace_type), } } } 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 a117d0839e..77d92fb33a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -156,14 +156,10 @@ pub struct RepeatedUserWorkspacePB { pub items: Vec, } -impl From<(AuthType, Vec)> for RepeatedUserWorkspacePB { - fn from(value: (AuthType, Vec)) -> Self { - let (auth_type, workspaces) = value; +impl From> for RepeatedUserWorkspacePB { + fn from(workspaces: Vec) -> Self { Self { - items: workspaces - .into_iter() - .map(|w| UserWorkspacePB::from((auth_type, w))) - .collect(), + items: workspaces.into_iter().map(UserWorkspacePB::from).collect(), } } } @@ -193,16 +189,16 @@ pub struct UserWorkspacePB { pub workspace_auth_type: AuthTypePB, } -impl From<(AuthType, UserWorkspace)> for UserWorkspacePB { - fn from(value: (AuthType, UserWorkspace)) -> Self { +impl From for UserWorkspacePB { + fn from(workspace: UserWorkspace) -> Self { Self { - workspace_id: value.1.id, - name: value.1.name, - created_at_timestamp: value.1.created_at.timestamp(), - icon: value.1.icon, - member_count: value.1.member_count, - role: value.1.role.map(AFRolePB::from), - workspace_auth_type: AuthTypePB::from(value.0), + workspace_id: workspace.id, + name: workspace.name, + created_at_timestamp: workspace.created_at.timestamp(), + icon: workspace.icon, + member_count: workspace.member_count, + role: workspace.role.map(AFRolePB::from), + workspace_auth_type: AuthTypePB::from(workspace.workspace_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 99544eede4..2781f29c09 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -200,7 +200,7 @@ pub struct OpenUserWorkspacePB { pub workspace_id: String, #[pb(index = 2)] - pub auth_type: AuthTypePB, + pub workspace_auth_type: AuthTypePB, } #[derive(ProtoBuf, Default, Clone, Validate)] diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bf865f24f0..cbcf6f4477 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -439,10 +439,7 @@ pub async fn get_all_workspace_handler( .get_all_user_workspaces(profile.uid, profile.auth_type) .await?; - data_result_ok(RepeatedUserWorkspacePB::from(( - profile.auth_type, - user_workspaces, - ))) + data_result_ok(RepeatedUserWorkspacePB::from(user_workspaces)) } #[tracing::instrument(level = "info", skip(data, manager), err)] @@ -454,7 +451,7 @@ pub async fn open_workspace_handler( let params = data.try_into_inner()?; let workspace_id = Uuid::from_str(¶ms.workspace_id)?; manager - .open_workspace(&workspace_id, AuthType::from(params.auth_type)) + .open_workspace(&workspace_id, AuthType::from(params.workspace_auth_type)) .await?; Ok(()) } @@ -627,7 +624,7 @@ pub async fn create_workspace_handler( let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; let new_workspace = manager.create_workspace(&data.name, auth_type).await?; - data_result_ok(UserWorkspacePB::from((auth_type, new_workspace))) + data_result_ok(UserWorkspacePB::from(new_workspace)) } #[tracing::instrument(level = "debug", skip_all, err)] 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 c641fde831..0101ef299e 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 @@ -451,7 +451,7 @@ impl UserManager { ); // only send notification if there were real changes if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) { - let repeated_pb = RepeatedUserWorkspacePB::from((auth_copy, updated_list)); + let repeated_pb = RepeatedUserWorkspacePB::from(updated_list); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) .payload(repeated_pb) .send(); From 343f7e4fd5f99e46f10b5c19038f3b9a12a36e3d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 21 Apr 2025 17:20:35 +0800 Subject: [PATCH 59/74] Revert "fix: loading exception when switching workspace" This reverts commit 10a536b1a2e3100fa3da99ab05851702f701a3c0. --- frontend/appflowy_flutter/lib/shared/loading.dart | 6 +----- .../menu/sidebar/workspace/sidebar_workspace.dart | 11 +++-------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/frontend/appflowy_flutter/lib/shared/loading.dart b/frontend/appflowy_flutter/lib/shared/loading.dart index 3958601ed8..f8d1c6fc86 100644 --- a/frontend/appflowy_flutter/lib/shared/loading.dart +++ b/frontend/appflowy_flutter/lib/shared/loading.dart @@ -40,11 +40,7 @@ class Loading { void stop() { if (loadingContext != null) { - if (loadingContext!.mounted) { - if (Navigator.canPop(loadingContext!)) { - Navigator.of(loadingContext!).pop(); - } - } + Navigator.of(loadingContext!).pop(); loadingContext = null; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 07a5aeec4e..50ea9d83c7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -32,10 +32,7 @@ class _SidebarWorkspaceState extends State { @override void dispose() { onHover.dispose(); - if (loadingIndicator != null) { - loadingIndicator?.stop(); - loadingIndicator = null; - } + super.dispose(); } @@ -103,10 +100,8 @@ class _SidebarWorkspaceState extends State { if (isLoading) { loadingIndicator ??= Loading(context)..start(); return; - } else if (loadingIndicator != null) { - if (mounted) { - loadingIndicator?.stop(); - } + } else { + loadingIndicator?.stop(); loadingIndicator = null; } From 1bcf4e6e8d7f452cfc7650aab1ace18c3e7a381e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 21 Apr 2025 17:20:44 +0800 Subject: [PATCH 60/74] Revert "fix: left side bar does not reflect right workspace content" This reverts commit 8cd0442a1f7f53b270e60e73b0e9e7b5569557e6. --- .../home/menu/sidebar/sidebar.dart | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index d05200cabf..9c19184217 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -176,43 +176,35 @@ class HomeSideBar extends StatelessWidget { listener: _onNotificationAction, ), BlocListener( - listenWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId || - current.actionResult?.actionType == - UserWorkspaceActionType.create || - current.actionResult?.actionType == - UserWorkspaceActionType.delete || - current.actionResult?.actionType == - UserWorkspaceActionType.open, listener: (context, state) { - final workspaceId = state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId; + final actionType = state.actionResult?.actionType; - // Reset all workspace-specific data - getIt().reset(); + if (actionType == UserWorkspaceActionType.create || + actionType == UserWorkspaceActionType.delete || + actionType == UserWorkspaceActionType.open) { + if (context.read().state.spaces.isEmpty) { + context.read().add( + SidebarSectionsEvent.reload( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ), + ); + } else { + context.read().add( + SpaceEvent.reset( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + true, + ), + ); + } - // Always reload sidebar sections to ensure fresh data - context.read().add( - SidebarSectionsEvent.reload( - userProfile, - workspaceId, - ), - ); - - // Reset space data with the current workspace - context.read().add( - SpaceEvent.reset( - userProfile, - workspaceId, - true, - ), - ); - - // Fetch updated favorites - context - .read() - .add(const FavoriteEvent.fetchFavorites()); + context + .read() + .add(const FavoriteEvent.fetchFavorites()); + } }, ), ], From 0cdecee771ac4210a49ebd6cd395ffadf80a77a6 Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 21 Apr 2025 20:38:52 +0800 Subject: [PATCH 61/74] fix: canLaunchUrl doesn't work with Flatpak (#7796) --- .../lib/core/helpers/url_launcher.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index 0502e79604..fd8aa03dfe 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -44,10 +44,18 @@ Future afLaunchUri( uri = Uri.parse('https://$url'); } - // try to launch the uri directly - bool result = await launcher.canLaunchUrl(uri); + /// opening an incorrect link will cause a system error dialog to pop up on macOS + /// only use [canLaunchUrl] on macOS + /// and there is an known issue with url_launcher on Linux where it fails to launch + /// see https://github.com/flutter/flutter/issues/88463 + bool result = true; + if (UniversalPlatform.isMacOS) { + result = await launcher.canLaunchUrl(uri); + } + if (result) { try { + // try to launch the uri directly result = await launcher.launchUrl( uri, mode: mode, From 9cd49c24477b4eb12d2254c47b897e7f7b9d3320 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 21:07:38 +0800 Subject: [PATCH 62/74] chore: create workspace member --- .../ai_chat/application/chat_member_bloc.dart | 1 + .../lib/user/application/user_service.dart | 7 ---- .../user/af_cloud_test/workspace_test.rs | 7 ++++ .../flowy-folder/src/entities/view.rs | 5 ++- .../flowy-folder/src/event_handler.rs | 2 +- .../rust-lib/flowy-folder/src/manager_init.rs | 7 +++- .../rust-lib/flowy-search-pub/src/entities.rs | 2 +- .../flowy-search/src/folder/indexer.rs | 13 +++---- .../src/local_server/impls/user.rs | 34 ++++++++++------ frontend/rust-lib/flowy-user-pub/src/cloud.rs | 4 +- .../flowy-user-pub/src/sql/user_sql.rs | 33 +++++++++++++++- .../flowy-user/src/entities/workspace.rs | 2 +- .../flowy-user/src/user_manager/manager.rs | 39 ++++--------------- 13 files changed, 88 insertions(+), 68 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart index 8718255cd9..2547ff668e 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart @@ -27,6 +27,7 @@ class ChatMemberBloc extends Bloc { final payload = WorkspaceMemberIdPB( uid: Int64.parseInt(userId), ); + await UserEventGetMemberInfo(payload).send().then((result) { result.fold( (member) { diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 918350d4af..ff1cfb6575 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -244,13 +244,6 @@ class UserBackendService implements IUserBackendService { return UserEventGetWorkspaceSubscriptionInfo(params).send(); } - Future> - getWorkspaceMember() async { - final data = WorkspaceMemberIdPB.create()..uid = userId; - - return UserEventGetMemberInfo(data).send(); - } - @override Future> createSubscription( String workspaceId, 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 625ceeb1b7..ce50575954 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 @@ -5,6 +5,7 @@ use collab_entity::CollabType; use collab_folder::Folder; use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; +use flowy_user::entities::AFRolePB; use flowy_user_pub::entities::AuthType; use std::time::Duration; use tokio::task::LocalSet; @@ -327,4 +328,10 @@ async fn af_cloud_create_local_workspace_test() { for view in views { test.get_view(&view.id).await; } + + let members = test + .get_workspace_members(&created_workspace.workspace_id) + .await; + assert_eq!(members.len(), 1); + assert_eq!(members[0].role, AFRolePB::Owner); } diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 4f2304846b..171bc39c7d 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -2,12 +2,14 @@ use collab_folder::{View, ViewIcon, ViewLayout}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_folder_pub::cloud::gen_view_id; +use lib_infra::validator_fn::required_not_empty_str; use std::collections::HashMap; use std::convert::TryInto; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use std::sync::Arc; use uuid::Uuid; +use validator::Validate; use crate::entities::icon::ViewIconPB; use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail}; @@ -394,9 +396,10 @@ impl TryInto for CreateOrphanViewPayloadPB { } } -#[derive(Default, ProtoBuf, Clone, Debug)] +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] pub struct ViewIdPB { #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] pub value: String, } diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index ec11d57517..809651a262 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -111,7 +111,7 @@ pub(crate) async fn get_view_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let view_id: ViewIdPB = data.into_inner(); + let view_id = data.try_into_inner()?; let view_pb = folder.get_view_pb(&view_id.value).await?; data_result_ok(view_pb) } diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index 62cce7c394..c581031f54 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -9,7 +9,7 @@ use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; use tokio::task::spawn_blocking; -use tracing::{event, info, Level}; +use tracing::{error, event, info, Level}; use uuid::Uuid; impl FolderManager { @@ -139,9 +139,12 @@ impl FolderManager { ); let weak_folder_indexer = Arc::downgrade(&self.folder_indexer); + let workspace_id = *workspace_id; tokio::spawn(async move { if let Some(folder_indexer) = weak_folder_indexer.upgrade() { - folder_indexer.initialize().await; + if let Err(err) = folder_indexer.initialize(&workspace_id).await { + error!("Failed to initialize folder indexer: {:?}", err); + } } }); diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs index fc4c19359c..4cc625af46 100644 --- a/frontend/rust-lib/flowy-search-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -38,7 +38,7 @@ pub trait IndexManager: Send + Sync { #[async_trait] pub trait FolderIndexManager: IndexManager { - async fn initialize(&self); + async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; fn index_all_views(&self, views: Vec>, workspace_id: Uuid); diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 71ac5d5e60..59622852d5 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -35,8 +35,6 @@ impl Drop for TantivyState { } } -const FOLDER_INDEX_DIR: &str = "folder_index"; - #[derive(Clone)] pub struct FolderIndexManagerImpl { auth_user: Weak, @@ -64,7 +62,7 @@ impl FolderIndexManagerImpl { } /// Initializes the state using the workspace directory. - async fn initialize(&self) -> FlowyResult<()> { + async fn initialize(&self, workspace_id: &Uuid) -> FlowyResult<()> { if let Some(state) = self.state.write().await.take() { info!("Re-initializing folder indexer"); drop(state); @@ -82,7 +80,7 @@ impl FolderIndexManagerImpl { .upgrade() .ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?; - let index_path = auth_user.get_index_path()?.join(FOLDER_INDEX_DIR); + let index_path = auth_user.get_index_path()?.join(workspace_id.to_string()); if !index_path.exists() { fs::create_dir_all(&index_path).map_err(|e| { error!("Failed to create folder index directory: {:?}", e); @@ -327,10 +325,9 @@ impl IndexManager for FolderIndexManagerImpl { #[async_trait] impl FolderIndexManager for FolderIndexManagerImpl { - async fn initialize(&self) { - if let Err(e) = self.initialize().await { - error!("Failed to initialize FolderIndexManager: {:?}", e); - } + async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + self.initialize(workspace_id).await?; + Ok(()) } fn index_all_views(&self, views: Vec>, workspace_id: Uuid) { 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 5a4caeb050..f011c16d90 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,6 +2,7 @@ use crate::af_cloud::define::LoggedUser; use crate::local_server::uid::UserIDGenerator; +use anyhow::Context; use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; @@ -13,10 +14,10 @@ use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; use flowy_user_pub::sql::{ - select_all_user_workspace, select_user_profile, select_user_workspace, select_workspace_member, - select_workspace_setting, update_user_profile, update_workspace_setting, upsert_workspace_member, - upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, WorkspaceSettingsChangeset, - WorkspaceSettingsTable, + insert_local_workspace, select_all_user_workspace, select_user_profile, select_user_workspace, + select_workspace_member, select_workspace_setting, update_user_profile, update_workspace_setting, + upsert_workspace_member, upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, + WorkspaceSettingsChangeset, WorkspaceSettingsTable, }; use flowy_user_pub::DEFAULT_USER_NAME; use lazy_static::lazy_static; @@ -161,10 +162,11 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn create_workspace(&self, workspace_name: &str) -> Result { let workspace_id = Uuid::new_v4(); - Ok(UserWorkspace::new_local( - workspace_id.to_string(), - workspace_name, - )) + let uid = self.logged_user.user_id()?; + let mut conn = self.logged_user.get_sqlite_db(uid)?; + let user_workspace = + insert_local_workspace(uid, &workspace_id.to_string(), workspace_name, &mut conn)?; + Ok(user_workspace) } async fn patch_workspace( @@ -180,6 +182,15 @@ impl UserCloudService for LocalServerUserServiceImpl { Ok(()) } + async fn get_workspace_members( + &self, + workspace_id: Uuid, + ) -> Result, FlowyError> { + let uid = self.logged_user.user_id()?; + let member = self.get_workspace_member(&workspace_id, uid).await?; + Ok(vec![member]) + } + async fn get_user_awareness_doc_state( &self, uid: i64, @@ -227,15 +238,16 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(err) => { if err.is_record_not_found() { let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn)?; + let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn) + .context("Can't find user profile when create workspace member")?; let row = WorkspaceMemberTable { email: profile.email.to_string(), - role: 0, + role: Role::Owner as i32, name: profile.name.to_string(), avatar_url: Some(profile.icon_url), uid, workspace_id: workspace_id.to_string(), - updated_at: Default::default(), + updated_at: chrono::Utc::now().naive_utc(), }; let member = WorkspaceMember::from(row.clone()); diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 9b223178ff..743d39ed4f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -231,9 +231,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_members( &self, workspace_id: Uuid, - ) -> Result, FlowyError> { - Ok(vec![]) - } + ) -> Result, FlowyError>; async fn get_user_awareness_doc_state( &self, diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index 384b66d51b..e8d186c484 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -1,6 +1,8 @@ use crate::cloud::UserUpdate; -use crate::entities::{AuthType, UpdateUserProfileParams, UserProfile}; -use crate::sql::select_user_workspace; +use crate::entities::{AuthType, Role, UpdateUserProfileParams, UserProfile, UserWorkspace}; +use crate::sql::{ + select_user_workspace, upsert_user_workspace, upsert_workspace_member, WorkspaceMemberTable, +}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; @@ -92,6 +94,33 @@ pub fn update_user_profile( Ok(()) } +pub fn insert_local_workspace( + uid: i64, + workspace_id: &str, + workspace_name: &str, + conn: &mut SqliteConnection, +) -> FlowyResult { + let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), workspace_name); + conn.immediate_transaction(|conn| { + let row = select_user_table_row(uid, conn)?; + let row = WorkspaceMemberTable { + email: row.email, + role: Role::Owner as i32, + name: row.name, + avatar_url: Some(row.icon_url), + uid, + workspace_id: workspace_id.to_string(), + updated_at: chrono::Utc::now().naive_utc(), + }; + + upsert_user_workspace(uid, AuthType::Local, user_workspace.clone(), conn)?; + upsert_workspace_member(conn, row)?; + Ok::<_, FlowyError>(()) + })?; + + Ok(user_workspace) +} + fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result { let row = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 2781f29c09..0f7b7f4587 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -147,7 +147,7 @@ pub struct UpdateWorkspaceMemberPB { } // Workspace Role -#[derive(Debug, ProtoBuf_Enum, Clone, Default)] +#[derive(Debug, ProtoBuf_Enum, Clone, Default, Eq, PartialEq)] pub enum AFRolePB { Owner = 0, Member = 1, 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 b9af5c330d..9cefddf44b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -7,7 +7,6 @@ use arc_swap::ArcSwapOption; use collab::lock::RwLock; use collab_user::core::UserAwareness; use dashmap::DashMap; -use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_table; use flowy_sqlite::ConnectionPool; @@ -131,30 +130,15 @@ impl UserManager { let user = self .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) .await?; - self.cloud_service.set_server_auth_type(&user.auth_type); - - // Get the current authenticator from the environment variable - let env_auth_type = current_authenticator(); - - // If the current authenticator is different from the authenticator in the session and it's - // not a local authenticator, we need to sign out the user. - if user.auth_type != AuthType::Local && user.auth_type != env_auth_type { - event!( - tracing::Level::INFO, - "Auth type changed from {:?} to {:?}", - user.auth_type, - env_auth_type - ); - self.sign_out().await?; - return Ok(()); - } + let auth_type = user.workspace_auth_type; + self.cloud_service.set_server_auth_type(&auth_type); event!( tracing::Level::INFO, "init user session: {}:{}, auth type: {:?}", user.uid, user.email, - user.auth_type, + auth_type, ); self.prepare_user(&session).await; @@ -253,7 +237,7 @@ impl UserManager { (Ok(collab_db), Ok(sqlite_pool)) => { run_collab_data_migration( &session, - &user.auth_type, + &auth_type, collab_db, sqlite_pool, self.store_preferences.clone(), @@ -267,7 +251,7 @@ impl UserManager { self.set_first_time_installed_version(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); // Init the user awareness. here we ignore the error - let _ = self.initial_user_awareness(&session, &user.auth_type).await; + let _ = self.initial_user_awareness(&session, &auth_type).await; user_status_callback .on_launch_if_authenticated( @@ -275,7 +259,7 @@ impl UserManager { &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, - &user.auth_type, + &auth_type, ) .await?; } else { @@ -357,7 +341,7 @@ impl UserManager { self.save_auth_data(&response, auth_type, &session).await?; let _ = self - .initial_user_awareness(&session, &user_profile.auth_type) + .initial_user_awareness(&session, &user_profile.workspace_auth_type) .await; self .user_status_callback @@ -556,7 +540,7 @@ impl UserManager { workspace_id: &str, ) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile - if old_user_profile.auth_type.is_local() { + if old_user_profile.workspace_auth_type.is_local() { return Ok(()); } @@ -800,13 +784,6 @@ impl UserManager { } } -fn current_authenticator() -> AuthType { - match AuthenticatorType::from_env() { - AuthenticatorType::Local => AuthType::Local, - AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, - } -} - pub fn upsert_user_profile_change( uid: i64, workspace_id: &str, From 4e2990e5996e4050c45504ac510201848e8781a3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 22:02:06 +0800 Subject: [PATCH 63/74] chore: remove local model --- frontend/rust-lib/flowy-ai-pub/src/lib.rs | 1 + .../rust-lib/flowy-ai-pub/src/user_service.rs | 14 + frontend/rust-lib/flowy-ai/src/ai_manager.rs | 21 +- frontend/rust-lib/flowy-ai/src/chat.rs | 2 +- frontend/rust-lib/flowy-ai/src/completion.rs | 2 +- .../flowy-ai/src/local_ai/controller.rs | 2 +- .../flowy-ai/src/local_ai/resource.rs | 2 +- .../src/middleware/chat_service_mw.rs | 2 +- .../src/offline/offline_message_sync.rs | 2 +- .../flowy-core/src/deps_resolve/chat_deps.rs | 5 +- .../rust-lib/flowy-core/src/server_layer.rs | 8 +- .../flowy-server/src/af_cloud/define.rs | 3 +- .../flowy-server/src/af_cloud/server.rs | 11 +- .../flowy-server/tests/af_cloud_test/mod.rs | 2 - .../tests/af_cloud_test/user_test.rs | 21 -- .../flowy-server/tests/af_cloud_test/util.rs | 119 ------- frontend/rust-lib/flowy-server/tests/logo.png | Bin 15694 -> 0 bytes frontend/rust-lib/flowy-server/tests/main.rs | 24 -- .../tests/supabase_test/database_test.rs | 63 ---- .../tests/supabase_test/file_test.rs | 78 ----- .../tests/supabase_test/folder_test.rs | 316 ------------------ .../flowy-server/tests/supabase_test/mod.rs | 5 - .../tests/supabase_test/user_test.rs | 141 -------- .../flowy-server/tests/supabase_test/util.rs | 162 --------- frontend/rust-lib/flowy-server/tests/test.txt | 1 - .../flowy-user-pub/src/sql/user_sql.rs | 22 +- .../src/migrations/anon_user_workspace.rs | 22 +- .../src/migrations/doc_key_with_workspace.rs | 2 +- .../src/migrations/document_empty_content.rs | 4 +- .../flowy-user/src/migrations/migration.rs | 6 +- .../migrations/workspace_and_favorite_v1.rs | 2 +- .../src/migrations/workspace_trash_v1.rs | 2 +- .../src/services/authenticate_user.rs | 41 +-- .../data_import/appflowy_data_import.rs | 20 +- .../flowy-user/src/user_manager/manager.rs | 10 +- .../src/user_manager/manager_history_user.rs | 12 + 36 files changed, 101 insertions(+), 1049 deletions(-) create mode 100644 frontend/rust-lib/flowy-ai-pub/src/user_service.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/logo.png delete mode 100644 frontend/rust-lib/flowy-server/tests/main.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/supabase_test/util.rs delete mode 100644 frontend/rust-lib/flowy-server/tests/test.txt diff --git a/frontend/rust-lib/flowy-ai-pub/src/lib.rs b/frontend/rust-lib/flowy-ai-pub/src/lib.rs index 9a7423ec3f..df7dc957e2 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/lib.rs @@ -1,2 +1,3 @@ pub mod cloud; pub mod persistence; +pub mod user_service; diff --git a/frontend/rust-lib/flowy-ai-pub/src/user_service.rs b/frontend/rust-lib/flowy-ai-pub/src/user_service.rs new file mode 100644 index 0000000000..e227c977fe --- /dev/null +++ b/frontend/rust-lib/flowy-ai-pub/src/user_service.rs @@ -0,0 +1,14 @@ +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::DBConnection; +use lib_infra::async_trait::async_trait; +use std::path::PathBuf; +use uuid::Uuid; + +#[async_trait] +pub trait AIUserService: Send + Sync + 'static { + fn user_id(&self) -> Result; + async fn is_local_model(&self) -> FlowyResult; + fn workspace_id(&self) -> Result; + fn sqlite_connection(&self, uid: i64) -> Result; + fn application_root_dir(&self) -> Result; +} diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 9055341b99..8a2ddeead5 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -14,7 +14,6 @@ use flowy_ai_pub::cloud::{ }; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; -use flowy_sqlite::DBConnection; use crate::notification::{chat_notification_builder, ChatNotification}; use crate::util::ai_available_models_key; @@ -22,6 +21,7 @@ use collab_integrate::persistence::collab_metadata_sql::{ batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata, }; use flowy_ai_pub::cloud::ai_dto::AvailableModel; +use flowy_ai_pub::user_service::AIUserService; use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; @@ -33,15 +33,6 @@ use tokio::sync::RwLock; use tracing::{error, info, instrument, trace}; use uuid::Uuid; -#[async_trait] -pub trait AIUserService: Send + Sync + 'static { - fn user_id(&self) -> Result; - async fn is_local_model(&self) -> FlowyResult; - fn workspace_id(&self) -> Result; - fn sqlite_connection(&self, uid: i64) -> Result; - fn application_root_dir(&self) -> Result; -} - /// AIExternalService is an interface for external services that AI plugin can interact with. #[async_trait] pub trait AIExternalService: Send + Sync + 'static { @@ -450,13 +441,9 @@ impl AIManager { pub async fn get_available_models(&self, source: String) -> FlowyResult { let is_local_mode = self.user_service.is_local_model().await?; if is_local_mode { - let mut selected_model = AIModel::default(); - let mut models = vec![]; - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - let model = AIModel::local(local_model, "".to_string()); - selected_model = model.clone(); - models.push(model); - } + let setting = self.local_ai.get_local_ai_setting(); + let selected_model = AIModel::local(setting.chat_model_name, "".to_string()); + let models = vec![selected_model.clone()]; Ok(AvailableModelsPB { models: models.into_iter().map(|m| m.into()).collect(), diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 3180227ed0..052599ef48 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, @@ -14,6 +13,7 @@ use flowy_ai_pub::persistence::{ select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages, ChatMessageTable, }; +use flowy_ai_pub::user_service::AIUserService; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 31acde4ae7..ffdccd0680 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB}; use allo_isolate::Isolate; use std::str::FromStr; @@ -14,6 +13,7 @@ use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; use crate::stream_message::StreamMessage; +use flowy_ai_pub::user_service::AIUserService; use std::sync::{Arc, Weak}; use tokio::select; use tracing::{error, info}; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index b9dc7a73c1..43dd7ce9b2 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use crate::entities::{LocalAIPB, RunningStatePB}; use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; use crate::notification::{ @@ -17,6 +16,7 @@ use af_local_ai::ollama_plugin::OllamaAIPlugin; use af_plugin::core::path::is_plugin_ready; use af_plugin::core::plugin::RunningState; use arc_swap::ArcSwapOption; +use flowy_ai_pub::user_service::AIUserService; use futures_util::SinkExt; use lib_infra::util::get_operating_system; use serde::{Deserialize, Serialize}; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index 6251ef8de5..36a56e171d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use crate::local_ai::controller::LocalAISetting; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; @@ -11,6 +10,7 @@ use crate::notification::{ }; use af_local_ai::ollama_plugin::OllamaPluginConfig; use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path}; +use flowy_ai_pub::user_service::AIUserService; use lib_infra::util::{get_operating_system, OperatingSystem}; use reqwest::Client; use serde::Deserialize; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 22a2bec674..74f5d5560b 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use crate::entities::{ChatStatePB, ModelTypePB}; use crate::local_ai::controller::LocalAIController; use crate::notification::{ @@ -19,6 +18,7 @@ use futures::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use crate::local_ai::stream_util::QuestionStream; +use flowy_ai_pub::user_service::AIUserService; use flowy_storage_pub::storage::StorageService; use serde_json::{json, Value}; use std::path::Path; diff --git a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs index 8d7e8d2e42..55daf6b77f 100644 --- a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs +++ b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs @@ -1,4 +1,3 @@ -use crate::ai_manager::AIUserService; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, @@ -8,6 +7,7 @@ use flowy_ai_pub::persistence::{ update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages, ChatMessageTable, ChatTable, }; +use flowy_ai_pub::user_service::AIUserService; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use serde_json::Value; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index c8c93a7f4c..a7d2bc15c1 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -5,9 +5,10 @@ use collab::preclude::{Collab, StateVector}; use collab::util::is_change_since_sv; use collab_entity::CollabType; use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; -use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; +use flowy_ai::ai_manager::{AIExternalService, AIManager}; use flowy_ai::local_ai::controller::LocalAIController; use flowy_ai_pub::cloud::ChatCloudService; +use flowy_ai_pub::user_service::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::ViewLayout; use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams}; @@ -153,7 +154,7 @@ impl AIExternalService for ChatQueryServiceImpl { } } -struct ChatUserServiceImpl(Weak); +pub struct ChatUserServiceImpl(Weak); impl ChatUserServiceImpl { fn upgrade_user(&self) -> Result, FlowyError> { let user = self diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index b666ab4749..6e5d35d726 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -5,10 +5,8 @@ use dashmap::mapref::one::Ref; use dashmap::DashMap; use flowy_ai::local_ai::controller::LocalAIController; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::{ - define::{AIUserServiceImpl, LoggedUser}, - AppFlowyCloudServer, -}; +use flowy_server::af_cloud::define::AIUserServiceImpl; +use flowy_server::af_cloud::{define::LoggedUser, AppFlowyCloudServer}; use flowy_server::local_server::LocalServer; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; @@ -117,12 +115,14 @@ impl ServerProvider { .cloud_config .clone() .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?; + let ai_user_service = Arc::new(AIUserServiceImpl(Arc::downgrade(&self.logged_user))); Arc::new(AppFlowyCloudServer::new( cfg, self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), Arc::downgrade(&self.logged_user), + ai_user_service, )) }, }; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 65808e5b6b..31114629ac 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,5 +1,5 @@ use collab_plugins::CollabKVDB; -use flowy_ai::ai_manager::AIUserService; +use flowy_ai_pub::user_service::AIUserService; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use lib_infra::async_trait::async_trait; @@ -28,6 +28,7 @@ pub trait LoggedUser: Send + Sync { fn application_root_dir(&self) -> Result; } +// pub struct AIUserServiceImpl(pub Weak); impl AIUserServiceImpl { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 66abb32031..500c78c930 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use std::time::Duration; -use crate::af_cloud::define::{AIUserServiceImpl, LoggedUser}; +use crate::af_cloud::define::LoggedUser; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -28,7 +28,9 @@ use crate::af_cloud::impls::{ AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl, }; +use crate::AppFlowyServer; use flowy_ai::offline::offline_message_sync::AutoSyncChatService; +use flowy_ai_pub::user_service::AIUserService; use rand::Rng; use semver::Version; use tokio::select; @@ -39,8 +41,6 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use uuid::Uuid; -use crate::AppFlowyServer; - use super::impls::AFCloudSearchCloudServiceImpl; pub(crate) type AFCloudClient = Client; @@ -54,6 +54,7 @@ pub struct AppFlowyCloudServer { pub device_id: String, ws_client: Arc, logged_user: Weak, + ai_user_service: Arc, } impl AppFlowyCloudServer { @@ -63,6 +64,7 @@ impl AppFlowyCloudServer { mut device_id: String, client_version: Version, logged_user: Weak, + ai_user_service: Arc, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -101,6 +103,7 @@ impl AppFlowyCloudServer { device_id, ws_client, logged_user, + ai_user_service, } } @@ -222,7 +225,7 @@ impl AppFlowyServer for AppFlowyCloudServer { Arc::new(CloudChatServiceImpl { inner: self.get_server_impl(), }), - Arc::new(AIUserServiceImpl(self.logged_user.clone())), + self.ai_user_service.clone(), )) } diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs deleted file mode 100644 index 94ad2e2e1d..0000000000 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod user_test; -mod util; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs deleted file mode 100644 index a14d8eaf25..0000000000 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -use flowy_server::AppFlowyServer; -use flowy_user_pub::entities::AuthResponse; -use lib_infra::box_any::BoxAny; - -use crate::af_cloud_test::util::{ - af_cloud_server, af_cloud_sign_up_param, generate_test_email, get_af_cloud_config, -}; - -#[tokio::test] -async fn sign_up_test() { - if let Some(config) = get_af_cloud_config() { - let server = af_cloud_server(config.clone()); - let user_service = server.user_service(); - let email = generate_test_email(); - let params = af_cloud_sign_up_param(&email, &config).await; - let resp: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - assert_eq!(resp.email.unwrap(), email); - assert!(resp.is_new_user); - assert_eq!(resp.user_workspaces.len(), 1); - } -} 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 deleted file mode 100644 index 7e38f423cc..0000000000 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ /dev/null @@ -1,119 +0,0 @@ -use client_api::ClientConfiguration; -use collab_plugins::CollabKVDB; -use flowy_error::{FlowyError, FlowyResult}; -use semver::Version; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use uuid::Uuid; - -use crate::setup_log; -use flowy_server::af_cloud::define::LoggedUser; -use flowy_server::af_cloud::AppFlowyCloudServer; -use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; - -/// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: -/// -/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000` -/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws` -/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998` -/// -/// - `GOTRUE_ADMIN_EMAIL=admin@example.com` -/// - `GOTRUE_ADMIN_PASSWORD=password` -pub fn get_af_cloud_config() -> Option { - dotenv::from_filename("./.env.ci").ok()?; - setup_log(); - AFCloudConfiguration::from_env().ok() -} - -pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc { - let fake_device_id = uuid::Uuid::new_v4().to_string(); - let logged_user = Arc::new(FakeServerUserImpl) as Arc; - Arc::new(AppFlowyCloudServer::new( - config, - true, - fake_device_id, - Version::new(0, 5, 8), - // do nothing, just for test - Arc::downgrade(&logged_user), - )) -} - -struct FakeServerUserImpl; - -#[async_trait] -impl LoggedUser for FakeServerUserImpl { - fn workspace_id(&self) -> FlowyResult { - todo!() - } - - fn user_id(&self) -> FlowyResult { - todo!() - } - - async fn is_local_mode(&self) -> FlowyResult { - Ok(true) - } - - fn get_sqlite_db(&self, _uid: i64) -> Result { - todo!() - } - - fn get_collab_db(&self, _uid: i64) -> Result, FlowyError> { - todo!() - } - - fn application_root_dir(&self) -> Result { - todo!() - } -} - -pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { - let client = client_api::Client::new( - &config.base_url, - &config.ws_base_url, - &config.gotrue_url, - "fake_device_id", - ClientConfiguration::default(), - "test", - ); - let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); - let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); - let admin_client = client_api::Client::new( - client.base_url(), - client.ws_addr(), - client.gotrue_url(), - "fake_device_id", - ClientConfiguration::default(), - &client.client_version.to_string(), - ); - admin_client - .sign_in_password(&admin_email, &admin_password) - .await - .unwrap(); - - let action_link = admin_client - .generate_sign_in_action_link(user_email) - .await - .unwrap(); - client.extract_sign_in_url(&action_link).await.unwrap() -} - -pub async fn af_cloud_sign_up_param( - email: &str, - config: &AFCloudConfiguration, -) -> HashMap { - let mut params = HashMap::new(); - params.insert( - "sign_in_url".to_string(), - generate_sign_in_url(email, config).await, - ); - params.insert("device_id".to_string(), Uuid::new_v4().to_string()); - params -} - -pub fn generate_test_email() -> String { - format!("{}@test.com", Uuid::new_v4()) -} diff --git a/frontend/rust-lib/flowy-server/tests/logo.png b/frontend/rust-lib/flowy-server/tests/logo.png deleted file mode 100644 index d6f09e3e2e0cef43e926fbec6b7b343069bb6111..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15694 zcmeIZWmH>V^eqY$Ee^$@K!MWY?oP2{EiT1ff=dZS3KWVv6bUXxf;$8X6t`fZNRT1{ zf@|@c-~Wv_-uryVxZm!FjEtN!v(7qupR?CqYt9w@K}&`3Iqh>aG&Djr)pt5*Xz08D zeQ>c+N5VdQAwYfMd8itDqoHxB|Mx*JmCmR@9YpumQBgpvnWW!C{lIjP*OW&?t53wg zx57e0liE~!C$IMz{Sa;%N52q+I1SNX`|O}grnKD9r}pZPJ(&Z6Q4X3*H4cCDuT`AW zug9#G81f`oaecp))8x+i6b|G}rbS3`V$p<{y3fC2SmU8PS;bwxqWd1r?xmoX3@Y?x z^W8{#g_y~dFn!uT`q;QAJv(ovA5^)2hb}>%@bk}(&vot7)441{Z16`I%D<{M3^cTA zMY(5amdqHmXlVGPP&727@1fXezZ5Wq(9kGf6`-Tlk)RWzg?__zMBV1Q{|o*9Mt}kf zP1$3;NKe+M1lY)Ym!p}r|7Gp_!SO?5!f^V!p-RRl!;A~0nNqvgC1=AMv|Fz@ek}A* z^s5KevJ7W|!>o0FJNlajex1vwkNQRRTR8s|?iM()3@IOm`t;-}c^F;4i6^OG3WZ8K z+TEXL)4S1TIYphOX=stG-o6oQr~|)}HM@OJvLm;Hnl+eWyVcguwW2)IPH>dpboz}2 z=ZI9gNW=F~<^B7I%=;KOO}{|j3Sgd|rgoLSX$hzn&fv989NZ^?V4@wbUD%3X9A{_k z$ngD3zlqbra=u-ZTP~bX%~W7j+7n__hIBc@avE-0vj@kp&py(f9PuxK)&I7tJ`7PT zq~~`$=KO1cGb#iSMr<3whiTL}%O5^KL~(xP zsa!lw>&^dogF(wO=F{ATXI7k1Bz3bp=GPM-IW>l?Z~{;*69=hqsBtENNS?Y8ve}0! z*9)37i8cDMQtjoh&h_S1${xv>@>gfYQBta6-w zJF^!mLC!osh>Y05xik)=^<*iie`{vpHg|71X-p@{)%u40Y1hHago{MOjjjg*pd;J? z2(()ssZ==;B2DfL-T#H87bxfAIug9&i$c7xJ(>>56wj&~#rJC484<7lhJB{x)Z74A zsHhgMi*o%&KKDJGQ;ry5_jKq!Oib(ImFz(@WRaQw@Q5zXMqcJG8p|z|>#r`+XS^3O z#qhY9cU3zu@kH@-e7D=I9{@S&x|chiqGZC*gBnp0gh1FD%Y46L46e7G9TYz?57{ZK zP;KR&Zq@y<%q(Hg#hGrou598!GMlU`sq7q83>$Me$AyPt>n`$V*{YffGL<1GOxa9@ zN`$LL>3Ce+r>q%;m6hZWD{DdKL^+XCsr$uZ+&f}kx6@-@I%GRI7i=u#Zr(_YrI#Z- z;F8Qzia&Ix;Xrly1n*SJzE=g_?~hGu;8zRpjcpR(tk^>U8Ikxu z99}kWoYUW?D;;YAom;a>@)NzgjNOp||hOZ_{r`@z37^#LF27)CN;D zbY7Y)hwpY)5)JLa8swjOJt9A}+NI}Z8lz(~6$l<32dit%$s}bOx#USbc$AFcr=UH3 z3sQQmw5A{VM>016Q~)!03MJ+uWN8Ez;susZ6QhGZBK5sl#Dv1Zx*5>s5a~iq>m7}T zFs-{h#LvCXYNBz%=gyT*1afi?B>Ii;Y+-S7|Egur6l3I3Y(>VSggiv?w)7VRGsY^oH@k*W|FXk((}A`EvA>d@Eh(F^`+Mu~DWyi(lkVy;E3YF9fO zuST96+E3FZV+IcNdOvk35Io70TO#y+f3KLteAV=ZXOqDY=?&?h-*^KX*L~fut(e$%XXf*>rl) zx2G5V?%Gtc)#gmOjpfikFPFE1K5kXBMhovc`ohY_%+_4o2P(UM9jL)bp>Iz_^LxC; z+nXPBMhkw1o-S3EXlr4G6&aDJqRgE8S;T|S=%)=5Ni=3yxG~=zRTK06#x0s zDF7MazY!qLGW-H#NuVQszgD*O^T;KK3LfJAMCvB&_twIgmwAe_w;?#ET^osKnXRoERZ@_k zL%DGg+1Q`>+A!|(9h-7`(YP9!l>E#ESYfjb!lS9`fb-V!>#B!eJJB2edt%%A?yQ>o z;IP@12%j^br+|eHA!ZOE=0${W-(tu&P4`qtgm=PT&?;GR5lcB69_Gc??tcgx%nMB4 zsB_9imT!^?_!vda5Hf<;fyB@}F2cR5%XHeA+W9RSc@9!2xzFa|?OQ@b%oBPm*(a-T zsjAv}8~9201%>g6WZ`K(*C6Zl)*fl$p=rHOM^4bp-a9$%?=Kasr4WuI$10vAczTd- z`an*BWGj9SN6B4b)k8dYIQvAj&7Pv1cIeBsJsNL?g`C!5X$)326b7|efl`N7V9_?) zR9g{D!nnH;SQ{%o_P%r!1GE@9KHq%csk)|5AFVrcv8G=YkY5~5 zS%_C}R9hfFP;bPkef~+0>GMUu$pD%=0?4dMG6xK|2g%0x51|#w%dv0m@k02w=S>~9 z_dghkQ#WGDT;a$jE2*JIfbvdynjmHov6)n3Zkmek+S9;@4niYD+(xZpJ&ifLz_SYZ zR@mA(nS-u>!*^=%UFYzeRLacFQoS*%NTGNq{Wo#^0`tcH*te>-5AB=#3%2|)ZbLoj zm2*^+r(NWTTPG=$f3}cMHaEPG00!buN_E8oTf@Bu3Vy(snBg9_8HN0TD99}gQA>{` z_?G^#l;!I{AP~J=<};@6sxK67edQ8$QEBBkZh+~d;R9csGgUdLU!%$Kd_F?({W1m| z1M?`G0LW^Fn&|}9PSaX`GsE5e;SQRl16lKrH*9(RClT~$<3Fk8GHlDEWB-twech6Q6PV4)GA)(N5#6=iTbRO8EXGLA3n`?Br zLaI)2h==eh*EB9OU!H_3tmNm{zKG$!y=<(PMJiDzk785~5c^3-nfld(##YKz3{ zSex$|;n9bgKlbxjF@PHKE}cPXz0n(udpd~Vo?__AJL00}OU6N{5`B7u3N z8K!9t0}8-deg4s;vM{aY4)(@79#4a7RuU-7%rmt(1pVQCw}Kc8>+;ev{3Qp@wUUZp zSD6b`!@E2)c|TBdaz76f1E^<)lfp+G38l|!t%txpZwk0>^e>#Ep97BcX2t2Ql-UzQ zU&`MlASx!0X$TaJwD|wZS5u?zK#Vl#J5zyfB4m1mr9jb{Fj5?O_pBLc&qrY8xGR0z z076c|n__sx&m73G2pwBYOW|`!qGvX0uL=}rdh3qXbfsCiwD(~(($^+;mAQ!rCuEho zb1u~4@)RI?42g&*;UK%RK)$$rn&O`;x-o75QS@X6^FAT}Tbhb)`>ix4QcNG*=B1(3 zIPiJP4O4W|Cvlf^4zAs^9x-HJUpaSP{^oBwV=9jWp z>~<>v!wziC97%E*ZSE`xH4a8*%auYunjwS{DSfD?umNDGH=XsXrRqN;yWqU$cFoJU zx)~E`|GA#m-Zt)?uTbHdLn0XSg0G{}VPHv6LEZCm1+0^_DL_>FAh+*CVfGl}r9nc6 z0VIb+rSP@K*GWB*n=e%%60Shgn`X)FuZO>PtQdDOnP6UEUwlY80_ex=(^v_EnrDrF z=1E?)5b@FkS;ooNb7d99BoL!RzTOfBMC{XSLWP$Hr5Ved*Us&iTFv4gd(@s~i}>4r zP#TH~T~%E$o2vZ%jT+Yp+;*`g8ho~1WIO)nnN3rBV+4o9Ys?EYNb0hQI5K|fR~_x- z{@;z{%7-Fz+8 zaIIR>@L(&I{7WgYw35}gXFbhQ5Qr&i;Hy7X$<>KT9of=Cw@o+12s!!n8DUVadRXNJ zEa7~Mx*Uek0#i$p-}fT~WKNxYCtG?=Pc&#B5FNf=X46 zN3jV+=X&&8Dkq7*I{Os&^!T0wAVUFZ^X}1ZgKZj{+SSpRTnL4b5u ziRzJ=ltAr=538oj5M^gyU8Vo5Y_b%TK6x0awLIth&N!*V2^Z56yIyef&1bl^fciSJ z3@4RYF#tbdHto|8={B|Ue~&zO-q=~$NdM~-?#Rr$P?5Yl)#K}!2F*}6ut#@$+uhf{ zr1Ed_!4YEEX(IkG4SintV&U%-F9xV~(Br6;Y|-)3bGbk|xWvY%j-ioDHLK$1zMJzg zOg0(uQm?4hYMymWcb%^QN;k%@IDD!{FE2gptpj}&*FF#A?T()=uS8+7jQGl3ySPgR zzk8-E)y{1gj?agSa3l!Yq)-X5e;$<)$J2@~{Sz{A#j$$|YTaURl5mOej<^`CDlF@` zgpx%L2y9ak&8`#=1O79^3ClkRZ-_Pv=f=;n=GySh>4^pw2@aj(?ta{A9e^V|$WYmn zp{5%)0MG-0$cYU({H$<;6$+NV#k_dqMFV{xV*f}xNhCStRo=MT^=e_j)3*C|@2`BH zxQrr$r4E&XgyMbKun-cuu`Lk4Pw4QgT)a=*U4k3%pil38%Zr+@VS~zlPE~=kjU6GILSVq1@|oq(6w}2Vi2O{udw@-1qlJSVRX}(dAM*=+^nT} zDjgW0YnEFiev>JiBf(u~a2_(McFkxzC?@Ta4zl7`ey20lCXW@*yvE z_@XDJ`f{vF_vuVIB_?bcIhyREY<||$>t*T4TL^A4B%Ps73(h2 zj!N8@f)7?b%o#CRa&;AX=XL}Ah1g^5!Yp1MQlfmKXyWg9TJ<7P1Q!l$Rk^5^4oji2lhw4>m# zi)=CiZcz%KS#J^0`YIP%QtqTkMb25|)BH@NY|i?-I)+kSvdKlAyKJez1sEr+Fqx$t zZ#N$MH|&W3UHT)ake6_~ThZcy4>!ZfA0#kXNA1Nb%k5T-oSKXbQC=t%uVxJhZ&3xr z$=6Bk29ExC`KhnY$XTCvWNwB^v~7R@=_SMIHId>g~7Z-&69w!B6f|`ZcWG zEqiOT6TfV->&AwC>ex)^#4oD}+dQp_V|0l?Q=$d_s7;!%ZzJ@aJIxP0sDH zHd;^f+ZCx{pYU7EAxz%B+FDp^(B@qR3|j*pSb^rO2H_gYKLFvrEM&$_K87Xw~MD(Xnn4xqZl*IhBzfJI@ zbjTwCVh4rph3sqi2=Zk-Phc^`R2&-*PJ%Kz>+e^wv#d}HseO)YGTboRnx?RIX9 z%>@!x7)BH>NqI;8CEiMA#q6y`B6qH2R07E)E~le!E38;4LusMhQoQ^ZWfh-*g}m%T zsc0E1E8$Oy8@ZMd^AUWI#R8Ik6P(uhQP}27YtJiuRK{gnclq8F-ib-c+18hJ!~8*> z7*%IY_sQ_py6j*(e%HY5Y@|2wedZR%$w0aJ=2?{{Dna|txMXWrA3UkdsZP3d!y-iN2ilWLgU5|OH5q>RbKo;tOK=cm zEX4rC(cYL>4Zh2M>MZHcs-*FJu0`X$-S+gGvWEc^-$@Q%VZ&`e(=myAEU;nV$+In#^t!A|KaV0J<0e$3bS9;m&7a+@9AlDv$anJiQE%g4F;(z4 z?iY2DFRjTwRo2r-G}+p=+V20`9{GE?W*-*#VzIV>bApaW*4@_nqT9aJHQ{NXuNK-J zmVTiVe|GNG6|6pqSsphnsFC|*_*i1enze=l0aU5$Q{f!BD8HZUem#}(E&=DRSiDx! zQ^P%Rl7G-Pn5EAm*^V+!Vk%*n1VB9Ctc&fgmLV2q2*e#4Z~1}oE}aKsyJ3E zLdK!wfjUF2A}U2oRZ%#LS+IP@KI2Hm`Z#lo#lTT_(Kw!RMr^Un15Ynu@32@Ixp5(t zOPW*<9DIzcOu_=rTfCFX`=SS};iA?q!z%nDg{dq_w^-;hL(!8`11G>9s??h~Edai> z;Nfr_@}$CI7E{;Z^CtMpTy?Q>1W*#+UAf?t;@BJhD6PUmMbYFrK~W|e)vsnbj1OmL z$QAQbvs{_ZIT{D4*%quJ<(u>gR_F%1>u!kKt?oM#5t;wnFp7S{70j zC%Ll_ieLU;c>Tothf758O}B-*A(nI#=nzyB+~9LNjZa+XK;N$4_NH-_GHaVJb-EtR zVbUJJA(3Frr)-ajq+iH|KUDlxOW4JHmyNT$T-P%C_9%rY5$1Q{HT~Bhn0k80ZPs_E z0QA79YfV>ENFhY1M;tf4HIx* zW#&+}dVM+;A4!J=@9dZ36O&HLeQs7fJ9rS?wkjX|M*Q$^qw8c5?)8Zz_LDv|L=cq` zw!fxgJO43`U9@93%f2sZ2YX6!HZf7O3XJvo?u+=h#ZjOSTV9dKGF#I9VZ$p(0{M>% zx$Crrao)PjiYtq;vlw!_ZQrJs+9qLp-d0BG+6f#l z!E3Cq%)X8S*TBk!gK7vX^0( zklGV^p%(Z7iO$+Fl$CL{wIxxI|2RXZ+e|#J?$_L53OG13K-`O(FZD2?>3|vLRN^rc z-`3OXH%g=S+53A@Yk4(r=TEsls{WPRpukxCruwh0mG`OiHInil7%9j8^>%H^YK5SD zFLQ2DJbH#&fEL_ECvQTibW;kL((sRNK=~e1WTSGyj`~3mWG;*Fg5@T- zS94qw$eEM2=?HR7t5 z@BAQV?($WZV;7YZ)7_IpFMq>&)Y(miS%X}& zt1Jc(4g51lbc?ihk3nQGYxPU~lCc;4W)JRoqFL*S?&fDhTwAn{Q0P^*&ZT1D67gXr zVkFJN>G(?Vy5^cqHlp@l4;RJ#U%9xIKL%+xYzf;MY#2aQVB5%9N@I_4?b>PBn*)zp z*X4a4(%)#IZ~lire&gS2jE>eix%+`A^U^WF&5er|_gCev?035ii@Eobb||c0)%V{M&SV(% z(?co9rilGHd*eXP~esU9L&0;KS8`_It5>XY>`# zs?B9t*#n%jxVD|K8GV8{&EwpRnbfnuW(TZfmI4-o+F~&LtA3! zWxKh&0M030t7qcngXpPsyYAUS-uwbs`~2}RjIT30H zR9JE?_n*sq;{5L3k&M3XtuYHxZ`gi}swgRzmrLCL)mnAAP&+9BHt^%1UXVt@TfGws zuyuHh41?hz$4$jEZH)dtT@dtn4y_1LJ6ks1r)7+lzFAe6UG))^^A7@2jval3H6_}Q<(77Pu-nE55 z6{kj?~M&1a-z zK)zb4jmPR;$ilQ%gZDz`E?H z({Ho%dMwe-hEXi(1$^k9<+|v{ZPv=SMggFVSYJ`tk=V(h^Le_z#pta;QM$OY}|JukUp0CsP{3BThDG!L93&lG}aVMgq<6E`8yl zg{L;wO>=3*YFnnj7rs~#*cg_4aKhM$T&?g|PdxSi{sk2bsTc@%7FRSJqO7t}o9gXg z!j6r{5pn)VZ9g05e~HM1M(CeTBrbAhm;#m+)y!(ae~lNJfef|p@ow_@e9$2l>)T?@ zOzhSMD>D3VskH@q=2yo_CUx zC4ZkE<1=rG6wp0pztvryMn*I>b^%Yp;I)G7DA#E}zq5WNSIhP9{VmPOqsHc%CiI5#EuLL-F=7nB#?JDsQYeaAw+kll}bO(VMJTZSfTN zdgbhki$@6Q>8XA;L@YwcwN(bwKvqeTE;XYn z6t=;~Pw-iz&%g;Ju@iM6=-L~WPK^=E$Ou#z(U*)<#5q@xxS0(^J=DX96qgePlLdj}~5$J++94 z+y{h7J)O#*R5k8;{-u6h;Y;?%m4l}liZ;&#^G8}QEPTu`y8S!1iRLmR(YcWN`OCly zEX5H`IhVjr55T2w@9(`pIg*)##`P~(xs3a_j2 zArK(1y>Dh>Bh*%-Wx{L1$|H*oKQRhvQFVwO|2$!{!XE$b%a`?G+kAKU*XoV(uU%#) zR<1TA&hK||qQGGh+YYMZxGqx})5M6|VB7uGx08p;uCL96U6@>im&};`F~l{Ge@W<- zJw2q-y%~di!SuPznWfjVW(+zzJi51U_S*qJTeI&YpU&2suVyYouLkeK4=n)Tq<-kr zqa+sIh?1PBVb|NQ=@KBB2Q)kLpzB<;k3ZhcSL-*ld=>t6QM@Cf2E^AD5GrXMQPlw5 z)bV+dLI+)-t1U+5^WdoAx|i|wY{tU=CJP{r0QI{-66Vq|JyJi7sliderVD*pP4`nIOe)w8C z&Y<7KAGHYl_A55M-^OqPf*g%F|JP?*GX#{UzM!)NX;Hv?VO6jmK_@E*kB%V^ ziy!2p6tD<6FAb)11u_(i=_QJepWwQG4otNMN!0#=>mTVU(FtAP^_1Kzz;wFatG5W9 zpT!c*CP%qnCtmoSMgCcJHE?u~8knVq&9EafR^1yeIhJ9q+Jv6cuH`27>n>-JyE=6^ z5>gnn21tkyd)wuHbJaA12D&6>xaE=fliB@TS-9n`Ozrhs|0}N9zPv7G4#yUwZ<#lt zZf@Q+xH?tdm}d-9$G>yZWo5aBBQKZB$Jg_nj#))q#{6i?P=zwZ;;_g|zl%G2uwNwD zkfv^t$>`cQ0d$w1EMX!o*vnwP^Q^<`vO-C|au0DNqdkY>152ju!!1#@sw-}XXPLHf ztc=(4l9NSD^DUQmuT5)boS;n9iWXVh-0q|inX#cFuS78`S^&SU#qGG1d`P(3f+`(I zsvGw}g|*|>a;`!*xQ+{4xH_8oKcAWZZR(gU5X6NACA2(#kevO~R4+JTT7%m+Hk9;Z zlLt@7eW)O0y|rVFOL`E-N#{E`ee@C&No4=L%Jt9s5ACJp;xRQjNj)eHpQTZC>BdAi zg6g0l;%Gduv*4`8t1A7p^97TKG?ky z`iyE9;CwP19ABbR#NQ47 zY{g?Lau&f~qQM4>b-i^)u?Wz*LOkAd#ueC*r>3<@vh{tv8OI&A9<<|e{6F-BnsuH_ zFxPMdL(T&_&z$P2tbX}J#ue5yWCHm5TaBO~T^eIJ#_f(l@9vrRTsg9V2t-Op6pIHw z`fc`xF*ThI{Pu7%V3D3aoww|%b@TK{bSU;;k{)(g=Qg!`H-RSP3#e_c9MB0RVE zML35%e-;T^&sy<2wRTtpW$F(8C5iC15pi^K5%cCX4;H=+=?sWH)ZZMP9~*C<@1$XD`4* zIHK~(OEq&rxhuBmrbT!{GXJ zM}HQ#-Y#|B;{lwtpg5^B)Gwy=JZv1=yHP0W1>Juc?^e1WA=q!ZYtVCNy0p4;QRldI zv~BPyzyxRDk6Su3Gf{QXJOd21Jp7&0CY{`^(#C?fiO>Ce9ho$inftcG0V}HzUHXDN z%i#Lt!WGc!7wMsF0R$Rp%i%<&@-b0ned<0yQF^g5(ftH3B%5yqn>s`bH$L_F$@?|A zmtX)dUEICJ)JnW=W>Bo1w3SSwx4|`u^TOoKhLn<+O@RzL{8JHxq4d7{KIX_t-kt!$ z-Vxr|firuT>pv{C5l4vnzplIr#}K;NU%mP`JV-t2>~R_SvMC+=pZDJgox}pA*MK3d zOeXDwy9E;yiz``VSF7|UaA_ZSyN>Jtp4FeZCHUHz+xhQklbRf(vRyu|3}!n##+jy$ z<;NR2{qbh}J~W=l>Dg5`9srdtLZb2KwnTw?Pq%ifDL)EYydvWQb1r8+HZYgmjyg|6 z%h{V-Nn7smi!yVcGH}x))t>1c){O|6Rn?5Qk z-AH0Lojg`QMccm*wYrFCG??mmjQf1jZmd7*9X3wQU(h@+eXe|?ov3Dh&)Cy!c44~w zs}8udJV6)XjnWJj#!O}>FR#dyuZ|D#7c4FD%q}V8PFxk=nd(i||J`03tTSqf`=o@7 zHw^eg2L>OKfdLV9K!p3E356_12UzcJvJpkgJQrY|BQ%6Gbc^2V_-4aI6s7_CkbbLU zetJq#jE>m1VtUgwh1v(6GRqD?cc1e?aq}D8QiL@@CBpVZ;85p_W4?JoZ!a4SKdjJb zP@_jb_#nbIo+xs9N!dvH5P(9FK;b;H-M_gF7x|B-@yd=S?48}4ULx9yA0yDLcDMW% zMy&LvMwkl%9VsSEd_>{|C*Wm)#retA9_F&lk^3~vAEmVk(BWgXMUbMq5+?G&A3JXR zJ07pG2YpQSD|#)d`wD@1Z!kKDUw1Ux9E4`X5yVl*lmL}K>rIg>$q`#}Y;T6m#THC} z9>3PFe;})Lh?w(sp_unR!`(#}GvktcRZ)aob#>OJ<_!9|k^3+^O#)tK45@H%)0IXk z7FKZ`d>jFIPajUSuh2AV*7pt5bt=QH+AWs z#hr_ig-~ZQT39dZ-^N4Wl@s5yfQ=VF;NT!{_{4PgyJ1`IkzC7h~y2n(owB&-nT6UY?Fq}46^(#FD3~Au=RhyzlPtl4X za`7uFyEQ^(fouEbBqcg?`b}L=$w4mD1IR>NJ}w3VmzHkZsYa9*=%d~!J66SuJ&C3% zR5s@B{#0f*U%4R0^@|UP)4%G#H;z&p;QCmOx1j>&oBdFO)zNI}Rx_;G?D3Oz#zI{~Ja=%OT=@%Y&MbjXArZ{}3S5b?QYWn*aVR0*hm$U5P|m z-4C4&^^`UP*imAb-c`qi?ZK+FXqJM*ilVI5L6QzFg5qWXFG_bau;Y(_iTz^Lx5jg< zFNg`=mP-#EEc3ArEWNaYE(QB1{?bAzePmWl#k)t*Usn`r->P0K`fGH=)Iik4Y;2a6 zj+V|pb$ZF-q!`8JYhb?Yx}E!bm?F7AQlj*_%CT|uLPP{6+&p2te1)okG}Kt~zltFX~zV!MYeDI*RT`{VCQ$3~0LU^jJ=nX(t@iwN+rTQ4mtWjOycNfm*6^BS@~wj0&^mgEbjMyt`G43J zB5tr?2RVed^>%09H#bN><{k{UP(A$p-ZE~nZf8mx_ob=^`Mm-15y)a>m9RQGnWlO zBvZ;@lqA-oyqvbGzp6{sU655@AQ>5bVa1>Xm%-x}h5VzVSZz~(R=BD_RA^qoQ4(y# zt;LFsxBmH(1MOu$S>}`(?0Gha1Zv}0;WLrtG5Hz;*w>+G(tjgu^_fde4ZCS=Cm}$) zNs$6XC5*TFChRL8ELSi#v_;Nel{0cECG`ZKX;q}P4L|H)mb_2KtF)yp(PL5!V~Grc z@)UeXA zAQTuRb~xUrtaw1Mi**aC;5y<5?O*BQ1MxFvFYVRGBqgl@-jd%Cn2Ud-yw69qDjvd2 zE6yVGTS_k|xo!^FUtVz*or8|~w?U+ug4~{-F$k>1!t!uPx5~czG3i#KO`Mt!9?=HG z__M2l)Nuknl@Hc?f0?s>y0y*}DTH@^*YVe}#Gyv_8w&>M>&V6)-N|ao(#L(sU-&4D z?@xwow?bmDDk?!N=(hoymHIHeD_mxxVN&Nkk99c(w0gysV^!?HPZn}4x@EmL;yiG7 z)1XL>i3ih{N9LJF1W9lbSec4&eUIqWZz^bdlu&`VK38e>@jv8*?D;~D%Zuy)6}fU% z2eYL%ua>(z6QvL)4Pc_RH97-AqUq(G8U;wg9=wZlE>rOoc4^;40)1PI%pexg4uQbW z%**{X1os&7*vYM}2^^IuKpgV(_p{pnzyOn_f7Iw8ox_Z4MXjmsg~<|8!10EVt#zfl zUjC1iwhv0>VNPv2A$IN`gpmt!qIslm%eaqmx{jsig5EhswLoZIEw5lcGs#UiJvE%B z|6~o`!7(lM+tcl>m6OHv!N$~GHRZs$)1Ng_b5YX#aBY}5!vA^){6U_=Oq{1~6gYn% z`!l?Ji^w;R@d2jFQnm|L-M)J88(I9qk9{bJp26dyw->YTL^M^e0A90po6cww8 z(#*ZX8NPSEwe4{>*;{pjmCG8WjtJywsF88xpJvL@*A(Rz9N_^f?FnKX-eWrnOFIHg zoH}y~DBHQk3&C!v*|w^b3>Z9~>#EPe1H?syPS&m8j^4FOv1&c-znzi?&wJNAiXt@) zzj3QzMo-WFf<12Q&ivbUtF(`caL^pwE4eO*90z?o&ho?7xbVZdELrYW8`&=8=>_`i zl`t7BP1ZaOt-_ovt8%d;r%`PSh1VKLij453)yhU;PULdqMTn`n%=Z<$4nJTq0!_JG z->V!1@0uhrk|i4BDkOo5lj+SKw)h`=7Q%(PI$9m@u6UzH24l~b}X`8FVhE_&96h)*sjx;-*1(cf4Eeycpu@^yC~n3GDD?d_`~k zugU*5);cz1S%PJCi3aafaEhGJo&k*|O$8Ah=(o)Hw6K7s$Wp+cgxx%Ql5p$3Bxv<0 z4H$D&MVcC`nQVYq!9NU_iA@(KnoSEq`1K>DLApAhk1?1Z_`}2{v;r0s(`7#geB=|t zpv?igAVuxFmX`R3Qi}z`v@EGQgX4h!CB*N4}9 zg-zZylRu^+Nln(d6Oegd3U1&BUB*sGQ`+E#->bwn@^`m>S?|TI62#QY+O}~4ho_{_ z@AJ#cJ>rL8Si-h&weQ5@qQ3Z9MQM3M0Cv5>{Lv+iX`e}zt#>Eu1aT`?580@iHv)8) zut^Z16dtNd~JHnfzEt$XQe~Vg)5u4cghD3UHhg7sM!I_Kx zrRZyiYJfAt!I@k709Pj7R$ z?;DE)zaoGHxQ<4JRULVFCZ#qdE) zzdeHwD7VU2TR>CNZD}d}%4j91R<=3(rOmvayv6D}mqi`nJdGRegNNIxFp`DlNoJEB zP?bphFvX^r;eCkz$cx->3KbW+=bV8x&<^Y*Rt(zqbLGqEipd4NUNl#w|7p^^dW-gd o`&$41Bk(oo|8x^?;S+IqL7fKk14AULy% panic!("should not be from disk"), - DataSource::DocStateV1(doc_state) => { - assert_eq!(doc_state.len(), 2); - }, - DataSource::DocStateV2(_) => {}, - } - } -} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs deleted file mode 100644 index 4377ce8e68..0000000000 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs +++ /dev/null @@ -1,78 +0,0 @@ -// use url::Url; -// use uuid::Uuid; -// -// use flowy_storage::StorageObject; -// -// use crate::supabase_test::util::{file_storage_service, get_supabase_ci_config}; -// -// #[tokio::test] -// async fn supabase_get_object_test() { -// if get_supabase_ci_config().is_none() { -// return; -// } -// -// let service = file_storage_service(); -// let file_name = format!("test-{}.txt", Uuid::new_v4()); -// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); -// -// // Upload a file -// let url = service -// .create_object(object) -// .await -// .unwrap() -// .parse::() -// .unwrap(); -// -// // The url would be something like: -// // https://acfrqdbdtbsceyjbxsfc.supabase.co/storage/v1/object/data/test-1693472809.txt -// let name = url.path_segments().unwrap().last().unwrap(); -// assert_eq!(name, &file_name); -// -// // Download the file -// let bytes = service.get_object(url.to_string()).await.unwrap(); -// let s = String::from_utf8(bytes.to_vec()).unwrap(); -// assert_eq!(s, "hello world"); -// } -// -// #[tokio::test] -// async fn supabase_upload_image_test() { -// if get_supabase_ci_config().is_none() { -// return; -// } -// -// let service = file_storage_service(); -// let file_name = format!("image-{}.png", Uuid::new_v4()); -// let object = StorageObject::from_file("1", &file_name, "tests/logo.png"); -// -// // Upload a file -// let url = service -// .create_object(object) -// .await -// .unwrap() -// .parse::() -// .unwrap(); -// -// // Download object by url -// let bytes = service.get_object(url.to_string()).await.unwrap(); -// assert_eq!(bytes.len(), 15694); -// } -// -// #[tokio::test] -// async fn supabase_delete_object_test() { -// if get_supabase_ci_config().is_none() { -// return; -// } -// -// let service = file_storage_service(); -// let file_name = format!("test-{}.txt", Uuid::new_v4()); -// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); -// let url = service.create_object(object).await.unwrap(); -// -// let result = service.get_object(url.clone()).await; -// assert!(result.is_ok()); -// -// let _ = service.delete_object(url.clone()).await; -// -// let result = service.get_object(url.clone()).await; -// assert!(result.is_err()); -// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs deleted file mode 100644 index a9037caa6c..0000000000 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs +++ /dev/null @@ -1,316 +0,0 @@ -use assert_json_diff::assert_json_eq; -use collab_entity::{CollabObject, CollabType}; -use serde_json::json; -use uuid::Uuid; -use yrs::types::ToJson; -use yrs::updates::decoder::Decode; -use yrs::{merge_updates_v1, Array, Doc, Map, MapPrelim, ReadTxn, StateVector, Transact, Update}; - -use flowy_user_pub::entities::AuthResponse; -use lib_infra::box_any::BoxAny; - -use crate::supabase_test::util::{ - collab_service, folder_service, get_supabase_ci_config, third_party_sign_up_param, - user_auth_service, -}; - -#[tokio::test] -async fn supabase_create_workspace_test() { - if get_supabase_ci_config().is_none() { - return; - } - - let service = folder_service(); - // will replace the uid with the real uid - let workspace = service.create_workspace(1, "test").await.unwrap(); - dbg!(workspace); -} - -#[tokio::test] -async fn supabase_get_folder_test() { - if get_supabase_ci_config().is_none() { - return; - } - - let folder_service = folder_service(); - let user_service = user_auth_service(); - let collab_service = collab_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - - let collab_object = CollabObject::new( - user.user_id, - user.latest_workspace.id.clone(), - CollabType::Folder, - user.latest_workspace.id.clone(), - "fake_device_id".to_string(), - ); - - let doc = Doc::with_client_id(1); - let map = { doc.get_or_insert_map("map") }; - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "1", "a"); - collab_service - .send_update(&collab_object, 0, txn.encode_update_v1()) - .await - .unwrap(); - }; - - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "2", "b"); - collab_service - .send_update(&collab_object, 1, txn.encode_update_v1()) - .await - .unwrap(); - }; - - // let updates = collab_service.get_all_updates(&collab_object).await.unwrap(); - let updates = folder_service - .get_folder_doc_state( - &user.latest_workspace.id, - user.user_id, - CollabType::Folder, - &user.latest_workspace.id, - ) - .await - .unwrap(); - assert_eq!(updates.len(), 2); - - for _ in 0..5 { - collab_service - .send_init_sync(&collab_object, 3, vec![]) - .await - .unwrap(); - } - let updates = folder_service - .get_folder_doc_state( - &user.latest_workspace.id, - user.user_id, - CollabType::Folder, - &user.latest_workspace.id, - ) - .await - .unwrap(); - - // Other the init sync, try to get the updates from the server. - let expected_update = doc - .transact_mut() - .encode_state_as_update_v1(&StateVector::default()); - - // check the update is the same as local document update. - assert_eq!(updates, expected_update); -} - -/// This async test function checks the behavior of updates duplication in Supabase. -/// It creates a new user and simulates two updates to the user's workspace with different values. -/// Then, it merges these updates and sends an initial synchronization request to test duplication handling. -/// Finally, it asserts that the duplicated updates don't affect the overall data consistency in Supabase. -#[tokio::test] -async fn supabase_duplicate_updates_test() { - if get_supabase_ci_config().is_none() { - return; - } - - let folder_service = folder_service(); - let user_service = user_auth_service(); - let collab_service = collab_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - - let collab_object = CollabObject::new( - user.user_id, - user.latest_workspace.id.clone(), - CollabType::Folder, - user.latest_workspace.id.clone(), - "fake_device_id".to_string(), - ); - let doc = Doc::with_client_id(1); - let map = { doc.get_or_insert_map("map") }; - let mut duplicated_updates = vec![]; - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "1", "a"); - let update = txn.encode_update_v1(); - duplicated_updates.push(update.clone()); - collab_service - .send_update(&collab_object, 0, update) - .await - .unwrap(); - }; - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "2", "b"); - let update = txn.encode_update_v1(); - duplicated_updates.push(update.clone()); - collab_service - .send_update(&collab_object, 1, update) - .await - .unwrap(); - }; - // send init sync - collab_service - .send_init_sync(&collab_object, 3, vec![]) - .await - .unwrap(); - let first_init_sync_update = folder_service - .get_folder_doc_state( - &user.latest_workspace.id, - user.user_id, - CollabType::Folder, - &user.latest_workspace.id, - ) - .await - .unwrap(); - - // simulate the duplicated updates. - let merged_update = merge_updates_v1( - &duplicated_updates - .iter() - .map(|update| update.as_ref()) - .collect::>(), - ) - .unwrap(); - collab_service - .send_init_sync(&collab_object, 4, merged_update) - .await - .unwrap(); - let second_init_sync_update = folder_service - .get_folder_doc_state( - &user.latest_workspace.id, - user.user_id, - CollabType::Folder, - &user.latest_workspace.id, - ) - .await - .unwrap(); - - let doc_2 = Doc::new(); - assert_eq!(first_init_sync_update.len(), second_init_sync_update.len()); - let map = { doc_2.get_or_insert_map("map") }; - { - let mut txn = doc_2.transact_mut(); - let update = Update::decode_v1(&second_init_sync_update).unwrap(); - txn.apply_update(update).unwrap(); - } - { - let txn = doc_2.transact(); - let json = map.to_json(&txn); - assert_json_eq!( - json, - json!({ - "1": "a", - "2": "b" - }) - ); - } -} - -/// The state vector of doc; -/// ```json -/// "map": {}, -/// "array": [] -/// ``` -/// The old version of doc: -/// ```json -/// "map": {} -/// ``` -/// -/// Try to apply the updates from doc to old version doc and check the result. -#[tokio::test] -async fn supabase_diff_state_vector_test() { - if get_supabase_ci_config().is_none() { - return; - } - - let folder_service = folder_service(); - let user_service = user_auth_service(); - let collab_service = collab_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - - let collab_object = CollabObject::new( - user.user_id, - user.latest_workspace.id.clone(), - CollabType::Folder, - user.latest_workspace.id.clone(), - "fake_device_id".to_string(), - ); - let doc = Doc::with_client_id(1); - let map = { doc.get_or_insert_map("map") }; - let array = { doc.get_or_insert_array("array") }; - - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "1", "a"); - map.insert(&mut txn, "inner_map", MapPrelim::::new()); - - array.push_back(&mut txn, "element 1"); - let update = txn.encode_update_v1(); - collab_service - .send_update(&collab_object, 0, update) - .await - .unwrap(); - }; - { - let mut txn = doc.transact_mut(); - map.insert(&mut txn, "2", "b"); - array.push_back(&mut txn, "element 2"); - let update = txn.encode_update_v1(); - collab_service - .send_update(&collab_object, 1, update) - .await - .unwrap(); - }; - - // restore the doc with given updates. - let old_version_doc = Doc::new(); - let map = { old_version_doc.get_or_insert_map("map") }; - let doc_state = folder_service - .get_folder_doc_state( - &user.latest_workspace.id, - user.user_id, - CollabType::Folder, - &user.latest_workspace.id, - ) - .await - .unwrap(); - { - let mut txn = old_version_doc.transact_mut(); - let update = Update::decode_v1(&doc_state).unwrap(); - txn.apply_update(update).unwrap(); - } - let txn = old_version_doc.transact(); - let json = map.to_json(&txn); - assert_json_eq!( - json, - json!({ - "1": "a", - "2": "b", - "inner_map": {} - }) - ); -} - -// #[tokio::test] -// async fn print_folder_object_test() { -// if get_supabase_dev_config().is_none() { -// return; -// } -// let secret = Some("43bSxEPHeNkk5ZxxEYOfAjjd7sK2DJ$vVnxwuNc5ru0iKFvhs8wLg==".to_string()); -// print_encryption_folder("f8b14b84-e8ec-4cf4-a318-c1e008ecfdfa", secret).await; -// } -// -// #[tokio::test] -// async fn print_folder_snapshot_object_test() { -// if get_supabase_dev_config().is_none() { -// return; -// } -// let secret = Some("NTXRXrDSybqFEm32jwMBDzbxvCtgjU$8np3TGywbBdJAzHtu1QIyQ==".to_string()); -// // let secret = None; -// print_encryption_folder_snapshot("12533251-bdd4-41f4-995f-ff12fceeaa42", secret).await; -// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs deleted file mode 100644 index ab82d37866..0000000000 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod database_test; -mod file_test; -mod folder_test; -mod user_test; -mod util; diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs deleted file mode 100644 index 13df930601..0000000000 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs +++ /dev/null @@ -1,141 +0,0 @@ -use uuid::Uuid; - -use flowy_encrypt::{encrypt_text, generate_encryption_secret}; -use flowy_error::FlowyError; -use flowy_user_pub::entities::*; -use lib_infra::box_any::BoxAny; - -use crate::supabase_test::util::{ - get_supabase_ci_config, third_party_sign_up_param, user_auth_service, -}; - -// ‼️‼️‼️ Warning: this test will create a table in the database -#[tokio::test] -async fn supabase_user_sign_up_test() { - if get_supabase_ci_config().is_none() { - return; - } - let user_service = user_auth_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - assert!(!user.latest_workspace.id.is_empty()); - assert!(!user.user_workspaces.is_empty()); - assert!(!user.latest_workspace.database_indexer_id.is_empty()); -} - -#[tokio::test] -async fn supabase_user_sign_up_with_existing_uuid_test() { - if get_supabase_ci_config().is_none() { - return; - } - let user_service = user_auth_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let _user: AuthResponse = user_service - .sign_up(BoxAny::new(params.clone())) - .await - .unwrap(); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - assert!(!user.latest_workspace.id.is_empty()); - assert!(!user.latest_workspace.database_indexer_id.is_empty()); - assert!(!user.user_workspaces.is_empty()); -} - -#[tokio::test] -async fn supabase_update_user_profile_test() { - if get_supabase_ci_config().is_none() { - return; - } - let user_service = user_auth_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service - .sign_up(BoxAny::new(params.clone())) - .await - .unwrap(); - - let params = UpdateUserProfileParams::new(user.user_id) - .with_name("123") - .with_email(format!("{}@test.com", Uuid::new_v4())); - - user_service - .update_user(UserCredentials::from_uid(user.user_id), params) - .await - .unwrap(); - - let user_profile = user_service - .get_user_profile(UserCredentials::from_uid(user.user_id)) - .await - .unwrap(); - - assert_eq!(user_profile.name, "123"); -} - -#[tokio::test] -async fn supabase_get_user_profile_test() { - if get_supabase_ci_config().is_none() { - return; - } - let user_service = user_auth_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service - .sign_up(BoxAny::new(params.clone())) - .await - .unwrap(); - - let credential = UserCredentials::from_uid(user.user_id); - user_service - .get_user_profile(credential.clone()) - .await - .unwrap(); -} - -#[tokio::test] -async fn supabase_get_not_exist_user_profile_test() { - if get_supabase_ci_config().is_none() { - return; - } - - let user_service = user_auth_service(); - let result: FlowyError = user_service - .get_user_profile(UserCredentials::from_uid(i64::MAX)) - .await - .unwrap_err(); - // user not found - assert!(result.is_record_not_found()); -} - -#[tokio::test] -async fn user_encryption_sign_test() { - if get_supabase_ci_config().is_none() { - return; - } - let user_service = user_auth_service(); - let uuid = Uuid::new_v4().to_string(); - let params = third_party_sign_up_param(uuid); - let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); - - // generate encryption sign - let secret = generate_encryption_secret(); - let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap(); - - user_service - .update_user( - UserCredentials::from_uid(user.user_id), - UpdateUserProfileParams::new(user.user_id) - .with_encryption_type(EncryptionType::SelfEncryption(sign.clone())), - ) - .await - .unwrap(); - - let user_profile: UserProfile = user_service - .get_user_profile(UserCredentials::from_uid(user.user_id)) - .await - .unwrap(); - assert_eq!( - user_profile.encryption_type, - EncryptionType::SelfEncryption(sign) - ); -} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs deleted file mode 100644 index 7fba91fe9a..0000000000 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use collab::core::collab::{DataSource, MutexCollab}; -use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; -use collab_plugins::cloud_storage::RemoteCollabStorage; -use uuid::Uuid; - -use flowy_database_pub::cloud::DatabaseCloudService; -use flowy_error::FlowyError; -use flowy_folder_pub::cloud::{Folder, FolderCloudService}; -use flowy_server::supabase::api::{ - RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, - SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl, -}; -use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; -use flowy_server::{AppFlowyEncryption, EncryptionImpl}; -use flowy_server_pub::supabase_config::SupabaseConfiguration; -use flowy_user_pub::cloud::UserCloudService; -use lib_infra::future::FutureResult; - -use crate::setup_log; - -pub fn get_supabase_ci_config() -> Option { - dotenv::from_filename("./.env.ci").ok()?; - setup_log(); - SupabaseConfiguration::from_env().ok() -} - -#[allow(dead_code)] -pub fn get_supabase_dev_config() -> Option { - dotenv::from_filename("./.env.dev").ok()?; - setup_log(); - SupabaseConfiguration::from_env().ok() -} - -pub fn collab_service() -> Arc { - let (server, encryption_impl) = supabase_server_service(None); - Arc::new(SupabaseCollabStorageImpl::new( - server, - None, - Arc::downgrade(&encryption_impl), - )) -} - -pub fn database_service() -> Arc { - let (server, _encryption_impl) = supabase_server_service(None); - Arc::new(SupabaseDatabaseServiceImpl::new(server)) -} - -pub fn user_auth_service() -> Arc { - let (server, _encryption_impl) = supabase_server_service(None); - Arc::new(SupabaseUserServiceImpl::new(server, vec![], None)) -} - -pub fn folder_service() -> Arc { - let (server, _encryption_impl) = supabase_server_service(None); - Arc::new(SupabaseFolderServiceImpl::new(server)) -} - -#[allow(dead_code)] -pub fn file_storage_service() -> Arc { - let encryption_impl: Arc = Arc::new(EncryptionImpl::new(None)); - let config = SupabaseConfiguration::from_env().unwrap(); - Arc::new( - SupabaseFileStorage::new( - &config, - Arc::downgrade(&encryption_impl), - Arc::new(TestFileStoragePlan), - ) - .unwrap(), - ) -} - -#[allow(dead_code)] -pub fn encryption_folder_service( - secret: Option, -) -> (Arc, Arc) { - let (server, encryption_impl) = supabase_server_service(secret); - let service = Arc::new(SupabaseFolderServiceImpl::new(server)); - (service, encryption_impl) -} - -#[allow(dead_code)] -pub fn encryption_collab_service( - secret: Option, -) -> (Arc, Arc) { - let (server, encryption_impl) = supabase_server_service(secret); - let service = Arc::new(SupabaseCollabStorageImpl::new( - server, - None, - Arc::downgrade(&encryption_impl), - )); - (service, encryption_impl) -} - -#[allow(dead_code)] -pub async fn print_encryption_folder( - uid: &i64, - folder_id: &str, - encryption_secret: Option, -) { - let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); - let folder_data = cloud_service.get_folder_data(folder_id, uid).await.unwrap(); - let json = serde_json::to_value(folder_data).unwrap(); - println!("{}", serde_json::to_string_pretty(&json).unwrap()); -} - -#[allow(dead_code)] -pub async fn print_encryption_folder_snapshot( - uid: &i64, - folder_id: &str, - encryption_secret: Option, -) { - let (cloud_service, _encryption) = encryption_collab_service(encryption_secret); - let snapshot = cloud_service - .get_snapshots(folder_id, 1) - .await - .pop() - .unwrap(); - let collab = Arc::new(MutexCollab::new( - Collab::new_with_source( - CollabOrigin::Empty, - folder_id, - DataSource::DocStateV1(snapshot.blob), - vec![], - false, - ) - .unwrap(), - )); - let folder_data = Folder::open(uid, collab, None) - .unwrap() - .get_folder_data(folder_id) - .unwrap(); - let json = serde_json::to_value(folder_data).unwrap(); - println!("{}", serde_json::to_string_pretty(&json).unwrap()); -} - -pub fn supabase_server_service( - 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) -} - -pub fn third_party_sign_up_param(uuid: String) -> HashMap { - let mut params = HashMap::new(); - params.insert(USER_UUID.to_string(), uuid); - params.insert( - USER_EMAIL.to_string(), - format!("{}@test.com", Uuid::new_v4()), - ); - params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); - params -} - -pub struct TestFileStoragePlan; diff --git a/frontend/rust-lib/flowy-server/tests/test.txt b/frontend/rust-lib/flowy-server/tests/test.txt deleted file mode 100644 index 95d09f2b10..0000000000 --- a/frontend/rust-lib/flowy-server/tests/test.txt +++ /dev/null @@ -1 +0,0 @@ -hello world \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index e8d186c484..a78172aa9f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -6,7 +6,7 @@ use crate::sql::{ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_table; use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use tracing::{trace, warn}; +use tracing::trace; /// The order of the fields in the struct must be the same as the order of the fields in the table. /// Check out the [schema.rs] for table schema. @@ -157,26 +157,12 @@ pub fn select_user_profile( Ok(user) } -pub fn select_workspace_auth_type( +pub fn select_user_auth_type( uid: i64, - workspace_id: &str, conn: &mut SqliteConnection, ) -> Result { - match select_user_workspace(workspace_id, conn) { - Ok(workspace) => Ok(AuthType::from(workspace.workspace_type)), - Err(err) => { - if err.is_record_not_found() { - let row = select_user_table_row(uid, conn)?; - warn!( - "user user auth type:{} as workspace auth type", - row.auth_type - ); - Ok(AuthType::from(row.auth_type)) - } else { - Err(err) - } - }, - } + let row = select_user_table_row(uid, conn)?; + Ok(AuthType::from(row.auth_type)) } pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs index 7c806d3aaf..1b1c3f890f 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs @@ -1,7 +1,7 @@ use diesel::SqliteConnection; use semver::Version; use std::sync::Arc; -use tracing::{info, instrument}; +use tracing::instrument; use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; @@ -9,7 +9,7 @@ use flowy_user_pub::entities::AuthType; use crate::migrations::migration::UserDataMigration; use flowy_user_pub::session::Session; -use flowy_user_pub::sql::{select_user_workspace, upsert_user_workspace}; +use flowy_user_pub::sql::upsert_user_workspace; pub struct AnonUserWorkspaceTableMigration; @@ -34,23 +34,15 @@ impl UserDataMigration for AnonUserWorkspaceTableMigration { &self, session: &Session, _collab_db: &Arc, - auth_type: &AuthType, + user_auth_type: &AuthType, db: &mut SqliteConnection, ) -> FlowyResult<()> { // For historical reason, anon user doesn't have a workspace in user_workspace_table. // So we need to create a new entry for the anon user in the user_workspace_table. - if matches!(auth_type, AuthType::Local) { - let user_workspace = &session.user_workspace; - let result = select_user_workspace(&user_workspace.id, db); - if let Err(e) = result { - if e.is_record_not_found() { - info!( - "Anon user workspace not found in the database, creating a new entry for user_id: {}", - session.user_id - ); - upsert_user_workspace(session.user_id, *auth_type, user_workspace.clone(), db)?; - } - } + if matches!(user_auth_type, AuthType::Local) { + let mut user_workspace = session.user_workspace.clone(); + user_workspace.workspace_type = AuthType::Local; + upsert_user_workspace(session.user_id, *user_auth_type, user_workspace, db)?; } Ok(()) diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 84acc0b56a..735d8f1f49 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -40,7 +40,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &AuthType, + _user_auth_type: &AuthType, _db: &mut SqliteConnection, ) -> FlowyResult<()> { trace!( 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 2e4581f7ec..996386cb5e 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 @@ -42,13 +42,13 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { &self, session: &Session, collab_db: &Arc, - authenticator: &AuthType, + user_auth_type: &AuthType, _db: &mut SqliteConnection, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. // - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version. - if !matches!(authenticator, AuthType::Local) { + if !matches!(user_auth_type, AuthType::Local) { return Ok(()); } collab_db.with_write_txn(|write_txn| { diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 0f5c2c2624..1cf8d6a943 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - auth_type: &AuthType, + user_auth_type: &AuthType, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -75,7 +75,7 @@ impl UserLocalDataMigration { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - migration.run(&self.session, &self.collab_db, auth_type, &mut conn)?; + migration.run(&self.session, &self.collab_db, user_auth_type, &mut conn)?; applied_migrations.push(migration.name().to_string()); save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); @@ -98,7 +98,7 @@ pub trait UserDataMigration { &self, user: &Session, collab_db: &Arc, - authenticator: &AuthType, + user_auth_type: &AuthType, db: &mut SqliteConnection, ) -> FlowyResult<()>; } 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 5f14051e26..d3cea0e976 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 @@ -40,7 +40,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &AuthType, + _user_auth_type: &AuthType, _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { 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 b5eeead8c6..ee9156199e 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 @@ -38,7 +38,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { &self, session: &Session, collab_db: &Arc, - _authenticator: &AuthType, + _user_auth_type: &AuthType, _db: &mut SqliteConnection, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { 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 84c1e9afe9..418f0638d3 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -10,7 +10,7 @@ use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; -use flowy_user_pub::entities::UserWorkspace; +use flowy_user_pub::entities::{AuthType, UserWorkspace}; use flowy_user_pub::session::Session; use std::path::PathBuf; use std::str::FromStr; @@ -48,14 +48,11 @@ impl AuthenticateUser { } pub async fn is_local_mode(&self) -> FlowyResult { - let uid = self.user_id()?; - if let Ok(anon_user) = self.get_anon_user().await { - if anon_user == uid { - return Ok(true); - } - } - - Ok(false) + let session = self.get_session()?; + Ok(matches!( + session.user_workspace.workspace_type, + AuthType::Local + )) } pub fn device_id(&self) -> FlowyResult { @@ -150,28 +147,24 @@ impl AuthenticateUser { 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", + "Can't find user session. Please login again", )), - Some(session) => { + Some(mut session) => { + // Set the workspace type to local if the user is anon. + if let Some(anon_session) = self.store_preferences.get_object::(ANON_USER) { + if session.user_id == anon_session.user_id { + session.user_workspace.workspace_type = AuthType::Local; + } + } + + let session = Arc::new(session); self.session.store(Some(session.clone())); Ok(session) }, } } - - async fn get_anon_user(&self) -> FlowyResult { - let anon_session = self - .store_preferences - .get_object::(ANON_USER) - .ok_or(FlowyError::new( - ErrorCode::RecordNotFound, - "Anon user not found", - ))?; - - Ok(anon_session.user_id) - } } 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 20b0c26368..90113b8062 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 @@ -3,7 +3,7 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::data_import::importer::load_collab_by_object_ids; use crate::services::db::UserDBPath; use crate::services::entities::UserPaths; -use crate::user_manager::run_collab_data_migration; +use crate::user_manager::run_data_migration; use anyhow::anyhow; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -36,7 +36,7 @@ use std::collections::{HashMap, HashSet}; use collab_document::blocks::TextDelta; use collab_document::document::Document; -use flowy_user_pub::sql::{select_user_profile, select_workspace_auth_type}; +use flowy_user_pub::sql::{select_user_auth_type, select_user_profile}; use semver::Version; use serde_json::json; use std::ops::{Deref, DerefMut}; @@ -103,23 +103,17 @@ pub(crate) fn prepare_import( ); let mut conn = imported_sqlite_db.get_connection()?; - let imported_workspace_auth_type = select_user_profile( + let imported_user_auth_type = select_user_profile( imported_session.user_id, &imported_session.user_workspace.id, &mut conn, ) - .map(|v| v.workspace_auth_type) - .or_else(|_| { - select_workspace_auth_type( - imported_session.user_id, - &imported_session.user_workspace.id, - &mut conn, - ) - })?; + .map(|v| v.auth_type) + .or_else(|_| select_user_auth_type(imported_session.user_id, &mut conn))?; - run_collab_data_migration( + run_data_migration( &imported_session, - &imported_workspace_auth_type, + &imported_user_auth_type, imported_collab_db.clone(), imported_sqlite_db.get_pool(), other_store_preferences.clone(), 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 9cefddf44b..2118362a8b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -235,9 +235,9 @@ impl UserManager { self.authenticate_user.database.get_pool(session.user_id), ) { (Ok(collab_db), Ok(sqlite_pool)) => { - run_collab_data_migration( + run_data_migration( &session, - &auth_type, + &user.auth_type, collab_db, sqlite_pool, self.store_preferences.clone(), @@ -844,9 +844,9 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc) { } } -pub(crate) fn run_collab_data_migration( +pub(crate) fn run_data_migration( session: &Session, - auth_type: &AuthType, + user_auth_type: &AuthType, collab_db: Arc, sqlite_pool: Arc, kv: Arc, @@ -855,7 +855,7 @@ pub(crate) fn run_collab_data_migration( let migrations = collab_migration_list(); match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( migrations, - auth_type, + user_auth_type, app_version, ) { Ok(applied_migrations) => { 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 a7191f0509..188cc3c5ac 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 @@ -53,6 +53,18 @@ impl UserManager { Ok(UserProfilePB::from(profile)) } + pub fn get_anon_user_id(&self) -> FlowyResult { + let anon_session = self + .store_preferences + .get_object::(ANON_USER) + .ok_or(FlowyError::new( + ErrorCode::RecordNotFound, + "Anon user not found", + ))?; + + Ok(anon_session.user_id) + } + /// Opens a historical user's session based on their user ID, device ID, and authentication type. /// /// This function facilitates the re-opening of a user's session from historical tracking. From b0c2b04a2da4c1238e36699c7c73282b9ea2abeb Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 22:10:52 +0800 Subject: [PATCH 64/74] fix: empty view id --- .../lib/mobile/presentation/home/mobile_home_page.dart | 2 +- frontend/appflowy_flutter/lib/plugins/blank/blank.dart | 2 +- .../lib/workspace/application/view/view_bloc.dart | 2 +- .../lib/workspace/application/view/view_service.dart | 9 +++++++++ .../lib/workspace/presentation/home/home_stack.dart | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) 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 fdea8322c3..345a4591d1 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 @@ -145,7 +145,7 @@ class _MobileHomePageState extends State { void _onLatestViewChange() async { final id = getIt().latestOpenView?.id; - if (id == null) { + if (id == null || id.isEmpty) { return; } await FolderEventSetLatestView(ViewIdPB(value: id)).send(); diff --git a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart index b25bb5af06..ebda487515 100644 --- a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart +++ b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart @@ -36,7 +36,7 @@ class BlankPagePlugin extends Plugin { PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder(); @override - PluginId get id => "BlankStack"; + PluginId get id => ""; @override PluginType get pluginType => PluginType.blank; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 7c2a4d9b64..553317f4e4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -404,7 +404,7 @@ class ViewBloc extends Bloc { }); } - if (update.updateChildViews.isNotEmpty) { + if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) { final view = await ViewBackendService.getView(update.parentViewId); final childViews = view.fold((l) => l.childViews, (r) => []); bool isSameOrder = true; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 709515f1b3..ea74f1861e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -111,6 +111,12 @@ class ViewBackendService { static Future, FlowyError>> getChildViews({ required String viewId, }) { + if (viewId.isEmpty) { + return Future.value( + FlowyResult, FlowyError>.success([]), + ); + } + final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send().then((result) { @@ -262,6 +268,9 @@ class ViewBackendService { static Future> getView( String viewId, ) async { + if (viewId.isEmpty) { + Log.error('ViewId is empty'); + } final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index ae3b92a702..464394cd39 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -631,7 +631,7 @@ class PageNotifier extends ChangeNotifier { } // Set the plugin view as the latest view. - if (setLatest) { + if (setLatest && newPlugin.id.isNotEmpty) { FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); } From 1be51d667972d3cdb7c49d79e2f529a29fa45aa4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Apr 2025 22:26:53 +0800 Subject: [PATCH 65/74] chore: clippy --- .../home/workspaces/workspace_menu_bottom_sheet.dart | 2 +- .../home/menu/sidebar/workspace/_sidebar_workspace_menu.dart | 2 +- .../flowy-user/src/user_manager/manager_user_workspace.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index 9743da966e..d306f48964 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget { context.read().add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 04eb701a66..4ff5ccbf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -389,7 +389,7 @@ class _CreateWorkspaceButton extends StatelessWidget { workspaceBloc.add( UserWorkspaceEvent.createWorkspace( name, - AuthTypePB.Local, + AuthTypePB.Server, ), ); }, 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 0101ef299e..78a13d7229 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 @@ -153,7 +153,7 @@ impl UserManager { #[instrument(skip(self), err)] pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { - info!("open workspace: {}, auth_type:{}", workspace_id, auth_type); + info!("open workspace: {}, auth type:{}", workspace_id, auth_type); let workspace_id_str = workspace_id.to_string(); self.cloud_service.set_server_auth_type(&auth_type); From 530e076838d3e4a5e8b399dd50df7f37fb21af7b Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Apr 2025 00:08:19 +0800 Subject: [PATCH 66/74] chore: update test --- .../user/af_cloud_test/workspace_test.rs | 131 ++++++++++++++---- .../src/deps_resolve/cloud_service_impl.rs | 7 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 6 +- .../flowy-user-pub/src/sql/user_sql.rs | 5 + .../flowy-user/src/entities/workspace.rs | 2 +- .../flowy-user/src/user_manager/manager.rs | 29 ++-- .../user_manager/manager_user_workspace.rs | 6 +- 7 files changed, 149 insertions(+), 37 deletions(-) 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 ce50575954..d390c0558e 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 @@ -6,6 +6,7 @@ use collab_folder::Folder; use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_user::entities::AFRolePB; +use flowy_user_pub::cloud::UserCloudServiceProvider; use flowy_user_pub::entities::AuthType; use std::time::Duration; use tokio::task::LocalSet; @@ -151,7 +152,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.workspace_auth_type.clone(), + first_workspace.workspace_auth_type, ) .await; sleep(Duration::from_millis(300)).await; @@ -162,7 +163,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.workspace_auth_type.clone(), + second_workspace.workspace_auth_type, ) .await; sleep(Duration::from_millis(200)).await; @@ -175,7 +176,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &first_workspace.workspace_id, - first_workspace.workspace_auth_type.clone(), + first_workspace.workspace_auth_type, ) .await; let views_1 = test.get_all_workspace_views().await; @@ -187,7 +188,7 @@ async fn af_cloud_open_workspace_test() { test .open_workspace( &second_workspace.workspace_id, - second_workspace.workspace_auth_type.clone(), + second_workspace.workspace_auth_type, ) .await; let views_2 = test.get_all_workspace_views().await; @@ -246,10 +247,7 @@ async fn af_cloud_different_open_same_workspace_test() { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; client - .open_workspace( - iter_workspace_id, - all_workspaces[index].workspace_auth_type.clone(), - ) + .open_workspace(iter_workspace_id, all_workspaces[index].workspace_auth_type) .await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; @@ -296,42 +294,127 @@ async fn af_cloud_different_open_same_workspace_test() { #[tokio::test] async fn af_cloud_create_local_workspace_test() { + // Setup: Initialize test environment with AppFlowyCloud use_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let _ = test.af_cloud_sign_up().await; - let workspaces = test.get_all_workspaces().await.items; - assert_eq!(workspaces.len(), 1); + // Verify initial state: User should have one default workspace + let initial_workspaces = test.get_all_workspaces().await.items; + assert_eq!( + initial_workspaces.len(), + 1, + "User should start with one default workspace" + ); - let created_workspace = test + // make sure the workspaces order is consistent + // tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + + // Test: Create a local workspace + let local_workspace = test .create_workspace("my local workspace", AuthType::Local) .await; - assert_eq!(created_workspace.name, "my local workspace"); - let workspaces = test.get_all_workspaces().await.items; - assert_eq!(workspaces.len(), 2); - assert_eq!(workspaces[1].name, "my local workspace"); + // Verify: Local workspace was created correctly + assert_eq!(local_workspace.name, "my local workspace"); + let updated_workspaces = test.get_all_workspaces().await.items; + assert_eq!( + updated_workspaces.len(), + 2, + "Should now have two workspaces" + ); + dbg!(&updated_workspaces); + // Find local workspace by name instead of using index + let found_local_workspace = updated_workspaces + .iter() + .find(|workspace| workspace.name == "my local workspace") + .expect("Local workspace should exist"); + assert_eq!(found_local_workspace.name, "my local workspace"); + + // Test: Open the local workspace test .open_workspace( - &created_workspace.workspace_id, - created_workspace.workspace_auth_type, + &local_workspace.workspace_id, + local_workspace.workspace_auth_type, ) .await; + // Verify: Views in the local workspace let views = test.get_all_views().await; - assert_eq!(views.len(), 2); - assert!(views - .iter() - .any(|view| view.parent_view_id == workspaces[1].workspace_id)); + assert_eq!( + views.len(), + 2, + "Local workspace should have 2 default views" + ); + assert!( + views + .iter() + .any(|view| view.parent_view_id == local_workspace.workspace_id), + "Views should belong to the local workspace" + ); + // Verify: Can access all views for view in views { test.get_view(&view.id).await; } + // Verify: Local workspace members let members = test - .get_workspace_members(&created_workspace.workspace_id) + .get_workspace_members(&local_workspace.workspace_id) .await; - assert_eq!(members.len(), 1); - assert_eq!(members[0].role, AFRolePB::Owner); + assert_eq!( + members.len(), + 1, + "Local workspace should have only one member" + ); + assert_eq!(members[0].role, AFRolePB::Owner, "User should be the owner"); + + // Test: Create a server workspace + let server_workspace = test + .create_workspace("my server workspace", AuthType::AppFlowyCloud) + .await; + + // Verify: Server workspace was created correctly + assert_eq!(server_workspace.name, "my server workspace"); + let final_workspaces = test.get_all_workspaces().await.items; + assert_eq!( + final_workspaces.len(), + 3, + "Should now have three workspaces" + ); + + dbg!(&final_workspaces); + + // Find workspaces by name instead of using indices + let found_local_workspace = final_workspaces + .iter() + .find(|workspace| workspace.name == "my local workspace") + .expect("Local workspace should exist"); + assert_eq!(found_local_workspace.name, "my local workspace"); + + let found_server_workspace = final_workspaces + .iter() + .find(|workspace| workspace.name == "my server workspace") + .expect("Server workspace should exist"); + assert_eq!(found_server_workspace.name, "my server workspace"); + + // Verify: Server-side only recognizes cloud workspaces (not local ones) + let user_profile = test.get_user_profile().await.unwrap(); + test + .server_provider + .set_server_auth_type(&AuthType::AppFlowyCloud, Some(user_profile.token.clone())) + .unwrap(); + test.server_provider.set_token(&user_profile.token).unwrap(); + + let user_service = test.server_provider.get_server().unwrap().user_service(); + let server_workspaces = user_service + .get_all_workspace(user_profile.id) + .await + .unwrap(); + assert_eq!( + server_workspaces.len(), + 2, + "Server should only see 2 workspaces (the default and server workspace, not the local one)" + ); } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index 6c9581d4a7..c49757f735 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -161,6 +161,7 @@ impl StorageCloudService for ServerProvider { impl UserCloudServiceProvider for ServerProvider { fn set_token(&self, token: &str) -> Result<(), FlowyError> { let server = self.get_server()?; + info!("Set token"); server.set_token(token)?; Ok(()) } @@ -191,8 +192,12 @@ impl UserCloudServiceProvider for ServerProvider { /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, /// it will be used when user open the app again. /// - fn set_server_auth_type(&self, auth_type: &AuthType) { + fn set_server_auth_type(&self, auth_type: &AuthType, token: Option) -> FlowyResult<()> { self.set_auth_type(*auth_type); + if let Some(token) = token { + self.set_token(&token)?; + } + Ok(()) } fn get_server_auth_type(&self) -> AuthType { diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 743d39ed4f..a99e8b8672 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -84,7 +84,11 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - fn set_server_auth_type(&self, auth_type: &AuthType); + fn set_server_auth_type( + &self, + auth_type: &AuthType, + token: Option, + ) -> Result<(), FlowyError>; fn get_server_auth_type(&self) -> AuthType; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs index a78172aa9f..ca117300f2 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs @@ -165,6 +165,11 @@ pub fn select_user_auth_type( Ok(AuthType::from(row.auth_type)) } +pub fn select_user_token(uid: i64, conn: &mut SqliteConnection) -> Result { + let row = select_user_table_row(uid, conn)?; + Ok(row.token) +} + pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> { conn.immediate_transaction(|conn| { // delete old user if exists diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 0f7b7f4587..860bda3be7 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -242,7 +242,7 @@ pub struct CreateWorkspacePB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Default, Debug, Clone, Eq, PartialEq)] +#[derive(ProtoBuf_Enum, Copy, Default, Debug, Clone, Eq, PartialEq)] #[repr(u8)] pub enum AuthTypePB { #[default] 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 2118362a8b..b95ac3baaf 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -131,7 +131,8 @@ impl UserManager { .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) .await?; let auth_type = user.workspace_auth_type; - self.cloud_service.set_server_auth_type(&auth_type); + let token = self.token_from_auth_type(&auth_type)?; + self.cloud_service.set_server_auth_type(&auth_type, token)?; event!( tracing::Level::INFO, @@ -326,7 +327,7 @@ impl UserManager { params: SignInParams, auth_type: AuthType, ) -> Result { - self.cloud_service.set_server_auth_type(&auth_type); + self.cloud_service.set_server_auth_type(&auth_type, None)?; let response: AuthResponse = self .cloud_service @@ -374,7 +375,7 @@ impl UserManager { auth_type: AuthType, params: BoxAny, ) -> Result { - self.cloud_service.set_server_auth_type(&auth_type); + self.cloud_service.set_server_auth_type(&auth_type, None)?; // sign out the current user if there is one let migration_user = self.get_migration_user(&auth_type).await; @@ -600,6 +601,16 @@ impl UserManager { self.authenticate_user.user_paths.user_data_dir(uid) } + pub fn token_from_auth_type(&self, auth_type: &AuthType) -> FlowyResult> { + match auth_type { + AuthType::Local => Ok(None), + AuthType::AppFlowyCloud => { + let uid = self.user_id()?; + let mut conn = self.db_connection(uid)?; + Ok(select_user_token(uid, &mut conn).ok()) + }, + } + } pub fn user_setting(&self) -> Result { let session = self.get_session()?; let user_setting = UserSettingPB { @@ -639,7 +650,9 @@ impl UserManager { authenticator: &AuthType, email: &str, ) -> Result { - self.cloud_service.set_server_auth_type(authenticator); + self + .cloud_service + .set_server_auth_type(authenticator, None)?; let auth_service = self.cloud_service.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; @@ -654,7 +667,7 @@ impl UserManager { ) -> Result { self .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud); + .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_password(email, password).await?; Ok(response) @@ -668,7 +681,7 @@ impl UserManager { ) -> Result<(), FlowyError> { self .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud); + .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; let auth_service = self.cloud_service.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) @@ -684,7 +697,7 @@ impl UserManager { ) -> Result { self .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud); + .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; let auth_service = self.cloud_service.get_user_service()?; let response = auth_service.sign_in_with_passcode(email, passcode).await?; Ok(response) @@ -697,7 +710,7 @@ impl UserManager { ) -> Result { self .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud); + .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; let auth_service = self.cloud_service.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) 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 78a13d7229..b78d635133 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 @@ -155,7 +155,8 @@ impl UserManager { pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { info!("open workspace: {}, auth type:{}", workspace_id, auth_type); let workspace_id_str = workspace_id.to_string(); - self.cloud_service.set_server_auth_type(&auth_type); + let token = self.token_from_auth_type(&auth_type)?; + self.cloud_service.set_server_auth_type(&auth_type, token)?; let uid = self.user_id()?; let profile = self @@ -227,7 +228,8 @@ impl UserManager { workspace_name: &str, auth_type: AuthType, ) -> FlowyResult { - self.cloud_service.set_server_auth_type(&auth_type); + let token = self.token_from_auth_type(&auth_type)?; + self.cloud_service.set_server_auth_type(&auth_type, token)?; let new_workspace = self .cloud_service From 14b5e4e184f3277a3f44b309d69a9ff775d1be32 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 22 Apr 2025 09:55:10 +0800 Subject: [PATCH 67/74] feat: add loading indicator in continue to sign in button (#7799) --- ...inue_with_magic_link_or_passcode_page.dart | 74 +++++++++++++++---- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart index ec4fd1bbee..c29a18ea30 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart @@ -35,6 +35,8 @@ class _ContinueWithMagicLinkOrPasscodePageState final inputPasscodeKey = GlobalKey(); + bool isSubmitting = false; + @override void dispose() { passcodeController.dispose(); @@ -54,6 +56,10 @@ class _ContinueWithMagicLinkOrPasscodePageState ); }); } + + if (state.isSubmitting != isSubmitting) { + setState(() => isSubmitting = state.isSubmitting); + } }, child: Scaffold( body: Center( @@ -81,6 +87,15 @@ class _ContinueWithMagicLinkOrPasscodePageState List _buildEnterCodeManually() { // todo: ask designer to provide the spacing final spacing = VSpace(20); + final textStyle = AFButtonSize.l.buildTextStyle(context); + final textHeight = textStyle.height; + final textFontSize = textStyle.fontSize; + + // the indicator height is the height of the text style. + double indicatorHeight = 20; + if (textHeight != null && textFontSize != null) { + indicatorHeight = textHeight * textFontSize; + } if (!isEnteringPasscode) { return [ @@ -116,26 +131,55 @@ class _ContinueWithMagicLinkOrPasscodePageState VSpace(12), // continue to login - AFFilledTextButton.primary( - text: LocaleKeys.signIn_continueToSignIn.tr(), - onTap: () { - final passcode = passcodeController.text; - if (passcode.isEmpty) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), - ); - } else { - widget.onEnterPasscode(passcode); - } - }, - size: AFButtonSize.l, - alignment: Alignment.center, - ), + !isSubmitting + ? _buildContinueButton(textStyle: textStyle) + : _buildIndicator(indicatorHeight: indicatorHeight), spacing, ]; } + Widget _buildContinueButton({ + required TextStyle textStyle, + }) { + return AFFilledTextButton.primary( + text: LocaleKeys.signIn_continueToSignIn.tr(), + onTap: () { + final passcode = passcodeController.text; + if (passcode.isEmpty) { + inputPasscodeKey.currentState?.syncError( + errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), + ); + } else { + widget.onEnterPasscode(passcode); + } + }, + textStyle: textStyle.copyWith( + color: AppFlowyTheme.of(context).textColorScheme.onFill, + ), + size: AFButtonSize.l, + alignment: Alignment.center, + ); + } + + Widget _buildIndicator({ + required double indicatorHeight, + }) { + return AFFilledButton.disabled( + size: AFButtonSize.l, + builder: (context, isHovering, disabled) { + return Align( + child: SizedBox.square( + dimension: indicatorHeight, + child: CircularProgressIndicator( + strokeWidth: 3.0, + ), + ), + ); + }, + ); + } + List _buildBackToLogin() { return [ AFGhostTextButton( From 3ae6888fee481950847eb90876c8966d97794be2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 22 Apr 2025 09:58:13 +0800 Subject: [PATCH 68/74] feat: invite member by link (#7780) * feat: invite member by link * feat: add invite by link section * feat: integrate invite by link and copy invite link * feat: integrate invite link apis * feat: add reset link dialog * feat: support redirect to admin panel * fix: flutter analyze * feat: remove expire time * fix: apply correct color in dark mode * fix: flutter analyze * chore: disable theme hotkey test --- .../desktop/uncategorized/hotkeys_test.dart | 16 +- .../workspace/invite_members_screen.dart | 8 +- .../setting/workspace/member_list.dart | 2 +- .../lib/shared/af_user_profile_extension.dart | 16 + .../lib/startup/tasks/app_widget.dart | 5 +- .../menu/sidebar/space/shared_widget.dart | 104 ++-- .../settings/shared/settings_body.dart | 12 +- .../shared/settings_category_spacer.dart | 4 +- .../settings/shared/settings_header.dart | 24 +- .../inivitation/inivite_member_by_link.dart | 154 +++++ .../inivitation/invite_member_by_email.dart | 79 +++ .../inivitation/member_http_service.dart | 181 ++++++ .../members/workspace_member_bloc.dart | 430 ++++++++----- .../members/workspace_member_page.dart | 576 +++++++----------- .../presentation/widgets/dialogs.dart | 2 + frontend/appflowy_flutter/macos/Podfile.lock | 46 +- .../lib/src/theme/appflowy_theme.dart | 2 +- .../theme/data/appflowy_default/semantic.dart | 2 + .../color_scheme/border_color_scheme.dart | 5 +- .../test/widget_test/confirm_dialog_test.dart | 20 +- frontend/resources/translations/en.json | 21 +- 21 files changed, 1095 insertions(+), 614 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart index 4a38dde920..1d0f13eebc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart @@ -1,13 +1,12 @@ import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -38,7 +37,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_light.tr(), ), ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(milliseconds: 250)); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.light); @@ -48,7 +47,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_dark.tr(), ), ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(milliseconds: 250)); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.dark); @@ -66,10 +65,11 @@ void main() { ], tester: tester, ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); - themeMode = tester.widget(appFinder).themeMode; - expect(themeMode, ThemeMode.light); + // disable it temporarily. It works on macOS but not on Linux. + // themeMode = tester.widget(appFinder).themeMode; + // expect(themeMode, ThemeMode.light); }); testWidgets('show or hide home menu', (tester) async { 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 62aa114ef3..18bce0588b 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 @@ -197,7 +197,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.add) { + if (actionType == WorkspaceMemberActionType.addByEmail) { result.fold( (s) { showToastNotification( @@ -223,7 +223,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { ); }, ); - } else if (actionType == WorkspaceMemberActionType.invite) { + } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { result.fold( (s) { showToastNotification( @@ -250,7 +250,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { ); }, ); - } else if (actionType == WorkspaceMemberActionType.remove) { + } else if (actionType == WorkspaceMemberActionType.removeByEmail) { result.fold( (s) { showToastNotification( @@ -284,7 +284,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { } context .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); + .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); // clear the email field after inviting emailController.clear(); } 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 index 501fd18ef7..b2805d5857 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart @@ -178,7 +178,7 @@ class _MemberItem extends StatelessWidget { showBottomBorder: false, onTap: () { workspaceMemberBloc.add( - WorkspaceMemberEvent.removeWorkspaceMember( + WorkspaceMemberEvent.removeWorkspaceMemberByEmail( member.email, ), ); diff --git a/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart b/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart new file mode 100644 index 0000000000..2632c22d49 --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; + +extension UserProfilePBExtension on UserProfilePB { + String? get authToken { + try { + final map = jsonDecode(token) as Map; + return map['access_token'] as String?; + } catch (e) { + Log.error('Failed to decode auth token: $e'); + return null; + } + } +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 98b76802d4..0ed66389d9 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -138,6 +138,8 @@ class _ApplicationWidgetState extends State { final _commandPaletteNotifier = ValueNotifier(false); + final themeBuilder = AppFlowyDefaultTheme(); + @override void initState() { super.initState(); @@ -235,10 +237,9 @@ class _ApplicationWidgetState extends State { locale: state.locale, routerConfig: routerConfig, builder: (context, child) { - final themeBuilder = AppFlowyDefaultTheme(); final brightness = Theme.of(context).brightness; - return AnimatedAppFlowyTheme( + return AppFlowyTheme( data: brightness == Brightness.light ? themeBuilder.light() : themeBuilder.dark(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index d06016dfb8..95130b029e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -13,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -173,42 +174,53 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { required this.onConfirm, required this.confirmButtonName, this.confirmButtonColor, + this.confirmButtonBuilder, }); final VoidCallback onCancel; final VoidCallback onConfirm; final String confirmButtonName; final Color? confirmButtonColor; - + final WidgetBuilder? confirmButtonBuilder; @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - OutlinedRoundedButton( + AFOutlinedTextButton.normal( text: LocaleKeys.button_cancel.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), onTap: onCancel, ), const HSpace(12.0), - DecoratedBox( - decoration: ShapeDecoration( - color: confirmButtonColor ?? Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + if (confirmButtonBuilder != null) ...[ + confirmButtonBuilder!(context), + ] else ...[ + DecoratedBox( + decoration: ShapeDecoration( + color: + confirmButtonColor ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + radius: BorderRadius.circular(8), + text: FlowyText.regular( + confirmButtonName, + lineHeight: 1.0, + color: Theme.of(context).colorScheme.onPrimary, + ), + onTap: onConfirm, ), ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), - radius: BorderRadius.circular(8), - text: FlowyText.regular( - confirmButtonName, - lineHeight: 1.0, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: onConfirm, - ), - ), + ], ], ); } @@ -249,17 +261,11 @@ enum ConfirmPopupStyle { class ConfirmPopupColor { static Color titleColor(BuildContext context) { - if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withValues(alpha: 0.8); - } - return const Color(0xFFffffff).withValues(alpha: 0.8); + return AppFlowyTheme.of(context).textColorScheme.primary; } static Color descriptionColor(BuildContext context) { - if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withValues(alpha: 0.7); - } - return const Color(0xFFffffff).withValues(alpha: 0.7); + return AppFlowyTheme.of(context).textColorScheme.primary; } } @@ -273,6 +279,7 @@ class ConfirmPopup extends StatefulWidget { this.onCancel, this.confirmLabel, this.confirmButtonColor, + this.confirmButtonBuilder, this.child, this.closeOnAction = true, this.showCloseButton = true, @@ -315,6 +322,10 @@ class ConfirmPopup extends StatefulWidget { /// final bool enableKeyboardListener; + /// Allows to build a custom confirm button. + /// + final WidgetBuilder? confirmButtonBuilder; + @override State createState() => _ConfirmPopupState(); } @@ -368,28 +379,28 @@ class _ConfirmPopupState extends State { } Widget _buildTitle() { + final theme = AppFlowyTheme.of(context); return Row( children: [ Expanded( - child: FlowyText( + child: Text( widget.title, - fontSize: 16.0, - figmaLineHeight: 22.0, - fontWeight: FontWeight.w500, + style: theme.textStyle.heading4.prominent( + color: ConfirmPopupColor.titleColor(context), + ), overflow: TextOverflow.ellipsis, - color: ConfirmPopupColor.titleColor(context), ), ), const HSpace(6.0), if (widget.showCloseButton) ...[ - FlowyButton( - margin: const EdgeInsets.all(3), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.upgrade_close_s, - size: Size.square(18.0), - ), + AFGhostButton.normal( + size: AFButtonSize.s, + padding: EdgeInsets.all(theme.spacing.xs), onTap: () => Navigator.of(context).pop(), + builder: (context, isHovering, disabled) => FlowySvg( + FlowySvgs.password_close_m, + size: const Size.square(20), + ), ), ], ], @@ -401,18 +412,24 @@ class _ConfirmPopupState extends State { return const SizedBox.shrink(); } - return FlowyText.regular( + final theme = AppFlowyTheme.of(context); + + return Text( widget.description, - fontSize: 16.0, - color: ConfirmPopupColor.descriptionColor(context), + style: theme.textStyle.body.standard( + color: ConfirmPopupColor.descriptionColor(context), + ), maxLines: 5, - figmaLineHeight: 22.0, ); } Widget _buildStyledButton(BuildContext context) { switch (widget.style) { case ConfirmPopupStyle.onlyOk: + if (widget.confirmButtonBuilder != null) { + return widget.confirmButtonBuilder!(context); + } + return SpaceOkButton( onConfirm: () { widget.onConfirm(); @@ -440,6 +457,7 @@ class _ConfirmPopupState extends State { widget.confirmLabel ?? LocaleKeys.space_delete.tr(), confirmButtonColor: widget.confirmButtonColor ?? Theme.of(context).colorScheme.error, + confirmButtonBuilder: widget.confirmButtonBuilder, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart index 8091a72684..5114218041 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -1,20 +1,21 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class SettingsBody extends StatelessWidget { const SettingsBody({ super.key, required this.title, this.description, + this.descriptionBuilder, this.autoSeparate = true, required this.children, }); final String title; final String? description; + final WidgetBuilder? descriptionBuilder; final bool autoSeparate; final List children; @@ -27,7 +28,12 @@ class SettingsBody extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsHeader(title: title, description: description), + SettingsHeader( + title: title, + description: description, + descriptionBuilder: descriptionBuilder, + ), + SettingsCategorySpacer(), Flexible( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index deec09c1d8..1ef7f13d0c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -11,8 +11,8 @@ class SettingsCategorySpacer extends StatelessWidget { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); return Divider( - height: 32, - color: theme.borderColorScheme.greyPrimary, + height: theme.spacing.xl * 2.0, + color: theme.borderColorScheme.primary, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart index 7409070ba9..332b25e686 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -1,15 +1,20 @@ import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; /// Renders a simple header for the settings view /// class SettingsHeader extends StatelessWidget { - const SettingsHeader({super.key, required this.title, this.description}); + const SettingsHeader({ + super.key, + required this.title, + this.description, + this.descriptionBuilder, + }); final String title; final String? description; + final WidgetBuilder? descriptionBuilder; @override Widget build(BuildContext context) { @@ -23,16 +28,19 @@ class SettingsHeader extends StatelessWidget { color: theme.textColorScheme.primary, ), ), - if (description?.isNotEmpty == true) ...[ - const VSpace(8), - FlowyText( + if (descriptionBuilder != null) ...[ + VSpace(theme.spacing.xs), + descriptionBuilder!(context), + ] else if (description?.isNotEmpty == true) ...[ + VSpace(theme.spacing.xs), + Text( description!, maxLines: 4, - fontSize: 12, - color: AFThemeExtension.of(context).secondaryTextColor, + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, + ), ), ], - const VSpace(16), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart new file mode 100644 index 0000000000..6f143a83c1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart @@ -0,0 +1,154 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class InviteMemberByLink extends StatelessWidget { + const InviteMemberByLink({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Title(), + _Description(), + ], + ), + Spacer(), + _CopyLinkButton(), + ], + ); + } +} + +class _Title extends StatelessWidget { + const _Title(); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Text( + LocaleKeys.settings_appearance_members_inviteLinkToAddMember.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ); + } +} + +class _Description extends StatelessWidget { + const _Description(); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_appearance_members_clickToCopyLink.tr(), + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.primary, + ), + ), + TextSpan( + text: ' ${LocaleKeys.settings_appearance_members_or.tr()} ', + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.primary, + ), + ), + TextSpan( + text: LocaleKeys.settings_appearance_members_generateANewLink.tr(), + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.action, + ), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer() + ..onTap = () => _onGenerateInviteLink(context), + ), + ], + ), + ); + } + + Future _onGenerateInviteLink(BuildContext context) async { + final inviteLink = context.read().state.inviteLink; + if (inviteLink != null) { + // show a dialog to confirm if the user wants to copy the link to the clipboard + await showConfirmDialog( + context: context, + style: ConfirmPopupStyle.cancelAndOk, + title: 'Reset the invite link?', + description: + 'Resetting will deactivate the current link for all space members and generate a new one. The old link will no longer be available.', + confirmLabel: 'Reset', + onConfirm: () { + context.read().add( + const WorkspaceMemberEvent.generateInviteLink(), + ); + }, + confirmButtonBuilder: (_) => AFFilledTextButton.destructive( + text: 'Reset', + onTap: () { + context.read().add( + const WorkspaceMemberEvent.generateInviteLink(), + ); + + Navigator.of(context).pop(); + }, + ), + ); + } else { + context.read().add( + const WorkspaceMemberEvent.generateInviteLink(), + ); + } + } +} + +class _CopyLinkButton extends StatelessWidget { + const _CopyLinkButton(); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return AFOutlinedTextButton.normal( + text: LocaleKeys.button_copyLink.tr(), + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + padding: EdgeInsets.symmetric( + horizontal: theme.spacing.l, + vertical: theme.spacing.s, + ), + onTap: () { + final link = context.read().state.inviteLink; + if (link != null) { + getIt().setData( + ClipboardServiceData( + plainText: link, + ), + ); + + showToastNotification( + message: LocaleKeys.document_inlineLink_copyLink.tr(), + ); + } else { + showToastNotification( + message: LocaleKeys.shareAction_copyLinkFailed.tr(), + ); + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart new file mode 100644 index 0000000000..9f8ce45a97 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart @@ -0,0 +1,79 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_ui/appflowy_ui.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'; + +class InviteMemberByEmail extends StatefulWidget { + const InviteMemberByEmail({super.key}); + + @override + State createState() => _InviteMemberByEmailState(); +} + +class _InviteMemberByEmailState extends State { + final _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.settings_appearance_members_inviteMemberByEmail.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + VSpace(theme.spacing.m), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: AFTextField( + controller: _emailController, + hintText: + LocaleKeys.settings_appearance_members_inviteHint.tr(), + onSubmitted: (value) => _inviteMember(), + ), + ), + HSpace(theme.spacing.l), + AFFilledTextButton.primary( + text: LocaleKeys.settings_appearance_members_sendInvite.tr(), + onTap: _inviteMember, + ), + ], + ), + ], + ); + } + + void _inviteMember() { + final email = _emailController.text; + if (!isEmail(email)) { + showToastNotification( + type: ToastificationType.error, + message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), + ); + return; + } + + context + .read() + .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); + // clear the email field after inviting + _emailController.clear(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart new file mode 100644 index 0000000000..c8c7e47706 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:http/http.dart' as http; + +enum InviteCodeEndpoint { + getInviteCode, + deleteInviteCode, + generateInviteCode; + + String get path { + switch (this) { + case InviteCodeEndpoint.getInviteCode: + case InviteCodeEndpoint.deleteInviteCode: + case InviteCodeEndpoint.generateInviteCode: + return '/api/workspace/{workspaceId}/invite-code'; + } + } + + String get method { + switch (this) { + case InviteCodeEndpoint.getInviteCode: + return 'GET'; + case InviteCodeEndpoint.deleteInviteCode: + return 'DELETE'; + case InviteCodeEndpoint.generateInviteCode: + return 'POST'; + } + } + + Uri uri(String baseUrl, String workspaceId) => + Uri.parse(path.replaceAll('{workspaceId}', workspaceId)).replace( + scheme: Uri.parse(baseUrl).scheme, + host: Uri.parse(baseUrl).host, + port: Uri.parse(baseUrl).port, + ); +} + +class MemberHttpService { + MemberHttpService({ + required this.baseUrl, + required this.authToken, + }); + + final String baseUrl; + final String authToken; + + final http.Client client = http.Client(); + + Map get headers => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $authToken', + }; + + /// Gets the invite code for a workspace + Future> getInviteCode({ + required String workspaceId, + }) async { + final result = await _makeRequest( + endpoint: InviteCodeEndpoint.getInviteCode, + workspaceId: workspaceId, + errorMessage: 'Failed to get invite code', + ); + + return result.fold( + (data) => FlowyResult.success(data['code'] as String), + (error) => FlowyResult.failure(error), + ); + } + + /// Deletes the invite code for a workspace + Future> deleteInviteCode({ + required String workspaceId, + }) async { + final result = await _makeRequest( + endpoint: InviteCodeEndpoint.deleteInviteCode, + workspaceId: workspaceId, + errorMessage: 'Failed to delete invite code', + ); + + return result.fold( + (data) => FlowyResult.success(true), + (error) => FlowyResult.failure(error), + ); + } + + /// Generates a new invite code for a workspace + /// + /// [workspaceId] - The ID of the workspace + Future> generateInviteCode({ + required String workspaceId, + int? validityPeriodHours, + }) async { + final result = await _makeRequest( + endpoint: InviteCodeEndpoint.generateInviteCode, + workspaceId: workspaceId, + errorMessage: 'Failed to generate invite code', + body: { + 'validity_period_hours': validityPeriodHours, + }, + ); + + try { + return result.fold( + (data) => FlowyResult.success(data['data']['code'].toString()), + (error) => FlowyResult.failure(error), + ); + } catch (e) { + return FlowyResult.failure( + FlowyError(msg: 'Failed to generate invite code: $e'), + ); + } + } + + /// Makes a request to the specified endpoint + Future> _makeRequest({ + required InviteCodeEndpoint endpoint, + required String workspaceId, + Map? body, + String errorMessage = 'Request failed', + }) async { + try { + final uri = endpoint.uri(baseUrl, workspaceId); + http.Response response; + + switch (endpoint.method) { + case 'GET': + response = await client.get( + uri, + headers: headers, + ); + break; + case 'DELETE': + response = await client.delete( + uri, + headers: headers, + ); + break; + case 'POST': + response = await client.post( + uri, + headers: headers, + body: body != null ? jsonEncode(body) : null, + ); + break; + default: + return FlowyResult.failure( + FlowyError(msg: 'Invalid request method: ${endpoint.method}'), + ); + } + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + return FlowyResult.success(jsonDecode(response.body)); + } + return FlowyResult.success(true); + } else { + final errorBody = + response.body.isNotEmpty ? jsonDecode(response.body) : {}; + + Log.info( + '${endpoint.name} request failed: ${response.statusCode}, $errorBody', + ); + + return FlowyResult.failure( + FlowyError( + msg: errorBody['msg'] ?? errorMessage, + ), + ); + } + } catch (e) { + Log.error('${endpoint.name} request failed: error: $e'); + + return FlowyResult.failure( + FlowyError(msg: 'Network error: ${e.toString()}'), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index c9fcb34204..5f98146118 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/shared/af_user_profile_extension.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/member_http_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'; @@ -34,163 +37,260 @@ class WorkspaceMemberBloc super(WorkspaceMemberState.initial()) { on((event, emit) async { await event.when( - initial: () async { - await _setCurrentWorkspaceId(workspaceId); - - final result = await _userBackendService.getWorkspaceMembers( - _workspaceId, - ); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - - if (myRole.isOwner) { - unawaited(_fetchWorkspaceSubscriptionInfo()); - } - emit( - state.copyWith( - members: members, - myRole: myRole, - isLoading: false, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - }, - getWorkspaceMembers: () async { - final result = await _userBackendService.getWorkspaceMembers( - _workspaceId, - ); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - emit( - state.copyWith( - members: members, - myRole: myRole, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - }, - addWorkspaceMember: (email) async { - final result = await _userBackendService.addWorkspaceMember( - _workspaceId, - email, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.add, - result: result, - ), - ), - ); - // the addWorkspaceMember doesn't return the updated members, - // so we need to get the members again - result.onSuccess((s) { - add(const WorkspaceMemberEvent.getWorkspaceMembers()); - }); - }, - inviteWorkspaceMember: (email) async { - final result = await _userBackendService.inviteWorkspaceMember( - _workspaceId, - email, - role: AFRolePB.Member, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.invite, - result: result, - ), - ), - ); - }, - removeWorkspaceMember: (email) async { - final result = await _userBackendService.removeWorkspaceMember( - _workspaceId, - email, - ); - final members = result.fold( - (s) => state.members.where((e) => e.email != email).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.remove, - result: result, - ), - ), - ); - }, - updateWorkspaceMember: (email, role) async { - final result = await _userBackendService.updateWorkspaceMember( - _workspaceId, - email, - role, - ); - final members = result.fold( - (s) => state.members.map((e) { - if (e.email == email) { - e.freeze(); - return e.rebuild((p0) => p0.role = role); - } - return e; - }).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.updateRole, - result: result, - ), - ), - ); - }, + initial: () async => _onInitial(emit, workspaceId), + getWorkspaceMembers: () async => _onGetWorkspaceMembers(emit), + addWorkspaceMember: (email) async => _onAddWorkspaceMember(emit, email), + inviteWorkspaceMemberByEmail: (email) async => + _onInviteWorkspaceMemberByEmail(emit, email), + removeWorkspaceMemberByEmail: (email) async => + _onRemoveWorkspaceMemberByEmail(emit, email), + inviteWorkspaceMemberByLink: (link) async => + _onInviteWorkspaceMemberByLink(emit, link), + generateInviteLink: () async => _onGenerateInviteLink(emit), + updateWorkspaceMember: (email, role) async => + _onUpdateWorkspaceMember(emit, email, role), updateSubscriptionInfo: (info) async => - emit(state.copyWith(subscriptionInfo: info)), - upgradePlan: () async { - final plan = state.subscriptionInfo?.plan; - if (plan == null) { - return Log.error('Failed to upgrade plan: plan is null'); - } - - if (plan == WorkspacePlanPB.FreePlan) { - final checkoutLink = await _userBackendService.createSubscription( - _workspaceId, - SubscriptionPlanPB.Pro, - ); - - checkoutLink.fold( - (pl) => afLaunchUrlString(pl.paymentLink), - (f) => Log.error('Failed to create subscription: ${f.msg}', f), - ); - } - }, + _onUpdateSubscriptionInfo(emit, info), + upgradePlan: () async => _onUpgradePlan(), ); }); } final UserProfilePB userProfile; - - // if the workspace is null, use the current workspace final UserWorkspacePB? workspace; - late final String _workspaceId; final UserBackendService _userBackendService; + MemberHttpService? _memberHttpService; + + Future _onInitial( + Emitter emit, + String? workspaceId, + ) async { + await _setCurrentWorkspaceId(workspaceId); + + final result = await _userBackendService.getWorkspaceMembers(_workspaceId); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + + if (myRole.isOwner) { + unawaited(_fetchWorkspaceSubscriptionInfo()); + } + + final baseUrl = await getAppFlowyCloudUrl(); + final authToken = userProfile.authToken; + if (authToken != null) { + _memberHttpService = MemberHttpService( + baseUrl: baseUrl, + authToken: authToken, + ); + unawaited( + _memberHttpService?.getInviteCode(workspaceId: _workspaceId).fold( + (s) async { + final inviteLink = await _buildInviteLink(inviteCode: s); + emit(state.copyWith(inviteLink: inviteLink)); + }, + (e) => Log.error('Failed to get invite code: ${e.msg}', e), + ), + ); + } else { + Log.error('Failed to get auth token'); + } + + emit( + state.copyWith( + members: members, + myRole: myRole, + isLoading: false, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + } + + Future _onGetWorkspaceMembers( + Emitter emit, + ) async { + final result = await _userBackendService.getWorkspaceMembers(_workspaceId); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + } + + Future _onAddWorkspaceMember( + Emitter emit, + String email, + ) async { + final result = await _userBackendService.addWorkspaceMember( + _workspaceId, + email, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.addByEmail, + result: result, + ), + ), + ); + // the addWorkspaceMember doesn't return the updated members, + // so we need to get the members again + result.onSuccess((s) { + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }); + } + + Future _onInviteWorkspaceMemberByEmail( + Emitter emit, + String email, + ) async { + final result = await _userBackendService.inviteWorkspaceMember( + _workspaceId, + email, + role: AFRolePB.Member, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.inviteByEmail, + result: result, + ), + ), + ); + } + + Future _onRemoveWorkspaceMemberByEmail( + Emitter emit, + String email, + ) async { + final result = await _userBackendService.removeWorkspaceMember( + _workspaceId, + email, + ); + final members = result.fold( + (s) => state.members.where((e) => e.email != email).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.removeByEmail, + result: result, + ), + ), + ); + } + + Future _onInviteWorkspaceMemberByLink( + Emitter emit, + String link, + ) async {} + + Future _onGenerateInviteLink(Emitter emit) async { + final result = await _memberHttpService?.generateInviteCode( + workspaceId: _workspaceId, + ); + + await result?.fold( + (s) async { + final inviteLink = await _buildInviteLink(inviteCode: s); + emit( + state.copyWith( + inviteLink: inviteLink, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.generateInviteLink, + result: result, + ), + ), + ); + }, + (e) async { + Log.error('Failed to generate invite link: ${e.msg}', e); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.generateInviteLink, + result: result, + ), + ), + ); + }, + ); + } + + Future _onUpdateWorkspaceMember( + Emitter emit, + String email, + AFRolePB role, + ) async { + final result = await _userBackendService.updateWorkspaceMember( + _workspaceId, + email, + role, + ); + final members = result.fold( + (s) => state.members.map((e) { + if (e.email == email) { + e.freeze(); + return e.rebuild((p0) => p0.role = role); + } + return e; + }).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.updateRole, + result: result, + ), + ), + ); + } + + Future _onUpdateSubscriptionInfo( + Emitter emit, + WorkspaceSubscriptionInfoPB info, + ) async { + emit(state.copyWith(subscriptionInfo: info)); + } + + Future _onUpgradePlan() async { + final plan = state.subscriptionInfo?.plan; + if (plan == null) { + return Log.error('Failed to upgrade plan: plan is null'); + } + + if (plan == WorkspacePlanPB.FreePlan) { + final checkoutLink = await _userBackendService.createSubscription( + _workspaceId, + SubscriptionPlanPB.Pro, + ); + + checkoutLink.fold( + (pl) => afLaunchUrlString(pl.paymentLink), + (f) => Log.error('Failed to create subscription: ${f.msg}', f), + ); + } + } AFRolePB _getMyRole(List members) { final role = members @@ -222,8 +322,6 @@ class WorkspaceMemberBloc } } - // We fetch workspace subscription info lazily as it's not needed in the first - // render of the page. Future _fetchWorkspaceSubscriptionInfo() async { final result = await UserBackendService.getWorkspaceSubscriptionInfo(_workspaceId); @@ -237,6 +335,15 @@ class WorkspaceMemberBloc (f) => Log.error('Failed to fetch subscription info: ${f.msg}', f), ); } + + Future _buildInviteLink({required String inviteCode}) async { + final baseUrl = await getAppFlowyShareDomain(); + final authToken = userProfile.authToken; + if (authToken != null) { + return '$baseUrl/app/invited/$inviteCode'; + } + return ''; + } } @freezed @@ -246,10 +353,15 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent { GetWorkspaceMembers; const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = AddWorkspaceMember; - const factory WorkspaceMemberEvent.inviteWorkspaceMember(String email) = - InviteWorkspaceMember; - const factory WorkspaceMemberEvent.removeWorkspaceMember(String email) = - RemoveWorkspaceMember; + const factory WorkspaceMemberEvent.inviteWorkspaceMemberByEmail( + String email, + ) = InviteWorkspaceMemberByEmail; + const factory WorkspaceMemberEvent.removeWorkspaceMemberByEmail( + String email, + ) = RemoveWorkspaceMemberByEmail; + const factory WorkspaceMemberEvent.inviteWorkspaceMemberByLink(String link) = + InviteWorkspaceMemberByLink; + const factory WorkspaceMemberEvent.generateInviteLink() = GenerateInviteLink; const factory WorkspaceMemberEvent.updateWorkspaceMember( String email, AFRolePB role, @@ -265,10 +377,12 @@ enum WorkspaceMemberActionType { none, get, // this event will send an invitation to the member - invite, + inviteByEmail, + inviteByLink, + generateInviteLink, // this event will add the member without sending an invitation - add, - remove, + addByEmail, + removeByEmail, updateRole, } @@ -292,6 +406,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState { @Default(null) WorkspaceMemberActionResult? actionResult, @Default(true) bool isLoading, @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo, + @Default(null) String? inviteLink, }) = _WorkspaceMemberState; factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); @@ -307,6 +422,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState { other.members == members && other.myRole == myRole && other.subscriptionInfo == subscriptionInfo && + other.inviteLink == inviteLink && identical(other.actionResult, actionResult); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index bf33ab9d72..3ead104ee3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,22 +1,23 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_ui/appflowy_ui.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/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; class WorkspaceMembersPage extends StatelessWidget { const WorkspaceMembersPage({ @@ -38,14 +39,14 @@ class WorkspaceMembersPage extends StatelessWidget { builder: (context, state) { return SettingsBody( title: LocaleKeys.settings_appearance_members_title.tr(), + // Enable it when the backend support admin panel + // descriptionBuilder: _buildDescription, autoSeparate: false, children: [ - if (state.actionResult != null) ...[ - _showMemberLimitWarning(context, state), - const VSpace(16), - ], if (state.myRole.canInvite) ...[ - const _InviteMember(), + const InviteMemberByLink(), + const SettingsCategorySpacer(), + const InviteMemberByEmail(), const SettingsCategorySpacer(), ], if (state.members.isNotEmpty) @@ -61,104 +62,141 @@ class WorkspaceMembersPage extends StatelessWidget { ); } - Widget _showMemberLimitWarning( - BuildContext context, - WorkspaceMemberState state, - ) { - // We promise that state.actionResult != null before calling - // this method - final actionResult = state.actionResult!.result; - final actionType = state.actionResult!.actionType; + // Enable it when the backend support admin panel + // Widget _buildDescription(BuildContext context) { + // final theme = AppFlowyTheme.of(context); + // return Text.rich( + // TextSpan( + // children: [ + // TextSpan( + // text: + // '${LocaleKeys.settings_appearance_members_memberPageDescription1.tr()} ', + // style: theme.textStyle.caption.standard( + // color: theme.textColorScheme.secondary, + // ), + // ), + // TextSpan( + // text: LocaleKeys.settings_appearance_members_adminPanel.tr(), + // style: theme.textStyle.caption.underline( + // color: theme.textColorScheme.secondary, + // ), + // mouseCursor: SystemMouseCursors.click, + // recognizer: TapGestureRecognizer() + // ..onTap = () async { + // final baseUrl = await getAppFlowyCloudUrl(); + // await afLaunchUrlString(baseUrl); + // }, + // ), + // TextSpan( + // text: + // ' ${LocaleKeys.settings_appearance_members_memberPageDescription2.tr()} ', + // style: theme.textStyle.caption.standard( + // color: theme.textColorScheme.secondary, + // ), + // ), + // ], + // ), + // ); + // } - if (actionType == WorkspaceMemberActionType.invite && - actionResult.isFailure) { - final error = actionResult.getFailure().code; - if (error == ErrorCode.WorkspaceMemberLimitExceeded) { - return Row( - children: [ - const FlowySvg( - FlowySvgs.warning_s, - blendMode: BlendMode.dst, - size: Size.square(20), - ), - const HSpace(12), - Expanded( - child: RichText( - text: TextSpan( - children: [ - if (state.subscriptionInfo?.plan == - WorkspacePlanPB.ProPlan) ...[ - TextSpan( - text: LocaleKeys - .settings_appearance_members_memberLimitExceededPro - .tr(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: AFThemeExtension.of(context).strongText, - ), - ), - WidgetSpan( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - // Hardcoded support email, in the future we might - // want to add this to an environment variable - onTap: () async => afLaunchUrlString( - 'mailto:support@appflowy.io', - ), - child: FlowyText( - LocaleKeys - .settings_appearance_members_memberLimitExceededProContact - .tr(), - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ] else ...[ - TextSpan( - text: LocaleKeys - .settings_appearance_members_memberLimitExceeded - .tr(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: AFThemeExtension.of(context).strongText, - ), - ), - WidgetSpan( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => context - .read() - .add(const WorkspaceMemberEvent.upgradePlan()), - child: FlowyText( - LocaleKeys - .settings_appearance_members_memberLimitExceededUpgrade - .tr(), - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ], - ], - ), - ), - ), - ], - ); - } - } + // Widget _showMemberLimitWarning( + // BuildContext context, + // WorkspaceMemberState state, + // ) { + // // We promise that state.actionResult != null before calling + // // this method + // final actionResult = state.actionResult!.result; + // final actionType = state.actionResult!.actionType; - return const SizedBox.shrink(); - } + // if (actionType == WorkspaceMemberActionType.inviteByEmail && + // actionResult.isFailure) { + // final error = actionResult.getFailure().code; + // if (error == ErrorCode.WorkspaceMemberLimitExceeded) { + // return Row( + // children: [ + // const FlowySvg( + // FlowySvgs.warning_s, + // blendMode: BlendMode.dst, + // size: Size.square(20), + // ), + // const HSpace(12), + // Expanded( + // child: RichText( + // text: TextSpan( + // children: [ + // if (state.subscriptionInfo?.plan == + // WorkspacePlanPB.ProPlan) ...[ + // TextSpan( + // text: LocaleKeys + // .settings_appearance_members_memberLimitExceededPro + // .tr(), + // style: TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w400, + // color: AFThemeExtension.of(context).strongText, + // ), + // ), + // WidgetSpan( + // child: MouseRegion( + // cursor: SystemMouseCursors.click, + // child: GestureDetector( + // // Hardcoded support email, in the future we might + // // want to add this to an environment variable + // onTap: () async => afLaunchUrlString( + // 'mailto:support@appflowy.io', + // ), + // child: FlowyText( + // LocaleKeys + // .settings_appearance_members_memberLimitExceededProContact + // .tr(), + // fontSize: 14, + // fontWeight: FontWeight.w400, + // color: Theme.of(context).colorScheme.primary, + // ), + // ), + // ), + // ), + // ] else ...[ + // TextSpan( + // text: LocaleKeys + // .settings_appearance_members_memberLimitExceeded + // .tr(), + // style: TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w400, + // color: AFThemeExtension.of(context).strongText, + // ), + // ), + // WidgetSpan( + // child: MouseRegion( + // cursor: SystemMouseCursors.click, + // child: GestureDetector( + // onTap: () => context + // .read() + // .add(const WorkspaceMemberEvent.upgradePlan()), + // child: FlowyText( + // LocaleKeys + // .settings_appearance_members_memberLimitExceededUpgrade + // .tr(), + // fontSize: 14, + // fontWeight: FontWeight.w400, + // color: Theme.of(context).colorScheme.primary, + // ), + // ), + // ), + // ), + // ], + // ], + // ), + // ), + // ), + // ], + // ); + // } + // } + + // return const SizedBox.shrink(); + // } void _showResultDialog(BuildContext context, WorkspaceMemberState state) { final actionResult = state.actionResult; @@ -170,12 +208,12 @@ class WorkspaceMembersPage extends StatelessWidget { final result = actionResult.result; // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.add) { + if (actionType == WorkspaceMemberActionType.addByEmail) { result.fold( (s) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + showToastNotification( + message: + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), ); }, (f) { @@ -189,12 +227,12 @@ class WorkspaceMembersPage extends StatelessWidget { ); }, ); - } else if (actionType == WorkspaceMemberActionType.invite) { + } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { result.fold( (s) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), + showToastNotification( + message: + LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), ); }, (f) { @@ -214,116 +252,27 @@ class WorkspaceMembersPage extends StatelessWidget { ); }, ); - } - } -} + } else if (actionType == WorkspaceMemberActionType.generateInviteLink) { + result.fold( + (s) { + showToastNotification( + message: 'Invite link generated successfully', + ); -class _InviteMember extends StatefulWidget { - const _InviteMember(); - - @override - State<_InviteMember> createState() => _InviteMemberState(); -} - -class _InviteMemberState extends State<_InviteMember> { - final _emailController = TextEditingController(); - - @override - void dispose() { - _emailController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.settings_appearance_members_inviteMembers.tr(), - fontSize: 16.0, - ), - const VSpace(8.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 48.0, - ), - child: FlowyTextField( - hintText: - LocaleKeys.settings_appearance_members_inviteHint.tr(), - controller: _emailController, - onEditingComplete: _inviteMember, - ), - ), - ), - const HSpace(10.0), - SizedBox( - height: 48.0, - child: IntrinsicWidth( - child: PrimaryRoundedButton( - text: LocaleKeys.settings_appearance_members_sendInvite.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - onTap: _inviteMember, - ), - ), - ), - ], - ), - /* Enable this when the feature is ready - PrimaryButton( - backgroundColor: const Color(0xFFE0E0E0), - child: Padding( - padding: const EdgeInsets.only( - left: 20, - right: 24, - top: 8, - bottom: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.invite_member_link_m, - color: Colors.black, - ), - const HSpace(8.0), - FlowyText( - LocaleKeys.settings_appearance_members_copyInviteLink.tr(), - color: Colors.black, - ), - ], - ), - ), - onPressed: () { - showSnackBarMessage(context, 'not implemented'); - }, - ), - const VSpace(16.0), - */ - ], - ); - } - - void _inviteMember() { - final email = _emailController.text; - if (!isEmail(email)) { - return showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_emailInvalidError.tr(), + // copy the invite link to the clipboard + final inviteLink = state.inviteLink; + if (inviteLink != null) { + getIt().setPlainText(inviteLink); + } + }, + (f) { + Log.error('generate invite link failed: $f'); + showToastNotification( + message: 'Failed to generate invite link', + ); + }, ); } - context - .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); - // clear the email field after inviting - _emailController.clear(); } } @@ -340,9 +289,12 @@ class _MemberList extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); return SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const Divider(), + separatorBuilder: () => Divider( + color: theme.borderColorScheme.primary, + ), children: [ const _MemberListHeader(), ...members.map( @@ -362,31 +314,34 @@ class _MemberListHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + final theme = AppFlowyTheme.of(context); + return Row( children: [ - FlowyText.semibold( - LocaleKeys.settings_appearance_members_label.tr(), - fontSize: 16.0, - ), - const VSpace(16.0), - Row( - children: [ - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_user.tr(), - fontSize: 14.0, - ), + Expanded( + child: Text( + LocaleKeys.settings_appearance_members_user.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, ), - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_role.tr(), - fontSize: 14.0, - ), - ), - const HSpace(28.0), - ], + ), ), + Expanded( + child: Text( + LocaleKeys.settings_appearance_members_role.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + ), + Expanded( + child: Text( + LocaleKeys.settings_accountPage_email_title.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + ), + const HSpace(28.0), ], ); } @@ -405,27 +360,42 @@ class _MemberItem extends StatelessWidget { @override Widget build(BuildContext context) { - final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; + final theme = AppFlowyTheme.of(context); return Row( children: [ Expanded( - child: FlowyText.medium( + child: Text( member.name, - color: textColor, - fontSize: 14.0, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), ), ), Expanded( child: member.role.isOwner || !myRole.canUpdate - ? FlowyText.medium( + ? Text( member.role.description, - color: textColor, - fontSize: 14.0, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), ) : _MemberRoleActionList( member: member, ), ), + Expanded( + child: FlowyTooltip( + message: member.email, + child: Text( + member.email, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + ), + ), + ), myRole.canDelete && member.email != userProfile.email // can't delete self ? _MemberMoreActionList(member: member) @@ -476,7 +446,7 @@ class _MemberMoreActionList extends StatelessWidget { .settings_appearance_members_areYouSureToRemoveMember .tr(), onOkPressed: () => context.read().add( - WorkspaceMemberEvent.removeWorkspaceMember( + WorkspaceMemberEvent.removeWorkspaceMemberByEmail( action.member.email, ), ), @@ -515,106 +485,12 @@ class _MemberRoleActionList extends StatelessWidget { @override Widget build(BuildContext context) { - return PopoverActionList<_MemberRoleActionWrapper>( - asBarrier: true, - direction: PopoverDirection.bottomWithLeftAligned, - actions: [AFRolePB.Member] - .map((e) => _MemberRoleActionWrapper(e, member)) - .toList(), - offset: const Offset(0, 10), - buildChild: (controller) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => controller.show(), - child: Row( - children: [ - FlowyText.medium( - member.role.description, - fontSize: 14.0, - ), - const HSpace(8.0), - const FlowySvg( - FlowySvgs.drop_menu_show_s, - ), - ], - ), - ), - ); - }, - onSelected: (action, controller) async { - switch (action.inner) { - case AFRolePB.Member: - case AFRolePB.Guest: - context.read().add( - WorkspaceMemberEvent.updateWorkspaceMember( - action.member.email, - action.inner, - ), - ); - break; - case AFRolePB.Owner: - break; - } - controller.close(); - }, - ); - } -} - -class _MemberRoleActionWrapper extends ActionCell { - _MemberRoleActionWrapper(this.inner, this.member); - - final AFRolePB inner; - final WorkspaceMemberPB member; - - @override - Widget? rightIcon(Color iconColor) { - return SizedBox( - width: 58.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyTooltip( - message: tooltip, - child: const FlowySvg( - FlowySvgs.information_s, - // color: iconColor, - ), - ), - const Spacer(), - if (member.role == inner) - const FlowySvg( - FlowySvgs.checkmark_tiny_s, - ), - ], + final theme = AppFlowyTheme.of(context); + return Text( + member.role.description, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, ), ); } - - @override - String get name { - switch (inner) { - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guest.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_member.tr(); - case AFRolePB.Owner: - return LocaleKeys.settings_appearance_members_owner.tr(); - } - throw UnimplementedError('Unknown role: $inner'); - } - - String get tooltip { - switch (inner) { - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guestHintText.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_memberHintText.tr(); - case AFRolePB.Owner: - return ''; - } - throw UnimplementedError('Unknown role: $inner'); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 7e30c4fa55..8d65ee23bb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -606,6 +606,7 @@ Future showConfirmDialog({ VoidCallback? onCancel, String? confirmLabel, ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk, + WidgetBuilder? confirmButtonBuilder, }) { return showDialog( context: context, @@ -619,6 +620,7 @@ Future showConfirmDialog({ child: ConfirmPopup( title: title, description: description, + confirmButtonBuilder: confirmButtonBuilder, onConfirm: () => onConfirm?.call(), onCancel: () => onCancel?.call(), confirmLabel: confirmLabel, diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index b4a1a3d20d..e06670c5a5 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart index 26e45ca8f1..b8dc5a1149 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart @@ -132,7 +132,7 @@ class _AnimatedThemeState @override Widget build(BuildContext context) { return AppFlowyTheme( - data: data!.evaluate(animation), + data: widget.data, child: widget.child, ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index fe774d3561..8de842e4b7 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -57,6 +57,7 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { ); final borderColorScheme = AppFlowyBorderColorScheme( + primary: AppFlowyPrimitiveTokens.neutral200, greyPrimary: AppFlowyPrimitiveTokens.neutral1000, greyPrimaryHover: AppFlowyPrimitiveTokens.neutral900, greySecondary: AppFlowyPrimitiveTokens.neutral800, @@ -211,6 +212,7 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { ); final borderColorScheme = AppFlowyBorderColorScheme( + primary: AppFlowyPrimitiveTokens.neutral800, greyPrimary: AppFlowyPrimitiveTokens.neutral100, greyPrimaryHover: AppFlowyPrimitiveTokens.neutral200, greySecondary: AppFlowyPrimitiveTokens.neutral300, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart index 28eee5b145..ca65ed1fb4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; class AppFlowyBorderColorScheme { - const AppFlowyBorderColorScheme({ + AppFlowyBorderColorScheme({ + required this.primary, required this.greyPrimary, required this.greyPrimaryHover, required this.greySecondary, @@ -25,6 +26,7 @@ class AppFlowyBorderColorScheme { required this.purpleThickHover, }); + final Color primary; final Color greyPrimary; final Color greyPrimaryHover; final Color greySecondary; @@ -52,6 +54,7 @@ class AppFlowyBorderColorScheme { double t, ) { return AppFlowyBorderColorScheme( + primary: Color.lerp(primary, other.primary, t)!, greyPrimary: Color.lerp(greyPrimary, other.greyPrimary, t)!, greyPrimaryHover: Color.lerp(greyPrimaryHover, other.greyPrimaryHover, t)!, diff --git a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart b/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart index 4458d588cc..8a370f74d5 100644 --- a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -29,14 +30,17 @@ void main() { showDialog( context: context, builder: (_) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ConfirmPopup( - description: "desc", - title: "title", - onConfirm: onConfirm, + return AppFlowyTheme( + data: AppFlowyDefaultTheme().light(), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: ConfirmPopup( + description: "desc", + title: "title", + onConfirm: onConfirm, + ), ), ); }, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 30e8c476ae..746833fd1f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1307,10 +1307,10 @@ "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page", "enableRTLToolbarItems": "Enable RTL toolbar items", "members": { - "title": "Members settings", + "title": "Members", "inviteMembers": "Invite members", "inviteHint": "Invite by email", - "sendInvite": "Send invite", + "sendInvite": "Invite", "copyInviteLink": "Copy invite link", "label": "Members", "user": "User", @@ -1345,7 +1345,22 @@ "inviteMemberSuccess": "The invitation has been sent successfully", "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" + "workspaceMembersErrorDescription": "We couldn't load the member list at this time. Please try again later", + "inviteLinkToAddMember": "Invite link to add member", + "clickToCopyLink": "Click to copy link", + "or": "or", + "generateANewLink": "generate a new link", + "inviteMemberByEmail": "Invite member by email", + "inviteMemberHintText": "Invite by email", + "resetInviteLink": "Reset the invite link", + "resetInviteLinkDescription": "Resetting will deactivate the current link for all space members and generate a new one. The previous link can only be managed through the", + "adminPanel": "Admin Panel", + "reset": "Reset", + "resetInviteLinkSuccess": "Invite link reset successfully", + "resetInviteLinkFailed": "Failed to reset the invite link", + "resetInviteLinkFailedDescription": "Please try again later", + "memberPageDescription1": "Access the", + "memberPageDescription2": "for guest and advanced user management." } }, "files": { From 40c1ae1d386e3290d52d8907654775a39c125ae3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Apr 2025 10:13:24 +0800 Subject: [PATCH 69/74] fix: click two times to enable local ai --- .../settings/ai/local_ai_bloc.dart | 20 +++++++++++++++---- .../setting_ai_view/local_ai_setting.dart | 15 +++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index a90f319a94..492c19ab73 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -30,6 +30,10 @@ class LocalAiPluginBloc extends Bloc { LocalAiPluginEvent event, Emitter emit, ) async { + if (isClosed) { + return; + } + await event.when( didReceiveAiState: (aiState) { emit( @@ -54,7 +58,9 @@ class LocalAiPluginBloc extends Bloc { emit(LocalAiPluginState.loading()); await AIEventToggleLocalAI().send().fold( (aiState) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); + if (!isClosed) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + } }, Log.error, ); @@ -69,10 +75,14 @@ class LocalAiPluginBloc extends Bloc { void _startListening() { listener.start( stateCallback: (pluginState) { - add(LocalAiPluginEvent.didReceiveAiState(pluginState)); + if (!isClosed) { + add(LocalAiPluginEvent.didReceiveAiState(pluginState)); + } }, resourceCallback: (data) { - add(LocalAiPluginEvent.didReceiveLackOfResources(data)); + if (!isClosed) { + add(LocalAiPluginEvent.didReceiveLackOfResources(data)); + } }, ); } @@ -80,7 +90,9 @@ class LocalAiPluginBloc extends Bloc { void _getLocalAiState() { AIEventGetLocalAIState().send().fold( (aiState) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); + if (!isClosed) { + add(LocalAiPluginEvent.didReceiveAiState(aiState)); + } }, Log.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index b836f15b03..08f3034ad6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -47,7 +47,6 @@ class _LocalAISettingState extends State { ), header: LocalAiSettingHeader( isEnabled: state.isEnabled, - isToggleable: state is ReadyLocalAiPluginState, ), collapsed: const SizedBox.shrink(), expanded: Padding( @@ -65,11 +64,9 @@ class LocalAiSettingHeader extends StatelessWidget { const LocalAiSettingHeader({ super.key, required this.isEnabled, - required this.isToggleable, }); final bool isEnabled; - final bool isToggleable; @override Widget build(BuildContext context) { @@ -91,15 +88,9 @@ class LocalAiSettingHeader extends StatelessWidget { ], ), ), - IgnorePointer( - ignoring: !isToggleable, - child: Opacity( - opacity: isToggleable ? 1 : 0.5, - child: Toggle( - value: isEnabled, - onChanged: (_) => _onToggleChanged(context), - ), - ), + Toggle( + value: isEnabled, + onChanged: (_) => _onToggleChanged(context), ), ], ); From eaac387c8d35f4f8b8906e33a8aea53ca9e0ef7d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 22 Apr 2025 11:26:08 +0800 Subject: [PATCH 70/74] fix: remove space migration --- .../application/sidebar/space/space_bloc.dart | 14 +----- frontend/appflowy_flutter/macos/Podfile.lock | 46 +++++++++---------- 2 files changed, 24 insertions(+), 36 deletions(-) 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 6d6ce05051..9de0c582cd 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 @@ -76,12 +76,6 @@ class SpaceBloc extends Bloc { final (spaces, publicViews, privateViews) = await _getSpaces(); - final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog( - spaces: spaces, - publicViews: publicViews, - privateViews: privateViews, - ); - final currentSpace = await _getLastOpenedSpace(spaces); final isExpanded = await _getSpaceExpandStatus(currentSpace); emit( @@ -89,17 +83,11 @@ class SpaceBloc extends Bloc { spaces: spaces, currentSpace: currentSpace, isExpanded: isExpanded, - shouldShowUpgradeDialog: shouldShowUpgradeDialog, + shouldShowUpgradeDialog: false, isInitialized: true, ), ); - if (shouldShowUpgradeDialog && !integrationMode().isTest) { - if (!isClosed) { - add(const SpaceEvent.migrate()); - } - } - if (openFirstPage) { if (currentSpace != null) { if (!isClosed) { diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index f4bedfef79..e06670c5a5 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -144,34 +144,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 + app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 + connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 + irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba + local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 From 8c5547da6484d7fcc0ffdaf6edbadf5631391bb0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 22 Apr 2025 12:05:53 +0800 Subject: [PATCH 71/74] fix: custom font doesn't apply to settings page (#7801) * fix: custom font doesn't apply to settings page * chore: update settings page icons * fix: add try catch in http services --- .../document/presentation/editor_style.dart | 5 +- .../lib/startup/tasks/app_widget.dart | 8 +- .../password/password_http_service.dart | 14 +- .../inivitation/member_http_service.dart | 14 +- .../members/workspace_member_bloc.dart | 2 +- .../settings/widgets/settings_menu.dart | 23 +-- .../theme/data/appflowy_default/semantic.dart | 12 +- .../src/theme/data/custom/custom_theme.dart | 10 +- .../text_style/base/default_text_style.dart | 174 ++++++++++-------- .../definition/text_style/text_style.dart | 28 ++- .../lib/src/theme/definition/theme_data.dart | 9 +- .../flowy_icons/20x/settings_page_ai.svg | 4 + .../flowy_icons/20x/settings_page_bell.svg | 4 + .../flowy_icons/20x/settings_page_cloud.svg | 3 + .../20x/settings_page_credit_card.svg | 3 + .../20x/settings_page_database.svg | 3 + .../flowy_icons/20x/settings_page_earth.svg | 3 + .../20x/settings_page_keyboard.svg | 11 ++ .../flowy_icons/20x/settings_page_plan.svg | 3 + .../flowy_icons/20x/settings_page_user.svg | 4 + .../flowy_icons/20x/settings_page_users.svg | 3 + .../20x/settings_page_workspace.svg | 3 + 22 files changed, 225 insertions(+), 118 deletions(-) create mode 100644 frontend/resources/flowy_icons/20x/settings_page_ai.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_bell.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_cloud.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_credit_card.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_database.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_earth.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_keyboard.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_plan.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_user.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_users.svg create mode 100644 frontend/resources/flowy_icons/20x/settings_page_workspace.svg diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 3664c9aee7..cd9d7bb5e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -316,9 +316,12 @@ class EditorStyleCustomizer { } TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { - if (fontFamily == null || fontFamily == defaultFontFamily) { + if (fontFamily == null) { return TextStyle(fontWeight: fontWeight); + } else if (fontFamily == defaultFontFamily) { + return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight); } + try { return getGoogleFontSafely(fontFamily, fontWeight: fontWeight); } on Exception { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 0ed66389d9..48e76cecbc 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -7,11 +7,13 @@ import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; +import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/notification/notification_service.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; @@ -238,11 +240,13 @@ class _ApplicationWidgetState extends State { routerConfig: routerConfig, builder: (context, child) { final brightness = Theme.of(context).brightness; + final fontFamily = + state.font.orDefault(defaultFontFamily); return AppFlowyTheme( data: brightness == Brightness.light - ? themeBuilder.light() - : themeBuilder.dark(), + ? themeBuilder.light(fontFamily: fontFamily) + : themeBuilder.dark(fontFamily: fontFamily), child: MediaQuery( // use the 1.0 as the textScaleFactor to avoid the text size // affected by the system setting. diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart index 723ded57e2..c56c4f595d 100644 --- a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart @@ -120,10 +120,16 @@ class PasswordHttpService { errorMessage: 'Failed to check password status', ); - return result.fold( - (data) => FlowyResult.success(data['has_password'] ?? false), - (error) => FlowyResult.failure(error), - ); + try { + return result.fold( + (data) => FlowyResult.success(data['has_password'] ?? false), + (error) => FlowyResult.failure(error), + ); + } catch (e) { + return FlowyResult.failure( + FlowyError(msg: 'Failed to check password status: $e'), + ); + } } /// Makes a request to the specified endpoint with the given body diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart index c8c7e47706..01d507ea24 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart @@ -64,10 +64,16 @@ class MemberHttpService { errorMessage: 'Failed to get invite code', ); - return result.fold( - (data) => FlowyResult.success(data['code'] as String), - (error) => FlowyResult.failure(error), - ); + try { + return result.fold( + (data) => FlowyResult.success(data['code'] as String), + (error) => FlowyResult.failure(error), + ); + } catch (e) { + return FlowyResult.failure( + FlowyError(msg: 'Failed to get invite code: $e'), + ); + } } /// Deletes the invite code for a workspace diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index 5f98146118..3fc13c7b18 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -92,7 +92,7 @@ class WorkspaceMemberBloc final inviteLink = await _buildInviteLink(inviteCode: s); emit(state.copyWith(inviteLink: inviteLink)); }, - (e) => Log.error('Failed to get invite code: ${e.msg}', e), + (e) => Log.info('Failed to get invite code: ${e.msg}', e), ), ); } else { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 979f19fbde..f628aadc6b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -52,14 +52,14 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.account, selectedPage: currentPage, label: LocaleKeys.settings_accountPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_account_m), + icon: const FlowySvg(FlowySvgs.settings_page_user_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.workspace, selectedPage: currentPage, label: LocaleKeys.settings_workspacePage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_workplace_m), + icon: const FlowySvg(FlowySvgs.settings_page_workspace_m), changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && @@ -68,35 +68,35 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.member, selectedPage: currentPage, label: LocaleKeys.settings_appearance_members_label.tr(), - icon: const Icon(Icons.people), + icon: const FlowySvg(FlowySvgs.settings_page_users_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.manageData, selectedPage: currentPage, label: LocaleKeys.settings_manageDataPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_data_m), + icon: const FlowySvg(FlowySvgs.settings_page_database_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.notifications, selectedPage: currentPage, label: LocaleKeys.settings_menu_notifications.tr(), - icon: const Icon(Icons.notifications_outlined), + icon: const FlowySvg(FlowySvgs.settings_page_bell_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.cloud, selectedPage: currentPage, label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: const Icon(Icons.sync), + icon: const FlowySvg(FlowySvgs.settings_page_cloud_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.shortcuts, selectedPage: currentPage, label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), + icon: const FlowySvg(FlowySvgs.settings_page_keyboard_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( @@ -104,7 +104,7 @@ class SettingsMenu extends StatelessWidget { selectedPage: currentPage, label: LocaleKeys.settings_aiPage_menuLabel.tr(), icon: const FlowySvg( - FlowySvgs.ai_summary_generate_s, + FlowySvgs.settings_page_ai_m, size: Size.square(24), ), changeSelectedPage: changeSelectedPage, @@ -114,7 +114,7 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.sites, selectedPage: currentPage, label: LocaleKeys.settings_sites_title.tr(), - icon: const Icon(Icons.web), + icon: const FlowySvg(FlowySvgs.settings_page_earth_m), changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[ @@ -122,14 +122,15 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.plan, selectedPage: currentPage, label: LocaleKeys.settings_planPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_plan_m), + icon: const FlowySvg(FlowySvgs.settings_page_plan_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.billing, selectedPage: currentPage, label: LocaleKeys.settings_billingPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_billing_m), + icon: + const FlowySvg(FlowySvgs.settings_page_credit_card_m), changeSelectedPage: changeSelectedPage, ), ], diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart index 8de842e4b7..3c97c06df3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart @@ -17,8 +17,10 @@ import 'primitive.dart'; class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { @override - AppFlowyThemeData light() { - final textStyle = AppFlowyBaseTextStyle(); + AppFlowyThemeData light({ + String? fontFamily, + }) { + final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); final borderRadius = AppFlowySharedTokens.buildBorderRadius(); final spacing = AppFlowySharedTokens.buildSpacing(); final shadow = AppFlowySharedTokens.buildShadow(Brightness.light); @@ -172,8 +174,10 @@ class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { } @override - AppFlowyThemeData dark() { - final textStyle = AppFlowyBaseTextStyle(); + AppFlowyThemeData dark({ + String? fontFamily, + }) { + final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); final borderRadius = AppFlowySharedTokens.buildBorderRadius(); final spacing = AppFlowySharedTokens.buildSpacing(); final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark); diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart index 6ef43076c5..ca058310b9 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart @@ -10,14 +10,16 @@ class CustomTheme implements AppFlowyThemeBuilder { final Map darkThemeJson; @override - AppFlowyThemeData light() { - // TODO: implement light + AppFlowyThemeData light({ + String? fontFamily, + }) { throw UnimplementedError(); } @override - AppFlowyThemeData dark() { - // TODO: implement dark + AppFlowyThemeData dark({ + String? fontFamily, + }) { throw UnimplementedError(); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart index 3cdf267fe0..006f364f96 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart @@ -1,44 +1,50 @@ import 'package:flutter/widgets.dart'; abstract class TextThemeType { - const TextThemeType(); + const TextThemeType({ + required this.fontFamily, + }); + + final String fontFamily; TextStyle standard({ - String family = '', + String? family, Color? color, FontWeight? weight, }); TextStyle enhanced({ - String family = '', + String? family, Color? color, FontWeight? weight, }); TextStyle prominent({ - String family = '', + String? family, Color? color, FontWeight? weight, }); TextStyle underline({ - String family = '', + String? family, Color? color, FontWeight? weight, }); } class TextThemeHeading1 extends TextThemeType { - const TextThemeHeading1(); + const TextThemeHeading1({ + required super.fontFamily, + }); @override TextStyle standard({ - String family = '', + String? family, Color? color, FontWeight? weight, }) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, fontSize: 36, height: 40 / 36, color: color, @@ -47,12 +53,12 @@ class TextThemeHeading1 extends TextThemeType { @override TextStyle enhanced({ - String family = '', + String? family, Color? color, FontWeight? weight, }) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, fontSize: 36, height: 40 / 36, color: color, @@ -61,12 +67,12 @@ class TextThemeHeading1 extends TextThemeType { @override TextStyle prominent({ - String family = '', + String? family, Color? color, FontWeight? weight, }) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, fontSize: 36, height: 40 / 36, color: color, @@ -75,12 +81,12 @@ class TextThemeHeading1 extends TextThemeType { @override TextStyle underline({ - String family = '', + String? family, Color? color, FontWeight? weight, }) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, fontSize: 36, height: 40 / 36, color: color, @@ -111,36 +117,38 @@ class TextThemeHeading1 extends TextThemeType { } class TextThemeHeading2 extends TextThemeType { - const TextThemeHeading2(); + const TextThemeHeading2({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w700, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, decoration: TextDecoration.underline, @@ -169,36 +177,38 @@ class TextThemeHeading2 extends TextThemeType { } class TextThemeHeading3 extends TextThemeType { - const TextThemeHeading3(); + const TextThemeHeading3({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w700, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, decoration: TextDecoration.underline, @@ -227,36 +237,38 @@ class TextThemeHeading3 extends TextThemeType { } class TextThemeHeading4 extends TextThemeType { - const TextThemeHeading4(); + const TextThemeHeading4({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w700, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w400, decoration: TextDecoration.underline, @@ -285,36 +297,38 @@ class TextThemeHeading4 extends TextThemeType { } class TextThemeHeadline extends TextThemeType { - const TextThemeHeadline(); + const TextThemeHeadline({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, @@ -343,36 +357,38 @@ class TextThemeHeadline extends TextThemeType { } class TextThemeTitle extends TextThemeType { - const TextThemeTitle(); + const TextThemeTitle({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, @@ -401,36 +417,38 @@ class TextThemeTitle extends TextThemeType { } class TextThemeBody extends TextThemeType { - const TextThemeBody(); + const TextThemeBody({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, @@ -459,36 +477,38 @@ class TextThemeBody extends TextThemeType { } class TextThemeCaption extends TextThemeType { - const TextThemeCaption(); + const TextThemeCaption({ + required super.fontFamily, + }); @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => + TextStyle standard({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, ); @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => + TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.w600, ); @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => + TextStyle prominent({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.bold, ); @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => + TextStyle underline({String? family, Color? color, FontWeight? weight}) => _defaultTextStyle( - family: family, + family: family ?? super.fontFamily, color: color, weight: weight ?? FontWeight.normal, decoration: TextDecoration.underline, diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart index d96ca0f557..89c1278d93 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart @@ -1,15 +1,27 @@ import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; class AppFlowyBaseTextStyle { + factory AppFlowyBaseTextStyle.customFontFamily(String fontFamily) => + AppFlowyBaseTextStyle( + heading1: TextThemeHeading1(fontFamily: fontFamily), + heading2: TextThemeHeading2(fontFamily: fontFamily), + heading3: TextThemeHeading3(fontFamily: fontFamily), + heading4: TextThemeHeading4(fontFamily: fontFamily), + headline: TextThemeHeadline(fontFamily: fontFamily), + title: TextThemeTitle(fontFamily: fontFamily), + body: TextThemeBody(fontFamily: fontFamily), + caption: TextThemeCaption(fontFamily: fontFamily), + ); + const AppFlowyBaseTextStyle({ - this.heading1 = const TextThemeHeading1(), - this.heading2 = const TextThemeHeading2(), - this.heading3 = const TextThemeHeading3(), - this.heading4 = const TextThemeHeading4(), - this.headline = const TextThemeHeadline(), - this.title = const TextThemeTitle(), - this.body = const TextThemeBody(), - this.caption = const TextThemeCaption(), + this.heading1 = const TextThemeHeading1(fontFamily: ''), + this.heading2 = const TextThemeHeading2(fontFamily: ''), + this.heading3 = const TextThemeHeading3(fontFamily: ''), + this.heading4 = const TextThemeHeading4(fontFamily: ''), + this.headline = const TextThemeHeadline(fontFamily: ''), + this.title = const TextThemeTitle(fontFamily: ''), + this.body = const TextThemeBody(fontFamily: ''), + this.caption = const TextThemeCaption(fontFamily: ''), }); final TextThemeType heading1; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart index 515e6b2ecf..1da45cfd2a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart @@ -81,6 +81,11 @@ class AppFlowyThemeData { abstract class AppFlowyThemeBuilder { const AppFlowyThemeBuilder(); - AppFlowyThemeData light(); - AppFlowyThemeData dark(); + AppFlowyThemeData light({ + String? fontFamily, + }); + + AppFlowyThemeData dark({ + String? fontFamily, + }); } diff --git a/frontend/resources/flowy_icons/20x/settings_page_ai.svg b/frontend/resources/flowy_icons/20x/settings_page_ai.svg new file mode 100644 index 0000000000..d98a0c90fd --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_ai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_bell.svg b/frontend/resources/flowy_icons/20x/settings_page_bell.svg new file mode 100644 index 0000000000..57031d1f90 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_bell.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_cloud.svg b/frontend/resources/flowy_icons/20x/settings_page_cloud.svg new file mode 100644 index 0000000000..44c20bb51b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_cloud.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg b/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg new file mode 100644 index 0000000000..e1c64ee509 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_database.svg b/frontend/resources/flowy_icons/20x/settings_page_database.svg new file mode 100644 index 0000000000..bfbae5f8fe --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_database.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_earth.svg b/frontend/resources/flowy_icons/20x/settings_page_earth.svg new file mode 100644 index 0000000000..0a205592b4 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_earth.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg b/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg new file mode 100644 index 0000000000..92efc30142 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_plan.svg b/frontend/resources/flowy_icons/20x/settings_page_plan.svg new file mode 100644 index 0000000000..9792bd41c4 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_plan.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_user.svg b/frontend/resources/flowy_icons/20x/settings_page_user.svg new file mode 100644 index 0000000000..94968ff06b --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_users.svg b/frontend/resources/flowy_icons/20x/settings_page_users.svg new file mode 100644 index 0000000000..eb65bf7192 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_users.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/20x/settings_page_workspace.svg b/frontend/resources/flowy_icons/20x/settings_page_workspace.svg new file mode 100644 index 0000000000..e9a6eb9a10 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/settings_page_workspace.svg @@ -0,0 +1,3 @@ + + + From 403f343371b5277667dcd2d4635483993ea5afea Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Apr 2025 10:15:19 +0800 Subject: [PATCH 72/74] chore: delete supabase test --- .../tests/database/supabase_test/helper.rs | 106 ---- .../tests/database/supabase_test/mod.rs | 2 - .../tests/database/supabase_test/test.rs | 108 ---- .../tests/document/supabase_test/edit_test.rs | 65 --- .../tests/document/supabase_test/file_test.rs | 118 ---- .../tests/document/supabase_test/helper.rs | 49 -- .../tests/document/supabase_test/mod.rs | 3 - .../tests/folder/supabase_test/helper.rs | 91 ---- .../tests/folder/supabase_test/mod.rs | 2 - .../tests/folder/supabase_test/test.rs | 122 ----- .../tests/user/supabase_test/auth_test.rs | 502 ------------------ .../supabase_test/history_user_db/README.md | 4 - .../history_user_db/workspace_sync.zip | Bin 44001 -> 0 bytes .../tests/user/supabase_test/mod.rs | 2 - .../user/supabase_test/workspace_test.rs | 43 -- 15 files changed, 1217 deletions(-) delete mode 100644 frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md delete mode 100644 frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip delete mode 100644 frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs delete mode 100644 frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs deleted file mode 100644 index c1874a5004..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::ops::Deref; - -use assert_json_diff::assert_json_eq; -use collab::core::collab::MutexCollab; -use collab::core::origin::CollabOrigin; -use collab::preclude::updates::decoder::Decode; -use collab::preclude::{Collab, JsonValue, Update}; -use collab_entity::CollabType; - -use event_integration_test::event_builder::EventBuilder; -use flowy_database2::entities::{DatabasePB, DatabaseViewIdPB, RepeatedDatabaseSnapshotPB}; -use flowy_database2::event_map::DatabaseEvent::*; -use flowy_folder::entities::ViewPB; - -use crate::util::FlowySupabaseTest; - -pub struct FlowySupabaseDatabaseTest { - pub uuid: String, - inner: FlowySupabaseTest, -} - -impl FlowySupabaseDatabaseTest { - #[allow(dead_code)] - pub async fn new_with_user(uuid: String) -> Option { - let inner = FlowySupabaseTest::new().await?; - inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); - Some(Self { uuid, inner }) - } - - pub async fn new_with_new_user() -> Option { - let inner = FlowySupabaseTest::new().await?; - let uuid = uuid::Uuid::new_v4().to_string(); - let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); - Some(Self { uuid, inner }) - } - - pub async fn create_database(&self) -> (ViewPB, DatabasePB) { - let current_workspace = self.inner.get_current_workspace().await; - let view = self - .inner - .create_grid(¤t_workspace.id, "my database".to_string(), vec![]) - .await; - let database = self.inner.get_database(&view.id).await; - (view, database) - } - - pub async fn get_collab_json(&self, database_id: &str) -> JsonValue { - let database_editor = self - .database_manager - .get_database(database_id) - .await - .unwrap(); - // let address = Arc::into_raw(database_editor.clone()); - let database = database_editor.get_mutex_database().lock(); - database.get_mutex_collab().to_json_value() - } - - pub async fn get_database_snapshots(&self, view_id: &str) -> RepeatedDatabaseSnapshotPB { - EventBuilder::new(self.inner.deref().clone()) - .event(GetDatabaseSnapshots) - .payload(DatabaseViewIdPB { - value: view_id.to_string(), - }) - .async_send() - .await - .parse::() - } - - pub async fn get_database_collab_update(&self, database_id: &str) -> Vec { - let workspace_id = self.user_manager.workspace_id().unwrap(); - let cloud_service = self.database_manager.get_cloud_service().clone(); - cloud_service - .get_database_object_doc_state(database_id, CollabType::Database, &workspace_id) - .await - .unwrap() - .unwrap() - } -} - -pub fn assert_database_collab_content( - database_id: &str, - collab_update: &[u8], - expected: JsonValue, -) { - let collab = MutexCollab::new(Collab::new_with_origin( - CollabOrigin::Server, - database_id, - vec![], - false, - )); - collab.lock().with_origin_transact_mut(|txn| { - let update = Update::decode_v1(collab_update).unwrap(); - txn.apply_update(update).unwrap(); - }); - - let json = collab.to_json_value(); - assert_json_eq!(json, expected); -} - -impl Deref for FlowySupabaseDatabaseTest { - type Target = FlowySupabaseTest; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs deleted file mode 100644 index 05fa1b00ed..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod helper; -mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs deleted file mode 100644 index 537cdf80d8..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::time::Duration; - -use flowy_database2::entities::{ - DatabaseSnapshotStatePB, DatabaseSyncState, DatabaseSyncStatePB, FieldChangesetPB, FieldType, -}; -use flowy_database2::notification::DatabaseNotification::DidUpdateDatabaseSnapshotState; - -use crate::database::supabase_test::helper::{ - assert_database_collab_content, FlowySupabaseDatabaseTest, -}; -use crate::util::receive_with_timeout; - -#[tokio::test] -async fn supabase_initial_database_snapshot_test() { - if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { - let (view, database) = test.create_database().await; - let rx = test - .notification_sender - .subscribe::(&database.id, DidUpdateDatabaseSnapshotState); - - receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap(); - - let expected = test.get_collab_json(&database.id).await; - let snapshots = test.get_database_snapshots(&view.id).await; - assert_eq!(snapshots.items.len(), 1); - assert_database_collab_content(&database.id, &snapshots.items[0].data, expected); - } -} - -#[tokio::test] -async fn supabase_edit_database_test() { - if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { - let (view, database) = test.create_database().await; - let existing_fields = test.get_all_database_fields(&view.id).await; - for field in existing_fields.items { - if !field.is_primary { - test.delete_field(&view.id, &field.id).await; - } - } - - let field = test.create_field(&view.id, FieldType::Checklist).await; - test - .update_field(FieldChangesetPB { - field_id: field.id.clone(), - view_id: view.id.clone(), - name: Some("hello world".to_string()), - ..Default::default() - }) - .await; - - // wait all updates are send to the remote - let rx = test - .notification_sender - .subscribe_with_condition::(&database.id, |pb| { - pb.value == DatabaseSyncState::SyncFinished - }); - receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap(); - - assert_eq!(test.get_all_database_fields(&view.id).await.items.len(), 2); - let expected = test.get_collab_json(&database.id).await; - let update = test.get_database_collab_update(&database.id).await; - assert_database_collab_content(&database.id, &update, expected); - } -} - -// #[tokio::test] -// async fn cloud_test_supabase_login_sync_database_test() { -// if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { -// let uuid = test.uuid.clone(); -// let (view, database) = test.create_database().await; -// // wait all updates are send to the remote -// let mut rx = test -// .notification_sender -// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); -// receive_with_timeout(&mut rx, Duration::from_secs(30)) -// .await -// .unwrap(); -// let expected = test.get_collab_json(&database.id).await; -// test.sign_out().await; -// // Drop the test will cause the test resources to be dropped, which will -// // delete the user data folder. -// drop(test); -// -// let new_test = FlowySupabaseDatabaseTest::new_with_user(uuid) -// .await -// .unwrap(); -// // let actual = new_test.get_collab_json(&database.id).await; -// // assert_json_eq!(actual, json!("")); -// -// new_test.open_database(&view.id).await; -// -// // wait all updates are synced from the remote -// let mut rx = new_test -// .notification_sender -// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); -// receive_with_timeout(&mut rx, Duration::from_secs(30)) -// .await -// .unwrap(); -// -// // when the new sync is finished, the database should be the same as the old one -// let actual = new_test.get_collab_json(&database.id).await; -// assert_json_eq!(actual, expected); -// } -// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs deleted file mode 100644 index d05e1ef95c..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::time::Duration; - -use event_integration_test::document_event::assert_document_data_equal; -use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; - -use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; -use crate::util::receive_with_timeout; - -#[tokio::test] -async fn supabase_document_edit_sync_test() { - if let Some(test) = FlowySupabaseDocumentTest::new().await { - let view = test.create_document().await; - let document_id = view.id.clone(); - - let cloned_test = test.clone(); - let cloned_document_id = document_id.clone(); - test.appflowy_core.dispatcher().spawn(async move { - cloned_test - .insert_document_text(&cloned_document_id, "hello world", 0) - .await; - }); - - // wait all update are send to the remote - let rx = test - .notification_sender - .subscribe_with_condition::(&document_id, |pb| { - pb.value != DocumentSyncState::Syncing - }); - receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap(); - - let document_data = test.get_document_data(&document_id).await; - let update = test.get_document_doc_state(&document_id).await; - assert_document_data_equal(&update, &document_id, document_data); - } -} - -#[tokio::test] -async fn supabase_document_edit_sync_test2() { - if let Some(test) = FlowySupabaseDocumentTest::new().await { - let view = test.create_document().await; - let document_id = view.id.clone(); - - for i in 0..10 { - test - .insert_document_text(&document_id, "hello world", i) - .await; - } - - // wait all update are send to the remote - let rx = test - .notification_sender - .subscribe_with_condition::(&document_id, |pb| { - pb.value != DocumentSyncState::Syncing - }); - receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap(); - - let document_data = test.get_document_data(&document_id).await; - let update = test.get_document_doc_state(&document_id).await; - assert_document_data_equal(&update, &document_id, document_data); - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs deleted file mode 100644 index e73273cde6..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs +++ /dev/null @@ -1,118 +0,0 @@ -// use std::fs::File; -// use std::io::{Cursor, Read}; -// use std::path::Path; -// -// use uuid::Uuid; -// use zip::ZipArchive; -// -// use flowy_storage::StorageObject; -// -// use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; -// -// #[tokio::test] -// async fn supabase_document_upload_text_file_test() { -// if let Some(test) = FlowySupabaseDocumentTest::new().await { -// let workspace_id = test.get_current_workspace().await.id; -// let storage_service = test -// .document_manager -// .get_file_storage_service() -// .upgrade() -// .unwrap(); -// -// let object = StorageObject::from_bytes( -// &workspace_id, -// &Uuid::new_v4().to_string(), -// "hello world".as_bytes(), -// "text/plain".to_string(), -// ); -// -// let url = storage_service.create_object(object).await.unwrap(); -// -// let bytes = storage_service -// .get_object(url.clone()) -// .await -// .unwrap(); -// let s = String::from_utf8(bytes.to_vec()).unwrap(); -// assert_eq!(s, "hello world"); -// -// // Delete the text file -// let _ = storage_service.delete_object(url).await; -// } -// } -// -// #[tokio::test] -// async fn supabase_document_upload_zip_file_test() { -// if let Some(test) = FlowySupabaseDocumentTest::new().await { -// let workspace_id = test.get_current_workspace().await.id; -// let storage_service = test -// .document_manager -// .get_file_storage_service() -// .upgrade() -// .unwrap(); -// -// // Upload zip file -// let object = StorageObject::from_file( -// &workspace_id, -// &Uuid::new_v4().to_string(), -// "./tests/asset/test.txt.zip", -// ); -// let url = storage_service.create_object(object).await.unwrap(); -// -// // Read zip file -// let zip_data = storage_service -// .get_object(url.clone()) -// .await -// .unwrap(); -// let reader = Cursor::new(zip_data); -// let mut archive = ZipArchive::new(reader).unwrap(); -// for i in 0..archive.len() { -// let mut file = archive.by_index(i).unwrap(); -// let name = file.name().to_string(); -// let mut out = Vec::new(); -// file.read_to_end(&mut out).unwrap(); -// -// if name.starts_with("__MACOSX/") { -// continue; -// } -// assert_eq!(name, "test.txt"); -// assert_eq!(String::from_utf8(out).unwrap(), "hello world"); -// } -// -// // Delete the zip file -// let _ = storage_service.delete_object(url).await; -// } -// } -// #[tokio::test] -// async fn supabase_document_upload_image_test() { -// if let Some(test) = FlowySupabaseDocumentTest::new().await { -// let workspace_id = test.get_current_workspace().await.id; -// let storage_service = test -// .document_manager -// .get_file_storage_service() -// .upgrade() -// .unwrap(); -// -// // Upload zip file -// let object = StorageObject::from_file( -// &workspace_id, -// &Uuid::new_v4().to_string(), -// "./tests/asset/logo.png", -// ); -// let url = storage_service.create_object(object).await.unwrap(); -// -// let image_data = storage_service -// .get_object(url.clone()) -// .await -// .unwrap(); -// -// // Read the image file -// let mut file = File::open(Path::new("./tests/asset/logo.png")).unwrap(); -// let mut local_data = Vec::new(); -// file.read_to_end(&mut local_data).unwrap(); -// -// assert_eq!(image_data, local_data); -// -// // Delete the image -// let _ = storage_service.delete_object(url).await; -// } -// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs deleted file mode 100644 index 07ff2d96fe..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::ops::Deref; - -use event_integration_test::event_builder::EventBuilder; -use flowy_document::entities::{OpenDocumentPayloadPB, RepeatedDocumentSnapshotMetaPB}; -use flowy_document::event_map::DocumentEvent::GetDocumentSnapshotMeta; -use flowy_folder::entities::ViewPB; - -use crate::util::FlowySupabaseTest; - -pub struct FlowySupabaseDocumentTest { - inner: FlowySupabaseTest, -} - -impl FlowySupabaseDocumentTest { - pub async fn new() -> Option { - let inner = FlowySupabaseTest::new().await?; - let uuid = uuid::Uuid::new_v4().to_string(); - let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; - Some(Self { inner }) - } - - pub async fn create_document(&self) -> ViewPB { - let current_workspace = self.inner.get_current_workspace().await; - self - .inner - .create_and_open_document(¤t_workspace.id, "my document".to_string(), vec![]) - .await - } - - #[allow(dead_code)] - pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotMetaPB { - EventBuilder::new(self.inner.deref().clone()) - .event(GetDocumentSnapshotMeta) - .payload(OpenDocumentPayloadPB { - document_id: view_id.to_string(), - }) - .async_send() - .await - .parse::() - } -} - -impl Deref for FlowySupabaseDocumentTest { - type Target = FlowySupabaseTest; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs deleted file mode 100644 index 165f5fdfc0..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod edit_test; -mod file_test; -mod helper; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs deleted file mode 100644 index a1179ce6cc..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::ops::Deref; - -use assert_json_diff::assert_json_eq; -use collab::core::collab::MutexCollab; -use collab::core::origin::CollabOrigin; -use collab::preclude::updates::decoder::Decode; -use collab::preclude::{Collab, JsonValue, Update}; -use collab_entity::CollabType; -use collab_folder::FolderData; - -use event_integration_test::event_builder::EventBuilder; -use flowy_folder::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB}; -use flowy_folder::event_map::FolderEvent::GetFolderSnapshots; - -use crate::util::FlowySupabaseTest; - -pub struct FlowySupabaseFolderTest { - inner: FlowySupabaseTest, -} - -impl FlowySupabaseFolderTest { - pub async fn new() -> Option { - let inner = FlowySupabaseTest::new().await?; - let uuid = uuid::Uuid::new_v4().to_string(); - let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; - Some(Self { inner }) - } - - pub async fn get_collab_json(&self) -> JsonValue { - let folder = self.folder_manager.get_mutex_folder().lock(); - folder.as_ref().unwrap().to_json_value() - } - - pub async fn get_local_folder_data(&self) -> FolderData { - let folder = self.folder_manager.get_mutex_folder().lock(); - folder.as_ref().unwrap().get_folder_data().unwrap() - } - - pub async fn get_folder_snapshots(&self, workspace_id: &str) -> Vec { - EventBuilder::new(self.inner.deref().clone()) - .event(GetFolderSnapshots) - .payload(WorkspaceIdPB { - value: workspace_id.to_string(), - }) - .async_send() - .await - .parse::() - .items - } - - pub async fn get_collab_update(&self, workspace_id: &str) -> Vec { - let cloud_service = self.folder_manager.get_cloud_service().clone(); - cloud_service - .get_folder_doc_state( - workspace_id, - self.user_manager.user_id().unwrap(), - CollabType::Folder, - workspace_id, - ) - .await - .unwrap() - } -} - -pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], expected: JsonValue) { - if collab_update.is_empty() { - panic!("collab update is empty"); - } - - let collab = MutexCollab::new(Collab::new_with_origin( - CollabOrigin::Server, - workspace_id, - vec![], - false, - )); - collab.lock().with_origin_transact_mut(|txn| { - let update = Update::decode_v1(collab_update).unwrap(); - txn.apply_update(update).unwrap(); - }); - - let json = collab.to_json_value(); - assert_json_eq!(json["folder"], expected); -} - -impl Deref for FlowySupabaseFolderTest { - type Target = FlowySupabaseTest; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs deleted file mode 100644 index 05fa1b00ed..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod helper; -mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs deleted file mode 100644 index 5f6a50988a..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::time::Duration; - -use assert_json_diff::assert_json_eq; -use serde_json::json; - -use flowy_folder::entities::{FolderSnapshotStatePB, FolderSyncStatePB}; -use flowy_folder::notification::FolderNotification::DidUpdateFolderSnapshotState; - -use crate::folder::supabase_test::helper::{assert_folder_collab_content, FlowySupabaseFolderTest}; -use crate::util::{get_folder_data_from_server, receive_with_timeout}; - -#[tokio::test] -async fn supabase_encrypt_folder_test() { - if let Some(test) = FlowySupabaseFolderTest::new().await { - let uid = test.user_manager.user_id().unwrap(); - let secret = test.enable_encryption().await; - - let local_folder_data = test.get_local_folder_data().await; - let workspace_id = test.get_current_workspace().await.id; - let remote_folder_data = get_folder_data_from_server(&uid, &workspace_id, Some(secret)) - .await - .unwrap() - .unwrap(); - - assert_json_eq!(json!(local_folder_data), json!(remote_folder_data)); - } -} - -#[tokio::test] -async fn supabase_decrypt_folder_data_test() { - if let Some(test) = FlowySupabaseFolderTest::new().await { - let uid = test.user_manager.user_id().unwrap(); - let secret = Some(test.enable_encryption().await); - let workspace_id = test.get_current_workspace().await.id; - test - .create_view(&workspace_id, "encrypt view".to_string()) - .await; - - let rx = test - .notification_sender - .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - - receive_with_timeout(rx, Duration::from_secs(10)) - .await - .unwrap(); - let folder_data = get_folder_data_from_server(&uid, &workspace_id, secret) - .await - .unwrap() - .unwrap(); - assert_eq!(folder_data.views.len(), 2); - assert_eq!(folder_data.views[1].name, "encrypt view"); - } -} - -#[tokio::test] -#[should_panic] -async fn supabase_decrypt_with_invalid_secret_folder_data_test() { - if let Some(test) = FlowySupabaseFolderTest::new().await { - let uid = test.user_manager.user_id().unwrap(); - let _ = Some(test.enable_encryption().await); - let workspace_id = test.get_current_workspace().await.id; - test - .create_view(&workspace_id, "encrypt view".to_string()) - .await; - let rx = test - .notification_sender - .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - receive_with_timeout(rx, Duration::from_secs(10)) - .await - .unwrap(); - - let _ = get_folder_data_from_server(&uid, &workspace_id, Some("invalid secret".to_string())) - .await - .unwrap(); - } -} -#[tokio::test] -async fn supabase_folder_snapshot_test() { - if let Some(test) = FlowySupabaseFolderTest::new().await { - let workspace_id = test.get_current_workspace().await.id; - let rx = test - .notification_sender - .subscribe::(&workspace_id, DidUpdateFolderSnapshotState); - receive_with_timeout(rx, Duration::from_secs(10)) - .await - .unwrap(); - - let expected = test.get_collab_json().await; - let snapshots = test.get_folder_snapshots(&workspace_id).await; - assert_eq!(snapshots.len(), 1); - assert_folder_collab_content(&workspace_id, &snapshots[0].data, expected); - } -} - -#[tokio::test] -async fn supabase_initial_folder_snapshot_test2() { - if let Some(test) = FlowySupabaseFolderTest::new().await { - let workspace_id = test.get_current_workspace().await.id; - - test - .create_view(&workspace_id, "supabase test view1".to_string()) - .await; - test - .create_view(&workspace_id, "supabase test view2".to_string()) - .await; - test - .create_view(&workspace_id, "supabase test view3".to_string()) - .await; - - let rx = test - .notification_sender - .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - - receive_with_timeout(rx, Duration::from_secs(10)) - .await - .unwrap(); - - let expected = test.get_collab_json().await; - let update = test.get_collab_update(&workspace_id).await; - assert_folder_collab_content(&workspace_id, &update, expected); - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs deleted file mode 100644 index 1b6d5f9cc6..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs +++ /dev/null @@ -1,502 +0,0 @@ -use std::collections::HashMap; - -use assert_json_diff::assert_json_eq; -use collab_database::rows::database_row_document_id_from_row_id; -use collab_document::blocks::DocumentData; -use collab_entity::CollabType; -use collab_folder::FolderData; -use nanoid::nanoid; -use serde_json::json; - -use event_integration_test::document::document_event::DocumentEventTest; -use event_integration_test::event_builder::EventBuilder; -use event_integration_test::EventIntegrationTest; -use flowy_core::DEFAULT_NAME; -use flowy_encrypt::decrypt_text; -use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; -use flowy_user::entities::{ - AuthenticatorPB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB, -}; -use flowy_user::errors::ErrorCode; -use flowy_user::event_map::UserEvent::*; - -use crate::util::*; - -#[tokio::test] -async fn third_party_sign_up_test() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - let mut map = HashMap::new(); - map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); - map.insert( - USER_EMAIL.to_string(), - format!("{}@appflowy.io", nanoid!(6)), - ); - map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); - let payload = OauthSignInPB { - map, - authenticator: AuthenticatorPB::Supabase, - }; - - let response = EventBuilder::new(test.clone()) - .event(OauthSignIn) - .payload(payload) - .async_send() - .await - .parse::(); - dbg!(&response); - } -} - -#[tokio::test] -async fn third_party_sign_up_with_encrypt_test() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - test.supabase_party_sign_up().await; - let user_profile = test.get_user_profile().await.unwrap(); - assert!(user_profile.encryption_sign.is_empty()); - - let secret = test.enable_encryption().await; - let user_profile = test.get_user_profile().await.unwrap(); - assert!(!user_profile.encryption_sign.is_empty()); - - let decryption_sign = decrypt_text(user_profile.encryption_sign, &secret).unwrap(); - assert_eq!(decryption_sign, user_profile.id.to_string()); - } -} - -#[tokio::test] -async fn third_party_sign_up_with_duplicated_uuid() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - let email = format!("{}@appflowy.io", nanoid!(6)); - let mut map = HashMap::new(); - map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); - map.insert(USER_EMAIL.to_string(), email.clone()); - map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); - - let response_1 = EventBuilder::new(test.clone()) - .event(OauthSignIn) - .payload(OauthSignInPB { - map: map.clone(), - authenticator: AuthenticatorPB::Supabase, - }) - .async_send() - .await - .parse::(); - dbg!(&response_1); - - let response_2 = EventBuilder::new(test.clone()) - .event(OauthSignIn) - .payload(OauthSignInPB { - map: map.clone(), - authenticator: AuthenticatorPB::Supabase, - }) - .async_send() - .await - .parse::(); - assert_eq!(response_1, response_2); - }; -} - -#[tokio::test] -async fn third_party_sign_up_with_duplicated_email() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - let email = format!("{}@appflowy.io", nanoid!(6)); - test - .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) - .await - .unwrap(); - let error = test - .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) - .await - .err() - .unwrap(); - assert_eq!(error.code, ErrorCode::Conflict); - }; -} - -#[tokio::test] -async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new_anon().await; - let old_views = test - .folder_manager - .get_current_workspace_public_views() - .await - .unwrap(); - let old_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - - let uuid = uuid::Uuid::new_v4().to_string(); - test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); - let new_views = test - .folder_manager - .get_current_workspace_public_views() - .await - .unwrap(); - let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - - assert_eq!(old_views.len(), new_views.len()); - assert_eq!(old_workspace.name, new_workspace.name); - assert_eq!(old_workspace.views.len(), new_workspace.views.len()); - for (index, view) in old_views.iter().enumerate() { - assert_eq!(view.name, new_views[index].name); - assert_eq!(view.layout, new_views[index].layout); - assert_eq!(view.create_time, new_views[index].create_time); - } - } -} - -#[tokio::test] -async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new_anon().await; - let uuid = uuid::Uuid::new_v4().to_string(); - - let email = format!("{}@appflowy.io", nanoid!(6)); - // The workspace of the guest will be migrated to the new user with given uuid - let _user_profile = test - .supabase_sign_up_with_uuid(&uuid, Some(email.clone())) - .await - .unwrap(); - let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - let old_cloud_views = test - .folder_manager - .get_current_workspace_public_views() - .await - .unwrap(); - assert_eq!(old_cloud_views.len(), 1); - assert_eq!(old_cloud_views.first().unwrap().child_views.len(), 1); - - // sign out and then sign in as a guest - test.sign_out().await; - - let _sign_up_context = test.sign_up_as_anon().await; - let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - test - .create_view(&new_workspace.id, "new workspace child view".to_string()) - .await; - let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - assert_eq!(new_workspace.views.len(), 2); - - // upload to cloud user with given uuid. This time the workspace of the guest will not be merged - // because the cloud user already has a workspace - test - .supabase_sign_up_with_uuid(&uuid, Some(email)) - .await - .unwrap(); - let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); - let new_cloud_views = test - .folder_manager - .get_current_workspace_public_views() - .await - .unwrap(); - assert_eq!(new_cloud_workspace, old_cloud_workspace); - assert_eq!(new_cloud_views, old_cloud_views); - } -} - -#[tokio::test] -async fn get_user_profile_test() { - if let Some(test) = FlowySupabaseTest::new().await { - let uuid = uuid::Uuid::new_v4().to_string(); - test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); - - let result = test.get_user_profile().await; - assert!(result.is_ok()); - } -} - -#[tokio::test] -async fn update_user_profile_test() { - if let Some(test) = FlowySupabaseTest::new().await { - let uuid = uuid::Uuid::new_v4().to_string(); - let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); - test - .update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas")) - .await; - - let new_profile = test.get_user_profile().await.unwrap(); - assert_eq!(new_profile.name, "lucas") - } -} - -#[tokio::test] -async fn update_user_profile_with_existing_email_test() { - if let Some(test) = FlowySupabaseTest::new().await { - let email = format!("{}@appflowy.io", nanoid!(6)); - let _ = test - .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) - .await; - - let profile = test - .supabase_sign_up_with_uuid( - &uuid::Uuid::new_v4().to_string(), - Some(format!("{}@appflowy.io", nanoid!(6))), - ) - .await - .unwrap(); - let error = test - .update_user_profile( - UpdateUserProfilePayloadPB::new(profile.id) - .name("lucas") - .email(&email), - ) - .await - .unwrap(); - assert_eq!(error.code, ErrorCode::Conflict); - } -} - -#[tokio::test] -async fn migrate_anon_document_on_cloud_signup() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - let user_profile = test.sign_up_as_anon().await.user_profile; - - let view = test - .create_view(&user_profile.workspace_id, "My first view".to_string()) - .await; - let document_event = DocumentEventTest::new_with_core(test.clone()); - let block_id = document_event - .insert_index(&view.id, "hello world", 1, None) - .await; - - let _ = test.supabase_party_sign_up().await; - - let workspace_id = test.user_manager.workspace_id().unwrap(); - // After sign up, the documents should be migrated to the cloud - // So, we can get the document data from the cloud - let data: DocumentData = test - .document_manager - .get_cloud_service() - .get_document_data(&view.id, &workspace_id) - .await - .unwrap() - .unwrap(); - let block = data.blocks.get(&block_id).unwrap(); - assert_json_eq!( - block.data, - json!({ - "delta": [ - { - "insert": "hello world" - } - ] - }) - ); - } -} - -#[tokio::test] -async fn migrate_anon_data_on_cloud_signup() { - if get_supabase_config().is_some() { - let (cleaner, user_db_path) = unzip( - "./tests/user/supabase_test/history_user_db", - "workspace_sync", - ) - .unwrap(); - let test = - EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; - let user_profile = test.supabase_party_sign_up().await; - - // Get the folder data from remote - let folder_data: FolderData = test - .folder_manager - .get_cloud_service() - .get_folder_data(&user_profile.workspace_id, &user_profile.id) - .await - .unwrap() - .unwrap(); - - let expected_folder_data = expected_workspace_sync_folder_data(); - assert_eq!(folder_data.views.len(), expected_folder_data.views.len()); - - // After migration, the ids of the folder_data should be different from the expected_folder_data - for i in 0..folder_data.views.len() { - let left_view = &folder_data.views[i]; - let right_view = &expected_folder_data.views[i]; - assert_ne!(left_view.id, right_view.id); - assert_ne!(left_view.parent_view_id, right_view.parent_view_id); - assert_eq!(left_view.name, right_view.name); - } - - assert_ne!(folder_data.workspace.id, expected_folder_data.workspace.id); - assert_ne!(folder_data.current_view, expected_folder_data.current_view); - - let database_views = folder_data - .views - .iter() - .filter(|view| view.layout.is_database()) - .collect::>(); - - // Try to load the database from the cloud. - for (i, database_view) in database_views.iter().enumerate() { - let cloud_service = test.database_manager.get_cloud_service(); - let database_id = test - .database_manager - .get_database_id_with_view_id(&database_view.id) - .await - .unwrap(); - let editor = test - .database_manager - .get_database(&database_id) - .await - .unwrap(); - - // The database view setting should be loaded by the view id - let _ = editor - .get_database_view_setting(&database_view.id) - .await - .unwrap(); - - let rows = editor.get_rows(&database_view.id).await.unwrap(); - assert_eq!(rows.len(), 3); - - let workspace_id = test.user_manager.workspace_id().unwrap(); - if i == 0 { - let first_row = rows.first().unwrap().as_ref(); - let icon_url = first_row.meta.icon_url.clone().unwrap(); - assert_eq!(icon_url, "😄"); - - let document_id = database_row_document_id_from_row_id(&first_row.row.id); - let document_data: DocumentData = test - .document_manager - .get_cloud_service() - .get_document_data(&document_id, &workspace_id) - .await - .unwrap() - .unwrap(); - - let editor = test - .document_manager - .get_document(&document_id) - .await - .unwrap(); - let expected_document_data = editor.lock().get_document_data().unwrap(); - - // let expected_document_data = test - // .document_manager - // .get_document_data(&document_id) - // .await - // .unwrap(); - assert_eq!(document_data, expected_document_data); - let json = json!(document_data); - assert_eq!( - json["blocks"]["LPMpo0Qaab"]["data"]["delta"][0]["insert"], - json!("Row document") - ); - } - assert!(cloud_service - .get_database_object_doc_state(&database_id, CollabType::Database, &workspace_id) - .await - .is_ok()); - } - - drop(cleaner); - } -} - -fn expected_workspace_sync_folder_data() -> FolderData { - serde_json::from_value::(json!({ - "current_view": "e0811131-9928-4541-a174-20b7553d9e4c", - "current_workspace_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", - "views": [ - { - "children": { - "items": [ - { - "id": "e0811131-9928-4541-a174-20b7553d9e4c" - }, - { - "id": "53333949-c262-447b-8597-107589697059" - } - ] - }, - "created_at": 1693147093, - "desc": "", - "icon": null, - "id": "e203afb3-de5d-458a-8380-33cd788a756e", - "is_favorite": false, - "layout": 0, - "name": "⭐️ Getting started", - "parent_view_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3" - }, - { - "children": { - "items": [ - { - "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b" - }, - { - "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c" - } - ] - }, - "created_at": 1693147096, - "desc": "", - "icon": null, - "id": "e0811131-9928-4541-a174-20b7553d9e4c", - "is_favorite": false, - "layout": 1, - "name": "database", - "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" - }, - { - "children": { - "items": [] - }, - "created_at": 1693147124, - "desc": "", - "icon": null, - "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b", - "is_favorite": false, - "layout": 3, - "name": "calendar", - "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" - }, - { - "children": { - "items": [] - }, - "created_at": 1693147125, - "desc": "", - "icon": null, - "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c", - "is_favorite": false, - "layout": 2, - "name": "board", - "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" - }, - { - "children": { - "items": [] - }, - "created_at": 1693147133, - "desc": "", - "icon": null, - "id": "53333949-c262-447b-8597-107589697059", - "is_favorite": false, - "layout": 0, - "name": "document", - "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" - } - ], - "workspaces": [ - { - "child_views": { - "items": [ - { - "id": "e203afb3-de5d-458a-8380-33cd788a756e" - } - ] - }, - "created_at": 1693147093, - "id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", - "name": "Workspace" - } - ] - })) - .unwrap() -} diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md deleted file mode 100644 index 426255b00d..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md +++ /dev/null @@ -1,4 +0,0 @@ - -## Don't modify the zip files in this folder - -The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues \ No newline at end of file diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip deleted file mode 100644 index 6fd5ca087169d72d4edb9ec3edc8f6da927077c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44001 zcmeFY1yE&8vMx%~xI2xzyE`=Q?(XjH?%KFZTFUzxVyuHf6mPOa_-!7--{P9 zH)2-owQI41m09^!RaREkS2E(Cfsg<`uKqd<<=-Cu>kAeD7r@!t&fMNcSKm{?XAu_)S0|Nm2gRs?ifoSi-w6!I9`K9Gm zXsEUSDEALyDgG)}Uk6#=SVvFiTv9^m99Ahr#s@I^%V!cej87M4xF8p2^l-qK{Cs}j zC8DFSzLtd*1b~x|jsCz*P0uP#Qjv*{D^Zq-*G^1LN={0LkhA|1k}vZMQXDZH;Yy7D zN)GLduT%wuYs}j7u6=fguWw*X{U;<94iu!OonJ}HY777XGUA|M+JoL%-tTsN*zz|k zKTw+c6J+>*g^Zqoo{o-@k(P~)iJ6X`mWhS=Zz$fpqxfO!NBO@*@%QpSG*kW_#Xn;B z4(eZ3{RZscHT~ZNZ1Fo z(?4kW4bHzS`M(9ukI{JWJ2=U)0RgD~3Ma3PjgW=4vnv(9g{h&H1GR;zPN8@!)@@eFa`Pg!-evVyKU0Keoo*-&ZY3tx>IBY4+QY$Bl}q z;_FqV_bbh`7B!7=-<5bjZGBXA^5M4;SR+F;VwahCqN+@_5zn2~RWSzTvrHyUCX{c} zH?1Aq%)SqoFt0xW9|1jC>2|#VhIf#l8$9VU%BjQ|)-?ejExdqr$F9Mv9Q3=O=7;lj z)j+)}er8dW+@5s&d@g%_uEe1R)_!}}{Q`TH%^`OhwCZce(bRl=(>m0Ddn4G%p`&7C z@@d+wNP&VO#2|kbsU)G8+rN_h_Wl@s>c+}YYRz=LE-;_dH;{dLgeRGTC))c}8!7sf zPZB6cZPG}mc}l^sV%+FaEbR-VqoS!TE8uct6}v zTxu8~z007yU!fj)mwdYSgW;ciliOOC!*7d9HJpXDtw6HkH^JH=k!5A2sB=MaA4DQQ z2gioZ57x4<6^WnGhal@%dXkGR!F{{332YrO{ zikk+iClkkqRPfc$zc3hLCYDUVk}hnc{H&P3bkfO!Eyc1Sjj9u+qhZ?G2Hqj1qJ6R271M`SA1_U!w!;02YB6hV^|$>6 zFfLl}XT&ST*c~G*5IsO;RCB&K)HJ@MjvDD+&zH(>w!XNV5+a89L(e9-aOCo*@Dhh3FJb^ra4lbgXybrK{dMWpT(zvz&26!Ksm zSyvLL^=%0!-VeV+;d2l|ME{yugfM>jHKRsOGeH%1h<-U5NHLkwAZFW@5q;~^BexqD z9a9l|2_R(%HAx8NyiVX_NRpb~=ZdpeTz-gvM{T?{8>v2>e6n?i6W4s7eqnj{03JuO zVsOL?Y7uhvO36dGTLpuDHT$0sOi|W=<;0(4_!q%;jV}@nN|T4K;u>U>9=G{aY1AV+ zaJED@162%lPXAt6IFNou2DQ^lvb*y>3OvP_FX*dhl7TNCb2~*A6#vR< z*hf03d_e1KVjVRO-OG`CO&Fl(UEkZlrS!eZge;SGI#&d@| zs9KZqKfdbhji%Iw)NT}dm_>K+gHAE86G_^-*XhC79Wjwa;*TeKX)bli!=Mcw?I6Vx zgg>B8f_42F$qM-EXc6yBc3eP5T zR09<%xeuz+>?q)ObP?yEEO{2VX=cjiA_YsqLT#Z@L{93Ma4)ikPvvb0S1xqSj)6UL_vvArew5$JzOZI z;Kz~Fj@jo}9AXc72Yy*Wvb1Dn{L=3)nT}v?qRk=75U4xatw;sGtm@E#(m%arp`W~` z%E3H~8OUpQ`xYAB`o(e3h{e3Cz5d0>9ocSh3iVrX5v!3WW?InaA|W_Y6DS+LT5%nx zSaAxman`l=rh^qt-V}V=(=`}cp_~oj!Lo59G3WCTr_b;z0mLP-IrFy6oiQD><1IHm z5%Z4{hBtv3g+Ci?^H5!_RpMOlu~HY{kD#T>1~m{ZVIp0vR)E?D#3Ut+hyVnfD8u9% z*nqbLfc5y}fjtn+IV6ZEimInX_p`y)!TqeI&BD_fi}_=RFPR;uUYWo*pE{#{m4JO> z0jfKomL#em_5)c?2h&%YB8;&tk&gn7+jZfD@6Ra-9M9*FFp}9GH^h34$t{cpa=JGl zd~zgI&r^$xV}+>^Ob0|8;2e)4VfrD4sAZUTnXJ$BLwY7Ck7?$D)q#FRm)<|sg#OtP z_34<<7mFKh&`a_M3U+>_FS!pHkKs-zl%sTfn#^_&-7?Vk?Ac~I&^Kot`umaZ*bHbM zMKJ_ESh~LuUeL?!uYv6hqwEEj``Xxx`5OzWp=pCaqyrNXd~7q2o7qWqW5g|TRjlbun20E*qf|(kxjO#tF$mp&ydp%ou^IgG$fNVe5UC` zj|>x&QbxQM(I%5WAEk97HkBX@*C-^>cIqdt8E|w{BjuB#E1+m<*t~(leg2_(>>(uW-E_FqVN0+8OrZ$!9{9I z9O!n8uD=+s|DyisJ^_7!y1%J=aIefX}5<4+}@yO{&*p?~y_9DR6;7 zx-~)~h+)aGm*4@e6#lNIIj@`uMnT~K_yvK|)ByN3suR34j_pz}u-Ee10@zND^CrFz z;`es0({4Zqu%M%!<4q*zCZ%C4NWTWfQA^5{FGqpE7Xxc(L__uXbc1sA{Iy~S*=-bY zoASG;;&R!fZuY-=x_QtYdS^q@Ln*u`91jO|tqZ?%nRQyjftB^uqF|-O?>^YEwz9T)cjIc??K|f(jK(bl; z+qS6E7lh{rGZHuWT#pb9rgrF|VJ+_&ES(ZF0ctm=iBR%K0rhtObzZKWXv|?oBf@CF z1kwHu&o=Xx1~p$**tHshJ{5u%TQ7s>m%seY@szH{to_ov>HGD#G;`jS;sx+%?Q6PVYG#G-^?TDrKZfQ>#vcN?yTBxeYC;l@bK% zhs+cax1`Ds>3|(l28=JEQzOxh$kWM8T;zJl%!fzS5U4IA;O2&q2UZzv$5%`A;^w-o zI+xMNSQa2m@iCTM;Pnr)()fDY5kmiX({KdX{VP3};Q$`3QSqmne{sdA!X9dt z0C0lQgWSNyL{i#;0hE0Keid0d40N^Y8{zX(DU))CpY>`X=gK$dGBW%7>B8DHI+^%^CD9 z)AP+=TYnyW7txT^K3CK5*a@Fj7CLpU>^7SpITC; zHlxZ!lr(NZ;TiP2a2UUKVS3sUVCS3Pa?s`^dLGL5h_8Qt8U%b{oZG5b?qJRTmC4Yo zk`Q)!S#qMyfY3Nhfq;3E=XtZE6JNh`kVj>jugSOK?B0=69zFir$K=XbE!}=}M9}!$ z1!Rdk4%8_lFNrZQ9#bzMxzL*k(Ze4TJtzzP3m6E|J=md5%9($BoZ~@ora0g5y>G-_ zyym|dcf6I)pjo2|Zbdj}8pj@&iwx^3ipSqjxHGyQ_QT808TF~vZ0~X&DOAK|PW80k z$7K%bDw;kLNI#nt_PEw09z(U{l$o)UCekZGb0ow+H51=kNn{IyXyLH6I#CbRL!qZU6&BDVI@*P4H z0Swvh-f{^A8#P!kfMS|yhw!T@h7-3F&FQ12x>ka$`#m^=IYLG~!iZY^WvlKLJpPQj zN-8)pN*&pNs_(4z?GNhLeP*jPpqmYaC-dS1fh6~wZWhB3LICSrP1Hac9VdN~o&C+@}c)AKK@HCEvRPovNmgRB_LtFYX=OGzzHMnBqjGa#&py@&*6Jmz*+{C#CC zxtbS8^j2K}5%;x$@S%olTOD7I2~@4ms2ZQckCkg)3B_S77!dR3iHxK3sv^;#l(^YD z^<*}F?o;$>(4f4gEp~Sl^SvtgseYqm(FIM_ElG79*>w^^wZaM4U8zAxRS+wgX&_j4 zT(SmNc;@41#gs4c7{J>Rz$|7{T1HhhL~PqcjUeF+Uo`%XkJ7&>ZpnVtJK_-rUR;}b zH_^KT4k@sBDNjh2cWtlIy5bs=^pf2`YB(uQqwPp;?-J#duYpWth5u{hURB{Ge0bCz zX;~vy9gk>&WV8w2UUN%yw-mYq6X1cP;3YV1wh-RMP%i2xhY2an^{7cO^7;l)6Eey)WB)`?C-F^4F z1n^|uRp4DuVD8l~WbUbF6ohP)Pr(f5dS$W_v$ny@SQhT*Tb_bQGcBYNg=M(kHmmfJ ztG-%6$b<;Mf1R8p_P~m2;^#@c*14&o-8}TmtGQXq8xy-B>yh`DBb#CE0Ix&uX=@m* z&j39^6n5KKc`GR9;L9@`PSCHNY#HEwv#zClJVL&Ek`qM1xd7m5_R$CGmd}O=RzJ}9 zEKsMR>Z6~hMev$aJSy%z2UIqkm`BnNEdvbj&Dq4*8%@KxLmr5K@AhpPzC>>3{p0f8 z>7OAz&$YY*AYWMUuPN@3#k2Iew@=0!z)m~}Exc~AQVj-_uNNoiv(bXJAl*u{tYt;Y zE93Kv$9=)8v-}nzf@WZi8O{bThYR^zE#ohs7V7&2)mo!Z>M0lLnT#gn>BlbXd#{i4 zOF$AHgSmO>RA%RFVs$NVlKrfMiq%s~aWc8i7vv2w#Wq3|=O>CFe5_rY$u8wGQ1i$c z-UDX7Pj!8ol4>0r6LEUkpMHgsUr@r%kkze}i~!0eE(Q@6{mjD)#~eSq^Zc>kOScMd zF}!yHyn|xtAj{Q(f!FxuE>i#Vpo+Gk&PJq$o8On^+H!T>D^?#1B!m4pZu!j(+Vr_Z zW@~sqE@VIdNtATMkdpk-)uEzu@E@bOEY=yL66}|ogeZO&Z0wt>Z2R?B)$`6smixq7 zHihU{qefp3Fq>E8n_Ws@-Kt={oo5)8i?MoKdyZW^2?aNVS9ampd|yYc2{2kb4XXSBq*DAgx}hPacGq8L>U%7;#Qv zu+=3ZKx+ppRF?2!ANlUiwhzNYp49$Y#PI`(EJTCRH_ZV1IVwNJcQjB$gqs^1GFs}uwaDLNt&qIG#adyi7?WsO_XQjX={>>}Y$*v5K6|hP^1lBFtw5aLL#rtA3Q~AT zyTO%)(Qg`_&ii8Lv?Yuc99K=$)fN*?6^!C^nXr)9e!O8k4j~vha6i4NvMqOIb-ZjX zPx1W~(a}$*Y?6Xn8CUW*siB=s(XS?KGl2tJ7gWBXSQ{y|r8L=?%7Zd7`xHM?G_U0f z>)wcss(F^#ZKDs{-uTf7%024d(|zfaGl zjyqm0M3YlNl@^ya)lIYN&R`m;^%I>7PxzrOd_jyXh1zIwkg$9*m&uK&WTojC&(g2P zq;Lo>!-*XyNr+?wmpir3?ciCl&m3#?J=*%3^TZlLnBHOLeaLP?nvr%ixl!=Vu+4Es^Fc2lBGn69$?g)JcMAms{L2B*iqpn~^2zUZt?r2iEEI24SSRe&6L| zkGR`Sh?H(D$4r{L%}ekNtFfhv@gR4Yq1zZw5m9{JusHYujcIE47q6y*&;XKAi>fO{ zQ~ojcY70Ta-9#J$p2n!|es~ypbzX2Ef`!W+EyiI^U*zt^nx88)>C(|Ki{q|u_JrF_ zdmLl#fyC3-rDCH9cX-r?OG%~Z;FO1-fE z?xC{EKIQS|>hfrYzf%ZS)5EV9F=PJ9OL=1au|sjWk?&synAMZh#0gi8*`LouZXUfAn0bPvdh<-$;KF* zEQYhT{z+!!CT8D#57dW?O`hKqlwWGmzVQ4bMVS%o4V+CppixUaL?s7-tu_K!pNbxb zVw5bVB(q8{36!L&3c$qRIi^!SeW0iw>LGk?> zjqJ+ptt+&r);emER&a(h20YZ<8AzHIj0PefEeKe`o~1G&ILq@Wt-DE6>wjf69S@t^ zy#(}){Us2haDqjbUc~8aZ7~8@z;&{quNr3&B;XWZy>e;RNNhSN-%eB>GK9PU@~?|v z9|6;Q?$gX`rtth)ru5ZJ2Q4@&5)ymtP8+7&nCZf;)RuFE9HzCJCV!Y)6cMEq`k|W* zUgRcerR(xrHWS7bWiwL`<(m}c<85%$qdA}(m$N?vaF|l4S|Gt9V3lV#_t|9^VG<;( z7GPI_+O0yje%^7iv0prL?vAT>=U_8$Y9SsE3({LNh;!2gF42h=xk?~ThN1>0&Yv%4IYmada+ zo5?4N0M|nY+v}CgH>HEQEYKUW`y0J$%dpc#EgFdvS#8zS;$l@@8`H+SldY=f%VBGp zmCDCET`g9P+EKE z;}Y=xo}Dk(FV^dnn+G*q(!tXw(nvd5kttwj!Pnwhk|{PmI7ZW4MA)w^*@{nkky)9kUhOdx88xFl54% z`fPca7hxySa$QvN!2$ug)i7jCNVPU1WcZ+1Q>!Py?8Iv*KIb(SbY}LH{6*AXZ++>n?`VKu5d=gQodY+7LmG@LK^`W=LA7}h3dLX5mD;zu=AMzkks$*$ z#2vO|mB*4tQE3`%mo~_5n>&Sf)0)R&FLc@&V^HH^|Jn~T@0#odXO^Xyp|Ni#B7JUt zoGq0R@Wx8}F>e&Gw0*R@Hd7nlupE_BvS6(@w~I|jbmN&=DDQ-R&cHzD)OcytRLJUj zTmHmG-y;@0GtuSYTeF*vLz9v%s*^Vz|4}D=*!?L*TyeLW52qwrVI>^(?oyCG=BI}W zsvWYk{}4yQ9l=h9fKIM{>UB>_*Py-K8$WO+ga^H^v}|6MEq%r;grAtl1%h@5*ejT+ zxIBE-Bt$5)TudQ;mTwGAxLPu6`*(V6yA&F29eaZuV$Bq(Sns@HW%6s=KyJfgMO!_L z6UeABugr|%@qLlQd&|;a!g7cW_C~%V)s3-vlcQ;TL~?%OLQ@6!?b};)ML`InHWCgR zTbFEuYOFB58%!CemWJ@CfW)1!=K#3MaVt+<6HKaODYPRX znTdZ=#LGDP1f1?usJfZ-HrtSbis;s#*Q%VoRTPq$F*+zUhNp1WU@QY{vJ{Nely&!o zb+CeXY-)E0x*RuyB5+HXAf~^3LS@?iwvo@+8caLab!fBVW!K+{L>}spGdlu zG1-lt!9KZ-o>^m->&9pD2adaRl?>yvrX2`b3I6@0oB-L>;Voqv8cMz>?VpX7_<)J$ zLL8S~PHzO=zdP_aSLRBnzI^St_@26V$X|6GCy{6z0qD_X5RNga>*ld7L;i^@tG{0t zja&Uy!4=&ed3YqCG{M`RqC&3sa;4FKP0{_l6tPzbh|KP~_)Y+Z6Dn~%us={i2J+47 z=gSXU_*7GLquw=Z7KxjsD(Xn4rDEqb2FPx`f_+guQBP+AXj(1TAWbjalIH;H2vyUe zVT*EE)X9_zNu?cgJ;`{sa$?3a3NMehmv2#YX}%YquljlDw24%$=3GUsY%poUB8~W? z)$+Ev>yACSLS!Hr-jnxeg?d0;hDs=3ff2Lnl327K$Unag$|b? zHN9c4>4i~<_?)3CvtBi``4k!J2Z;&`HjFX;OTIH}T>l0ojB$xB+cP_i zaScnlD6ziv8+*TfEciw49wDkNt9-+}0AbR28LV*_w<8^LytqB9{6PJ6@S*chnu2~{ zeY~$Z=2ky>;Sj&r@w{l+h5QP7#MQ*p2Swl|BtUh=MvkF1)*|;zM+K`HCcHPM+(uIPGxQnRigGw6NR&iCLK=>_fz=Ec!Qk%Sx{H}$X%UR}m ztGT(8taNW(Z;9J?nBuBjC8t!Y$3|sw|0iJd?yAg`l%n}8ZYQfJC6~QXN9~zjt*hL6 zETV+nY~JsNCWZROpchq98LO3;^VM;b#R7(0II7aZGcZ{9{*y=uT&!i1K*D7*x`jWZ z&IF`PGHTD1f^0=719PUd+Ujt)&GCud*hHQ#P}os5!LU08d)r-XX|im`g^{Ww?+<$D zXWEif)`pGOL<6Xoe(dGZE%fr{vCH_E;GS;%{E9BddHP}Mkocl_79;)QWzf(wy(8mtgDjZO_XaeA(2mpe#VpZtF=J?8uLUrHG)flk z@ett=bytY;9Zq34Jn4af{}DUzgLT#SJJuBdEdT)OUt!eO)i*JuHqiUc#@To9{(k=n#FOMzt8D=! zpA*8J!kjNw`w&G{9>8)d_dGnx2FnV3c@CzvQ`BJ4^6u#oX6_MOQY>nA)sP}>JsqId zKd35wT|QB>Ii+cSN(kEk#2X`#q_-bSXD}X15lCfwau_0-)|bswIIOq~$|)HNZmA$0 zLaA%G1VmQbgWXP z!Ltr^(89P2i=6R3=R!nJg4y-;xf{raDdRX$hL<=*le) z(cVC&^C%9+iRuSact$d*Sp+D;w8 zFn+jTzkLRHb`DxI#?F^nOf&mKn>1o#6NpgJz)hQUzD1T80B^?7*=h7QaE+rcdFgN@ zuEVwn^(1l{L@rgGvHPC1e7>@=Szvkb_tl3vrt^U68mUevxEo+bxZj2;kUoW>lfH+>}RE4qqH3z~_@wux&)wG46;QJ|y$m4@<50wun}++5;z>f8^9 zM}}pLX8)LzQ&6~y={KkXA*2eWI)l9UobOjhE}kTHk5Q;8o;=6hVb$iDwYnzk9fq)E zS6pJA`t4qihOOIPci54Ge)G4SdEmt?vRXR@j6zxKvH3!-SPyjmJ{y2dxONHtKMv7} zvWx?a1uo+gVhft-YW%a!-963s{T|u((}#dr_g5=XPc-SVLoKkC+sSxnOsWsDLL}re zXNP3+p3>Yp_IpH$h$!5GqoekewmGGB#1Y6VvJ_pZAzZ^1fp^pfHmp#4<Jta zHWy3cDS=I)RDCbFV)i&Gc%#E47=^!Sl#WGR>qq;N!^#s6FfLy?l;o{JhBiNu$=NZVym& zH}P5jXa4{9Hou$v8}0Z{rv8;{OrvjYVWF$1ZJ_tJDdF#)?4$gXH~U|s%lt3U{clvh z1O9iVfAFCH1~cyec$WA*#~a`W006^ZXNmvDa--k4x(2!qzj2`dK7-u6WBu}ef0Td1 z{3Ju&0&^D4=gwum^%lg~AF>(_&ay$)G8k&akp^CbHOdysi5jC-2TpBlB)vNLxOXw* zrcab06|Ag5qz)PE=GMnYtjN;@XQ_}|xx}SB+5`d_RwSmV2T{b+%zf9+=$5{95de?x zIm=`5>g}!h?JDb{?D=ewVYonWN~m0`OGz#4I2;$rl`$hq03Y%CDN5w4pI86V5UHI$ zF+ObOW0%0C7-32iN6j5haU+8BnWy`>J7V)i)(IEK?ctj4(nmk^sSGYn$_Xv@2b~nR zM^PHiTg1JQn=f7Pu+_D4#A|1Fws6J68klAntSc^5M$J@{%;FU6oKH-eE({l^uPjy)i+h^F@6hWs{KPE}b@SXkVlSej*Sp=98)prAppWPvh+fWyD6 zY>7<{%it^d!tuKFr3gAp`Dko?t>Bb6i83k*YFN9|a|~m#*-Zx3aNf3)MK{KXfl;Lf zrnJQ9cy4KuAJcqwpg6L9Zd3-yp+M>EO&@?_jkEVp=o9E#=Q87c%e2Hyb#<#t2QY6Ne3W%X( zg1pMwV8mw}0>zaUWzH>{DB1&b#Z{sdSn6td!_(BWhd}PrM{*68uJo2P&0!dMGgW4e zZMOeHR*)vC2EVHZ)e zCvxn{@4KVq&Qd;d@>wLQv$eBaPKl-opcNc~B+w@y#A_j-J@MbWAxu@8YM3UBPqd1c z3%*fhvCo;)+3UKwF|$31aB@5DenO%vWpAEf)h_VnPuQQENw+IDhZpBd!mYkziAlmL z) zH*OGZ=he>dFg~_NG_}3HRVzLaJ-ns^wZzgt8z|cY|UVt~+umhJ= zmG@VIhErgOYx=B%>IP65Mtb^mEoNekDm6`bOVLg(0u2}^{0*N{B5PSVCrN2)*G|3P zSaA@ZL9NPsZ2)IjV?%^pdHvOQ)#3Cqs{6OFr0VrI;bLk0Ysi1<_Oa%477n~vGlqJv zSG5WieRU=yBF8l_0Kkv(%|@D@zWKS+tmKe(ceNq&Al-8a5)9yV1Gi;~)u#2fv7?lZ%2V(^lmjs0@j#UNj{ch`l)<*k6lZw<>$z%n2Bv z_a0Mn>r;M4P7Q4egO?XB=kKoLIGGK>ASOG4?itEmCN`}{rs(+zJ?BMf#*PKnk{E{^ zYU5fq1#|7!r(3qm8ajTFQ}L@Rlfs26qGz0vhnmv5mUwFnjteiY_xPH<2$@6Fn@*K} z;AbH9QmMG|hG#)K>2{D}$&5oBjP}_q$*7QNsg;tAC#dGRdxMjPI0iAMWlBr^!Lat7 z`T{dXfmx!Y+JPTS(mgf@tbFkG8F{qjn##Dei@oUVNqRN7O!h@O2bP4;XFN_Jd6)#`;DkyvHG z*%ZYl$@=txZ&`_JAcaYogr=PA0R>yXE-w+_4XPiJY*$MNbk|0WT4GCiJe7=bLJ?k{ zYwWgS=Bu7EU-ewhbbHjP=iN8&ha=Wjl$CsA;AL}FUzjqv?9aLCr*k$mvAQh=Oj~I^ zD+_fKJHaV=`k@}W_YKWMUdzlU2f|Ep)GQMmQe>M)m9CG~_h`d}d*k?(?FK5w^B(#3 ziX5ih&nH-hV&meuM|oWWo*>SnSffoe_X{|YXNyv<+6yM!w+VQ;q4fDIsSAf5M|8wT z4C&uiH}gEYZggx_qHTwsx`XUjm4l#y+4{Y#Sw-~yxb>T(FdZ3v`(z>770@nTH0duF zIp++cLOe3#4E=*dMZfbnEDjht^3(llGuWUUg8(cvgV;5YoM`4#+F|npmK+}@tSmMA z*7AFB;CZ!t^YMvRvj+X-^;E3o zc@;6}z23)XXBC<6bv`_I7M}Wj6|i1U!yX@7b77ueK6^8EbaL#-c)$Hn{_UNJvx>5D z8*ZCm#QUeJ#orHO&FTOIfG_GT@-kNUg+Z;UCF^{~XpR>vU5_{#XU zyPkyqblmv3E-w%{9$GWDgG42K0v#$Z%DXKPhzgNRO5YduF68WwLe^=I@7{u4wRAe) za2B0gg@5Oaev|{fzJmTkp^r6aIe!wp)!dfQ?mojf_TawJ=TbwtDYG3lF8i4uJ0Q)N z2Bk)-UotK~(WuuG7|vy$T;E)AxNbwo=+XYtr4yK%Cxx1Ktt(b|b1t!r@iRD>hFL=8GUnF_R?AS3*(@7alRcy|3rU7mB zpigjDo2BZq#yUv7I))Ic)*{BO8?}e!uzpub2HSGxP)Vk(8_Tu4VmYBt!xXP2a-X;+ z;#jNeB+E1%uU0AcMy<3oQaERspkYUbO}Sm8)Pfn-T`^cxdZqA?Hl?k#v|}5}!(07O zR@jEeth_EbTpUe)9CLJ`S)4(bzTs&y_N%sA7lD0?>NYmnM# zwAV3VAuo@fJCD(g#_nZIr3vTNtIO*SL{#ov8mYn8poJO#sJw=##FL$4%-MOS$KdBl z?^qiw+VP5?i24=Z?!5YbgjOBDk))q9&9S!7dFuumXW#WSDU}`;ATqz8Tg``i&1bdv z^|w+bK1Z!yB3hob#u|1)T=ec@<3~$bIt);r{(U87CLRiF{@8?R3mQ?SYraijm zoV}1LHi}mZNQbO8wV0wcJQABV63p)!dczF4al&zR3*_nSg^9fZ??lNkm&sl5{1Q=B ze9Z=xiQ)34P8FIJ3b2$n_>+38-kq+@F*{UPMldhkHSI#vVE6Ygc3;wSgL$3nupVvh z<$`%lD!VaZP92*OTn@isc6AS=)gBQ}AKN&ETX>H(uDd$|`;)Y$d2rVa?zN;^1Tlp^ ztItSn&xusNvq;qy+(PLWzf&~K?xD=i2e0NdEb$+LAv&$*Xq~HrH49n>uI9YL0o!gs z)bOK{q1C$qgU)L+%BUOV3uDc|3`=lNslM}W!=3ebd%pDn6BQn|mDm8%T%=P%0!xh| z6OKiTD)r{zA-lX0I6K8)L4;gR#fwdaMkH{Sb&k$MJdTKMEn<*3&6!|*U=M>vBkiVk zZc%U7y_(-|SQYRdYuh40~UgU}JU2gD%h6OxTBzNe3(<|)9Qh#UA`v`_TXK`=$QxS7IMa zeeVC$#pu6Yl>HklO8=Gy2rK?`8W`s}LGbr!V2JnZB>=$hPs+ak3BdQ)mFXXj$I|H9 z*!%{8_8)$JKTT??uj^oHZS{Vr^xeaKoFV@>M*8>rPYczL_W98E_v@d@g5THYqkSa* zMf?1r2JdI1|Ie%TZ_`}=`>g%{U(ID=>SXwD+FXCB+W&RU_50nBkG}qK4*gF_wtsAa z(EQ_x+RWbC>bExe*lPLv{XZT;zgPM(gy{d(5cnH* z8q&L4`{TCEGdchO`(J_oZxU|*NHo$&%P5FSOUYBweq4V$8UK;v{Ai2+Cf7NuvTV1_ zjNo~vTtr`qAslNcs)dZRmD0qlXfGOROj4XGmW_)LUxg1&4Ys5t_i&ePL|ghWw>Jk2>CBrl&+>F_ZzZQ5o@ua5$A%kJ)KL@h<0Tdb6i)YCP8pp< z%j2oU!oO>shB(gZ(f=*JjK(;L!Q*Ziqq74eFFV}&%!=oMS513}q&ne1*H}{B5T9nr zs514J`w{_@A(cFHH+jk+*DDG7OR!kM7t8e7V#MWK`N*OK3^_Ozc2Q?;|eE% zEp!LItjk}B;n#sbmo8}_H8Td!&Y}H=A14K&32s>@*LgBTIRIzOKn#aFwXwU> zLS+^tHA1;X7;)ulU8&xzX$Xy&lOcgk0Rg{QN9l&3Whjz#L<;GU7kO|T=Zrh_j>q%m zz|)(zu@RFsU{|&jACYSEyd?Mw34)Y*{k-CwbvnOHz*sM7EZVu3n6SsVy-xDM?!>p( z$8Gmb4c>GX#W`AOX$_+U{(*xbvL{@~^W5zi7O11CIW-kRGtw=g8BrVzW?`+HGdy z%-=8vo$j&Dn-|3)VY-Mki!K1=(CqSsaeT#dS=E0LWSCLtGZ?l`oOEt$rH}*?ms#!9w8{pCiogOKTgUMJq4Rf{o z-TVwBkse@_cEV5SYZqI75%01~2Z@wA!N6>@Xw^lsUQ!H3!CFOo7aF@b(3^tjglM!b zHl57kWRxsSm+l-94v**SIRP-JRnZlJ`jQelUFfKYi5nIesy)gO=>`nE5Og8Dm7^|ydJKRE_x6jS26bT#;7c7_YT+y=nns* zfl+dIK{pnO+_X{xel%e|haRV^M4B$j6EkeVZI=+?F%}&hp8EuJKISe3lrd-!5!r~( zV15w$R>TaFrKp_tAzx{AGH9x3(kbvx_L4B|UQ5Us67S3G@LX{re6cP@9ibS07a%Lq z7dqU)vgj$n60B2y1z|h>kY+7mPPq4@U)F0nw%`2u+oCU3<&h#l(UMK!P=7H38Uq<7 z<@RtCQNn^4-Qe%Zs|L7BXMcKLba1R!8=1T6EuNb86WKSOMr>`vjcgxH!i4><;O>qp zRzK*RGGwUUbk<~YbX-<$J0v_W6OTDFY2I!-@SP~|fKdz!1qW(h)U~c zE3y%V)0EXt)Hzw04fWd+d)(gOwVfB%CFpom1Kg=f>E0kZ&s>2OWKM`SU&&h97wc;~ zyrEEM2v<5RNjMj`V2LCRTQMJ{bLMUca0!m+YHIR#+? z$=3tTAbxB{R&Bnr_GxAd{_QAWX7^=l+i#V3*B6kARd-c-{LBeMfUUwA&7Sn@Jvz3( zy2_AJP?5KSj$|H%PA8jT0Ov-Nr3*1M@utL{L7v*EB$oF>dY_GKYc)o#D_Gi+@+D&z zcWI=GlTl+Z*e8k%no}j`kBGvzVO*zBmhPzh^e7N0Sn~U%Ae0LxwmZ*aFFndo zG3sc=UYTju69guu>=BRB{7n~2?08oJ@{5#IH?MQfHW*k1bLOXdYg-XXk{isMc2E-e zM77Jj%1s|fyzF#5%k3P$Sds1FMm+F#xN86N?^SW!pE8t~_VWGD&0={(2VYGzK-m0~ zd8=hN?kOyv2eX4#WgJ7~!?Qt_&)I{b69bR8ct; zew%3pNvp{Gp#>d8CSsRD+*CyQYJ9ziOm+<(NxkfIkG6Eu zkr}1ArM}J`2Ydr~S9HgzSmi3bh>DR}#jBjz8*s#|>QX#nKhYt#ldLj2erC%5oC^J%)hQ+rJ)=Yt z4BPS;(55la0&<&mkbF_baPS3ZZ5p@_(KE{JWx)8x5>~hI7~9$F+wJz)+Zt8>3%os) zdP92~aP&9Q<@HclkJAACsEH^IGctNAB`OgD+G`;$%Zd^TQ*CN>^*DjY5Uk@4y)j3Nt zRo$_itu#8nR7?oP_KJS{VG*lX=xbYNAk3I|?K0CpZ#p!KJmOpwxw6JmS_$?md^wnw zwXYofRy3TMf0|PvahuHZo6Xj=EyGutb0*c>FZ?JQ9df(q zMG8_i$git}3fvR(W?(F&$rbyPglsFk;s=6u)88`Da`oodVZ(|t$42hMb2D0YYNFVY zL`ZVsmMU1=Z#>a>vhQf-=V>N^Ul}JkR&}hB8QV6<4&z(isn>q+WV0-D4L3!!Q}3sW z(YyCe>oQkI_~X+tGHLc0lxo9tdTgN!t+KGeLy`JWgal{xrnSz=X5QSmw9hgkI z$hKx?Ktv&)9-+VEwvtoVw<1{6wX5C+KV%*&DNkXaCDa|f`e_G&e_ujk=ZXrThTDIz zN&v_4ykq_$5{!GGQpibj`Z9h;9-_sM+?#@}>wv0MDv>>_M>2Zlmeri>z~E2+aMxBVb_;N`iADdzyr0RZq3g8p-j z`}?i;j{sfpUj*p?GD@Y^{ugoTzfa8mzfDE_HD-VL{~EKi{evs;pP2nWG5ddF_W#7} z|B2cE6SMy(X8(T^v(KXyPOjVyPRFIadRIrLM!)5&*faK(&W<{`)~ZAxNBgAH%!ijA z_1;XeIGJ2bKKJd|&-C4RD$n%88lEFk-8K7&rMBb~(oWFZ87qj;a_At04Uls6*pF8Q zObb}BmL~GiX|st%t{4EgMhD$}Mx9$xn&|`7D{+%vGBpD=8vX|>38`i%Y3pYhhN9`!`9|E0mg z-4zlSN4Bx^B$yKq%DkDf?urR>7TkCWy)#KhNQVd`Ynk)U2UIwdGTb0j1$-q8W-%AXr<;2GGUuFq`?S_wxMAUy zwQ5>(Ko?Nf;a)t%FIAkTM{(aJOzNIhISp?iMXNquDs0o_n=lc(S%AyMATV9gj*BGC zm7aWxD#%eksE=Jw8YLlsf39xrv5(2%!FQZlQK^{&eQ%r-_~FwMt+7+5>=d~@uS6cm z8yRw|GV^_D{XB}{Gp2y?Nh7Y0$$_M7ES#DnhGj?i8?IXG&<^F!PHy6`?GKlg6x?wJ zw>B`g#iS7$Uw2H#d}O+FR#esTA?tWd;op> z3X!Lt%+3W1QXOyX+cG&K_wA**S2Qf|M74yo#?oEpu{?ZhfJg{~c;^mvnL_fr${vZ7 z)4NG6A-8zP>jPbVw?d;^jU3V|s4;f+b7za``U-AtiM5MTL0%lkiihRC@_l7RYF`@n zaRJv!y6C$bdsCRLQUQC6hDBKc#)KF_p0O*u6@=q)Zpo$bDDZtDu3Z{VPQCKb|H%;s zygkoEc@ElNV$lEpgZAGCs{eIC`;P}|x(xsTUXpwNu*31UuQ0zYurt0?qWvvkecc!1tlJ1FCm2_HB$#*&V#!&^PhKGh%%#t^tG zJ4{G5K#HPjOVwJrH^3kNExWG7lNuJtaYHXGvp%$WQ^1?9JgPH zKHfeSOeWXO-CnghUO9eozk1vJ_0hi_4G13SY&7^W6l$UOT&cIwW6hL{j|`C`Az`?o z3;L+WP+@}LYBLl}B5Mqqtf!|o@ae)XjDjS*nl(M74XY@DE13AG32gwoWNg9q3*w;IXQqHe4Ffl@`P3;qU0=W_4*#=hoeD zrDMa0g0=!e2}d^O(Hng-qDoa+m@K|d`nc5{ut615uqA6FEelPR8qwHY8?sO%Jcv4t zh}nHodLs0=lfc-RAh&FzFj=#4AKi!ckmcYeAOV1l^-XojS|hkVcy%2thWK zdmYOm;Cu79hQ=@|qJ!hTYPHN+kw;Eu#Kn;1@2!CGijl0ZCR&IrV+<_;gH+EX)57?j zz35Rce|=VdO|s%!cPQhUY@%|BqTK{>kL=hi3cCd+v{T%jG1#MpVLKH*I|K+=mT}{7 ze+kntcI;#`w38O?DG`Q!@{I}hmV;aDlk*c(DfE(%1Mf<$ zHBbAGE?dv^P$x~e#zpX0u`1cAO7#RMih*jcU7%ZMFfZ6=8YM4Z9cD(6pe5{Gpo2q= zw$9y76$QJZ(I9osx(CwiUt$;$7hGX7c;k9BFvvmE8ola9Q{9z%izrn=CWt+iH- zyDoD?VwoM!eSKRKt6h8rM&*D&#vm_3pu4*1nWr?i1S&qDJDAmNP+p-$IzZU^~7S{o1pq=nMrqzA>)&V3~y#Ef{O45G|qPUi#azZ2}~iuVH=N zXW$SecbMSJnhPor6k>{_2<$};&N#_$DV4rSVtA}AC7;J} zru43oQp+UMV`^n^$|=ClExfSz{r-0y_hu6 zkfYNy782r4s3#)gs$@>NyJ$y2qR}8F7)rJb2zCd#>;KRq=+^TXec8&d5q$po?iq;< zbq?z6@SJ|iSwrn+?%XY6gk0a~@lUNba9uSr!@&psHefN=Px%R zPqR87zF-fW7xv1v#cOUIb*muD}pkzH1^H5TqfoqtOe4vG*!s>}# zOrZCy!XW*6jG13>Qp^$`D0y@Uc|d%+#AU692;S@H`=?JN8e8s*6VHW&kHfQpzXFAy zhkQC;e}rP84{=3h$N4a5?(^07wn0tdAlLnsxt_BlU^$uc!2Udxxe=+nKK$#4FlCKk z2zftzLHF!dR}NyNQ4NEBZ5^R!-g zVG01cm7boiP^w5H`$xq1(T0$eRJE=IHd$C@5_!BIcAvX$20b6s+G9J zbmX6X`4~#X1n7oo)C(orWe%;_e`KVB%k{4F<0TzsxLlt#eioV@tC>xWSckMAe_~*l zJ)r4%kQO{OwibM@*kS&TgLzeLE}wxC`6SdNRohU4P6fPu6X`2I$73vM2YP9S_j1qb zQkEPsfmPG$^^)2V)Y4$Dd}n5yo_ziWo#cXG&zUx)QssL24u@lF^1 z+>3V{zQLbH?xlj2D~;w)5W4)U!1c^OTAG#6438$`5_Up?1S*#Ey*0u=0UsPm;cz$? z69;o3z7ZooA^XN|$1@N~HZSvaRN6g4c0r#FV|)rO`1A}&E{F=okuY(0AsrWMPd<6g z!l&B?=nZ@U=7#uKrv(dd?wWwChb1A}Wn2xGIMtP-L}}~zSzH)ww+#>VWIG2_ir5C2 z_6>9Xaqz9ryjNsck55QqcuC&51s7X!s%03N_`8p1=h#_U;`!4dP0e zL$;x}roMLMx;0O6XG$l@#z~f%pUs5Ri^_zp`#SvI19F8GtI%gsMGzNGioDUg=_Pif z$}uOWtLl0KcX2gHxnNN^PD~4=og#117}Bsfm_s$D2~{M^txq~Io)F9*hKjK5(0g@G zap-;SKzyn$r{nz^LN@rgCg!?=J~-Zc?(xuKDk&zos3n8-r z0XmE)sm@1I^|R{-u@wz-nT$OM#{v9^g0rK|Wv%vI50(d`wok({=*sExAK-oRrPyMz zSY5OvpXBV|WN%>Pm9;=R-)}J}kY|#~vZquw+~9b^8c8p7Fyi8y z=+6~L%Ybc&Z^ejE(7b8mueB+Haj9dKo+o6pR91&7LtH^s5z1a)7a&Yc3pak9Ff`rN z$tG-!)Y)Ft*S!qQr%J|COG=%XUu-c@3#*+CD7C`eg>-LB!az4DC^r_lb*Bx?sL>~r zPUQ>e@O?=h*E7D50GJisc770t>Wj}B-H46zBSDky#36%+bsbxiKJ8oV1gsnt)Ks!$ zYROtrIqh_z@X&rJ|Ej3D_|?x=?D_Z8&)@tVcYCWLH2144g?n>%dmlg+bf!7gz#*Ed zu7fIL8EaV{AxHCD6bMfK)ETD$gz z2ESI}aL{5fp`U09(eZqX7>keb852eS{lqHWGSEP!fQJZwQEd+pKQqoU2+4VT;AV|= zwwI079LCUjhUi5Do&Ru_3ytf=8i(gYILDIuW)6t)QYjSXR-B>fSTj?IO3(%ak#mKp zXi-cy^VAbvHw;*V?Tt^gi3qZQ(o_c-b?}NLa?BKU80t3{X3RBwHjZhNZCW6g`-M7| z9kz4HVWaVp@&+Tm$0n9_IPwf3QXF0&LLeOF@LsORs9DtbDJiL#cXs0#789I`*0EpN zY+}C_XmUTn=rm+uFwAeq=f=>iLJCY*fkz7r0eSn8q(lK(^F!5vc6tmQjH_yX?LIXG zX7KS{4Ncqxs|2yCHVzo8Jmf6YTx^E#WbwRePBc;SF&gvCQb(n-w0cTLRBQoTMWBF! zJ#mNFr-iIrfh0q^0vYQLTKh<4dRWv~Faavn&fpB8P1pakuDZgJhWA}YpBvYl%*)_ zpWcsQLJ1l5h2QB*O1%Q(lqOoc5IQZQ>;typkv860YT)q;d7MXMM+5%^`rtTw3~1$_ z%yj-+j1OS9Vt)%wvdv%`72?Ac#pTV}v_5bBKnmM2Sw8WF8+uiQVgUApA4w*u&Xx<0 zGD!W9^s~Vfk^F-mw4|9qRBx~xK2hNWnVh(|O_p%V9Xj8LxG1Q(9%pG@R4kIWrr>G4 zHB-94i1XL#<5_}jHKk!TZ$G4jPVP*>h(JO~+9O%c@v4K1q8OTrI?!om{)A%NB{0J# ziZs58l2&_j+Q76)Gn01@B@5;j>uGNvulkNn)52{nirq~>*tQ8M2D;gj7;Rv+dDCY0#}o5aWGftL((=XQr3uiXo%b(C0TI5w9fZU6Dk)%^jHQhrvuOA60-T)of${z0dT+yz08iR(sB?soHx%ucT_ zAvY}={nZ5u;7o!{NW$icqz#n7qOAH}%DF1ln|UniS~4r{bNXeJu`cY@4_BZ9Av;+@ zr=dYdApKfmjt@YfzRJ68MqtaR==PcGX|FzTpjU+)g$~(i>J{Ai({|XYZsjPtN;zR! z;&(ya?mF;=#*hoysOYOpJ52lg2PViw9%<{P7&~d<`O+cU>oE+yA66hX$@uhs*nrrC zIVA|x%@!GvDK)uhe(3$aR?u#P22DTowF5GONeHaff{J)p?-uMLbkwku<}_Y1dn*Q^ z6~%_;O80?AK>mUTcP5rwH?|$K`9g5ea8e*ey)oiFWwqF|5y)p7Gka!Xa(CV_;r0+( z6Ui8hw-b&6*yrAu(UD}BLeIAl<7~r!u61^+ZOh=vm9RO=U!Mf;%+#*x4-?VlRxQb@ z?qOT&Ff`}-)i^#mDy9KpC ztkk&xa(RihKwQ-t1P3}dv4FWT1KbUzEM?^LswN}gKkA8K8s}uGskXDCsPpa z9bZNV!2NAH)6)|q3&dcJjHC(b9jE1?^Sx=aIAV;j6Nsc)hc#QW-AP5e~|qoOa~;1pg~iHR1Y%!be%M`{#CqM|f# zVl$J{D>WhJw7&L%sSbEwjO*qU3?$ULN`B4x_w)?Qtg0Co+IaT3O%edI>8m zb}Ni!2b?FBUdUDqqFDFdp~(Be_mNyfDi;+rXCkIz6}{@4)a0;9NCS*SA3p0z9JMh{FuS-jtb?k6-9k zktcXW_qqe8l{7z%lK`G8DSrS<^PieJ?5l8tti5PXgUeM&`EilbIipiW*s2j#Z)aA`@#_ABuxiaM|h)v-N zdu|97L+h{TN2lEm*9<40e-ccokZ)_KDd2Kl9%D~61HAqG zFUrtd8XKQe_PsO3FPy^P3}4{&pJBbzl0SC}H`oVbJ%(#ve!vC*R`6a0$~0)UVmdv5 zlGwGE2CBjV_F(41Y8#0rb^!>;bKwaFEFnE*e^r8f(ooxO!7Bn%?o)z7;l11#2NExY zXBhrJ?4l>- zF*$S`W)T4k9m_rpy9h5|<4J4IF)nbkt?Ik`AA!j!>?<}&Sq~eIsm(#9$DGZT0|=Dq zId~Jg1Jd)nKUybv;s88{8UPvV0wo@G2Wy7N(@di^vY!BTHL2iOz2@igGbdg@QIwQ8 z0du|B{NUg6plz9?xw3APhrnW_smV$LDfx9!euSfvsNw9Vrs85(! zCWJ$Vb&-4O?d2y>Y^@0*Q@&TBaWSNrQRXjAaXjT)D?Sz~j@+iTc|PB3@T|;T)+L_4B+Aq^|6nnw^aW8v3A7e|OW{-}ZAVqs@i*R1g)uNJcmx*; zFmqz6!i+*Q)hoEpdXH=-W`_!S(+}~4kx4O}YP>Z@0tjcYPB*JDx-NK$PT*=2O5iPe z^*W;R%%Tx2`(BA*sVTmI6zr?9X+FZwf0+Sn#|h!I0&YvqiXg1Hu@)|STgYQtd}?e& zw^*iQ6(`A^FdS7U#G>2>umso6ICHlN3~xcdJlhbHpb&1vt?@23(p2f&xP;BuVZqeT zxRK9QG(Tq!C1reqOk>719PewX%eiqf*4MD3_cJa#{hEut%LX_3btAv!#;NbE$Ich# zcTV~8pfbSRds)c+oHE``N}%+|4~lr-O62gq4N*inKfeJJ7#f>1B1I!tVx3*L09KNq zo%3_q5MrDV`j{vs+z0bal|G{HAtjnV=48tTrdid>r%wRjRQigZ#IQm-XX^;+HQYsZ7EKa+DN6 zX%9zhrE^?vlO-krwCdGvS5s_e76A~mxj%~| zLG6D+PJx}$!66nHSOjYb!=Ba^1&*g`y*)$SXeIPY^qln*l^#;M)1xSVFC}RSi!i z@-&11O9N3^>oUnKJKrcEKW{@iZ}8UD>1P%WZ;Lx2H4mf0kjco!FP!Ap9IJs}6Vcz# zTNQ_`;#~*1etK@9q5U!V%?W&;J^RVNMAN1}riI&>@nz#K#JhWQ_Ol`Cqm4pi&(w%8 zy64MLqqQA6NkzK&9N8?uZsM_aaT0hei|YUeZ?yI`F+6=ul%sfkCe9Chi+xqSmg>4e zle7?4$$7xl_tz2XYZbi2h!2Q%&r5Lenq~M}r>!Z*CUFmt9~!tm#vNrOHIzqci@!(m z9V73kDxg>H*i5(N_VuamBF~-m#+*xD6^R*FtO~OrnQ{_nWJuCKA12IQOzt$0YmTgP zzPx(PtVnnXYmG1WeI6*-%*Mjiat90dIG&BJ4ppifmJSWcW75=eb{@0StTCiIQR_Uw3 zL0BMGE&eZ{%+j~@ejcwV)#cLL7CW-Q+HTHkM@pj+%h0BLDad@;JF`#{mQY|sz+mf7 zzM2xFU;|kdA_sz1CEHlNhPAJ%>kaW87qLGn_C6ie1YV`4Knd4^)|Zx4YSJq1iQ!-A z=mMQZ)@XWE0dQ13H3GuT;>3W6bW!>+ml8;qwzsx8OYSOl8=_2?oLfl4nH^1Q+%aoJ zl}rjo{pzF~Q_}^_dR{l)4#PR-0AIM!)*-QheQnp3`vIz?J%BXP_#zQW%FS96ll>!+ zo3>;T6~dV5O_AVf_5%t0WeL6~(yXTBiX(1iyhbrx>k zBm3R*aYnkKg6C_$qV|4<()N?dy4%n&x+%?F9`cw2i!ala<78t1I*3LOM9<^4s2!qP z$@6tkbyILpUJMe1IY(8xc6iUL0M=K&0Exw(vpm%M%i(E#dN~(~JXTKn5BwcDk0}KL z$`4F)xI&;VIm;L%Mg%!%dF=#~+T!{(Ud7*@&JvX&;FS2y5U-e1P^qak+<#4tl>G@Hw#|l6!dY!Z!sGTHkF>YsYf5?E-K7as)n=c0uscaS@29!t8q{?8u@kAB zd}5_4RT}X2EP$QCTTT-!!?J3&R4C4x%swHSK-`q*he~MBjX8FH-F&MSI*{<1;`X<{ zKt7b{Ll;=qdAz!J-te(4GGjCA0eHUKJ8W&KdNX{O_2*GJb~U80$)_Kx*8}*%C)BD) zr9ldoA2^g9?XgSpGWUW=wYuY3HYrIZIVx4OP-|c~RI`KmT8MZmYYLnfHqRsSnay@K zM$%N*T3@|^oD`{onAN-w2_pir{2qVP*t)-;?Dfn3Dn8K zp$1x$5Rz|iovtbY#FVO&!7~D?a7L55Sfq?#KMviV?6Lplh(6r?FIWz*Nf; z5XDQ>Hdp$O`F@=6YYf9rauCZJJyuo}U{utIP)lh|N9u9Dg5``EqqZjqI&Knh-1 zzC*Ld<^-DEzSKJ&O{)Zp3g@;j66f1jjz+n~=0v1j)j2XAZKVW=%la7JC#NW`F+8&h{1TI1>P%EcI``@}V){ z9PXLp3)i3f$}bi-_@`jhntvtQf3;M30e%Vz7|6v>4|CgEchhx}>YNnQp0*R0J zw~8=TN_b>tNi5MZ+~tR63t0{rbC5NQ1bC!2gto(~&ei2tv&U=d##lvajn6fSl27mn2il}BE!r;NB+%q5sdf8=y4ThWXrQBknY zDDPiSzZv1I6~N8FgkD?Q=cHQqiyEiP8?5KSU zZU_!H3ppau&8?lF`x0woq^ad$tE>FsX6E7WTjdNmdfcuHU(wwijUMo^R5a@wR80!4 zt{#T)T+T&{l+aWsb+E0Yb@tIcypa=~q{3sLoH@dqZx)_a!YLmLwtDWeU~dblLkWVr zvU;soSoot8FS<86`a2?bCP86_8L|7GlYM6E!kV``V}%jQ9*o3unm9=`&)9|f3yIZV zZ=|(%h6rgcAbvemMep)c=N`i&V7W=)dbo1-oHkYInvHw#ps*5}=1@s+-tEK8Z0nYP zb-=LMF~VpCj+uaT4ro1z=Et~L5hRx_X6Z2y6q*H7!x?dZ;_b6S?yZh)1r8=wiQ^`9 z|J(;bHf81$d$vasi*h;0rJVp}Ovxag+qp00UOyMO@K#@PCYW+dwtV^KgN~n8;L^Ih z{2`0-y&{RUu_k)^(^+0@?(0Q*ODGA`R$UQ2 zj1eFm88)_J@n#w8USyfvuJ-im9l6I1WQ%L2OcrJ}m)?^BTi4VGrw zQ+^!~6>%!3pCFo6lkSW>Xfpo9X> zAT0?A^pv95JO>P528Pa@2eocN`T-%YOT!NUtzM%tdc$z7@8yChNNUm`Dr*STO})cn za@2tbYItil5aW|_-fRF(ISao(H-_E9qPU+>-U@N7lk#3f%Q%T@AruA8K2rPkRV;%n zPKipxP(TEMVt*=r9ToTtxkXT&$R_9QCGt(2peXd_)EO|;DBuiDrCp*!Z6UY zv>%laifF~d*Y~6$OOfoQh4B$0;AL~g^w~sQxrR!lPkq4)9uUXFOBtu#^C6ZADj}GQ z+w+r#{p=$KzA?EDoo$PW@TkM=K$JD5n4a?%0jTfyqe7}|l$o$RI z=Dr9rnedqn+bVYg*i!OT^5R*G8-yB@DWH^hq46Y0pgb3&JxECbJm6v!#NpUZRZ+n! zJ4xtkbLX}%qG;ki>Zs#4XuOD&!!8+ypP}m-IG1tH8hb&>z77(XrBF%sf^X!}^1v6R zVOMQY!F=1m@~LK}w2fn&WP+*3SUh{LM%dpfIF-jQ=ntw}0i}}c7ut~?JJGA4;LU#k z4lpuann6G=8~oURuM=|IxE}_kpEh*PzsUL83OcD^M z`q~}YMw4U=7W^Q%TUAFOIXNYTiC41XN}3Qd#vFEL9luJK=mGoMK-XytKzN{DA2XRTPBZreBry)cCwMgnP|Q`m&A9;eGk}wGZ1pNYLXZ@XTJZ+$sYrMyTa6+#F2AAU`jl>(AB7}qqJSPu(#)Mt1uGx z@!R8In6q)Rm21LWnU@$k_25AiRfWC>3B&2EGA9r(^_nF7BZ$_R_!!}ZV{$oA4ipJR zl5tS%nGHX}?fY1xha)Jg4qLVH{l}CzKeL%%?$x_zRPcWdtP<;5Y9V<6tg(^jcmWmAi`z8%ySUte@Nh zL>u{|k*!xNDJ^la!jw5bHDNe;x5CrhBn4IW4=>M}-hW%wVaYdSUDO6B z0lxR`HqoX*si@W}tR=pfUnq$TE%k#7iefG}z*!F^^n(kBhqnnkSiIn^vMwK3t+%M* zu{rxx`t6Ds#1yfn)STHT|N61#s>LV~FFcpCyHtkP z7WTABqTVK!lq%!K{7Lm-V;~vX%1MH)LUj|L<&8kgBw})?*E+ue=S4ogS*~Y&Ks2A7 z3%9^%mclOzz{*LJ<5u% zStw)4kh%f}BcY?eDjSFxkKzPB6F}I53|_ClOY-kN)9c0*F3ay2LV>C(I8-VC_x8u~ zAm2norMIKZa@11qNcxtMmQ1F|EW3=H!xuVy7%~SRMNy#m#zNsWU?%ui^HL?A6Eo8~ z?tI?}M3f7A45Dyp>)Gtn(UPyGohQV2Ba?A)*o5RFNm_)E?`7`3~;6<$Dr?WoQNQ(Rw2*ygGt!mViuUxUDASmM)DZI}S+ zM^xvyD**<1>#O16i7m&D-_VlI8jEyQhWNSnA-1QqSgF?ID6QU=#)u1}-l@Ojj<4Bj?>U6pHmZO#5x5Q29OZyfu0O@--NX>UD}}vM4c`ZsmLu^*`w|bM*@iJE z2NXJpP6DWEqV;XMqXA>igQ+SP%t7N7ry_79R4qzXDHu+)VRpIq6tbzuwkqr4|C7m=C4FNw| z&XRs&fbu84xMl4Soj4@_vP$lOa$5cd+}CiLqiP;{RE-_Jl5EMxG#pWjsFMsRVVjcE z(zr=f@CZ#UeHr(4rOpXPw1R7xW0eidCzotok3dny$k~TtS58}B6?~$C#$3lRHR)cS z1LhGA*BpHWBv7k!*y(o?rc-+`c#H37ju?CBwW3;dAM2$S3Mn+*>p2WN`#=tEoQ46V6I` zI<_(|d09WAvMMX?x5=tN0dwdcCl`cRb zd1j1*kJ^uEjdm4$N?LTm2ROX=SoK0Z)-2BUUjvjOa;~b#E8m%#ZEinY4OE+$V<3}F z*#l|lwACT)tqL)RUvlbmB}VbU@avOq-$la`$gIiifRaky{!gE@sdY)OF%d0;?*ptDwqhYnk$cR>vQRQ z0Z-gyU2Q~bf@7uAsmhl?W~EcMHp+0=wiYR%`{3a@>hQKxjV9d4j@qNtk7gvMiQp^S z=bgcEjXSWZ{ zbZPk()pB#4fo3^h^;yf0-sb3DNJh`R(01`ILX)?vGLs7B(}ad!FQpPk z?@+d=BZ?CyJ9Ot5)M1ZHHB2Hxj3^)FbYyFGGi@)s8j*93`C@{@x3cE*Ri>j-jCL!# z!AAR7R^a#^vOa^ddX~&UTgX1InX{8xEB+BRQdd?Lv#)XmNex`6qO_iFA6xFu)9=00 z_2>%7QJK1Rv5aBzW8Aq zcTU@OULDwc6Y*$c-v)0~M4gUWcz&siMzZ)^w2%ru=X9HvUUbTVY$6r)_A>j8uK!cE zGf8`yh2oW4x4HP*f$rVeQwmEPGT7|o?fyz*;FPQCV^4=`OJa-mM@C*Ov`^@o$Z7SN z`bpIYTQH)8$anPkB2#Sw6CyJ!0zP5YKzJ_4a0eV=ep*<@m#)rn^t{y zM1Og#u8?%#6&`}Cf1ZrJ?jrq#N+$sreNkFd8)QIAX-W^V=8nZUC~#%rI!*2Vb4DedM<$m382PBuIr3 zG%#=KsNPh9?Bpo29HX?ZQ&w~ViD4dQZ3r*I4{X6{mQYvbvaSmZ{2!*`Chh0 ztO6DXlY8Qehwjc7XoTz*%T)*eP6kJ%r7oDI1S_Bl-38W`r6=))h0iOa52mYv4?0cB zM7=OQdc6|$+|lN36$W$J9KMvW1k;CNXw%IH!e7PX;RbevX^z$H6c@QXj#dXAqaFNh zXv5G|v$41-TMgm5Hr=mb-d4AJA9}61D&8)mKGDq;oLW|Z16s820=?B7CJtqkui7eU z1mnGCXG`sUZAY02%e#V;hC>=KXP$9m5WaQ^}%M zWFl8&9#b~EGoZ4_QRedG#J@%DUB@KPn4rfnUGy)=)D%)?rtDV{y5} z%yXKD!|V>%?`Z2E>|B+^%jXprCe4saE`ABd)QKv|&`=3|U1Gyf=p@S3S{EL%qB$7> z5@*h}TzALz5PBOIRhQwh;F@{|EZ!|7DamCn9jCFV8U}K4KFOVsGh2uChL;P}9wxY^ zrD6>Za^JXkQe+(N3XnipGBU4Qx4mQ^ zZ6VY_vJ{<6nrO#P0%pJ>c+BCJdni-AVixBIc+MZhnV6E%nv|2PJSm**4Lq;2o5rHCJ8wt)co$WX zAj={iJn8B_i92>#N9kuA40sB64n!c%<&KS{=F!3r|9Nk>w5NNQW)JIqjmmC~v(%om zJ|F(XWGzfCe@5&bOms$uepMe;*C%kFndK|*1)JEkl8lOljRx)Kb02*D0nr|^^c9aZ zFZ(Tfyp*N_&+R5&7OH=~r2kUL@@K7y{`R0PwWgTl?^m<`tC#a7|KHxzubed&=amo^ z;Fp#ApQ@fr|5)1p$tIZY^o*5(mKMxi^F|;H@S9vY2@_=#36Yo#S$$y>X9MdJTMr9f z9cdk5k8xCk3{n&@0b($efVb=>Z|Bj_*wB;amC!C{>Je{SKQDfnXP{~Bup0wDn-JtG7F@M9s{mut9ZWWLiO@5@hru>NlreW~|^_pC6{ z^YxJKsYEk~k&pY;+-#^s)y;9Hh zv;WNccbxx)raxBhe$f#7N6x%qK!4Ko_vO2PWA#!l{DsZS=iey#!sCL?)P%mzZv#rCioY6y`cM&((q$nz?T-D-zm>qn(R+{{iy-qPaYsYn(&3mk0$(a z9?0`Xq531NJDD@)zk9is|*T0(wc%i8Oq|~3C|CgHm{nLIgoPTuAmx>_Y>CoHm zne)%=_ZK4kLi4{L(SDf)f9(J8l9=(G^1K~s{vGZAzR|;Ze-EmTxBpwH{?yUne+88k&;KD*e-iCqjH*9A4SswE=XqQ5{u!$8bMr4ZI{4?@ z^1}4Tg8whA4Zc&J_vSOxpAMaWaR1dt2S3g$FLT?EHC-W&4N&GLM`F)A3 z{}nXs%Ktae{9dG=xXR0W%=hr^$GqeppNP)^hRUx&^A`!oFOd8jA>@xE<|PCAJLP#_ zIsM15{DnCGB0TvXbN!g_`;vY9o$|b)Tz(CbzfA1?#c}eISNxswylb8%_zzv^FNOKZ z2>Jfp`D3*9lGFR0^1QoTe+`a5CD#5T6fZfw-zm?V*X=)s;!lG71cvX9@&dz;nXSJM z+a%n74Tir;Vf{rUejm22JPYt24vN2$<=+6~$2Gy1*V-x>lKz=6E3ow5P4k*|Q RU*hL4JVXG1(xjKG{|^(kLQDVv diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs deleted file mode 100644 index b31fdaa002..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod auth_test; -mod workspace_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs deleted file mode 100644 index 2ccbc9438f..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::collections::HashMap; - -use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_folder::entities::WorkspaceSettingPB; -use flowy_folder::event_map::FolderEvent::GetCurrentWorkspaceSetting; -use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; -use flowy_user::entities::{AuthenticatorPB, OauthSignInPB, UserProfilePB}; -use flowy_user::event_map::UserEvent::*; - -use crate::util::*; - -#[tokio::test] -async fn initial_workspace_test() { - if get_supabase_config().is_some() { - let test = EventIntegrationTest::new().await; - let mut map = HashMap::new(); - map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); - map.insert( - USER_EMAIL.to_string(), - format!("{}@gmail.com", uuid::Uuid::new_v4()), - ); - let payload = OauthSignInPB { - map, - authenticator: AuthenticatorPB::Supabase, - }; - - let _ = EventBuilder::new(test.clone()) - .event(OauthSignIn) - .payload(payload) - .async_send() - .await - .parse::(); - - let workspace_settings = EventBuilder::new(test.clone()) - .event(GetCurrentWorkspaceSetting) - .async_send() - .await - .parse::(); - - assert!(workspace_settings.latest_view.is_some()); - dbg!(&workspace_settings); - } -} From 514eeb846611a0a8bf2109676629a048cdeebd78 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Apr 2025 12:11:32 +0800 Subject: [PATCH 73/74] chore: init local ai when switching workspace --- .../setting_ai_view/local_ai_setting.dart | 12 ++- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 94 ++++++++++++++----- .../flowy-ai/src/local_ai/controller.rs | 52 +++++----- .../flowy-core/src/user_state_callback.rs | 33 ++++--- frontend/rust-lib/flowy-user/src/event_map.rs | 5 + 5 files changed, 127 insertions(+), 69 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 08f3034ad6..4992864f99 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -90,14 +90,18 @@ class LocalAiSettingHeader extends StatelessWidget { ), Toggle( value: isEnabled, - onChanged: (_) => _onToggleChanged(context), + onChanged: (value) { + _onToggleChanged(value, context); + }, ), ], ); } - void _onToggleChanged(BuildContext context) { - if (isEnabled) { + void _onToggleChanged(bool value, BuildContext context) { + if (value) { + context.read().add(const LocalAiPluginEvent.toggle()); + } else { showConfirmDialog( context: context, title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), @@ -110,8 +114,6 @@ class LocalAiSettingHeader extends StatelessWidget { .add(const LocalAiPluginEvent.toggle()); }, ); - } else { - context.read().add(const LocalAiPluginEvent.toggle()); } } } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 8a2ddeead5..2e9fc7e720 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -12,7 +12,7 @@ use dashmap::DashMap; use flowy_ai_pub::cloud::{ AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, }; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use crate::notification::{chat_notification_builder, ChatNotification}; @@ -104,36 +104,86 @@ impl AIManager { } } - #[instrument(skip_all, err)] - pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - if let Err(err) = local_ai.destroy_plugin().await { - error!("Failed to destroy plugin: {}", err); + async fn reload_with_workspace_id(&self, workspace_id: &str) { + // Check if local AI is enabled for this workspace and if we're in local mode + let result = self.user_service.is_local_model().await; + if let Err(err) = &result { + if matches!(err.code, ErrorCode::UserNotLogin) { + info!("[AI Manager] User not logged in, skipping local AI reload"); + return; } + } - if let Err(err) = local_ai.reload().await { - error!("[AI Manager] failed to reload local AI: {:?}", err); - } - }); + let is_local = result.unwrap_or(false); + let is_enabled = self.local_ai.is_enabled_on_workspace(workspace_id); + let is_running = self.local_ai.is_running(); + info!( + "[AI Manager] Reloading workspace: {}, is_local: {}, is_enabled: {}, is_running: {}", + workspace_id, is_local, is_enabled, is_running + ); + + // Shutdown AI if it's running but shouldn't be (not enabled and not in local mode) + if is_running && !is_enabled && !is_local { + info!("[AI Manager] Local AI is running but not enabled, shutting it down"); + let local_ai = self.local_ai.clone(); + tokio::spawn(async move { + // Wait for 5 seconds to allow other services to initialize + // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + if let Err(err) = local_ai.toggle_plugin(false).await { + error!("[AI Manager] failed to shutdown local AI: {:?}", err); + } + }); + return; + } + + // Start AI if it's enabled but not running + if is_enabled && !is_running { + info!("[AI Manager] Local AI is enabled but not running, starting it now"); + let local_ai = self.local_ai.clone(); + tokio::spawn(async move { + // Wait for 5 seconds to allow other services to initialize + // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + if let Err(err) = local_ai.toggle_plugin(true).await { + error!("[AI Manager] failed to start local AI: {:?}", err); + } + }); + return; + } + + // Log status for other cases + if is_running { + info!("[AI Manager] Local AI is already running"); + } + } + + #[instrument(skip_all, err)] + pub async fn on_launch_if_authenticated(&self, workspace_id: &str) -> Result<(), FlowyError> { + self.reload_with_workspace_id(workspace_id).await; + Ok(()) + } + + pub async fn initialize_after_sign_in(&self, workspace_id: &str) -> Result<(), FlowyError> { + self.reload_with_workspace_id(workspace_id).await; + Ok(()) + } + + pub async fn initialize_after_sign_up(&self, workspace_id: &str) -> Result<(), FlowyError> { + self.reload_with_workspace_id(workspace_id).await; Ok(()) } #[instrument(skip_all, err)] pub async fn initialize_after_open_workspace( &self, - _workspace_id: &Uuid, + workspace_id: &Uuid, ) -> Result<(), FlowyError> { - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - if let Err(err) = local_ai.destroy_plugin().await { - error!("Failed to destroy plugin: {}", err); - } - - if let Err(err) = local_ai.reload().await { - error!("[AI Manager] failed to reload local AI: {:?}", err); - } - }); + self + .reload_with_workspace_id(&workspace_id.to_string()) + .await; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 43dd7ce9b2..6435b77fc3 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -99,7 +99,7 @@ impl LocalAIController { continue; }; - let key = local_ai_enabled_key(&workspace_id); + let key = local_ai_enabled_key(&workspace_id.to_string()); info!("[AI Plugin] state: {:?}", state); // Read whether plugin is enabled from store; default to true @@ -157,14 +157,15 @@ impl LocalAIController { } #[instrument(level = "debug", skip_all)] pub async fn observe_plugin_resource(&self) { - debug!( - "[AI Plugin] init plugin when first run. thread: {:?}", - std::thread::current().id() - ); let sys = get_operating_system(); if !sys.is_desktop() { return; } + + debug!( + "[AI Plugin] observer plugin state. thread: {:?}", + std::thread::current().id() + ); async fn try_init_plugin( resource: &Arc, ai_plugin: &Arc, @@ -196,12 +197,6 @@ impl LocalAIController { }); } - pub async fn reload(&self) -> FlowyResult<()> { - let is_enabled = self.is_enabled(); - self.toggle_plugin(is_enabled).await?; - Ok(()) - } - fn upgrade_store_preferences(&self) -> FlowyResult> { self .store_preferences @@ -211,9 +206,6 @@ impl LocalAIController { /// Indicate whether the local AI plugin is running. pub fn is_running(&self) -> bool { - if !self.is_enabled() { - return false; - } self.ai_plugin.get_plugin_running_state().is_running() } @@ -225,20 +217,25 @@ impl LocalAIController { return false; } - if let Ok(key) = self - .user_service - .workspace_id() - .map(|workspace_id| local_ai_enabled_key(&workspace_id)) - { - match self.upgrade_store_preferences() { - Ok(store) => store.get_bool(&key).unwrap_or(false), - Err(_) => false, - } + if let Ok(workspace_id) = self.user_service.workspace_id() { + self.is_enabled_on_workspace(&workspace_id.to_string()) } else { false } } + pub fn is_enabled_on_workspace(&self, workspace_id: &str) -> bool { + let key = local_ai_enabled_key(workspace_id); + if !get_operating_system().is_desktop() { + return false; + } + + match self.upgrade_store_preferences() { + Ok(store) => store.get_bool(&key).unwrap_or(false), + Err(_) => false, + } + } + pub fn get_plugin_chat_model(&self) -> Option { if !self.is_enabled() { return None; @@ -298,7 +295,8 @@ impl LocalAIController { ); if self.resource.set_llm_setting(setting).await.is_ok() { - self.reload().await?; + let is_enabled = self.is_enabled(); + self.toggle_plugin(is_enabled).await?; } Ok(()) } @@ -373,7 +371,7 @@ impl LocalAIController { pub async fn toggle_local_ai(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; - let key = local_ai_enabled_key(&workspace_id); + let key = local_ai_enabled_key(&workspace_id.to_string()); let store_preferences = self.upgrade_store_preferences()?; let enabled = !store_preferences.get_bool(&key).unwrap_or(true); store_preferences.set_bool(&key, enabled)?; @@ -482,7 +480,7 @@ impl LocalAIController { } #[instrument(level = "debug", skip_all)] - async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> { + pub(crate) async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> { info!( "[AI Plugin] enable: {}, thread id: {:?}", enabled, @@ -618,6 +616,6 @@ impl LLMResourceService for LLMResourceServiceImpl { } const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; -fn local_ai_enabled_key(workspace_id: &Uuid) -> String { +fn local_ai_enabled_key(workspace_id: &str) -> String { format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index 3be1bf15ed..f191d3c1ad 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -38,15 +38,6 @@ pub(crate) struct UserStatusCallbackImpl { } impl UserStatusCallbackImpl { - fn init_ai_component(&self, workspace_id: String) { - let cloned_ai_manager = self.ai_manager.clone(); - self.runtime.spawn(async move { - if let Err(err) = cloned_ai_manager.initialize(&workspace_id).await { - error!("Failed to initialize AIManager: {:?}", err); - } - }); - } - async fn folder_init_data_source( &self, user_id: i64, @@ -95,7 +86,6 @@ impl UserStatusCallback for UserStatusCallbackImpl { auth_type: &AuthType, ) -> FlowyResult<()> { let workspace_id = user_workspace.workspace_id()?; - if let Some(cloud_config) = cloud_config { self .server_provider @@ -124,7 +114,15 @@ impl UserStatusCallback for UserStatusCallbackImpl { self.document_manager.initialize(user_id).await?; let workspace_id = user_workspace.id.clone(); - self.init_ai_component(workspace_id); + let cloned_ai_manager = self.ai_manager.clone(); + self.runtime.spawn(async move { + if let Err(err) = cloned_ai_manager + .on_launch_if_authenticated(&workspace_id) + .await + { + error!("Failed to initialize AIManager: {:?}", err); + } + }); Ok(()) } @@ -158,8 +156,11 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize_after_sign_in(user_id) .await?; - let workspace_id = user_workspace.id.clone(); - self.init_ai_component(workspace_id); + self + .ai_manager + .initialize_after_sign_in(&user_workspace.id) + .await?; + Ok(()) } @@ -207,8 +208,10 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await .context("DocumentManager error")?; - let workspace_id = user_workspace.id.clone(); - self.init_ai_component(workspace_id); + self + .ai_manager + .initialize_after_sign_up(&user_workspace.id) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index fbee0a96d9..ba242e6d46 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -291,6 +291,11 @@ pub trait UserStatusCallback: Send + Sync + 'static { ) -> FlowyResult<()> { Ok(()) } + + async fn did_launch(&self) -> FlowyResult<()> { + Ok(()) + } + /// Fires right after the user successfully signs in. async fn on_sign_in( &self, From 6bdaee3a002c0db5da1fb1f2094ebc9a9dbc01e1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Apr 2025 14:10:02 +0800 Subject: [PATCH 74/74] chore: set toggle default value --- frontend/rust-lib/flowy-ai/src/local_ai/controller.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 6435b77fc3..1ec08854e0 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -314,7 +314,7 @@ impl LocalAIController { std::thread::current().id() ); return LocalAIPB { - enabled: false, + enabled, plugin_downloaded: false, state: RunningStatePB::from(RunningState::ReadyToConnect), lack_of_resource: None, @@ -373,7 +373,8 @@ impl LocalAIController { let workspace_id = self.user_service.workspace_id()?; let key = local_ai_enabled_key(&workspace_id.to_string()); let store_preferences = self.upgrade_store_preferences()?; - let enabled = !store_preferences.get_bool(&key).unwrap_or(true); + let enabled = !store_preferences.get_bool(&key).unwrap_or(false); + tracing::trace!("[AI Plugin] toggle local ai, enabled: {}", enabled,); store_preferences.set_bool(&key, enabled)?; self.toggle_plugin(enabled).await?; Ok(enabled)