From 1977cf663729f51c62a0ce1a7fdabe61bb61b7ab Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 23 Apr 2025 13:41:52 +0800 Subject: [PATCH] feat: display joined at and user avatar in members page (#7809) * feat: new settings page design * feat: support display member join at and avatar * chore: bump version 0.9.0 * chore: update translations --- frontend/Makefile.toml | 2 +- .../settings/settings_dialog.dart | 10 +- .../inivitation/inivite_member_by_link.dart | 14 +-- .../members/workspace_member_page.dart | 47 ++++++-- .../settings/widgets/settings_menu.dart | 18 ++-- .../widgets/settings_menu_element.dart | 101 +++++++++++------- .../presentation/widgets/user_avatar.dart | 7 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/resources/translations/en.json | 2 +- frontend/rust-lib/Cargo.lock | 24 ++--- frontend/rust-lib/Cargo.toml | 4 +- .../src/af_cloud/impls/user/dto.rs | 1 + .../src/local_server/impls/user.rs | 1 + .../down.sql | 3 + .../up.sql | 3 + frontend/rust-lib/flowy-sqlite/src/schema.rs | 25 ++--- .../rust-lib/flowy-user-pub/src/entities.rs | 1 + .../flowy-user-pub/src/sql/member_sql.rs | 2 + .../flowy-user-pub/src/sql/user_sql.rs | 1 + .../flowy-user/src/entities/workspace.rs | 4 + .../user_manager/manager_user_workspace.rs | 2 + 21 files changed, 182 insertions(+), 92 deletions(-) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/up.sql diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 41fdffb1af..3e53c0bba9 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.9" +APPFLOWY_VERSION = "0.9.0" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" 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 cd33c62090..60e3d12086 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -27,6 +27,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/web_url_hint_wi import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.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_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -60,6 +61,7 @@ class SettingsDialog extends StatelessWidget { @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width * 0.6; + final theme = AppFlowyTheme.of(context); return BlocProvider( create: (context) => SettingsDialogBloc( user, @@ -72,12 +74,12 @@ class SettingsDialog extends StatelessWidget { constraints: const BoxConstraints(minWidth: 564), child: ScaffoldMessenger( child: Scaffold( - backgroundColor: Colors.transparent, + backgroundColor: theme.backgroundColorScheme.primary, body: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 200, + width: 204, child: SettingsMenu( userProfile: user, changeSelectedPage: (index) => context @@ -88,6 +90,10 @@ class SettingsDialog extends StatelessWidget { isBillingEnabled: state.isBillingEnabled, ), ), + AFDivider( + axis: Axis.vertical, + color: theme.borderColorScheme.primary, + ), Expanded( child: getSettingsView( context 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 index 6f143a83c1..5d878afa41 100644 --- 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 @@ -88,17 +88,18 @@ class _Description extends StatelessWidget { 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', + title: LocaleKeys.settings_appearance_members_resetInviteLink.tr(), + description: LocaleKeys + .settings_appearance_members_resetInviteLinkDescription + .tr(), + confirmLabel: LocaleKeys.settings_appearance_members_reset.tr(), onConfirm: () { context.read().add( const WorkspaceMemberEvent.generateInviteLink(), ); }, confirmButtonBuilder: (_) => AFFilledTextButton.destructive( - text: 'Reset', + text: LocaleKeys.settings_appearance_members_reset.tr(), onTap: () { context.read().add( const WorkspaceMemberEvent.generateInviteLink(), @@ -145,7 +146,8 @@ class _CopyLinkButton extends StatelessWidget { ); } else { showToastNotification( - message: LocaleKeys.shareAction_copyLinkFailed.tr(), + message: 'You haven\'t generated an invite link yet.', + type: ToastificationType.error, ); } }, 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 3ead104ee3..51dc0935b6 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 @@ -10,6 +10,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/inivita 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/workspace/presentation/widgets/user_avatar.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'; @@ -318,6 +319,7 @@ class _MemberListHeader extends StatelessWidget { return Row( children: [ Expanded( + flex: 4, child: Text( LocaleKeys.settings_appearance_members_user.tr(), style: theme.textStyle.body.standard( @@ -326,6 +328,7 @@ class _MemberListHeader extends StatelessWidget { ), ), Expanded( + flex: 2, child: Text( LocaleKeys.settings_appearance_members_role.tr(), style: theme.textStyle.body.standard( @@ -334,6 +337,7 @@ class _MemberListHeader extends StatelessWidget { ), ), Expanded( + flex: 3, child: Text( LocaleKeys.settings_accountPage_email_title.tr(), style: theme.textStyle.body.standard( @@ -364,14 +368,39 @@ class _MemberItem extends StatelessWidget { return Row( children: [ Expanded( - child: Text( - member.name, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), + flex: 4, + child: Row( + children: [ + UserAvatar( + iconUrl: member.avatarUrl, + name: member.name, + size: 24, + fontSize: 12, + emojiFontSize: 20, + ), + HSpace(8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + member.name, + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.primary, + ), + ), + Text( + _formatJoinedDate(member.joinedAt.toInt()), + style: theme.textStyle.caption.standard( + color: theme.textColorScheme.secondary, + ), + ), + ], + ), + ], ), ), Expanded( + flex: 2, child: member.role.isOwner || !myRole.canUpdate ? Text( member.role.description, @@ -384,6 +413,7 @@ class _MemberItem extends StatelessWidget { ), ), Expanded( + flex: 3, child: FlowyTooltip( message: member.email, child: Text( @@ -403,6 +433,11 @@ class _MemberItem extends StatelessWidget { ], ); } + + String _formatJoinedDate(int joinedAt) { + final date = DateTime.fromMillisecondsSinceEpoch(joinedAt * 1000); + return 'Joined on ${DateFormat('MMM d, y').format(date)}'; + } } enum _MemberMoreAction { @@ -428,7 +463,7 @@ class _MemberMoreActionList extends StatelessWidget { return FlowyButton( useIntrinsicWidth: true, text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, + FlowySvgs.three_dots_s, ), onTap: () { controller.show(); 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 f628aadc6b..edad976f1d 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 @@ -4,6 +4,7 @@ import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.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_ui/flowy_infra_ui.dart'; import 'package:flutter/foundation.dart'; @@ -25,28 +26,27 @@ class SettingsMenu extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); // Column > Expanded for full size no matter the content return Column( children: [ Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8) + - const EdgeInsets.only(left: 8, right: 4), + child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: theme.backgroundColorScheme.secondary, borderRadius: const BorderRadiusDirectional.only( topStart: Radius.circular(8), bottomStart: Radius.circular(8), ), ), child: SingleChildScrollView( - // Right padding is added to make the scrollbar centered - // in the space between the menu and the content - padding: const EdgeInsets.only(right: 4) + - const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric( + vertical: 24, + horizontal: theme.spacing.l, + ), physics: const ClampingScrollPhysics(), child: SeparatedColumn( - separatorBuilder: () => const VSpace(16), + separatorBuilder: () => VSpace(theme.spacing.xs), children: [ SettingsMenuElement( page: SettingsPage.account, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart index b1bef7cceb..3265c76794 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart @@ -1,8 +1,6 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; class SettingsMenuElement extends StatelessWidget { @@ -23,42 +21,67 @@ class SettingsMenuElement extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyHover( - isSelected: () => page == selectedPage, - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greyHover, - borderRadius: BorderRadius.circular(4), - ), - builder: (_, isHovering) => ListTile( - dense: true, - leading: iconWidget( - isHovering || page == selectedPage - ? Theme.of(context).colorScheme.onSurface - : AFThemeExtension.of(context).textColor, - ), - onTap: () => changeSelectedPage(page), - selected: page == selectedPage, - selectedColor: Theme.of(context).colorScheme.onSurface, - selectedTileColor: Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - minLeadingWidth: 0, - title: FlowyText.medium( - label, - fontSize: FontSizes.s14, - overflow: TextOverflow.ellipsis, - color: page == selectedPage - ? Theme.of(context).colorScheme.onSurface - : null, - ), - ), + final theme = AppFlowyTheme.of(context); + return AFBaseButton( + onTap: () => changeSelectedPage(page), + padding: EdgeInsets.all(theme.spacing.m), + borderRadius: theme.borderRadius.m, + borderColor: (_, __, ___, ____) => theme.fillColorScheme.transparent, + backgroundColor: (_, isHovering, __) { + if (isHovering) { + return theme.fillColorScheme.primaryAlpha5; + } else if (page == selectedPage) { + return theme.fillColorScheme.themeSelect; + } + return theme.fillColorScheme.transparent; + }, + builder: (_, __, ___) { + return Row( + children: [ + icon, + HSpace(theme.spacing.m), + Text( + label, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + ), + ], + ); + }, ); } - Widget iconWidget(Color color) => IconTheme( - data: IconThemeData(color: color), - child: icon, - ); + // return FlowyHover( + // isSelected: () => page == selectedPage, + // resetHoverOnRebuild: false, + // style: HoverStyle( + // hoverColor: AFThemeExtension.of(context).greyHover, + // borderRadius: BorderRadius.circular(4), + // ), + // builder: (_, isHovering) => ListTile( + // dense: true, + // leading: iconWidget( + // isHovering || page == selectedPage + // ? Theme.of(context).colorScheme.onSurface + // : AFThemeExtension.of(context).textColor, + // ), + // onTap: () => changeSelectedPage(page), + // selected: page == selectedPage, + // selectedColor: Theme.of(context).colorScheme.onSurface, + // selectedTileColor: Theme.of(context).colorScheme.primary, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(5), + // ), + // minLeadingWidth: 0, + // title: FlowyText.medium( + // label, + // fontSize: FontSizes.s14, + // overflow: TextOverflow.ellipsis, + // color: page == selectedPage + // ? Theme.of(context).colorScheme.onSurface + // : null, + // ), + // ), + // ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 347d95d01d..1c2d74eabc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -17,12 +17,14 @@ class UserAvatar extends StatelessWidget { required this.fontSize, this.isHovering = false, this.decoration, + this.emojiFontSize, }); final String iconUrl; final String name; final double size; final double fontSize; + final double? emojiFontSize; final Decoration? decoration; // If true, a border will be applied on top of the avatar @@ -127,7 +129,10 @@ class UserAvatar extends StatelessWidget { FlowySvgData('emoji/$iconUrl'), blendMode: null, ) - : FlowyText.emoji(iconUrl, fontSize: fontSize), + : FlowyText.emoji( + iconUrl, + fontSize: emojiFontSize ?? fontSize, + ), ), ), ), diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 961115f5d4..4cedb20d5e 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.9 +version: 0.9.0 environment: flutter: ">=3.27.4" diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ba94205e0c..76702c792b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1353,7 +1353,7 @@ "generateANewLink": "generate a new link", "inviteMemberByEmail": "Invite member by email", "inviteMemberHintText": "Invite by email", - "resetInviteLink": "Reset the invite link", + "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", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 51a3f1a3b2..4baaebcb72 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" 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=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" dependencies = [ "bincode", "bytes", @@ -3426,7 +3426,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" dependencies = [ "anyhow", "getrandom 0.2.10", @@ -3441,7 +3441,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" dependencies = [ "app-error", "jsonwebtoken", @@ -4065,7 +4065,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" dependencies = [ "anyhow", "bytes", @@ -6643,7 +6643,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=954c323#954c32332487f5e17a7fb5be0bc339db1cb00e17" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1561c7ea7d..c9bdb7c640 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -108,8 +108,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 = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "954c323" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "954c323" } [profile.dev] opt-level = 0 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 838e9dd6ca..34230a13d2 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 @@ -71,6 +71,7 @@ pub fn from_af_workspace_member(member: AFWorkspaceMember) -> WorkspaceMember { role: from_af_role(member.role), name: member.name, avatar_url: member.avatar_url, + joined_at: member.joined_at.map(|dt| dt.timestamp()), } } 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 f011c16d90..ba95fffc3f 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 @@ -248,6 +248,7 @@ impl UserCloudService for LocalServerUserServiceImpl { uid, workspace_id: workspace_id.to_string(), updated_at: chrono::Utc::now().naive_utc(), + joined_at: None, }; let member = WorkspaceMember::from(row.clone()); diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/down.sql new file mode 100644 index 0000000000..fae350d18a --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE workspace_members_table +DROP COLUMN joined_at; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/up.sql new file mode 100644 index 0000000000..9d699414db --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-22-150142_workspace_member_joined_at/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE workspace_members_table + ADD COLUMN joined_at BIGINT DEFAULT NULL; diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f91d187b75..06aaa7874c 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -120,6 +120,7 @@ diesel::table! { uid -> BigInt, workspace_id -> Text, updated_at -> Timestamp, + joined_at -> Nullable, } } @@ -132,16 +133,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/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index a870b9c0b0..92a1cc69ea 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -370,6 +370,7 @@ pub struct WorkspaceMember { pub role: Role, pub name: String, pub avatar_url: Option, + pub joined_at: Option, } /// represent the user awareness object id for the workspace. 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 58ca65e732..1bfcfd923b 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 @@ -16,6 +16,7 @@ pub struct WorkspaceMemberTable { pub uid: i64, pub workspace_id: String, pub updated_at: chrono::NaiveDateTime, + pub joined_at: Option, } impl From for WorkspaceMember { @@ -25,6 +26,7 @@ impl From for WorkspaceMember { role: Role::from(value.role), name: value.name, avatar_url: value.avatar_url, + joined_at: value.joined_at, } } } 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 ca117300f2..e4b3323d74 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 @@ -111,6 +111,7 @@ pub fn insert_local_workspace( uid, workspace_id: workspace_id.to_string(), updated_at: chrono::Utc::now().naive_utc(), + joined_at: None, }; upsert_user_workspace(uid, AuthType::Local, user_workspace.clone(), conn)?; diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 860bda3be7..7fde248777 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -24,6 +24,9 @@ pub struct WorkspaceMemberPB { #[pb(index = 4, one_of)] pub avatar_url: Option, + + #[pb(index = 5, one_of)] + pub joined_at: Option, } impl From for WorkspaceMemberPB { @@ -33,6 +36,7 @@ impl From for WorkspaceMemberPB { name: value.name, role: value.role.into(), avatar_url: value.avatar_url, + joined_at: value.joined_at, } } } 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 2dd99558a7..af5b176106 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 @@ -673,6 +673,7 @@ impl UserManager { role: member_record.role.into(), name: member_record.name, avatar_url: member_record.avatar_url, + joined_at: member_record.joined_at, }); } @@ -703,6 +704,7 @@ impl UserManager { uid, workspace_id: workspace_id.to_string(), updated_at: Utc::now().naive_utc(), + joined_at: member.joined_at, }; let mut db = self.authenticate_user.get_sqlite_connection(uid)?;