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 /Timeline/ClientApp/src/app/data | |
| 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.
Diffstat (limited to 'Timeline/ClientApp/src/app/data')
| -rw-r--r-- | Timeline/ClientApp/src/app/data/timeline.ts | 2 | ||||
| -rw-r--r-- | Timeline/ClientApp/src/app/data/user.ts | 226 | 
2 files changed, 151 insertions, 77 deletions
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);
 +          }
          });
      },
    });
  | 
