aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/services
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-02-13 11:58:36 +0800
committercrupest <crupest@outlook.com>2021-02-13 11:58:36 +0800
commit303f66d90472716191ae71377b3e83e1537089aa (patch)
tree24eb173e0f45b01e622820351957c2981f752468 /FrontEnd/src/app/services
parentc3d0a5f88de0fbdf6bc584548832017087ab1248 (diff)
downloadtimeline-303f66d90472716191ae71377b3e83e1537089aa.tar.gz
timeline-303f66d90472716191ae71377b3e83e1537089aa.tar.bz2
timeline-303f66d90472716191ae71377b3e83e1537089aa.zip
...
Diffstat (limited to 'FrontEnd/src/app/services')
-rw-r--r--FrontEnd/src/app/services/DataHub2.ts191
-rw-r--r--FrontEnd/src/app/services/common.ts24
-rw-r--r--FrontEnd/src/app/services/timeline.ts508
-rw-r--r--FrontEnd/src/app/services/user.ts222
4 files changed, 31 insertions, 914 deletions
diff --git a/FrontEnd/src/app/services/DataHub2.ts b/FrontEnd/src/app/services/DataHub2.ts
deleted file mode 100644
index f0fb724b..00000000
--- a/FrontEnd/src/app/services/DataHub2.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Observable } from "rxjs";
-
-export type DataStatus = "syncing" | "synced" | "offline";
-
-export function mergeDataStatus(statusList: DataStatus[]): DataStatus {
- if (statusList.includes("offline")) {
- return "offline";
- } else if (statusList.includes("syncing")) {
- return "syncing";
- } else {
- return "synced";
- }
-}
-
-export type Subscriber<TData> = (data: TData) => void;
-
-export interface DataAndStatus<TData> {
- data: TData | null;
- status: DataStatus;
-}
-
-export class DataLine2<TData> {
- constructor(
- private config: {
- saveData: (data: TData) => Promise<void>;
- getSavedData: () => Promise<TData | null>;
- // return null for offline
- fetchData: (savedData: TData | null) => Promise<TData | null>;
- }
- ) {}
-
- private _current: DataAndStatus<TData> | null = null;
- private _observers: Subscriber<DataAndStatus<TData>>[] = [];
-
- private _syncPromise: Promise<void> | null = null;
-
- get currentData(): DataAndStatus<TData> | null {
- return this._current;
- }
-
- get isDestroyable(): boolean {
- const { _observers, currentData } = this;
- return (
- _observers.length === 0 &&
- (currentData == null || currentData.status !== "syncing")
- );
- }
-
- private next(data: DataAndStatus<TData>): void {
- this._current = data;
- this._observers.forEach((o) => o(data));
- }
-
- subscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
- void this.sync(); // TODO: Should I sync at this point or let the user sync explicitly.
- this._observers.push(subsriber);
- const { currentData } = this;
- if (currentData != null) {
- subsriber(currentData);
- }
- }
-
- unsubscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
- const index = this._observers.indexOf(subsriber);
- if (index > -1) this._observers.splice(index, 1);
- }
-
- getObservalble(): Observable<DataAndStatus<TData>> {
- return new Observable<DataAndStatus<TData>>((observer) => {
- const f = (data: DataAndStatus<TData>): void => {
- observer.next(data);
- };
- this.subscribe(f);
-
- return () => {
- this.unsubscribe(f);
- };
- });
- }
-
- private syncWithAction(action: () => Promise<void>): Promise<void> {
- if (this._syncPromise != null) return this._syncPromise;
- this._syncPromise = action().then(() => {
- this._syncPromise = null;
- });
- return this._syncPromise;
- }
-
- sync(): Promise<void> {
- return this.syncWithAction(this.doSync.bind(this));
- }
-
- private async doSync(): Promise<void> {
- const { currentData } = this;
- this.next({ data: currentData?.data ?? null, status: "syncing" });
- const savedData = await this.config.getSavedData();
- if (currentData == null && savedData != null) {
- this.next({ data: savedData, status: "syncing" });
- }
- const data = await this.config.fetchData(savedData);
- if (data == null) {
- this.next({
- data: savedData,
- status: "offline",
- });
- } else {
- await this.config.saveData(data);
- this.next({ data: data, status: "synced" });
- }
- }
-
- save(data: TData): Promise<void> {
- return this.syncWithAction(this.doSave.bind(this, data));
- }
-
- private async doSave(data: TData): Promise<void> {
- await this.config.saveData(data);
- this.next({ data: data, status: "synced" });
- }
-
- getSavedData(): Promise<TData | null> {
- return this.config.getSavedData();
- }
-}
-
-export class DataHub2<TKey, TData> {
- private readonly subscriptionLineMap = new Map<string, DataLine2<TData>>();
-
- private keyToString: (key: TKey) => string;
-
- private cleanTimerId = 0;
-
- // setup is called after creating line and if it returns a function as destroyer, then when the line is destroyed the destroyer will be called.
- constructor(
- private config: {
- saveData: (key: TKey, data: TData) => Promise<void>;
- getSavedData: (key: TKey) => Promise<TData | null>;
- fetchData: (key: TKey, savedData: TData | null) => Promise<TData | null>;
- keyToString?: (key: TKey) => string;
- }
- ) {
- this.keyToString =
- config.keyToString ??
- ((value): string => {
- if (typeof value === "string") return value;
- else
- throw new Error(
- "Default keyToString function only pass string value."
- );
- });
- }
-
- private cleanLines(): void {
- const toDelete: string[] = [];
- for (const [key, line] of this.subscriptionLineMap.entries()) {
- if (line.isDestroyable) {
- toDelete.push(key);
- }
- }
-
- if (toDelete.length === 0) return;
-
- for (const key of toDelete) {
- this.subscriptionLineMap.delete(key);
- }
-
- if (this.subscriptionLineMap.size === 0) {
- window.clearInterval(this.cleanTimerId);
- this.cleanTimerId = 0;
- }
- }
-
- private createLine(key: TKey): DataLine2<TData> {
- const keyString = this.keyToString(key);
- const newLine: DataLine2<TData> = new DataLine2<TData>({
- saveData: (data) => this.config.saveData(key, data),
- getSavedData: () => this.config.getSavedData(key),
- fetchData: (savedData) => this.config.fetchData(key, savedData),
- });
- this.subscriptionLineMap.set(keyString, newLine);
- if (this.subscriptionLineMap.size === 1) {
- this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000);
- }
- return newLine;
- }
-
- getLine(key: TKey): DataLine2<TData> {
- const keyString = this.keyToString(key);
- return this.subscriptionLineMap.get(keyString) ?? this.createLine(key);
- }
-}
diff --git a/FrontEnd/src/app/services/common.ts b/FrontEnd/src/app/services/common.ts
deleted file mode 100644
index 9208737b..00000000
--- a/FrontEnd/src/app/services/common.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import localforage from "localforage";
-
-const dataVersion = 1;
-
-export const dataStorage = localforage.createInstance({
- name: "data",
- description: "Database for offline data.",
- driver: localforage.INDEXEDDB,
-});
-
-void (async () => {
- const currentVersion = await dataStorage.getItem<number | null>("version");
- if (currentVersion !== dataVersion) {
- console.log("Data storage version has changed. Clear all data.");
- await dataStorage.clear();
- await dataStorage.setItem("version", dataVersion);
- }
-})();
-
-export class ForbiddenError extends Error {
- constructor(message?: string) {
- super(message);
- }
-}
diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts
index 46671ea1..d803521b 100644
--- a/FrontEnd/src/app/services/timeline.ts
+++ b/FrontEnd/src/app/services/timeline.ts
@@ -1,515 +1,7 @@
-import React from "react";
import XRegExp from "xregexp";
-import { Observable, from } from "rxjs";
-
-import { convertError } from "@/utilities/rxjs";
-import {
- TimelineVisibility,
- HttpTimelineInfo,
- HttpTimelinePatchRequest,
- HttpTimelinePostPostRequest,
- HttpTimelinePostPostRequestContent,
- HttpTimelinePostPostRequestTextContent,
- HttpTimelinePostPostRequestImageContent,
- HttpTimelinePostInfo,
- HttpTimelinePostTextContent,
- getHttpTimelineClient,
- HttpTimelineNotExistError,
- HttpTimelineNameConflictError,
-} from "@/http/timeline";
-import { HttpForbiddenError, HttpNetworkError } from "@/http/common";
-
-export { kTimelineVisibilities } from "@/http/timeline";
-
-export type { TimelineVisibility } from "@/http/timeline";
-
-import { dataStorage } from "./common";
-import { userInfoService, AuthUser } from "./user";
-import { DataAndStatus, DataHub2 } from "./DataHub2";
-import { getHttpBookmarkClient } from "@/http/bookmark";
-import { getHttpHighlightClient } from "@/http/highlight";
-
-export type TimelineInfo = HttpTimelineInfo;
-export type TimelineChangePropertyRequest = HttpTimelinePatchRequest;
-export type TimelineCreatePostRequest = HttpTimelinePostPostRequest;
-export type TimelineCreatePostContent = HttpTimelinePostPostRequestContent;
-export type TimelineCreatePostTextContent = HttpTimelinePostPostRequestTextContent;
-export type TimelineCreatePostImageContent = HttpTimelinePostPostRequestImageContent;
-
-export type TimelinePostTextContent = HttpTimelinePostTextContent;
-
-export interface TimelinePostImageContent {
- type: "image";
- data: Blob;
- etag: string;
-}
-
-export type TimelinePostContent =
- | TimelinePostTextContent
- | TimelinePostImageContent;
-
-export type TimelinePostInfo = Omit<HttpTimelinePostInfo, "content"> & {
- content: TimelinePostContent;
-};
-
-export interface TimelinePostsInfo {
- lastUpdated: Date;
- posts: TimelinePostInfo[];
-}
-
-export const timelineVisibilityTooltipTranslationMap: Record<
- TimelineVisibility,
- string
-> = {
- Public: "timeline.visibilityTooltip.public",
- Register: "timeline.visibilityTooltip.register",
- Private: "timeline.visibilityTooltip.private",
-};
-
-export class TimelineNameConflictError extends Error {}
-
-type TimelineData = Omit<HttpTimelineInfo, "owner" | "members"> & {
- owner: string;
- members: string[];
-};
-
-type TimelinePostData = Omit<TimelinePostInfo, "author"> & {
- author: string;
-};
-
-interface TimelinePostsData {
- lastUpdated: Date;
- posts: TimelinePostData[];
-}
-
-export class TimelineService {
- private async clearTimelineData(timelineName: string): Promise<void> {
- const keys = (await dataStorage.keys()).filter((k) =>
- k.startsWith(`timeline.${timelineName}`)
- );
- await Promise.all(keys.map((k) => dataStorage.removeItem(k)));
- }
-
- private generateTimelineDataStorageKey(timelineName: string): string {
- return `timeline.${timelineName}`;
- }
-
- private convertHttpTimelineToData(timeline: HttpTimelineInfo): TimelineData {
- return {
- ...timeline,
- owner: timeline.owner.username,
- members: timeline.members.map((m) => m.username),
- };
- }
-
- readonly timelineHub = new DataHub2<string, HttpTimelineInfo | "notexist">({
- saveData: async (timelineName, data) => {
- if (data === "notexist") return;
-
- // TODO: Avoid save same user.
- void userInfoService.saveUser(data.owner);
- void userInfoService.saveUsers(data.members);
-
- await dataStorage.setItem<TimelineData>(
- this.generateTimelineDataStorageKey(timelineName),
- this.convertHttpTimelineToData(data)
- );
- },
- getSavedData: async (timelineName) => {
- const savedData = await dataStorage.getItem<TimelineData | null>(
- this.generateTimelineDataStorageKey(timelineName)
- );
-
- if (savedData == null) return null;
-
- const owner = await userInfoService.getCachedUser(savedData.owner);
- if (owner == null) return null;
- const members = await userInfoService.getCachedUsers(savedData.members);
- if (members == null) return null;
-
- return { ...savedData, owner, members };
- },
- fetchData: async (timelineName, savedData) => {
- try {
- const timeline = await getHttpTimelineClient().getTimeline(
- timelineName
- );
-
- if (
- savedData != null &&
- savedData !== "notexist" &&
- savedData.uniqueId !== timeline.uniqueId
- ) {
- console.log(
- `Timeline with name ${timelineName} has changed to a new one. Clear old data.`
- );
-
- void this.clearTimelineData(timelineName); // If timeline has changed, clear all old data.
- }
-
- return timeline;
- } catch (e) {
- if (e instanceof HttpTimelineNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- syncTimeline(timelineName: string): Promise<void> {
- return this.timelineHub.getLine(timelineName).sync();
- }
-
- createTimeline(timelineName: string): Observable<TimelineInfo> {
- return from(
- getHttpTimelineClient().postTimeline({
- name: timelineName,
- })
- ).pipe(
- convertError(HttpTimelineNameConflictError, TimelineNameConflictError)
- );
- }
-
- changeTimelineProperty(
- timelineName: string,
- req: TimelineChangePropertyRequest
- ): Promise<void> {
- return getHttpTimelineClient()
- .patchTimeline(timelineName, req)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- deleteTimeline(timelineName: string): Observable<unknown> {
- return from(getHttpTimelineClient().deleteTimeline(timelineName));
- }
-
- addMember(timelineName: string, username: string): Promise<void> {
- return getHttpTimelineClient()
- .memberPut(timelineName, username)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- removeMember(timelineName: string, username: string): Promise<void> {
- return getHttpTimelineClient()
- .memberDelete(timelineName, username)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- private generatePostsDataStorageKey(timelineName: string): string {
- return `timeline.${timelineName}.posts`;
- }
-
- readonly postsHub = new DataHub2<
- string,
- TimelinePostsInfo | "notexist" | "forbid"
- >({
- saveData: async (timelineName, data) => {
- if (data === "notexist" || data === "forbid") return;
-
- const savedData: TimelinePostsData = {
- ...data,
- posts: data.posts.map((p) => ({ ...p, author: p.author.username })),
- };
-
- data.posts.forEach((p) => {
- void userInfoService.saveUser(p.author);
- });
-
- await dataStorage.setItem<TimelinePostsData>(
- this.generatePostsDataStorageKey(timelineName),
- savedData
- );
- },
- getSavedData: async (timelineName) => {
- const savedData = await dataStorage.getItem<TimelinePostsData | null>(
- this.generatePostsDataStorageKey(timelineName)
- );
- if (savedData == null) return null;
-
- const authors = await userInfoService.getCachedUsers(
- savedData.posts.map((p) => p.author)
- );
-
- if (authors == null) return null;
-
- return {
- ...savedData,
- posts: savedData.posts.map((p, index) => ({
- ...p,
- author: authors[index],
- })),
- };
- },
- fetchData: async (timelineName, savedData) => {
- const convert = async (
- post: HttpTimelinePostInfo
- ): Promise<TimelinePostInfo> => {
- const { content } = post;
- if (content.type === "text") {
- return { ...post, content };
- } else {
- const data = await getHttpTimelineClient().getPostData(
- timelineName,
- post.id
- );
- return {
- ...post,
- content: {
- type: "image",
- data: data.data,
- etag: data.etag,
- },
- };
- }
- };
-
- const convertList = (
- posts: HttpTimelinePostInfo[]
- ): Promise<TimelinePostInfo[]> =>
- Promise.all(posts.map((p) => convert(p)));
-
- const now = new Date();
-
- try {
- if (
- savedData == null ||
- savedData === "forbid" ||
- savedData === "notexist"
- ) {
- const httpPosts = await getHttpTimelineClient().listPost(
- timelineName
- );
-
- return {
- lastUpdated: now,
- posts: await convertList(httpPosts),
- };
- } else {
- const httpPosts = await getHttpTimelineClient().listPost(
- timelineName,
- {
- modifiedSince: savedData.lastUpdated,
- includeDeleted: true,
- }
- );
-
- const deletedIds = httpPosts
- .filter((p) => p.deleted)
- .map((p) => p.id);
-
- const changed = await convertList(
- httpPosts.filter((p): p is HttpTimelinePostInfo => !p.deleted)
- );
-
- const posts = savedData.posts.filter(
- (p) => !deletedIds.includes(p.id)
- );
-
- for (const changedPost of changed) {
- const savedChangedPostIndex = posts.findIndex(
- (p) => p.id === changedPost.id
- );
- if (savedChangedPostIndex === -1) {
- posts.push(await convert(changedPost));
- } else {
- posts[savedChangedPostIndex] = await convert(changedPost);
- }
- }
-
- return { lastUpdated: now, posts };
- }
- } catch (e) {
- if (e instanceof HttpTimelineNotExistError) {
- return "notexist";
- } else if (e instanceof HttpForbiddenError) {
- return "forbid";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- syncPosts(timelineName: string): Promise<void> {
- return this.postsHub.getLine(timelineName).sync();
- }
-
- createPost(
- timelineName: string,
- request: TimelineCreatePostRequest
- ): Promise<void> {
- return getHttpTimelineClient()
- .postPost(timelineName, request)
- .then(() => {
- void this.syncPosts(timelineName);
- });
- }
-
- deletePost(timelineName: string, postId: number): Promise<void> {
- return getHttpTimelineClient()
- .deletePost(timelineName, postId)
- .then(() => {
- void this.syncPosts(timelineName);
- });
- }
-
- isMemberOf(username: string, timeline: TimelineInfo): boolean {
- return timeline.members.findIndex((m) => m.username == username) >= 0;
- }
-
- hasReadPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- const { visibility } = timeline;
- if (visibility === "Public") {
- return true;
- } else if (visibility === "Register") {
- if (user != null) return true;
- } else if (visibility === "Private") {
- if (
- user != null &&
- (user.username === timeline.owner.username ||
- this.isMemberOf(user.username, timeline))
- ) {
- return true;
- }
- }
- return false;
- }
-
- hasPostPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return (
- user != null &&
- (timeline.owner.username === user.username ||
- this.isMemberOf(user.username, timeline))
- );
- }
-
- hasManagePermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return user != null && user.username == timeline.owner.username;
- }
-
- hasModifyPostPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo,
- post: TimelinePostInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return (
- user != null &&
- (user.username === timeline.owner.username ||
- user.username === post.author.username)
- );
- }
-
- setHighlight(timelineName: string, highlight: boolean): Promise<void> {
- const client = getHttpHighlightClient();
- const promise = highlight
- ? client.put(timelineName)
- : client.delete(timelineName);
- return promise.then(() => {
- void timelineService.syncTimeline(timelineName);
- });
- }
-
- setBookmark(timelineName: string, bookmark: boolean): Promise<void> {
- const client = getHttpBookmarkClient();
- const promise = bookmark
- ? client.put(timelineName)
- : client.delete(timelineName);
- return promise.then(() => {
- void timelineService.syncTimeline(timelineName);
- });
- }
-}
-
-export const timelineService = new TimelineService();
const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u");
export function validateTimelineName(name: string): boolean {
return timelineNameReg.test(name);
}
-
-export function useTimeline(
- timelineName: string
-): DataAndStatus<TimelineInfo | "notexist"> {
- const [state, setState] = React.useState<
- DataAndStatus<TimelineInfo | "notexist">
- >({
- status: "syncing",
- data: null,
- });
- React.useEffect(() => {
- const subscription = timelineService.timelineHub
- .getLine(timelineName)
- .getObservalble()
- .subscribe((data) => {
- setState(data);
- });
- return () => {
- subscription.unsubscribe();
- };
- }, [timelineName]);
- return state;
-}
-
-export function usePosts(
- timelineName: string
-): DataAndStatus<TimelinePostsInfo | "notexist" | "forbid"> {
- const [state, setState] = React.useState<
- DataAndStatus<TimelinePostsInfo | "notexist" | "forbid">
- >({ status: "syncing", data: null });
- React.useEffect(() => {
- const subscription = timelineService.postsHub
- .getLine(timelineName)
- .getObservalble()
- .subscribe((data) => {
- setState(data);
- });
- return () => {
- subscription.unsubscribe();
- };
- }, [timelineName]);
- return state;
-}
-
-export async function getAllCachedTimelineNames(): Promise<string[]> {
- const keys = await dataStorage.keys();
- return keys
- .filter(
- (key) =>
- key.startsWith("timeline.") && (key.match(/\./g) ?? []).length === 1
- )
- .map((key) => key.substr("timeline.".length));
-}
diff --git a/FrontEnd/src/app/services/user.ts b/FrontEnd/src/app/services/user.ts
index 611a86ae..4814bf7c 100644
--- a/FrontEnd/src/app/services/user.ts
+++ b/FrontEnd/src/app/services/user.ts
@@ -1,33 +1,23 @@
-import React, { useState, useEffect } from "react";
-import { BehaviorSubject, Observable, from } from "rxjs";
+import { useState, useEffect } from "react";
+import { BehaviorSubject, Observable } from "rxjs";
import { UiLogicError } from "@/common";
-import {
- HttpNetworkError,
- BlobWithEtag,
- NotModified,
- setHttpToken,
-} from "@/http/common";
+import { HttpNetworkError, setHttpToken } from "@/http/common";
import {
getHttpTokenClient,
HttpCreateTokenBadCredentialError,
} from "@/http/token";
-import {
- getHttpUserClient,
- HttpUserNotExistError,
- HttpUser,
- UserPermission,
-} from "@/http/user";
+import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user";
-import { DataHub2 } from "./DataHub2";
-import { dataStorage } from "./common";
import { pushAlert } from "./alert";
-export type User = HttpUser;
+interface IAuthUser extends HttpUser {
+ token: string;
+}
-export class AuthUser implements User {
- constructor(user: User, public token: string) {
+export class AuthUser implements IAuthUser {
+ constructor(user: HttpUser, public token: string) {
this.uniqueId = user.uniqueId;
this.username = user.username;
this.permissions = user.permissions;
@@ -87,9 +77,17 @@ export class UserService {
console.warn("Already checked user. Can't check twice.");
}
- const savedUser = await dataStorage.getItem<AuthUser | null>(
- USER_STORAGE_KEY
- );
+ const savedUserString = localStorage.getItem(USER_STORAGE_KEY);
+
+ const savedAuthUserData =
+ savedUserString == null
+ ? null
+ : (JSON.parse(savedUserString) as IAuthUser);
+
+ const savedUser =
+ savedAuthUserData == null
+ ? null
+ : new AuthUser(savedAuthUserData, savedAuthUserData.token);
if (savedUser == null) {
this.userSubject.next(null);
@@ -102,7 +100,7 @@ export class UserService {
try {
const res = await getHttpTokenClient().verify({ token: savedToken });
const user = new AuthUser(res.user, savedToken);
- await dataStorage.setItem<AuthUser>(USER_STORAGE_KEY, user);
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
this.userSubject.next(user);
pushAlert({
type: "success",
@@ -120,7 +118,7 @@ export class UserService {
});
return savedUser;
} else {
- await dataStorage.removeItem(USER_STORAGE_KEY);
+ localStorage.removeItem(USER_STORAGE_KEY);
this.userSubject.next(null);
pushAlert({
type: "danger",
@@ -145,7 +143,7 @@ export class UserService {
});
const user = new AuthUser(res.user, res.token);
if (rememberMe) {
- await dataStorage.setItem<AuthUser>(USER_STORAGE_KEY, user);
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
}
this.userSubject.next(user);
} catch (e) {
@@ -157,34 +155,29 @@ export class UserService {
}
}
- async logout(): Promise<void> {
+ logout(): Promise<void> {
if (this.currentUser === undefined) {
throw new UiLogicError("Please check user first.");
}
if (this.currentUser === null) {
throw new UiLogicError("No login.");
}
- await dataStorage.removeItem(USER_STORAGE_KEY);
+ localStorage.removeItem(USER_STORAGE_KEY);
this.userSubject.next(null);
+ return Promise.resolve();
}
- changePassword(
- oldPassword: string,
- newPassword: string
- ): Observable<unknown> {
+ changePassword(oldPassword: string, newPassword: string): Promise<void> {
if (this.currentUser == undefined) {
throw new UiLogicError("Not login or checked now, can't log out.");
}
- const $ = from(
- getHttpUserClient().changePassword({
+
+ return getHttpUserClient()
+ .changePassword({
oldPassword,
newPassword,
})
- );
- $.subscribe(() => {
- void this.logout();
- });
- return $;
+ .then(() => this.logout());
}
}
@@ -236,156 +229,3 @@ export function useUserLoggedIn(): AuthUser {
}
return user;
}
-
-export function checkLogin(): AuthUser {
- const user = userService.currentUser;
- if (user == null) {
- throw new UiLogicError("You must login to perform the operation.");
- }
- return user;
-}
-
-export class UserNotExistError extends Error {}
-
-export class UserInfoService {
- saveUser(user: HttpUser): Promise<void> {
- return this.userHub.getLine(user.username).save(user);
- }
-
- saveUsers(users: HttpUser[]): Promise<void> {
- return Promise.all(users.map((user) => this.saveUser(user))).then();
- }
-
- async getCachedUser(username: string): Promise<HttpUser | null> {
- const user = await this.userHub.getLine(username).getSavedData();
- if (user == null || user === "notexist") return null;
- return user;
- }
-
- async getCachedUsers(usernames: string[]): Promise<HttpUser[] | null> {
- const users = await Promise.all(
- usernames.map((username) => this.userHub.getLine(username).getSavedData())
- );
-
- for (const u of users) {
- if (u == null || u === "notexist") {
- return null;
- }
- }
-
- return users as HttpUser[];
- }
-
- private generateUserDataStorageKey(username: string): string {
- return `user.${username}`;
- }
-
- readonly userHub = new DataHub2<string, HttpUser | "notexist">({
- saveData: (username, data) => {
- if (typeof data === "string") return Promise.resolve();
- return dataStorage
- .setItem<HttpUser>(this.generateUserDataStorageKey(username), data)
- .then();
- },
- getSavedData: (username) => {
- return dataStorage.getItem<HttpUser | null>(
- this.generateUserDataStorageKey(username)
- );
- },
- fetchData: async (username) => {
- try {
- return await getHttpUserClient().get(username);
- } catch (e) {
- if (e instanceof HttpUserNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- }
- throw e;
- }
- },
- });
-
- private generateAvatarDataStorageKey(username: string): string {
- return `user.${username}.avatar`;
- }
-
- readonly avatarHub = new DataHub2<string, BlobWithEtag | "notexist">({
- saveData: async (username, data) => {
- if (typeof data === "string") return;
- await dataStorage.setItem<BlobWithEtag>(
- this.generateAvatarDataStorageKey(username),
- data
- );
- },
- getSavedData: (username) =>
- dataStorage.getItem<BlobWithEtag | null>(
- this.generateAvatarDataStorageKey(username)
- ),
- fetchData: async (username, savedData) => {
- try {
- if (savedData == null || savedData === "notexist") {
- return await getHttpUserClient().getAvatar(username);
- } else {
- const res = await getHttpUserClient().getAvatar(
- username,
- savedData.etag
- );
- if (res instanceof NotModified) {
- return savedData;
- } else {
- return res;
- }
- }
- } catch (e) {
- if (e instanceof HttpUserNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- async setAvatar(username: string, blob: Blob): Promise<void> {
- const etag = await getHttpUserClient().putAvatar(username, blob);
- await this.avatarHub.getLine(username).save({ data: blob, etag });
- }
-
- async setNickname(username: string, nickname: string): Promise<void> {
- return getHttpUserClient()
- .patch(username, { nickname })
- .then((user) => this.saveUser(user));
- }
-}
-
-export const userInfoService = new UserInfoService();
-
-export function useAvatar(username?: string): Blob | undefined {
- const [state, setState] = React.useState<Blob | undefined>(undefined);
- React.useEffect(() => {
- if (username == null) {
- setState(undefined);
- return;
- }
-
- const subscription = userInfoService.avatarHub
- .getLine(username)
- .getObservalble()
- .subscribe((data) => {
- if (data.data != null && data.data !== "notexist") {
- setState(data.data.data);
- } else {
- setState(undefined);
- }
- });
-
- return () => {
- subscription.unsubscribe();
- };
- }, [username]);
-
- return state;
-}