From 76ca05a801a51be8d65424a6b52965813c37bf3d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 6 Aug 2020 19:39:35 +0800 Subject: Add fetchAndCacheAvatar. --- Timeline/ClientApp/src/app/data/user.ts | 61 +++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts index 65b53a6f..e893a6c0 100644 --- a/Timeline/ClientApp/src/app/data/user.ts +++ b/Timeline/ClientApp/src/app/data/user.ts @@ -6,9 +6,10 @@ 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, @@ -55,8 +56,9 @@ export class UserService { } checkLoginState(): Observable { - if (this.currentUser !== undefined) - throw new UiLogicError("Already checked user. Can't check twice."); + if (this.currentUser !== undefined) { + console.warn("Already checked user. Can't check twice."); + } const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY); if (savedToken) { @@ -229,6 +231,59 @@ export function checkLogin(): UserWithToken { export class UserNotExistError extends Error {} export class UserInfoService { + private getAvatarKey(username: string): string { + return `user.${username}.avatar`; + } + + private async fetchAndCacheAvatar( + username: string + ): Promise<{ data: Blob; type: 'synced' | 'cache' } | 'offline'> { + const key = this.getAvatarKey(username); + const cache = await dataStorage.getItem(key); + if (cache == null) { + try { + const avatar = await getHttpUserClient().getAvatar(key); + await dataStorage.setItem(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(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({ setup: (key, line) => { void getHttpUserClient() -- cgit v1.2.3 From 1087499e6cbbee1a506fcd7c762703b90526a7ab Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 6 Aug 2020 23:33:47 +0800 Subject: Avatar use cache first. --- Timeline/ClientApp/src/app/data/user.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts index e893a6c0..14f4e943 100644 --- a/Timeline/ClientApp/src/app/data/user.ts +++ b/Timeline/ClientApp/src/app/data/user.ts @@ -19,6 +19,7 @@ import { HttpUserNotExistError, HttpUser, } from '../http/user'; +import { queue } from './queue'; export type User = HttpUser; @@ -235,8 +236,22 @@ export class UserInfoService { return `user.${username}.avatar`; } + private getCachedAvatar(username: string): Promise { + return dataStorage + .getItem(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(key); @@ -286,10 +301,19 @@ export class UserInfoService { private _avatarSubscriptionHub = new SubscriptionHub({ 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); + } }); }, }); -- cgit v1.2.3 From 978c9071613177aea8b3d773827c87fa329522cd Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 7 Aug 2020 00:01:45 +0800 Subject: Use cache login user if offline. --- Timeline/ClientApp/src/app/data/user.ts | 131 ++++++++++++++---------------- Timeline/ClientApp/src/app/user/Login.tsx | 4 +- 2 files changed, 65 insertions(+), 70 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts index 14f4e943..defcb4e4 100644 --- a/Timeline/ClientApp/src/app/data/user.ts +++ b/Timeline/ClientApp/src/app/data/user.ts @@ -1,6 +1,5 @@ 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'; @@ -41,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( @@ -56,96 +55,92 @@ export class UserService { return this.userSubject.value; } - checkLoginState(): Observable { + async checkLoginState(): Promise { if (this.currentUser !== undefined) { console.warn("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); + const savedUser = await dataStorage.getItem( + 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(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 { + ): Promise { 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(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 { 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); } @@ -166,7 +161,7 @@ export class UserService { ) ); $.subscribe(() => { - this.logout(); + void this.logout(); }); return $; } 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 { -- cgit v1.2.3 From 61f3c5a591688087ad1af114048f8574a13b7290 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 7 Aug 2020 00:20:56 +0800 Subject: Use eval-cheap-module-source-map for development. --- Timeline/ClientApp/.babelrc | 3 +-- Timeline/ClientApp/src/tsconfig.json | 2 +- Timeline/ClientApp/webpack.config.dev.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) (limited to 'Timeline/ClientApp/src') 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/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, -- cgit v1.2.3 From 9c0458c733ac3c7186688770045f8c41dfa235e5 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 7 Aug 2020 00:24:34 +0800 Subject: Fix a bug in timeline service. --- Timeline/ClientApp/src/app/data/timeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Timeline/ClientApp/src') 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 { - const cache = await dataStorage.getItem(timelineName); const key = this.getTimelineKey(timelineName); + const cache = await dataStorage.getItem(key); const save = (cache: TimelineCache): Promise => dataStorage.setItem(key, cache); -- cgit v1.2.3 From 394842105d4ebf2d01523eae8ccf5091113f7cbd Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 7 Aug 2020 00:30:35 +0800 Subject: Fix lint problem in Settings. --- Timeline/ClientApp/src/app/settings/Settings.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'Timeline/ClientApp/src') 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 = (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 = (_) => { setDialog(null)} onConfirm={() => { - userService.logout(); - history.push('/'); + void userService.logout().then(() => { + history.push('/'); + }); }} /> ); -- cgit v1.2.3