Merge pull request #7794 from AppFlowy-IO/optimize_write_user_workspaces

refactor: only notify when user workspaces were changed
This commit is contained in:
Nathan.fooo 2025-04-21 13:09:13 +08:00 committed by GitHub
commit 65b7916a6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 550 additions and 557 deletions

View file

@ -202,7 +202,8 @@ class MobileViewBottomSheetBody extends StatelessWidget {
List<Widget> _buildPublishActions(BuildContext context) {
final userProfile = context.read<MobileViewPageBloc>().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 [];
}

View file

@ -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,

View file

@ -167,7 +167,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
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,

View file

@ -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(),

View file

@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {
emit(
state.copyWith(
shouldShowIndicator:
userProfile?.authType == AuthTypePB.Server &&
userProfile?.workspaceAuthType == AuthTypePB.Server &&
databaseId != null,
),
);

View file

@ -69,7 +69,8 @@ class RowBanner extends StatefulWidget {
class _RowBannerState extends State<RowBanner> {
final _isHovering = ValueNotifier(false);
late final isLocalMode =
(widget.userProfile?.authType ?? AuthTypePB.Local) == AuthTypePB.Local;
(widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) ==
AuthTypePB.Local;
@override
void dispose() {

View file

@ -101,7 +101,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
bool get isLocalMode {
final userProfilePB = state.userProfilePB;
final type = userProfilePB?.authType ?? AuthTypePB.Local;
final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local;
return type == AuthTypePB.Local;
}

View file

@ -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;

View file

@ -30,7 +30,8 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
);
emit(
state.copyWith(
shouldShowIndicator: userProfile?.authType == AuthTypePB.Server,
shouldShowIndicator:
userProfile?.workspaceAuthType == AuthTypePB.Server,
),
);
_syncStateListener.start(

View file

@ -185,7 +185,7 @@ Future<void> 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<void> 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();

View file

@ -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);

View file

@ -193,7 +193,7 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
Future<void> _updatePublishStatus(Emitter<ShareState> 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,
);

View file

@ -294,8 +294,8 @@ class _IconUploaderState extends State<IconUploader> {
(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 {

View file

@ -46,7 +46,7 @@ class PasswordBloc extends Bloc<PasswordEvent, PasswordState> {
bool _isInitialized = false;
Future<void> _init() async {
if (userProfile.authType == AuthTypePB.Local) {
if (userProfile.workspaceAuthType == AuthTypePB.Local) {
Log.debug('PasswordBloc: skip init because user is local authenticator');
return;
}

View file

@ -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(

View file

@ -91,7 +91,7 @@ class SettingsDialogBloc
]) async {
if ([
AuthTypePB.Local,
].contains(userProfile.authType)) {
].contains(userProfile.workspaceAuthType)) {
return false;
}

View file

@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
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}, '

View file

@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
) {
final userProfile = context.read<SpaceBloc>().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(

View file

@ -70,7 +70,7 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
// 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<SettingsAccountView> {
),
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<SettingsAccountView> {
),
// user deletion
if (widget.userProfile.authType == AuthTypePB.Server)
if (widget.userProfile.workspaceAuthType == AuthTypePB.Server)
const AccountDeletionButton(),
],
);

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -96,7 +96,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
if (state.spaces.isEmpty &&
userProfile.authType == AuthTypePB.Server) {
userProfile.workspaceAuthType == AuthTypePB.Server) {
return const SizedBox.shrink();
}

View file

@ -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))?;

View file

@ -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::<WorkspacePB>()
}

View file

@ -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::<flowy_folder::entities::ViewPB>();
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;

View file

@ -1,4 +1,3 @@
mod local_test;
// #[cfg(feature = "supabase_cloud_test")]
// mod supabase_test;

View file

@ -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);

View file

@ -12,7 +12,7 @@ pub async fn get_synced_workspaces(
test: &EventIntegrationTest,
user_id: i64,
) -> Vec<UserWorkspacePB> {
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
}
}

View file

@ -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;
}
}

View file

@ -24,7 +24,7 @@ async fn anon_user_profile_get() {
.await
.parse::<UserProfilePB>();
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]

View file

@ -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<Workspace, FlowyError> {
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<Vec<WorkspaceRecord>, 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<Option<FolderData>, 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<Vec<FolderSnapshot>, 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<Vec<u8>, 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<PublishPayload>,
) -> 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<Uuid>,
) -> 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<String, FlowyError> {
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<String, FlowyError> {
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]

View file

@ -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<Workspace, FlowyError>;
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<Vec<WorkspaceRecord>, FlowyError>;
async fn get_folder_data(
&self,
workspace_id: &Uuid,
uid: &i64,
) -> Result<Option<FolderData>, FlowyError>;
async fn get_folder_snapshots(
&self,
workspace_id: &str,

View file

@ -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<CreateWorkspacePayloadPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<WorkspacePB, FlowyError> {
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::<Vec<ViewPB>>();
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<CreateWorkspacePayloadPB>,

View file

@ -11,7 +11,6 @@ use crate::manager::FolderManager;
pub fn init(folder: Weak<FolderManager>) -> 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<FolderManager>) -> 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

View file

@ -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<Workspace> {
let uid = self.user.user_id()?;
let new_workspace = self
.cloud_service
.create_workspace(uid, &params.name)
.await?;
Ok(new_workspace)
}
pub async fn get_workspace_setting_pb(&self) -> FlowyResult<WorkspaceLatestPB> {
let workspace_id = self.user.workspace_id()?;
let latest_view = self.get_current_view().await;

View file

@ -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<T> FolderCloudService for AFCloudFolderCloudServiceImpl<T>
where
T: AFServer,
{
async fn create_workspace(&self, _uid: i64, name: &str) -> Result<Workspace, FlowyError> {
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<Vec<WorkspaceRecord>, 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::<Vec<_>>();
Ok(records)
}
#[instrument(level = "debug", skip_all)]
async fn get_folder_data(
&self,
workspace_id: &Uuid,
uid: &i64,
) -> Result<Option<FolderData>, 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<String, FlowyError> {
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<String, FlowyError> {
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()?;

View file

@ -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<UserProfile, FlowyError> {
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<UserProfile, FlowyError> {
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<UserWorkspace, FlowyError> {
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<String>,
new_workspace_icon: Option<String>,
) -> 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,

View file

@ -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<UserProfile, Error> {
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,
})
}

View file

@ -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<Workspace, FlowyError> {
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<Vec<WorkspaceRecord>, FlowyError> {
Ok(vec![])
}
async fn get_folder_data(
&self,
workspace_id: &Uuid,
uid: &i64,
) -> Result<Option<FolderData>, 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<String, FlowyError> {
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<String, FlowyError> {
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())
}
}

View file

@ -41,7 +41,7 @@ impl UserCloudService for LocalServerUserServiceImpl {
let params = params.unbox_or_error::<SignUpParams>()?;
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<UserProfile, FlowyError> {
async fn get_user_profile(
&self,
uid: i64,
workspace_id: &str,
) -> Result<UserProfile, FlowyError> {
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<Vec<UserWorkspace>, 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,

View file

@ -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<dyn UserCloudService> {
Arc::new(LocalServerUserServiceImpl {
logged_user: self.logged_user.clone(),

View file

@ -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(())

View file

@ -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<UserProfile, FlowyError>;
async fn get_user_profile(&self, uid: i64, workspace_id: &str)
-> Result<UserProfile, FlowyError>;
async fn open_workspace(&self, workspace_id: &Uuid) -> Result<UserWorkspace, FlowyError>;

View file

@ -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,

View file

@ -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, 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.
@ -35,20 +37,6 @@ impl From<(UserProfile, AuthType)> for UserTable {
}
}
impl From<UserTable> for UserProfile {
fn from(table: UserTable) -> Self {
UserProfile {
uid: table.id.parse::<i64>().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)
@ -103,11 +92,8 @@ pub fn update_user_profile(
Ok(())
}
pub fn select_user_profile(
uid: i64,
conn: &mut SqliteConnection,
) -> Result<UserProfile, FlowyError> {
let user: UserProfile = user_table::dsl::user_table
fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result<UserTable, FlowyError> {
let row = user_table::dsl::user_table
.filter(user_table::id.eq(&uid.to_string()))
.first::<UserTable>(conn)
.map_err(|err| {
@ -115,12 +101,55 @@ pub fn select_user_profile(
"Can't find the user profile for user id: {}, error: {:?}",
uid, err
))
})?
.into();
})?;
Ok(row)
}
pub fn select_user_profile(
uid: i64,
workspace_id: &str,
conn: &mut SqliteConnection,
) -> Result<UserProfile, FlowyError> {
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::<i64>().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)
}
pub fn select_workspace_auth_type(
uid: i64,
workspace_id: &str,
conn: &mut SqliteConnection,
) -> Result<AuthType, FlowyError> {
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

View file

@ -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<String>,
pub icon: Option<String>,
pub role: Option<i32>,
pub member_count: Option<i64>,
}
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<Self, FlowyError> {
@ -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<UserWorkspaceTable> {
let row = user_workspace_table::dsl::user_workspace_table
let row = dsl::user_workspace_table
.filter(user_workspace_table::id.eq(workspace_id))
.first::<UserWorkspaceTable>(conn)?;
Ok(row)
}
pub fn select_all_user_workspace(
user_id: i64,
mut conn: DBConnection,
uid: i64,
conn: &mut SqliteConnection,
) -> Result<Vec<UserWorkspace>, FlowyError> {
let rows = user_workspace_table::dsl::user_workspace_table
.filter(user_workspace_table::uid.eq(user_id))
.load::<UserWorkspaceTable>(&mut *conn)?;
.filter(user_workspace_table::uid.eq(uid))
.order(user_workspace_table::created_at.desc())
.load::<UserWorkspaceTable>(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<usize, FlowyError> {
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<Vec<WorkspaceChange>> {
let diff = conn.immediate_transaction(|conn| {
// 1) Load all existing workspaces into a map
let existing_rows: Vec<UserWorkspaceTable> = 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<String, UserWorkspaceTable> = 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<String> = user_workspaces.iter().map(|uw| uw.id.clone()).collect();
let to_delete: Vec<String> = 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)
}

View file

@ -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<UserProfile> 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(),
}
}
}

View file

@ -91,16 +91,22 @@ pub async fn get_user_profile_handler(
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<UserProfilePB, FlowyError> {
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<Weak<UserManager>>,
) -> DataResult<RepeatedUserWorkspacePB, FlowyError> {
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(())

View file

@ -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,14 +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,
&mut *imported_sqlite_db.get_connection()?,
)?;
&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,
)
})?;
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(),

View file

@ -125,9 +125,10 @@ impl UserDB {
&self,
pool: &Arc<ConnectionPool>,
uid: i64,
workspace_id: &str,
) -> Result<UserProfile, FlowyError> {
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)
}

View file

@ -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);
}
}
@ -250,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(),
@ -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<UserProfile> {
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<UserProfile, FlowyError> {
pub async fn get_user_profile_from_disk(
&self,
uid: i64,
workspace_id: &str,
) -> Result<UserProfile, FlowyError> {
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<UserProfile, FlowyError> = 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::<UserTable>(&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)]
@ -862,7 +869,7 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc<ConnectionPool>) {
pub(crate) fn run_collab_data_migration(
session: &Session,
user: &UserProfile,
auth_type: &AuthType,
collab_db: Arc<CollabKVDB>,
sqlite_pool: Arc<ConnectionPool>,
kv: Arc<KVStorePreferences>,
@ -871,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) => {
@ -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<dyn UserCloudServiceProvider>,
session: &Session,

View file

@ -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))
}

View file

@ -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?;
}

View file

@ -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, &current_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,
&current_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<UserWorkspace> {
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<Vec<UserWorkspace>> {
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,