mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support-OAuth-login (#4899)
* feat: support-OAuth-login * fix: modified ubuntu version and devtool * fix: ts lint error
This commit is contained in:
parent
57e3a2ce68
commit
6d4cfe7316
7
.github/workflows/tauri_ci.yaml
vendored
7
.github/workflows/tauri_ci.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ubuntu-latest]
|
platform: [ubuntu-20.04]
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Maximize build space (ubuntu only)
|
- name: Maximize build space (ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
sudo rm -rf /usr/share/dotnet
|
sudo rm -rf /usr/share/dotnet
|
||||||
sudo rm -rf /opt/ghc
|
sudo rm -rf /opt/ghc
|
||||||
@ -80,7 +80,7 @@ jobs:
|
|||||||
vcpkg integrate install
|
vcpkg integrate install
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
- name: install dependencies (ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-20.04'
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@ -111,3 +111,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tauriScript: pnpm tauri
|
tauriScript: pnpm tauri
|
||||||
projectPath: frontend/appflowy_tauri
|
projectPath: frontend/appflowy_tauri
|
||||||
|
args: "--debug"
|
10
.github/workflows/tauri_release.yml
vendored
10
.github/workflows/tauri_release.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
args: "--target x86_64-apple-darwin"
|
args: "--target x86_64-apple-darwin"
|
||||||
target: "macos-x86_64"
|
target: "macos-x86_64"
|
||||||
- platform: ubuntu-latest
|
- platform: ubuntu-20.04
|
||||||
args: "--target x86_64-unknown-linux-gnu"
|
args: "--target x86_64-unknown-linux-gnu"
|
||||||
target: "linux-x86_64"
|
target: "linux-x86_64"
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
ref: ${{ github.event.inputs.branch }}
|
ref: ${{ github.event.inputs.branch }}
|
||||||
|
|
||||||
- name: Maximize build space (ubuntu only)
|
- name: Maximize build space (ubuntu only)
|
||||||
if: matrix.settings.platform == 'ubuntu-latest'
|
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
sudo rm -rf /usr/share/dotnet
|
sudo rm -rf /usr/share/dotnet
|
||||||
sudo rm -rf /opt/ghc
|
sudo rm -rf /opt/ghc
|
||||||
@ -88,7 +88,7 @@ jobs:
|
|||||||
vcpkg integrate install
|
vcpkg integrate install
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
- name: install dependencies (ubuntu only)
|
||||||
if: matrix.settings.platform == 'ubuntu-latest'
|
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@ -140,14 +140,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Deb package(ubuntu only)
|
- name: Upload Deb package(ubuntu only)
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: matrix.settings.platform == 'ubuntu-latest'
|
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||||
with:
|
with:
|
||||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||||
|
|
||||||
- name: Upload AppImage package(ubuntu only)
|
- name: Upload AppImage package(ubuntu only)
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: matrix.settings.platform == 'ubuntu-latest'
|
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||||
with:
|
with:
|
||||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||||
|
2
frontend/appflowy_tauri/.gitignore
vendored
2
frontend/appflowy_tauri/.gitignore
vendored
@ -29,3 +29,5 @@ dist-ssr
|
|||||||
|
|
||||||
coverage
|
coverage
|
||||||
**/AppFlowy-Collab
|
**/AppFlowy-Collab
|
||||||
|
|
||||||
|
.env
|
2
frontend/appflowy_tauri/src-tauri/.gitignore
vendored
2
frontend/appflowy_tauri/src-tauri/.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
|
.env
|
||||||
|
92
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
92
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -182,6 +182,7 @@ name = "appflowy_tauri"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"dotenv",
|
||||||
"flowy-config",
|
"flowy-config",
|
||||||
"flowy-core",
|
"flowy-core",
|
||||||
"flowy-date",
|
"flowy-date",
|
||||||
@ -194,6 +195,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -1439,6 +1441,15 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -1449,6 +1460,18 @@ dependencies = [
|
|||||||
"dirs-sys-next",
|
"dirs-sys-next",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -1466,6 +1489,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@ -3088,6 +3117,19 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"rustc_version",
|
||||||
|
"to_method",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@ -3831,6 +3873,28 @@ dependencies = [
|
|||||||
"objc_id",
|
"objc_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d"
|
||||||
|
dependencies = [
|
||||||
|
"objc-sys",
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc_exception"
|
name = "objc_exception"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -3934,6 +3998,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_pipe"
|
name = "os_pipe"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@ -6016,6 +6086,22 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-deep-link"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"interprocess",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"once_cell",
|
||||||
|
"tauri-utils",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
"winreg 0.50.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@ -6242,6 +6328,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "to_method"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.36.0"
|
version = "1.36.0"
|
||||||
|
@ -67,7 +67,10 @@ flowy-document = { path = "../../rust-lib/flowy-document", features = [
|
|||||||
flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
|
flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
|
||||||
"tauri_ts",
|
"tauri_ts",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
uuid = "1.5.0"
|
uuid = "1.5.0"
|
||||||
|
tauri-plugin-deep-link = "0.1.2"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
19
frontend/appflowy_tauri/src-tauri/Info.plist
Normal file
19
frontend/appflowy_tauri/src-tauri/Info.plist
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!-- Add this file next to your tauri.conf.json file -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<!-- Obviously needs to be replaced with your app's bundle identifier -->
|
||||||
|
<string>appflowy-flutter</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>appflowy-flutter</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
4
frontend/appflowy_tauri/src-tauri/env.development
Normal file
4
frontend/appflowy_tauri/src-tauri/env.development
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud
|
||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1
|
||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue
|
||||||
|
APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2
|
4
frontend/appflowy_tauri/src-tauri/env.production
Normal file
4
frontend/appflowy_tauri/src-tauri/env.production
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud
|
||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1
|
||||||
|
APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue
|
||||||
|
APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2
|
@ -3,10 +3,33 @@ use flowy_core::{AppFlowyCore, DEFAULT_NAME};
|
|||||||
use lib_dispatch::runtime::AFPluginRuntime;
|
use lib_dispatch::runtime::AFPluginRuntime;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use dotenv::dotenv;
|
||||||
|
|
||||||
|
pub fn read_env() {
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
let env = if cfg!(debug_assertions) {
|
||||||
|
include_str!("../env.development")
|
||||||
|
} else {
|
||||||
|
include_str!("../env.production")
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in env.lines() {
|
||||||
|
if let Some((key, value)) = line.split_once('=') {
|
||||||
|
// Check if the environment variable is not already set in the system
|
||||||
|
let current_value = std::env::var(key).unwrap_or_default();
|
||||||
|
if current_value.is_empty() {
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_flowy_core() -> AppFlowyCore {
|
pub fn init_flowy_core() -> AppFlowyCore {
|
||||||
let config_json = include_str!("../tauri.conf.json");
|
let config_json = include_str!("../tauri.conf.json");
|
||||||
let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap();
|
let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap();
|
||||||
|
|
||||||
|
let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string());
|
||||||
let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
|
let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
data_path.push("data_dev");
|
data_path.push("data_dev");
|
||||||
@ -18,10 +41,11 @@ pub fn init_flowy_core() -> AppFlowyCore {
|
|||||||
let application_path = data_path.to_str().unwrap().to_string();
|
let application_path = data_path.to_str().unwrap().to_string();
|
||||||
let device_id = uuid::Uuid::new_v4().to_string();
|
let device_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
read_env();
|
||||||
std::env::set_var("RUST_LOG", "trace");
|
std::env::set_var("RUST_LOG", "trace");
|
||||||
// TODO(nathan): pass the real version here
|
|
||||||
let config = AppFlowyCoreConfig::new(
|
let config = AppFlowyCoreConfig::new(
|
||||||
"1.0.0".to_string(),
|
app_version,
|
||||||
custom_application_path,
|
custom_application_path,
|
||||||
application_path,
|
application_path,
|
||||||
device_id,
|
device_id,
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter";
|
||||||
|
pub const OPEN_DEEP_LINK: &str = "open_deep_link";
|
||||||
|
|
||||||
mod init;
|
mod init;
|
||||||
mod notification;
|
mod notification;
|
||||||
mod request;
|
mod request;
|
||||||
@ -12,8 +16,11 @@ use init::*;
|
|||||||
use notification::*;
|
use notification::*;
|
||||||
use request::*;
|
use request::*;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
extern crate dotenv;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME);
|
||||||
|
|
||||||
let flowy_core = init_flowy_core();
|
let flowy_core = init_flowy_core();
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![invoke_request])
|
.invoke_handler(tauri::generate_handler![invoke_request])
|
||||||
@ -26,6 +33,7 @@ fn main() {
|
|||||||
unregister_all_notification_sender();
|
unregister_all_notification_sender();
|
||||||
register_notification_sender(TSNotificationSender::new(app_handler.clone()));
|
register_notification_sender(TSNotificationSender::new(app_handler.clone()));
|
||||||
// tauri::async_runtime::spawn(async move {});
|
// tauri::async_runtime::spawn(async move {});
|
||||||
|
|
||||||
window.listen_global(AF_EVENT, move |event| {
|
window.listen_global(AF_EVENT, move |event| {
|
||||||
on_event(app_handler.clone(), event);
|
on_event(app_handler.clone(), event);
|
||||||
});
|
});
|
||||||
@ -33,18 +41,29 @@ fn main() {
|
|||||||
.setup(|_app| {
|
.setup(|_app| {
|
||||||
let splashscreen_window = _app.get_window("splashscreen").unwrap();
|
let splashscreen_window = _app.get_window("splashscreen").unwrap();
|
||||||
let window = _app.get_window("main").unwrap();
|
let window = _app.get_window("main").unwrap();
|
||||||
|
let handle = _app.handle();
|
||||||
|
|
||||||
// we perform the initialization code on a new task so the app doesn't freeze
|
// we perform the initialization code on a new task so the app doesn't freeze
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
// initialize your app here instead of sleeping :)
|
// initialize your app here instead of sleeping :)
|
||||||
println!("Initializing...");
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
println!("Done initializing.");
|
|
||||||
|
|
||||||
// After it's done, close the splashscreen and display the main window
|
// After it's done, close the splashscreen and display the main window
|
||||||
splashscreen_window.close().unwrap();
|
splashscreen_window.close().unwrap();
|
||||||
window.show().unwrap();
|
window.show().unwrap();
|
||||||
|
// If you need macOS support this must be called in .setup() !
|
||||||
|
// Otherwise this could be called right after prepare() but then you don't have access to tauri APIs
|
||||||
|
// On macOS You still have to install a .app bundle you got from tauri build --debug for this to work!
|
||||||
|
tauri_plugin_deep_link::register(
|
||||||
|
DEEP_LINK_SCHEME,
|
||||||
|
move |request| {
|
||||||
|
dbg!(&request);
|
||||||
|
handle.emit_all(OPEN_DEEP_LINK, request).unwrap();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "AppFlowy",
|
"productName": "AppFlowy",
|
||||||
"version": "0.0.0"
|
"version": "0.0.1"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||||
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
||||||
import { createTheme } from '@mui/material/styles';
|
import { createTheme } from '@mui/material/styles';
|
||||||
import { getDesignTokens } from '$app/utils/mui';
|
import { getDesignTokens } from '$app/utils/mui';
|
||||||
@ -10,6 +10,8 @@ import { UserService } from '$app/application/user/user.service';
|
|||||||
export function useUserSetting() {
|
export function useUserSetting() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||||
|
|
||||||
const { themeMode = ThemeMode.System, theme: themeType = ThemeType.Default } = useAppSelector((state) => {
|
const { themeMode = ThemeMode.System, theme: themeType = ThemeType.Default } = useAppSelector((state) => {
|
||||||
return {
|
return {
|
||||||
themeMode: state.currentUser.userSetting.themeMode,
|
themeMode: state.currentUser.userSetting.themeMode,
|
||||||
@ -22,6 +24,7 @@ export function useUserSetting() {
|
|||||||
(themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
(themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loginState !== LoginState.Success && loginState !== undefined) return;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const settings = await UserService.getAppearanceSetting();
|
const settings = await UserService.getAppearanceSetting();
|
||||||
|
|
||||||
@ -29,7 +32,7 @@ export function useUserSetting() {
|
|||||||
dispatch(currentUserActions.setUserSetting(settings));
|
dispatch(currentUserActions.setUserSetting(settings));
|
||||||
await i18n.changeLanguage(settings.language);
|
await i18n.changeLanguage(settings.language);
|
||||||
})();
|
})();
|
||||||
}, [dispatch, i18n]);
|
}, [dispatch, i18n, loginState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
@ -8,6 +8,7 @@ import { useUserSetting } from '$app/AppMain.hooks';
|
|||||||
import TrashPage from '$app/views/TrashPage';
|
import TrashPage from '$app/views/TrashPage';
|
||||||
import DocumentPage from '$app/views/DocumentPage';
|
import DocumentPage from '$app/views/DocumentPage';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
import AppFlowyDevTool from '$app/components/_shared/devtool/AppFlowyDevTool';
|
||||||
|
|
||||||
function AppMain() {
|
function AppMain() {
|
||||||
const { muiTheme } = useUserSetting();
|
const { muiTheme } = useUserSetting();
|
||||||
@ -22,6 +23,7 @@ function AppMain() {
|
|||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
{process.env.NODE_ENV === 'development' && <AppFlowyDevTool />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
UserEventOpenWorkspace,
|
UserEventOpenWorkspace,
|
||||||
UserEventRenameWorkspace,
|
UserEventRenameWorkspace,
|
||||||
UserEventChangeWorkspaceIcon,
|
UserEventChangeWorkspaceIcon,
|
||||||
|
UserEventGetAllWorkspace,
|
||||||
} from '@/services/backend/events/flowy-user';
|
} from '@/services/backend/events/flowy-user';
|
||||||
import {
|
import {
|
||||||
FolderEventCreateView,
|
FolderEventCreateView,
|
||||||
@ -62,17 +63,13 @@ export async function getWorkspaceChildViews(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkspaces() {
|
export async function getWorkspaces() {
|
||||||
const result = await FolderEventReadCurrentWorkspace();
|
const result = await UserEventGetAllWorkspace();
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const item = result.val;
|
return result.val.items.map((workspace) => ({
|
||||||
|
id: workspace.workspace_id,
|
||||||
return [
|
name: workspace.name,
|
||||||
{
|
}));
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@ -92,12 +89,7 @@ export async function getCurrentWorkspace() {
|
|||||||
const result = await FolderEventReadCurrentWorkspace();
|
const result = await FolderEventReadCurrentWorkspace();
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const workspace = result.val;
|
return result.val.id;
|
||||||
|
|
||||||
return {
|
|
||||||
id: workspace.id,
|
|
||||||
name: workspace.name,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -111,9 +103,7 @@ export async function createCurrentWorkspaceChildView(
|
|||||||
const result = await FolderEventCreateView(payload);
|
const result = await FolderEventCreateView(payload);
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const view = result.val;
|
return result.val;
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(result.err);
|
return Promise.reject(result.err);
|
||||||
|
@ -1,33 +1,63 @@
|
|||||||
import { SignInPayloadPB, SignUpPayloadPB } from '@/services/backend';
|
|
||||||
import {
|
import {
|
||||||
UserEventSignInWithEmailPassword,
|
SignUpPayloadPB,
|
||||||
|
OauthProviderPB,
|
||||||
|
ProviderTypePB,
|
||||||
|
OauthSignInPB,
|
||||||
|
AuthenticatorPB,
|
||||||
|
SignInPayloadPB,
|
||||||
|
} from '@/services/backend';
|
||||||
|
import {
|
||||||
UserEventSignOut,
|
UserEventSignOut,
|
||||||
UserEventSignUp,
|
UserEventSignUp,
|
||||||
|
UserEventGetOauthURLWithProvider,
|
||||||
|
UserEventOauthSignIn,
|
||||||
|
UserEventSignInWithEmailPassword,
|
||||||
} from '@/services/backend/events/flowy-user';
|
} from '@/services/backend/events/flowy-user';
|
||||||
import { nanoid } from '@reduxjs/toolkit';
|
|
||||||
import { Log } from '$app/utils/log';
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
export const AuthService = {
|
export const AuthService = {
|
||||||
signIn: async (params: { email: string; password: string }) => {
|
getOAuthURL: async (provider: ProviderTypePB) => {
|
||||||
const payload = SignInPayloadPB.fromObject({ email: params.email, password: params.password });
|
const providerDataRes = await UserEventGetOauthURLWithProvider(
|
||||||
|
OauthProviderPB.fromObject({
|
||||||
|
provider,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const res = await UserEventSignInWithEmailPassword(payload);
|
if (!providerDataRes.ok) {
|
||||||
|
Log.error(providerDataRes.val.msg);
|
||||||
if (res.ok) {
|
throw new Error(providerDataRes.val.msg);
|
||||||
return res.val;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.error(res.val.msg);
|
const providerData = providerDataRes.val;
|
||||||
throw new Error(res.val.msg);
|
|
||||||
|
return providerData.oauth_url;
|
||||||
},
|
},
|
||||||
|
|
||||||
signUp: async (params: { name: string; email: string; password: string }) => {
|
signInWithOAuth: async ({ uri, deviceId }: { uri: string; deviceId: string }) => {
|
||||||
const deviceId = nanoid(8);
|
const payload = OauthSignInPB.fromObject({
|
||||||
|
authenticator: AuthenticatorPB.AppFlowyCloud,
|
||||||
|
map: {
|
||||||
|
sign_in_url: uri,
|
||||||
|
device_id: deviceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await UserEventOauthSignIn(payload);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
Log.error(res.val.msg);
|
||||||
|
throw new Error(res.val.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.val;
|
||||||
|
},
|
||||||
|
|
||||||
|
signUp: async (params: { deviceId: string; name: string; email: string; password: string }) => {
|
||||||
const payload = SignUpPayloadPB.fromObject({
|
const payload = SignUpPayloadPB.fromObject({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
email: params.email,
|
email: params.email,
|
||||||
password: params.password,
|
password: params.password,
|
||||||
device_id: deviceId,
|
device_id: params.deviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await UserEventSignUp(payload);
|
const res = await UserEventSignUp(payload);
|
||||||
@ -43,4 +73,20 @@ export const AuthService = {
|
|||||||
signOut: () => {
|
signOut: () => {
|
||||||
return UserEventSignOut();
|
return UserEventSignOut();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
signIn: async (email: string, password: string) => {
|
||||||
|
const payload = SignInPayloadPB.fromObject({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await UserEventSignInWithEmailPassword(payload);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
Log.error(res.val.msg);
|
||||||
|
throw new Error(res.val.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.val;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import SpeedDial from '@mui/material/SpeedDial';
|
||||||
|
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
|
||||||
|
import SpeedDialAction from '@mui/material/SpeedDialAction';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { CloseOutlined, BuildOutlined, LoginOutlined, VisibilityOff } from '@mui/icons-material';
|
||||||
|
import ManualSignInDialog from '$app/components/_shared/devtool/ManualSignInDialog';
|
||||||
|
import { Portal } from '@mui/material';
|
||||||
|
|
||||||
|
function AppFlowyDevTool() {
|
||||||
|
const [openManualSignIn, setOpenManualSignIn] = React.useState(false);
|
||||||
|
const [hidden, setHidden] = React.useState(false);
|
||||||
|
const actions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: <LoginOutlined />,
|
||||||
|
name: 'Manual SignIn',
|
||||||
|
onClick: () => {
|
||||||
|
setOpenManualSignIn(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <VisibilityOff />,
|
||||||
|
name: 'Hide Dev Tool',
|
||||||
|
onClick: () => {
|
||||||
|
setHidden(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<SpeedDial
|
||||||
|
hidden={hidden}
|
||||||
|
direction={'left'}
|
||||||
|
draggable={true}
|
||||||
|
title={'AppFlowy Dev Tool'}
|
||||||
|
ariaLabel='SpeedDial basic example'
|
||||||
|
sx={{ position: 'absolute', zIndex: 1500, top: 64, right: 16 }}
|
||||||
|
icon={<SpeedDialIcon className={'text-content-on-fill'} openIcon={<CloseOutlined />} icon={<BuildOutlined />} />}
|
||||||
|
>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<SpeedDialAction onClick={action.onClick} key={action.name} icon={action.icon} tooltipTitle={action.name} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{openManualSignIn && (
|
||||||
|
<ManualSignInDialog
|
||||||
|
open={openManualSignIn}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenManualSignIn(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SpeedDial>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppFlowyDevTool;
|
@ -0,0 +1,114 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CircularProgress, DialogActions, DialogProps, Tab, Tabs, TextareaAutosize } from '@mui/material';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
|
||||||
|
function ManualSignInDialog(props: DialogProps) {
|
||||||
|
const [uri, setUri] = React.useState('');
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const { signInWithOAuth, signInWithEmailPassword } = useAuth();
|
||||||
|
const [tab, setTab] = React.useState(0);
|
||||||
|
const [email, setEmail] = React.useState('');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
|
const [domain, setDomain] = React.useState('');
|
||||||
|
const handleSignIn = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (tab === 1) {
|
||||||
|
if (!email || !password) return;
|
||||||
|
await signInWithEmailPassword(email, password, domain);
|
||||||
|
} else {
|
||||||
|
await signInWithOAuth(uri);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
props?.onClose?.({}, 'backdropClick');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
zIndex: 1500,
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleSignIn();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className={'pt-3'}>
|
||||||
|
<Tabs
|
||||||
|
className={'mb-4'}
|
||||||
|
defaultValue={0}
|
||||||
|
value={tab}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
setTab(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab value={0} label={'OAuth URI'} />
|
||||||
|
<Tab value={1} label={'Email & Password'} />
|
||||||
|
</Tabs>
|
||||||
|
{tab === 1 ? (
|
||||||
|
<div className={'flex flex-col gap-3'}>
|
||||||
|
<TextField
|
||||||
|
label={'Email'}
|
||||||
|
size={'small'}
|
||||||
|
required={true}
|
||||||
|
placeholder={'name@gmail.com'}
|
||||||
|
type={'email'}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size={'small'}
|
||||||
|
required={true}
|
||||||
|
label={'Password'}
|
||||||
|
placeholder={'Password'}
|
||||||
|
type={'password'}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size={'small'}
|
||||||
|
label={'Domain(Optional)'}
|
||||||
|
placeholder={'test.appflowy.cloud'}
|
||||||
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TextareaAutosize
|
||||||
|
value={uri}
|
||||||
|
autoFocus
|
||||||
|
className={'max-h-[300px] w-[400px] overflow-hidden rounded-md border border-line-border p-2 text-xs'}
|
||||||
|
placeholder={'Paste the OAuth URI here'}
|
||||||
|
minRows={3}
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUri(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions className={'mb-4 w-full px-6'}>
|
||||||
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
variant={'outlined'}
|
||||||
|
color={'inherit'}
|
||||||
|
onClick={() => props?.onClose?.({}, 'backdropClick')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button disabled={loading} size={'small'} className={'w-auto'} variant={'outlined'} onClick={handleSignIn}>
|
||||||
|
{loading ? <CircularProgress size={14} /> : 'Sign In'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManualSignInDialog;
|
@ -1,26 +0,0 @@
|
|||||||
import Button from '@mui/material/Button';
|
|
||||||
import GoogleIcon from '$app/assets/settings/google.png';
|
|
||||||
import GithubIcon from '$app/assets/settings/github.png';
|
|
||||||
import DiscordIcon from '$app/assets/settings/discord.png';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export const LoginButtonGroup = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'flex w-full flex-col items-center gap-4'}>
|
|
||||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
|
||||||
<img src={GoogleIcon} alt={'Google'} className={'mr-2 h-6 w-6'} />
|
|
||||||
{t('button.signInGoogle')}
|
|
||||||
</Button>
|
|
||||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
|
||||||
<img src={GithubIcon} alt={'Github'} className={'mr-2 h-6 w-6'} />
|
|
||||||
{t('button.signInGithub')}
|
|
||||||
</Button>
|
|
||||||
<Button className={'w-full rounded-lg border-text-title py-3 text-sm'} color={'inherit'} variant={'outlined'}>
|
|
||||||
<img src={DiscordIcon} alt={'Discord'} className={'mr-2 h-6 w-6'} />
|
|
||||||
{t('button.signInDiscord')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './LoginButtonGroup';
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import GoogleIcon from '$app/assets/settings/google.png';
|
||||||
|
import GithubIcon from '$app/assets/settings/github.png';
|
||||||
|
import DiscordIcon from '$app/assets/settings/discord.png';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
import { ProviderTypePB } from '@/services/backend';
|
||||||
|
|
||||||
|
export const LoginButtonGroup = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'flex w-full flex-col items-center gap-4'}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
void signIn(ProviderTypePB.Google);
|
||||||
|
}}
|
||||||
|
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||||
|
color={'inherit'}
|
||||||
|
variant={'outlined'}
|
||||||
|
>
|
||||||
|
<img src={GoogleIcon} alt={'Google'} className={'mr-2 h-6 w-6'} />
|
||||||
|
{t('button.signInGoogle')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
void signIn(ProviderTypePB.Github);
|
||||||
|
}}
|
||||||
|
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||||
|
color={'inherit'}
|
||||||
|
variant={'outlined'}
|
||||||
|
>
|
||||||
|
<img src={GithubIcon} alt={'Github'} className={'mr-2 h-6 w-6'} />
|
||||||
|
{t('button.signInGithub')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
void signIn(ProviderTypePB.Discord);
|
||||||
|
}}
|
||||||
|
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||||
|
color={'inherit'}
|
||||||
|
variant={'outlined'}
|
||||||
|
>
|
||||||
|
<img src={DiscordIcon} alt={'Discord'} className={'mr-2 h-6 w-6'} />
|
||||||
|
{t('button.signInDiscord')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -3,15 +3,24 @@ import { useAuth } from './auth.hooks';
|
|||||||
import Layout from '$app/components/layout/Layout';
|
import Layout from '$app/components/layout/Layout';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Welcome } from '$app/components/auth/Welcome';
|
import { Welcome } from '$app/components/auth/Welcome';
|
||||||
import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
import { isTauri } from '$app/utils/env';
|
||||||
|
import { notify } from '$app/components/_shared/notify';
|
||||||
|
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||||
|
import { CircularProgress, Portal } from '@mui/material';
|
||||||
|
import { ReactComponent as Logo } from '$app/assets/logo.svg';
|
||||||
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
|
||||||
export const ProtectedRoutes = () => {
|
export const ProtectedRoutes = () => {
|
||||||
const { currentUser, checkUser, subscribeToUser } = useAuth();
|
const { currentUser, checkUser, subscribeToUser, signInWithOAuth } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const isLoading = currentUser?.loginState === LoginState.Loading;
|
||||||
|
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
const checkUserStatus = useCallback(async () => {
|
const checkUserStatus = useCallback(async () => {
|
||||||
await checkUser();
|
await checkUser();
|
||||||
setIsLoading(false);
|
setChecked(true);
|
||||||
}, [checkUser]);
|
}, [checkUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -24,21 +33,73 @@ export const ProtectedRoutes = () => {
|
|||||||
}
|
}
|
||||||
}, [currentUser.isAuthenticated, subscribeToUser]);
|
}, [currentUser.isAuthenticated, subscribeToUser]);
|
||||||
|
|
||||||
if (isLoading) {
|
const onDeepLink = useCallback(async () => {
|
||||||
// It's better to make a fading effect to disappear the loading page
|
if (!isTauri()) return;
|
||||||
return <StartLoading />;
|
const { event } = await import('@tauri-apps/api');
|
||||||
} else {
|
|
||||||
return <SplashScreen isAuthenticated={currentUser.isAuthenticated} />;
|
// On macOS You still have to install a .app bundle you got from tauri build --debug for this to work!
|
||||||
}
|
return await event.listen('open_deep_link', async (e) => {
|
||||||
|
const payload = e.payload as string;
|
||||||
|
|
||||||
|
const [, hash] = payload.split('//#');
|
||||||
|
const obj = parseHash(hash);
|
||||||
|
|
||||||
|
if (!obj.access_token) {
|
||||||
|
notify.error('Failed to sign in, the access token is missing');
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signInWithOAuth(payload);
|
||||||
|
} catch (e) {
|
||||||
|
notify.error('Failed to sign in, please try again');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [dispatch, signInWithOAuth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void onDeepLink();
|
||||||
|
}, [onDeepLink]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'relative h-screen w-screen'}>
|
||||||
|
{checked ? (
|
||||||
|
<SplashScreen isAuthenticated={currentUser.isAuthenticated} />
|
||||||
|
) : (
|
||||||
|
<div className={'flex h-screen w-screen items-center justify-center'}>
|
||||||
|
<Logo className={'h-20 w-20'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && <StartLoading />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StartLoading = () => {
|
const StartLoading = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const preventDefault = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(currentUserActions.resetLoginState());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', preventDefault, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', preventDefault, true);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
return (
|
return (
|
||||||
<div className='flex h-screen w-full flex-col items-center justify-center'>
|
<Portal>
|
||||||
<div className='h-40 w-40 justify-center'>
|
<div className={'fixed inset-0 z-[1400] flex h-full w-full items-center justify-center bg-bg-mask bg-opacity-50'}>
|
||||||
<AppflowyLogo className={'h-24 w-24'} />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,3 +114,14 @@ const SplashScreen = ({ isAuthenticated }: { isAuthenticated: boolean }) => {
|
|||||||
return <Welcome />;
|
return <Welcome />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseHash(hash: string) {
|
||||||
|
const hashParams = new URLSearchParams(hash);
|
||||||
|
const hashObject: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of hashParams) {
|
||||||
|
hashObject[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashObject;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LoginButtonGroup } from '$app/components/_shared/login';
|
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||||
import { AuthenticatorPB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
||||||
import { UserService } from '$app/application/user/user.service';
|
import { UserService } from '$app/application/user/user.service';
|
||||||
import { AuthService } from '$app/application/user/auth.service';
|
import { AuthService } from '$app/application/user/auth.service';
|
||||||
import { useAppSelector, useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { getCurrentWorkspaceSetting } from '$app/application/folder/workspace.service';
|
import { getCurrentWorkspaceSetting } from '$app/application/folder/workspace.service';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { open } from '@tauri-apps/api/shell';
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -31,72 +32,49 @@ export const useAuth = () => {
|
|||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// Check if the user is authenticated
|
const setUser = useCallback(
|
||||||
const checkUser = useCallback(async () => {
|
async (userProfile?: Partial<UserProfilePB>) => {
|
||||||
const userProfile = await UserService.getUserProfile();
|
if (!userProfile) return;
|
||||||
|
|
||||||
if (!userProfile) return;
|
|
||||||
const workspaceSetting = await getCurrentWorkspaceSetting();
|
|
||||||
|
|
||||||
const isLocal = userProfile.authenticator === AuthenticatorPB.Local;
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
currentUserActions.checkUser({
|
|
||||||
id: userProfile.id,
|
|
||||||
token: userProfile.token,
|
|
||||||
email: userProfile.email,
|
|
||||||
displayName: userProfile.name,
|
|
||||||
iconUrl: userProfile.icon_url,
|
|
||||||
isAuthenticated: true,
|
|
||||||
workspaceSetting: workspaceSetting,
|
|
||||||
isLocal,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return userProfile;
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const register = useCallback(
|
|
||||||
async (email: string, password: string, name: string): Promise<UserProfilePB> => {
|
|
||||||
const userProfile = await AuthService.signUp({ email, password, name });
|
|
||||||
|
|
||||||
// Get the workspace setting after user registered. The workspace setting
|
|
||||||
// contains the latest visiting page and the current workspace data.
|
|
||||||
const workspaceSetting = await getCurrentWorkspaceSetting();
|
const workspaceSetting = await getCurrentWorkspaceSetting();
|
||||||
|
|
||||||
|
const isLocal = userProfile.authenticator === AuthenticatorPB.Local;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
currentUserActions.updateUser({
|
currentUserActions.updateUser({
|
||||||
id: userProfile.id,
|
id: userProfile.id,
|
||||||
token: userProfile.token,
|
token: userProfile.token,
|
||||||
email: userProfile.email,
|
email: userProfile.email,
|
||||||
displayName: userProfile.name,
|
displayName: userProfile.name,
|
||||||
|
iconUrl: userProfile.icon_url,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
workspaceSetting,
|
workspaceSetting: workspaceSetting,
|
||||||
|
isLocal,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return userProfile;
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const login = useCallback(
|
// Check if the user is authenticated
|
||||||
async (email: string, password: string): Promise<UserProfilePB> => {
|
const checkUser = useCallback(async () => {
|
||||||
const user = await AuthService.signIn({ email, password });
|
const userProfile = await UserService.getUserProfile();
|
||||||
const { id, token, name } = user;
|
|
||||||
|
|
||||||
dispatch(
|
await setUser(userProfile);
|
||||||
currentUserActions.updateUser({
|
|
||||||
id: id,
|
return userProfile;
|
||||||
token: token,
|
}, [setUser]);
|
||||||
email,
|
|
||||||
displayName: name,
|
const register = useCallback(
|
||||||
isAuthenticated: true,
|
async (email: string, password: string, name: string): Promise<UserProfilePB> => {
|
||||||
})
|
const deviceId = currentUser?.deviceId ?? nanoid(8);
|
||||||
);
|
const userProfile = await AuthService.signUp({ deviceId, email, password, name });
|
||||||
return user;
|
|
||||||
|
await setUser(userProfile);
|
||||||
|
|
||||||
|
return userProfile;
|
||||||
},
|
},
|
||||||
[dispatch]
|
[setUser, currentUser?.deviceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
@ -112,5 +90,97 @@ export const useAuth = () => {
|
|||||||
await register(fakeEmail, fakePassword, fakeName);
|
await register(fakeEmail, fakePassword, fakeName);
|
||||||
}, [register]);
|
}, [register]);
|
||||||
|
|
||||||
return { currentUser, checkUser, register, login, logout, subscribeToUser, signInAsAnonymous };
|
const signIn = useCallback(
|
||||||
|
async (provider: ProviderTypePB) => {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||||
|
try {
|
||||||
|
const url = await AuthService.getOAuthURL(provider);
|
||||||
|
|
||||||
|
await open(url);
|
||||||
|
} catch {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const signInWithOAuth = useCallback(
|
||||||
|
async (uri: string) => {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||||
|
try {
|
||||||
|
const deviceId = currentUser?.deviceId ?? nanoid(8);
|
||||||
|
|
||||||
|
await AuthService.signInWithOAuth({ uri, deviceId });
|
||||||
|
const userProfile = await UserService.getUserProfile();
|
||||||
|
|
||||||
|
await setUser(userProfile);
|
||||||
|
|
||||||
|
return userProfile;
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, currentUser?.deviceId, setUser]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only for development purposes
|
||||||
|
const signInWithEmailPassword = useCallback(
|
||||||
|
async (email: string, password: string, domain?: string) => {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Loading));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://${domain ? domain : 'test.appflowy.cloud'}/gotrue/token?grant_type=password`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
redirect: 'follow',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let uri = `appflowy-flutter://#`;
|
||||||
|
const params: string[] = [];
|
||||||
|
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (typeof data[key] === 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(`${key}=${data[key]}`);
|
||||||
|
});
|
||||||
|
uri += params.join('&');
|
||||||
|
|
||||||
|
return signInWithOAuth(uri);
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(currentUserActions.setLoginState(LoginState.Error));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, signInWithOAuth]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentUser,
|
||||||
|
checkUser,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
subscribeToUser,
|
||||||
|
signInAsAnonymous,
|
||||||
|
signIn,
|
||||||
|
signInWithOAuth,
|
||||||
|
signInWithEmailPassword,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice';
|
import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice';
|
||||||
import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
@ -10,29 +10,29 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
export function useLoadWorkspaces() {
|
export function useLoadWorkspaces() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { workspaces, currentWorkspace } = useAppSelector((state) => state.workspace);
|
const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace);
|
||||||
|
|
||||||
|
const currentWorkspace = useMemo(() => {
|
||||||
|
return workspaces.find((workspace) => workspace.id === currentWorkspaceId);
|
||||||
|
}, [workspaces, currentWorkspaceId]);
|
||||||
|
|
||||||
const initializeWorkspaces = useCallback(async () => {
|
const initializeWorkspaces = useCallback(async () => {
|
||||||
const workspaces = await workspaceService.getWorkspaces();
|
const workspaces = await workspaceService.getWorkspaces();
|
||||||
const currentWorkspace = await workspaceService.getCurrentWorkspace();
|
|
||||||
|
const currentWorkspaceId = await workspaceService.getCurrentWorkspace();
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
workspaceActions.initWorkspaces({
|
workspaceActions.initWorkspaces({
|
||||||
workspaces,
|
workspaces,
|
||||||
currentWorkspace,
|
currentWorkspaceId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void (async () => {
|
|
||||||
await initializeWorkspaces();
|
|
||||||
})();
|
|
||||||
}, [initializeWorkspaces]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaces,
|
workspaces,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
|
initializeWorkspaces,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +82,10 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
|||||||
{
|
{
|
||||||
[FolderNotification.DidUpdateWorkspace]: async (changeset) => {
|
[FolderNotification.DidUpdateWorkspace]: async (changeset) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
workspaceActions.updateCurrentWorkspace({
|
workspaceActions.updateWorkspace({
|
||||||
|
id: String(changeset.id),
|
||||||
name: changeset.name,
|
name: changeset.name,
|
||||||
|
icon: changeset.icon_url,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -43,8 +43,20 @@ function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: bo
|
|||||||
>
|
>
|
||||||
<Tooltip disableInteractive={true} placement={'top-start'} title={t('sideBar.clickToHidePersonal')}>
|
<Tooltip disableInteractive={true} placement={'top-start'} title={t('sideBar.clickToHidePersonal')}>
|
||||||
<div className={'flex items-center gap-2 rounded px-2 py-1 text-xs font-medium hover:bg-fill-list-active'}>
|
<div className={'flex items-center gap-2 rounded px-2 py-1 text-xs font-medium hover:bg-fill-list-active'}>
|
||||||
<WorkplaceAvatar workplaceName={workspace.name} width={18} height={18} className={'text-[70%]'} />
|
{!workspace.name ? (
|
||||||
{workspace.name}
|
t('sideBar.personal')
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WorkplaceAvatar
|
||||||
|
icon={workspace.icon}
|
||||||
|
workplaceName={workspace.name}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
className={'text-[70%]'}
|
||||||
|
/>
|
||||||
|
{workspace.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showAdd && (
|
{showAdd && (
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import NewPageButton from '$app/components/layout/workspace_manager/NewPageButton';
|
import NewPageButton from '$app/components/layout/workspace_manager/NewPageButton';
|
||||||
import { useLoadWorkspaces } from '$app/components/layout/workspace_manager/Workspace.hooks';
|
import { useLoadWorkspaces } from '$app/components/layout/workspace_manager/Workspace.hooks';
|
||||||
import Workspace from './Workspace';
|
import Workspace from './Workspace';
|
||||||
import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
|
import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
|
||||||
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
|
import { LoginState } from '$app_reducers/current-user/slice';
|
||||||
|
|
||||||
function WorkspaceManager() {
|
function WorkspaceManager() {
|
||||||
const { workspaces, currentWorkspace } = useLoadWorkspaces();
|
const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces();
|
||||||
|
|
||||||
|
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginState === LoginState.Success || loginState === undefined) {
|
||||||
|
void initializeWorkspaces();
|
||||||
|
}
|
||||||
|
}, [initializeWorkspaces, loginState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { LoginButtonGroup } from '$app/components/_shared/login';
|
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
||||||
|
|
||||||
export const Login = ({ onBack }: { onBack?: () => void }) => {
|
export const Login = ({ onBack }: { onBack?: () => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import Dialog, { DialogProps } from '@mui/material/Dialog';
|
import Dialog, { DialogProps } from '@mui/material/Dialog';
|
||||||
import { Settings } from '$app/components/settings/Settings';
|
import { Settings } from '$app/components/settings/Settings';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import { ReactComponent as CloseIcon } from '$app/assets/close.svg';
|
import { ReactComponent as CloseIcon } from '$app/assets/close.svg';
|
||||||
@ -14,10 +14,15 @@ import { SettingsRoutes } from '$app/components/settings/workplace/const';
|
|||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import { Login } from '$app/components/settings/Login';
|
import { Login } from '$app/components/settings/Login';
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
|
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export const SettingsDialog = (props: DialogProps) => {
|
export const SettingsDialog = (props: DialogProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [routes, setRoutes] = useState<SettingsRoutes[]>([]);
|
const [routes, setRoutes] = useState<SettingsRoutes[]>([]);
|
||||||
|
const loginState = useAppSelector((state) => state.currentUser.loginState);
|
||||||
|
const lastLoginStateRef = useRef(loginState);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleForward = useCallback((route: SettingsRoutes) => {
|
const handleForward = useCallback((route: SettingsRoutes) => {
|
||||||
setRoutes((prev) => {
|
setRoutes((prev) => {
|
||||||
@ -29,14 +34,28 @@ export const SettingsDialog = (props: DialogProps) => {
|
|||||||
setRoutes((prevState) => {
|
setRoutes((prevState) => {
|
||||||
return prevState.slice(0, -1);
|
return prevState.slice(0, -1);
|
||||||
});
|
});
|
||||||
}, []);
|
dispatch(currentUserActions.resetLoginState());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = useCallback(() => {
|
||||||
|
dispatch(currentUserActions.resetLoginState());
|
||||||
props?.onClose?.({}, 'backdropClick');
|
props?.onClose?.({}, 'backdropClick');
|
||||||
};
|
}, [dispatch, props]);
|
||||||
|
|
||||||
const currentRoute = routes[routes.length - 1];
|
const currentRoute = routes[routes.length - 1];
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) {
|
||||||
|
navigate('/');
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLoginStateRef.current = loginState;
|
||||||
|
}, [loginState, handleClose, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -4,9 +4,13 @@ import Button from '@mui/material/Button';
|
|||||||
import { Divider } from '@mui/material';
|
import { Divider } from '@mui/material';
|
||||||
import { DeleteAccount } from '$app/components/settings/my_account/DeleteAccount';
|
import { DeleteAccount } from '$app/components/settings/my_account/DeleteAccount';
|
||||||
import { SettingsRoutes } from '$app/components/settings/workplace/const';
|
import { SettingsRoutes } from '$app/components/settings/workplace/const';
|
||||||
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
|
||||||
export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => {
|
export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { currentUser, logout } = useAuth();
|
||||||
|
|
||||||
|
const isLocal = currentUser.isLocal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -15,12 +19,17 @@ export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes
|
|||||||
{t('newSettings.myAccount.accountLogin')}
|
{t('newSettings.myAccount.accountLogin')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
onForward?.(SettingsRoutes.LOGIN);
|
if (isLocal) {
|
||||||
|
onForward?.(SettingsRoutes.LOGIN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await logout();
|
||||||
}}
|
}}
|
||||||
variant={'contained'}
|
variant={'contained'}
|
||||||
>
|
>
|
||||||
{t('button.login')}
|
{!isLocal ? t('button.logout') : t('button.login')}
|
||||||
</Button>
|
</Button>
|
||||||
<Divider className={'my-4'} />
|
<Divider className={'my-4'} />
|
||||||
<DeleteAccount />
|
<DeleteAccount />
|
||||||
|
@ -1,39 +1,65 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Divider, OutlinedInput } from '@mui/material';
|
import { Divider, OutlinedInput } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { changeWorkspaceIcon, renameWorkspace } from '$app/application/folder/workspace.service';
|
import { changeWorkspaceIcon, renameWorkspace } from '$app/application/folder/workspace.service';
|
||||||
import { notify } from '$app/components/_shared/notify';
|
import { notify } from '$app/components/_shared/notify';
|
||||||
import { WorkplaceAvatar } from '$app/components/_shared/avatar';
|
import { WorkplaceAvatar } from '$app/components/_shared/avatar';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
import { PopoverCommonProps } from '$app/components/editor/components/tools/popover';
|
||||||
import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker';
|
import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker';
|
||||||
|
import { workspaceActions } from '$app_reducers/workspace/slice';
|
||||||
|
import debounce from 'lodash-es/debounce';
|
||||||
|
|
||||||
export const WorkplaceDisplay = () => {
|
export const WorkplaceDisplay = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isLocal = useAppSelector((state) => state.currentUser.isLocal);
|
const isLocal = useAppSelector((state) => state.currentUser.isLocal);
|
||||||
const workspace = useAppSelector((state) => state.workspace.currentWorkspace);
|
const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace);
|
||||||
|
const workspace = useMemo(
|
||||||
|
() => workspaces.find((workspace) => workspace.id === currentWorkspaceId),
|
||||||
|
[workspaces, currentWorkspaceId]
|
||||||
|
);
|
||||||
const [name, setName] = useState(workspace?.name ?? '');
|
const [name, setName] = useState(workspace?.name ?? '');
|
||||||
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||||
const openEmojiPicker = Boolean(emojiPickerAnchor);
|
const openEmojiPicker = Boolean(emojiPickerAnchor);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const debounceUpdateWorkspace = useMemo(() => {
|
||||||
|
return debounce(async ({ id, name, icon }: { id: string; name?: string; icon?: string }) => {
|
||||||
|
if (!id || !name) return;
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
try {
|
||||||
|
await changeWorkspaceIcon(id, icon);
|
||||||
|
} catch {
|
||||||
|
notify.error(t('newSettings.workplace.updateIconError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
try {
|
||||||
|
await renameWorkspace(id, name);
|
||||||
|
} catch {
|
||||||
|
notify.error(t('newSettings.workplace.renameError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!workspace || !name) return;
|
if (!workspace || !name) return;
|
||||||
try {
|
dispatch(workspaceActions.updateWorkspace({ id: workspace.id, name }));
|
||||||
await renameWorkspace(workspace.id, name);
|
|
||||||
} catch {
|
await debounceUpdateWorkspace({ id: workspace.id, name });
|
||||||
notify.error(t('newSettings.workplace.renameError'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmojiSelect = async (icon: string) => {
|
const handleEmojiSelect = async (icon: string) => {
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
try {
|
dispatch(workspaceActions.updateWorkspace({ id: workspace.id, icon }));
|
||||||
await changeWorkspaceIcon(workspace.id, icon);
|
|
||||||
} catch {
|
await debounceUpdateWorkspace({ id: workspace.id, icon });
|
||||||
notify.error(t('newSettings.workplace.updateIconError'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
@ -93,6 +119,7 @@ export const WorkplaceDisplay = () => {
|
|||||||
workplaceName={name}
|
workplaceName={name}
|
||||||
width={62}
|
width={62}
|
||||||
height={62}
|
height={62}
|
||||||
|
icon={workspace?.icon}
|
||||||
className={'rounded-lg border border-bg-body p-[2px] hover:opacity-90'}
|
className={'rounded-lg border border-bg-body p-[2px] hover:opacity-90'}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -17,8 +17,15 @@ export enum Theme {
|
|||||||
Lavender = 'lavender',
|
Lavender = 'lavender',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LoginState {
|
||||||
|
Loading = 'loading',
|
||||||
|
Success = 'success',
|
||||||
|
Error = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICurrentUser {
|
export interface ICurrentUser {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
deviceId?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
@ -27,6 +34,7 @@ export interface ICurrentUser {
|
|||||||
workspaceSetting?: WorkspaceSettingPB;
|
workspaceSetting?: WorkspaceSettingPB;
|
||||||
userSetting: UserSetting;
|
userSetting: UserSetting;
|
||||||
isLocal: boolean;
|
isLocal: boolean;
|
||||||
|
loginState?: LoginState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ICurrentUser | null = {
|
const initialState: ICurrentUser | null = {
|
||||||
@ -39,17 +47,11 @@ export const currentUserSlice = createSlice({
|
|||||||
name: 'currentUser',
|
name: 'currentUser',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
checkUser: (state, action: PayloadAction<Partial<ICurrentUser>>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUser: (state, action: PayloadAction<Partial<ICurrentUser>>) => {
|
updateUser: (state, action: PayloadAction<Partial<ICurrentUser>>) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...action.payload,
|
...action.payload,
|
||||||
|
loginState: LoginState.Success,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
@ -61,6 +63,14 @@ export const currentUserSlice = createSlice({
|
|||||||
...action.payload,
|
...action.payload,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setLoginState: (state, action: PayloadAction<LoginState>) => {
|
||||||
|
state.loginState = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetLoginState: (state) => {
|
||||||
|
state.loginState = undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,16 +3,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
export interface WorkspaceItem {
|
export interface WorkspaceItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceState {
|
interface WorkspaceState {
|
||||||
workspaces: WorkspaceItem[];
|
workspaces: WorkspaceItem[];
|
||||||
currentWorkspace: WorkspaceItem | null;
|
currentWorkspaceId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: WorkspaceState = {
|
const initialState: WorkspaceState = {
|
||||||
workspaces: [],
|
workspaces: [],
|
||||||
currentWorkspace: null,
|
currentWorkspaceId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const workspaceSlice = createSlice({
|
export const workspaceSlice = createSlice({
|
||||||
@ -23,18 +24,21 @@ export const workspaceSlice = createSlice({
|
|||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
workspaces: WorkspaceItem[];
|
workspaces: WorkspaceItem[];
|
||||||
currentWorkspace: WorkspaceItem | null;
|
currentWorkspaceId: string | null;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
return action.payload;
|
return action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentWorkspace: (state, action: PayloadAction<Partial<WorkspaceItem>>) => {
|
updateWorkspace: (state, action: PayloadAction<Partial<WorkspaceItem>>) => {
|
||||||
if (!state.currentWorkspace) return;
|
const index = state.workspaces.findIndex((workspace) => workspace.id === action.payload.id);
|
||||||
state.currentWorkspace = {
|
|
||||||
...state.currentWorkspace,
|
if (index !== -1) {
|
||||||
...action.payload,
|
state.workspaces[index] = {
|
||||||
};
|
...state.workspaces[index],
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -22,10 +22,29 @@ export const colorMap = {
|
|||||||
[ColorEnum.Blue]: 'var(--tint-blue)',
|
[ColorEnum.Blue]: 'var(--tint-blue)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert ARGB to RGBA
|
||||||
|
// Flutter uses ARGB, but CSS uses RGBA
|
||||||
|
function argbToRgba(color: string): string {
|
||||||
|
const hex = color.replace(/^#|0x/, '');
|
||||||
|
|
||||||
|
const hasAlpha = hex.length === 8;
|
||||||
|
|
||||||
|
if (!hasAlpha) {
|
||||||
|
return color.replace('0x', '#');
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const g = parseInt(hex.slice(4, 6), 16);
|
||||||
|
const b = parseInt(hex.slice(6, 8), 16);
|
||||||
|
const a = hasAlpha ? parseInt(hex.slice(0, 2), 16) / 255 : 1;
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderColor(color: string) {
|
export function renderColor(color: string) {
|
||||||
if (colorMap[color as ColorEnum]) {
|
if (colorMap[color as ColorEnum]) {
|
||||||
return colorMap[color as ColorEnum];
|
return colorMap[color as ColorEnum];
|
||||||
}
|
}
|
||||||
|
|
||||||
return color.replace('0x', '#');
|
return argbToRgba(color);
|
||||||
}
|
}
|
||||||
|
@ -695,6 +695,7 @@ impl UserManager {
|
|||||||
|
|
||||||
save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
|
save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
|
||||||
event!(tracing::Level::INFO, "Save new user profile to disk");
|
event!(tracing::Level::INFO, "Save new user profile to disk");
|
||||||
|
|
||||||
self.authenticate_user.set_session(Some(session.clone()))?;
|
self.authenticate_user.set_session(Some(session.clone()))?;
|
||||||
self
|
self
|
||||||
.save_user(uid, (user_profile, authenticator.clone()).into())
|
.save_user(uid, (user_profile, authenticator.clone()).into())
|
||||||
|
Loading…
Reference in New Issue
Block a user