diff --git a/frontend/app_flowy/assets/images/emoji/1F42F.svg b/frontend/app_flowy/assets/images/emoji/1F42F.svg new file mode 100644 index 0000000000..a6e8e3e81f --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F42F.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F431.svg b/frontend/app_flowy/assets/images/emoji/1F431.svg new file mode 100644 index 0000000000..26aa279abc --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F431.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F435.svg b/frontend/app_flowy/assets/images/emoji/1F435.svg new file mode 100644 index 0000000000..0220a6e58e --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F435.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F43A.svg b/frontend/app_flowy/assets/images/emoji/1F43A.svg new file mode 100644 index 0000000000..3e29b3a6a9 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F43A.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F600.svg b/frontend/app_flowy/assets/images/emoji/1F600.svg new file mode 100644 index 0000000000..e9e1d0ea88 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F600.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F984.svg b/frontend/app_flowy/assets/images/emoji/1F984.svg new file mode 100644 index 0000000000..a5f8206cbc --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F984.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9CC.svg b/frontend/app_flowy/assets/images/emoji/1F9CC.svg new file mode 100644 index 0000000000..eb30038228 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9CC.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DB.svg b/frontend/app_flowy/assets/images/emoji/1F9DB.svg new file mode 100644 index 0000000000..590829d25c --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DB.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg b/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg new file mode 100644 index 0000000000..62e5101f53 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg b/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg new file mode 100644 index 0000000000..e662de70a6 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DF.svg b/frontend/app_flowy/assets/images/emoji/1F9DF.svg new file mode 100644 index 0000000000..e2ea11f33a --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DF.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/lib/user/application/user_service.dart b/frontend/app_flowy/lib/user/application/user_service.dart index 48bea6aa41..35e32f2eb1 100644 --- a/frontend/app_flowy/lib/user/application/user_service.dart +++ b/frontend/app_flowy/lib/user/application/user_service.dart @@ -11,7 +11,8 @@ class UserService { UserService({ required this.userId, }); - Future> getUserProfile({required String userId}) { + Future> getUserProfile( + {required String userId}) { return UserEventGetUserProfile().send(); } @@ -19,6 +20,7 @@ class UserService { String? name, String? password, String? email, + String? iconUrl, }) { var payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -34,10 +36,15 @@ class UserService { payload.email = email; } + if (iconUrl != null) { + payload.iconUrl = iconUrl; + } + return UserEventUpdateUserProfile(payload).send(); } - Future> deleteWorkspace({required String workspaceId}) { + Future> deleteWorkspace( + {required String workspaceId}) { throw UnimplementedError(); } @@ -70,7 +77,8 @@ class UserService { }); } - Future> createWorkspace(String name, String desc) { + Future> createWorkspace( + String name, String desc) { final request = CreateWorkspacePayloadPB.create() ..name = name ..desc = desc; diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart index 7435778471..de63777812 100644 --- a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -35,6 +35,14 @@ class SettingsUserViewBloc extends Bloc { ); }); }, + updateUserIcon: (String iconUrl) { + _userService.updateUserProfile(iconUrl: iconUrl).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, ); }); } @@ -52,7 +60,8 @@ class SettingsUserViewBloc extends Bloc { void _profileUpdated(Either userProfileOrFailed) { userProfileOrFailed.fold( - (newUserProfile) => add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (newUserProfile) => + add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), (err) => Log.error(err), ); } @@ -62,7 +71,10 @@ class SettingsUserViewBloc extends Bloc { class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; - const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; + const factory SettingsUserEvent.updateUserIcon(String iconUrl) = + _UpdateUserIcon; + const factory SettingsUserEvent.didReceiveUserProfile( + UserProfilePB newUserProfile) = _DidReceiveUserProfile; } @freezed @@ -72,7 +84,8 @@ class SettingsUserState with _$SettingsUserState { required Either successOrFailure, }) = _SettingsUserState; - factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( + factory SettingsUserState.initial(UserProfilePB userProfile) => + SettingsUserState( userProfile: userProfile, successOrFailure: left(unit), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index d7b8dce4af..baf9dbffab 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -19,7 +19,8 @@ class MenuUser extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt(param1: user)..add(const MenuUserEvent.initial()), + create: (context) => + getIt(param1: user)..add(const MenuUserEvent.initial()), child: BlocBuilder( builder: (context, state) => Row( children: [ @@ -39,20 +40,16 @@ class MenuUser extends StatelessWidget { } Widget _renderAvatar(BuildContext context) { - return const SizedBox( + String iconUrl = context.read().state.userProfile.iconUrl; + + return SizedBox( width: 25, height: 25, child: ClipRRect( borderRadius: Corners.s5Border, child: CircleAvatar( - backgroundColor: Color.fromRGBO(132, 39, 224, 1.0), - child: Text( - 'M', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w300, - ), - ), + backgroundColor: Colors.transparent, + child: svgWidget('emoji/$iconUrl'), )), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index f8f094d1b0..a235fe7dcf 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -2,7 +2,11 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:flutter/material.dart'; import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra/image.dart'; + +import 'dart:convert'; class SettingsUserView extends StatelessWidget { final UserProfilePB user; @@ -11,12 +15,17 @@ class SettingsUserView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt(param1: user)..add(const SettingsUserEvent.initial()), + create: (context) => getIt(param1: user) + ..add(const SettingsUserEvent.initial()), child: BlocBuilder( builder: (context, state) => SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_renderUserNameInput(context)], + children: [ + _renderUserNameInput(context), + const VSpace(20), + _renderCurrentIcon(context) + ], ), ), ), @@ -27,6 +36,12 @@ class SettingsUserView extends StatelessWidget { String name = context.read().state.userProfile.name; return _UserNameInput(name); } + + Widget _renderCurrentIcon(BuildContext context) { + String iconUrl = + context.read().state.userProfile.iconUrl; + return _CurrentIcon(iconUrl); + } } class _UserNameInput extends StatelessWidget { @@ -44,7 +59,121 @@ class _UserNameInput extends StatelessWidget { labelText: 'Name', ), onSubmitted: (val) { - context.read().add(SettingsUserEvent.updateUserName(val)); + context + .read() + .add(SettingsUserEvent.updateUserName(val)); }); } } + +class _CurrentIcon extends StatelessWidget { + final String iconUrl; + const _CurrentIcon(this.iconUrl, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + _setIcon(String iconUrl) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl)); + Navigator.of(context).pop(); + } + + return Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select an Icon'), + children: [ + SizedBox( + height: 300, width: 300, child: IconGallery(_setIcon)) + ]); + }, + ); + }, + child: Column(children: [ + const Align( + alignment: Alignment.topLeft, + child: Text( + "Icon", + style: TextStyle(color: Colors.grey), + )), + Align( + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.all(5.0), + decoration: + BoxDecoration(border: Border.all(color: Colors.grey)), + child: svgWithSize('emoji/$iconUrl', const Size(60, 60)), + )), + ])), + ); + } +} + +class IconGallery extends StatelessWidget { + final Function setIcon; + const IconGallery(this.setIcon, {Key? key}) : super(key: key); + + Future> _getIcons(BuildContext context) async { + final manifestContent = + await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); + + final Map manifestMap = json.decode(manifestContent); + + final iconUrls = manifestMap.keys + .where((String key) => + key.startsWith('assets/images/emoji/') && key.endsWith('.svg')) + .map((String key) => key.split('/').last.split('.').first) + .toList(); + + return iconUrls; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _getIcons(context), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return GridView.count( + padding: const EdgeInsets.all(20), + crossAxisCount: 5, + children: (snapshot.data ?? []).map((String iconUrl) { + return IconOption(iconUrl, setIcon); + }).toList(), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } +} + +class IconOption extends StatelessWidget { + final String iconUrl; + final Function setIcon; + + IconOption(this.iconUrl, this.setIcon, {Key? key}) + : super(key: ValueKey(iconUrl)); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + setIcon(iconUrl); + }, + child: svgWidget('emoji/$iconUrl'), + ), + ); + } +} diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 56e96e96dd..d04277a8b4 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -120,6 +120,7 @@ flutter: - assets/images/home/ - assets/images/editor/ - assets/images/grid/ + - assets/images/emoji/ - assets/images/grid/field/ - assets/images/grid/setting/ - assets/translations/ diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql new file mode 100644 index 0000000000..505fbd4b2f --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql @@ -0,0 +1 @@ +ALTER TABLE user_table DROP COLUMN icon_url; diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql new file mode 100644 index 0000000000..c2aee5e3de --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql @@ -0,0 +1 @@ +ALTER TABLE user_table ADD COLUMN icon_url TEXT NOT NULL DEFAULT ''; diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index e41fd6d865..eda9cd888b 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -88,6 +88,7 @@ table! { token -> Text, email -> Text, workspace -> Text, + icon_url -> Text, } } diff --git a/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs b/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs index 71259509f2..792af0c146 100644 --- a/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs +++ b/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs @@ -1,11 +1,13 @@ // https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ mod user_email; +mod user_icon; mod user_id; mod user_name; mod user_password; mod user_workspace; pub use user_email::*; +pub use user_icon::*; pub use user_id::*; pub use user_name::*; pub use user_password::*; diff --git a/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs b/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs new file mode 100644 index 0000000000..69258ca848 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs @@ -0,0 +1,16 @@ +use crate::errors::ErrorCode; + +#[derive(Debug)] +pub struct UserIcon(pub String); + +impl UserIcon { + pub fn parse(s: String) -> Result { + Ok(Self(s)) + } +} + +impl AsRef for UserIcon { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 276894ffc8..4b423db3af 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -2,7 +2,7 @@ use flowy_derive::ProtoBuf; use std::convert::TryInto; use crate::{ - entities::parser::{UserEmail, UserId, UserName, UserPassword}, + entities::parser::{UserEmail, UserIcon, UserId, UserName, UserPassword}, errors::ErrorCode, }; @@ -25,6 +25,9 @@ pub struct UserProfilePB { #[pb(index = 4)] pub token: String, + + #[pb(index = 5)] + pub icon_url: String, } #[derive(ProtoBuf, Default)] @@ -40,6 +43,9 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 4, one_of)] pub password: Option, + + #[pb(index = 5, one_of)] + pub icon_url: Option, } impl UpdateUserProfilePayloadPB { @@ -64,6 +70,11 @@ impl UpdateUserProfilePayloadPB { self.password = Some(password.to_owned()); self } + + pub fn icon_url(mut self, icon_url: &str) -> Self { + self.icon_url = Some(icon_url.to_owned()); + self + } } #[derive(ProtoBuf, Default, Clone, Debug)] @@ -79,6 +90,9 @@ pub struct UpdateUserProfileParams { #[pb(index = 4, one_of)] pub password: Option, + + #[pb(index = 5, one_of)] + pub icon_url: Option, } impl UpdateUserProfileParams { @@ -88,6 +102,7 @@ impl UpdateUserProfileParams { name: None, email: None, password: None, + icon_url: None, } } @@ -105,6 +120,11 @@ impl UpdateUserProfileParams { self.password = Some(password.to_owned()); self } + + pub fn icon_url(mut self, icon_url: &str) -> Self { + self.icon_url = Some(icon_url.to_owned()); + self + } } impl TryInto for UpdateUserProfilePayloadPB { @@ -128,11 +148,17 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(password) => Some(UserPassword::parse(password)?.0), }; + let icon_url = match self.icon_url { + None => None, + Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), + }; + Ok(UpdateUserProfileParams { id, name, email, password, + icon_url, }) } } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index 64ebd705a7..62b74c9f22 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -82,6 +82,7 @@ pub struct UserTable { pub(crate) token: String, pub(crate) email: String, pub(crate) workspace: String, // deprecated + pub(crate) icon_url: String, } impl UserTable { @@ -91,6 +92,7 @@ impl UserTable { name, email, token, + icon_url: "".to_owned(), workspace: "".to_owned(), } } @@ -120,6 +122,7 @@ impl std::convert::From for UserProfilePB { email: table.email, name: table.name, token: table.token, + icon_url: table.icon_url, } } } @@ -131,6 +134,7 @@ pub struct UserTableChangeset { pub workspace: Option, // deprecated pub name: Option, pub email: Option, + pub icon_url: Option, } impl UserTableChangeset { @@ -140,6 +144,7 @@ impl UserTableChangeset { workspace: None, name: params.name, email: params.email, + icon_url: params.icon_url, } } }