diff options
author | crupest <crupest@outlook.com> | 2020-08-07 00:32:35 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-07 00:32:35 +0800 |
commit | ef5f490cf3155234a12d42f1b43630e38cb49a38 (patch) | |
tree | 3a8ed8665909001aa8c201b1d51cda2bfd4b9688 | |
parent | f235e3c601399fb4300d44b2406fe6744a16eccd (diff) | |
parent | 394842105d4ebf2d01523eae8ccf5091113f7cbd (diff) | |
download | timeline-ef5f490cf3155234a12d42f1b43630e38cb49a38.tar.gz timeline-ef5f490cf3155234a12d42f1b43630e38cb49a38.tar.bz2 timeline-ef5f490cf3155234a12d42f1b43630e38cb49a38.zip |
Merge pull request #134 from crupest/offline-user
Make user offline usable.
-rw-r--r-- | Timeline/ClientApp/.babelrc | 3 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/data/timeline.ts | 2 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/data/user.ts | 226 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/settings/Settings.tsx | 7 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/user/Login.tsx | 4 | ||||
-rw-r--r-- | Timeline/ClientApp/src/tsconfig.json | 2 | ||||
-rw-r--r-- | Timeline/ClientApp/webpack.config.dev.js | 2 |
7 files changed, 160 insertions, 86 deletions
diff --git a/Timeline/ClientApp/.babelrc b/Timeline/ClientApp/.babelrc index 78d36a3d..092f2f73 100644 --- a/Timeline/ClientApp/.babelrc +++ b/Timeline/ClientApp/.babelrc @@ -1,8 +1,7 @@ {
"presets": [
"@babel/env",
- "@babel/preset-react",
- "@babel/preset-typescript"
+ "@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts index 508363a4..c0d2141f 100644 --- a/Timeline/ClientApp/src/app/data/timeline.ts +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -156,8 +156,8 @@ export class TimelineService { private async doFetchAndCacheTimeline(
timelineName: string
): Promise<FetchAndCacheTimelineResult> {
- const cache = await dataStorage.getItem<TimelineCache | null>(timelineName);
const key = this.getTimelineKey(timelineName);
+ const cache = await dataStorage.getItem<TimelineCache | null>(key);
const save = (cache: TimelineCache): Promise<TimelineCache> =>
dataStorage.setItem<TimelineCache>(key, cache);
diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts index 65b53a6f..defcb4e4 100644 --- a/Timeline/ClientApp/src/app/data/user.ts +++ b/Timeline/ClientApp/src/app/data/user.ts @@ -1,14 +1,14 @@ import React, { useState, useEffect } from 'react';
-import { BehaviorSubject, Observable, of, from } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { BehaviorSubject, Observable, from } from 'rxjs';
import { UiLogicError } from '../common';
import { convertError } from '../utilities/rxjs';
import { pushAlert } from '../common/alert-service';
+import { dataStorage } from './common';
import { SubscriptionHub, ISubscriptionHub } from './SubscriptionHub';
-import { HttpNetworkError } from '../http/common';
+import { HttpNetworkError, BlobWithEtag, NotModified } from '../http/common';
import {
getHttpTokenClient,
HttpCreateTokenBadCredentialError,
@@ -18,6 +18,7 @@ import { HttpUserNotExistError,
HttpUser,
} from '../http/user';
+import { queue } from './queue';
export type User = HttpUser;
@@ -39,7 +40,7 @@ export class BadCredentialError { message = 'login.badCredential';
}
-const TOKEN_STORAGE_KEY = 'token';
+const USER_STORAGE_KEY = 'currentuser';
export class UserService {
private userSubject = new BehaviorSubject<UserWithToken | null | undefined>(
@@ -54,95 +55,92 @@ export class UserService { return this.userSubject.value;
}
- checkLoginState(): Observable<UserWithToken | null> {
- if (this.currentUser !== undefined)
- throw new UiLogicError("Already checked user. Can't check twice.");
-
- const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY);
- if (savedToken) {
- const u$ = from(getHttpTokenClient().verify({ token: savedToken })).pipe(
- map(
- (res) =>
- ({
- ...res.user,
- token: savedToken,
- } as UserWithToken)
- )
- );
- u$.subscribe(
- (user) => {
- if (user != null) {
- pushAlert({
- type: 'success',
- message: {
- type: 'i18n',
- key: 'user.welcomeBack',
- },
- });
- }
- this.userSubject.next(user);
+ async checkLoginState(): Promise<UserWithToken | null> {
+ if (this.currentUser !== undefined) {
+ console.warn("Already checked user. Can't check twice.");
+ }
+
+ const savedUser = await dataStorage.getItem<UserWithToken | null>(
+ USER_STORAGE_KEY
+ );
+
+ if (savedUser == null) {
+ this.userSubject.next(null);
+ return null;
+ }
+
+ this.userSubject.next(savedUser);
+
+ const savedToken = savedUser.token;
+ try {
+ const res = await getHttpTokenClient().verify({ token: savedToken });
+ const user: UserWithToken = { ...res.user, token: savedToken };
+ await dataStorage.setItem<UserWithToken>(USER_STORAGE_KEY, user);
+ this.userSubject.next(user);
+ pushAlert({
+ type: 'success',
+ message: {
+ type: 'i18n',
+ key: 'user.welcomeBack',
},
- (error) => {
- if (error instanceof HttpNetworkError) {
- pushAlert({
- type: 'danger',
- message: { type: 'i18n', key: 'user.verifyTokenFailedNetwork' },
- });
- } else {
- window.localStorage.removeItem(TOKEN_STORAGE_KEY);
- pushAlert({
- type: 'danger',
- message: { type: 'i18n', key: 'user.verifyTokenFailed' },
- });
- }
- this.userSubject.next(null);
- }
- );
- return u$;
+ });
+ return user;
+ } catch (error) {
+ if (error instanceof HttpNetworkError) {
+ pushAlert({
+ type: 'danger',
+ message: { type: 'i18n', key: 'user.verifyTokenFailedNetwork' },
+ });
+ return savedUser;
+ } else {
+ await dataStorage.removeItem(USER_STORAGE_KEY);
+ this.userSubject.next(null);
+ pushAlert({
+ type: 'danger',
+ message: { type: 'i18n', key: 'user.verifyTokenFailed' },
+ });
+ return null;
+ }
}
- this.userSubject.next(null);
- return of(null);
}
- login(
+ async login(
credentials: LoginCredentials,
rememberMe: boolean
- ): Observable<UserWithToken> {
+ ): Promise<void> {
if (this.currentUser) {
throw new UiLogicError('Already login.');
}
- const u$ = from(
- getHttpTokenClient().create({
+ try {
+ const res = await getHttpTokenClient().create({
...credentials,
expire: 30,
- })
- ).pipe(
- map(
- (res) =>
- ({
- ...res.user,
- token: res.token,
- } as UserWithToken)
- ),
- convertError(HttpCreateTokenBadCredentialError, BadCredentialError)
- );
- u$.subscribe((user) => {
+ });
+ const user: UserWithToken = {
+ ...res.user,
+ token: res.token,
+ };
if (rememberMe) {
- window.localStorage.setItem(TOKEN_STORAGE_KEY, user.token);
+ await dataStorage.setItem<UserWithToken>(USER_STORAGE_KEY, user);
}
this.userSubject.next(user);
- });
- return u$;
+ } catch (e) {
+ if (e instanceof HttpCreateTokenBadCredentialError) {
+ throw new BadCredentialError();
+ } else {
+ throw e;
+ }
+ }
}
- logout(): void {
+ async logout(): Promise<void> {
if (this.currentUser === undefined) {
throw new UiLogicError('Please check user first.');
}
if (this.currentUser === null) {
throw new UiLogicError('No login.');
}
- window.localStorage.removeItem(TOKEN_STORAGE_KEY);
+ await dataStorage.removeItem(USER_STORAGE_KEY);
this.userSubject.next(null);
}
@@ -163,7 +161,7 @@ export class UserService { )
);
$.subscribe(() => {
- this.logout();
+ void this.logout();
});
return $;
}
@@ -229,12 +227,88 @@ export function checkLogin(): UserWithToken { export class UserNotExistError extends Error {}
export class UserInfoService {
+ private getAvatarKey(username: string): string {
+ return `user.${username}.avatar`;
+ }
+
+ private getCachedAvatar(username: string): Promise<Blob | null> {
+ return dataStorage
+ .getItem<BlobWithEtag | null>(this.getAvatarKey(username))
+ .then((data) => data?.data ?? null);
+ }
+
+ private async fetchAndCacheAvatar(
+ username: string
+ ): Promise<{ data: Blob; type: 'synced' | 'cache' } | 'offline'> {
+ return queue(`UserService.fetchAndCacheAvatar.${username}`, () =>
+ this.doFetchAndCacheAvatar(username)
+ );
+ }
+
+ private async doFetchAndCacheAvatar(
+ username: string
+ ): Promise<{ data: Blob; type: 'synced' | 'cache' } | 'offline'> {
+ const key = this.getAvatarKey(username);
+ const cache = await dataStorage.getItem<BlobWithEtag | null>(key);
+ if (cache == null) {
+ try {
+ const avatar = await getHttpUserClient().getAvatar(key);
+ await dataStorage.setItem<BlobWithEtag>(key, avatar);
+ return {
+ data: avatar.data,
+ type: 'synced',
+ };
+ } catch (e) {
+ if (e instanceof HttpNetworkError) {
+ return 'offline';
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ try {
+ const res = await getHttpUserClient().getAvatar(key, cache.etag);
+ if (res instanceof NotModified) {
+ return {
+ data: cache.data,
+ type: 'synced',
+ };
+ } else {
+ const avatar = res;
+ await dataStorage.setItem<BlobWithEtag>(key, avatar);
+ return {
+ data: avatar.data,
+ type: 'synced',
+ };
+ }
+ } catch (e) {
+ if (e instanceof HttpNetworkError) {
+ return {
+ data: cache.data,
+ type: 'cache',
+ };
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
private _avatarSubscriptionHub = new SubscriptionHub<string, Blob>({
setup: (key, line) => {
- void getHttpUserClient()
- .getAvatar(key)
- .then((res) => {
- line.next(res.data);
+ void this.getCachedAvatar(key)
+ .then((avatar) => {
+ if (avatar != null) {
+ line.next(avatar);
+ }
+ })
+ .then(() => {
+ return this.fetchAndCacheAvatar(key);
+ })
+ .then((result) => {
+ if (result !== 'offline') {
+ line.next(result.data);
+ }
});
},
});
diff --git a/Timeline/ClientApp/src/app/settings/Settings.tsx b/Timeline/ClientApp/src/app/settings/Settings.tsx index a247557d..13e6f4f3 100644 --- a/Timeline/ClientApp/src/app/settings/Settings.tsx +++ b/Timeline/ClientApp/src/app/settings/Settings.tsx @@ -81,7 +81,7 @@ const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => { await userService
.changePassword(oldPassword as string, newPassword as string)
.toPromise();
- userService.logout();
+ await userService.logout();
setRedirect(true);
}}
close={() => {
@@ -204,8 +204,9 @@ const Settings: React.FC = (_) => { <ConfirmLogoutDialog
toggle={() => setDialog(null)}
onConfirm={() => {
- userService.logout();
- history.push('/');
+ void userService.logout().then(() => {
+ history.push('/');
+ });
}}
/>
);
diff --git a/Timeline/ClientApp/src/app/user/Login.tsx b/Timeline/ClientApp/src/app/user/Login.tsx index a615d8ed..2f2a3188 100644 --- a/Timeline/ClientApp/src/app/user/Login.tsx +++ b/Timeline/ClientApp/src/app/user/Login.tsx @@ -62,8 +62,8 @@ const Login: React.FC = (_) => { },
rememberMe
)
- .subscribe(
- (_) => {
+ .then(
+ () => {
if (history.length === 0) {
history.push('/');
} else {
diff --git a/Timeline/ClientApp/src/tsconfig.json b/Timeline/ClientApp/src/tsconfig.json index f429a553..00277635 100644 --- a/Timeline/ClientApp/src/tsconfig.json +++ b/Timeline/ClientApp/src/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react",
+ "jsx": "preserve",
"sourceMap": true
}
}
\ No newline at end of file diff --git a/Timeline/ClientApp/webpack.config.dev.js b/Timeline/ClientApp/webpack.config.dev.js index 80295664..fe5528ce 100644 --- a/Timeline/ClientApp/webpack.config.dev.js +++ b/Timeline/ClientApp/webpack.config.dev.js @@ -15,7 +15,7 @@ module.exports = (env) => { return {
entry,
mode: 'development',
- devtool: 'eval-source-map',
+ devtool: 'eval-cheap-module-source-map',
module: {
rules: [
...commonRules,
|