aboutsummaryrefslogtreecommitdiff
path: root/Timeline
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-08-08 21:41:41 +0800
committercrupest <crupest@outlook.com>2020-08-08 21:41:41 +0800
commit2ea82a4325a7d80718dfb11dd225cb2c9e7357bd (patch)
tree55fbfcf14b5c7222b55330e42c85d3b5a609da80 /Timeline
parentbfcd27afb82d44505a2b7b0bfd1335ea2d8205b7 (diff)
downloadtimeline-2ea82a4325a7d80718dfb11dd225cb2c9e7357bd.tar.gz
timeline-2ea82a4325a7d80718dfb11dd225cb2c9e7357bd.tar.bz2
timeline-2ea82a4325a7d80718dfb11dd225cb2c9e7357bd.zip
...
Diffstat (limited to 'Timeline')
-rw-r--r--Timeline/ClientApp/src/app/data/timeline.ts326
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx10
2 files changed, 112 insertions, 224 deletions
diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts
index 7ef7a8bb..31f6657f 100644
--- a/Timeline/ClientApp/src/app/data/timeline.ts
+++ b/Timeline/ClientApp/src/app/data/timeline.ts
@@ -1,12 +1,12 @@
import React from 'react';
import XRegExp from 'xregexp';
-import { Observable, from, combineLatest } from 'rxjs';
+import { Observable, from, combineLatest, of } from 'rxjs';
import { map, switchMap, filter } from 'rxjs/operators';
import { convertError } from '../utilities/rxjs';
import { dataStorage } from './common';
-import { SubscriptionHub, ISubscriptionHub } from './SubscriptionHub';
+import { SubscriptionHub } from './SubscriptionHub';
import syncStatusHub from './SyncStatusHub';
import { UserAuthInfo, checkLogin, userService, userInfoService } from './user';
@@ -90,9 +90,8 @@ export type TimelineWithSyncStatus =
};
export interface TimelinePostsWithSyncState {
- state:
- | 'loadcache'
- | 'syncing' // Syncing now.
+ type:
+ | 'cache'
| 'offline' // Sync failed and use cache.
| 'synced' // Sync succeeded.
| 'forbid' // The list is forbidden to see.
@@ -100,12 +99,6 @@ export interface TimelinePostsWithSyncState {
posts: TimelinePostInfo[];
}
-type FetchAndCachePostsResult =
- | TimelinePostInfo[]
- | 'notexist'
- | 'forbid'
- | 'offline';
-
type TimelineData = Omit<HttpTimelineInfo, 'owner' | 'members'> & {
owner: string;
members: string[];
@@ -281,226 +274,130 @@ export class TimelineService {
);
}
- private getPostsKey(timelineName: string): string {
- return `timeline.${timelineName}.posts`;
+ private convertHttpPostToData(post: HttpTimelinePostInfo): TimelinePostData {
+ return {
+ ...post,
+ author: post.author.username,
+ };
}
- private getPostDataKey(timelineName: string, id: number): string {
- return `timeline.${timelineName}.post.${id}.data`;
+ private convertHttpPostToDataList(
+ posts: HttpTimelinePostInfo[]
+ ): TimelinePostData[] {
+ return posts.map((post) => this.convertHttpPostToData(post));
}
- private convertPost = async (
- post: HttpTimelinePostInfo,
- dataProvider: () => Promise<Blob | null | undefined>
- ): Promise<TimelinePostInfo> => {
- const { content } = post;
- if (content.type === 'text') {
- return {
- ...post,
- content,
- };
- } else {
- const data = await dataProvider();
- if (data == null) throw new Error('This post requires data.');
- return {
- ...post,
- content: {
- type: 'image',
- data,
- },
- };
- }
- };
-
- private convertPostList = (
- posts: HttpTimelinePostInfo[],
- dataProvider: (
- post: HttpTimelinePostInfo,
- index: number
- ) => Promise<Blob | null | undefined>
- ): Promise<TimelinePostInfo[]> => {
- return Promise.all(
- posts.map((post, index) =>
- this.convertPost(post, () => dataProvider(post, index))
- )
- );
- };
-
private async getCachedPosts(
timelineName: string
- ): Promise<TimelinePostInfo[]> {
- const key = this.getPostsKey(timelineName);
- const httpPosts = await dataStorage.getItem<HttpTimelinePostInfo[] | null>(
- key
- );
-
- if (httpPosts == null) return [];
-
- const posts = await this.convertPostList(httpPosts, (post) =>
- dataStorage
- .getItem<BlobWithEtag | null>(
- this.getPostDataKey(timelineName, post.id)
- )
- .then((d) => d?.data)
+ ): Promise<TimelinePostData[]> {
+ const posts = await dataStorage.getItem<TimelinePostData[] | null>(
+ `timeline.${timelineName}.posts`
);
-
+ if (posts == null) return [];
return posts;
}
- private async fetchAndCachePosts(
- timelineName: string,
- notUseDataCache = false
- ): Promise<FetchAndCachePostsResult> {
- try {
- const token = userService.currentUser?.token;
+ private async syncPosts(timelineName: string): Promise<void> {
+ const syncStatusKey = `timeline.posts.${timelineName}`;
+ const dataKey = `timeline.${timelineName}.posts`;
+ if (syncStatusHub.get(syncStatusKey)) return;
+ syncStatusHub.begin(syncStatusKey);
+
+ try {
const httpPosts = await getHttpTimelineClient().listPost(
timelineName,
- token
- );
-
- const dataList: (
- | (BlobWithEtag & { cache: boolean })
- | null
- )[] = await Promise.all(
- httpPosts.map(async (post) => {
- const { content } = post;
- if (content.type === 'image') {
- if (notUseDataCache) {
- const data = await getHttpTimelineClient().getPostData(
- timelineName,
- post.id,
- token
- );
- return { ...data, cache: false };
- } else {
- const savedData = await dataStorage.getItem<BlobWithEtag | null>(
- this.getPostDataKey(timelineName, post.id)
- );
- if (savedData == null) {
- const data = await getHttpTimelineClient().getPostData(
- timelineName,
- post.id,
- token
- );
- return { ...data, cache: false };
- } else {
- const res = await getHttpTimelineClient().getPostData(
- timelineName,
- post.id,
- token,
- savedData.etag
- );
- if (res instanceof NotModified) {
- return { ...savedData, cache: true };
- } else {
- await dataStorage.setItem<BlobWithEtag>(
- this.getPostDataKey(timelineName, post.id),
- res
- );
- return { ...res, cache: false };
- }
- }
- }
- } else {
- return null;
- }
- })
- );
-
- for (const [i, post] of httpPosts.entries()) {
- const data = dataList[i];
- if (data != null && !data.cache) {
- await dataStorage.setItem<BlobWithEtag>(
- this.getPostDataKey(timelineName, post.id),
- data
- );
- }
- }
-
- await dataStorage.setItem<HttpTimelinePostInfo[]>(
- this.getPostsKey(timelineName),
- httpPosts
+ userService.currentUser?.token
);
+ const posts = this.convertHttpPostToDataList(httpPosts);
+ await dataStorage.setItem<TimelinePostData[]>(dataKey, posts);
- const posts: TimelinePostInfo[] = await this.convertPostList(
- httpPosts,
- (post, i) => Promise.resolve(dataList[i]?.data)
- );
-
- return posts;
+ syncStatusHub.end(syncStatusKey);
+ this._postsHub.getLine(timelineName)?.next({ type: 'synced', posts });
} catch (e) {
- if (e instanceof HttpNetworkError) {
- return 'offline';
+ syncStatusHub.end(syncStatusKey);
+ if (e instanceof HttpTimelineNotExistError) {
+ this._postsHub
+ .getLine(timelineName)
+ ?.next({ type: 'notexist', posts: [] });
} else if (e instanceof HttpForbiddenError) {
- return 'forbid';
- } else if (e instanceof HttpTimelineNotExistError) {
- return 'notexist';
+ this._postsHub
+ .getLine(timelineName)
+ ?.next({ type: 'forbid', posts: [] });
+ } else if (e instanceof HttpNetworkError) {
+ const cache = await this.getCachedPosts(timelineName);
+ if (cache == null)
+ this._postsHub
+ .getLine(timelineName)
+ ?.next({ type: 'offline', posts: [] });
+ else
+ this._postsHub
+ .getLine(timelineName)
+ ?.next({ type: 'offline', posts: cache });
} else {
throw e;
}
}
}
- private async syncPosts(
- timelineName: string,
- notUseCachedData = false
- ): Promise<void> {
- const line = this._postsSubscriptionHub.getLine(timelineName);
- if (line == null) return;
-
- if (
- line.value != null &&
- (line.value.state === 'loadcache' || line.value.state === 'syncing')
- ) {
- return;
- }
-
- const next = (value: TimelinePostsWithSyncState): void => {
- line.next(value);
- };
-
- if (line.value == null) {
- next({
- state: 'loadcache',
- posts: [],
- });
- const posts = await this.getCachedPosts(timelineName);
- next({
- state: 'syncing',
- posts,
- });
- } else {
- next({
- state: 'syncing',
- posts: line.value.posts,
- });
- }
-
- const result = await this.fetchAndCachePosts(
- timelineName,
- notUseCachedData
- );
- if (result === 'offline') {
- next({ state: 'offline', posts: line.value?.posts ?? [] });
- } else if (Array.isArray(result)) {
- next({ state: 'synced', posts: result });
- } else {
- next({ state: result, posts: [] });
- }
- }
-
- private _postsSubscriptionHub = new SubscriptionHub<
+ private _postsHub = new SubscriptionHub<
string,
- TimelinePostsWithSyncState
+ {
+ type: 'cache' | 'offline' | 'synced' | 'forbid' | 'notexist';
+ posts: TimelinePostData[];
+ }
>({
- setup: (key) => {
- void this.syncPosts(key);
+ setup: (key, line) => {
+ void this.getCachedPosts(key).then((posts) => {
+ if (posts != null) {
+ line.next({ type: 'cache', posts });
+ }
+ return this.syncPosts(key);
+ });
},
});
- get postsHub(): ISubscriptionHub<string, TimelinePostsWithSyncState> {
- return this._postsSubscriptionHub;
+ getPosts$(timelineName: string): Observable<TimelinePostsWithSyncState> {
+ return this._postsHub.getObservable(timelineName).pipe(
+ switchMap((state) => {
+ return combineLatest([
+ combineLatest(
+ state.posts.map((post) => userInfoService.getUser$(post.author))
+ ),
+ combineLatest(
+ state.posts.map((post) => {
+ if (post.content.type === 'image') {
+ return this.getPostData$(timelineName, post.id);
+ } else {
+ return of(null);
+ }
+ })
+ ),
+ ]).pipe(
+ map(([authors, datas]) => {
+ return {
+ type: state.type,
+ posts: state.posts.map((post, i) => {
+ const { content } = post;
+
+ return {
+ ...post,
+ author: authors[i],
+ content: (() => {
+ if (content.type === 'text') return content;
+ else
+ return {
+ type: 'image',
+ data: datas[i],
+ } as TimelinePostImageContent;
+ })(),
+ };
+ }),
+ };
+ })
+ );
+ })
+ );
}
private getCachedPostData(
@@ -603,23 +500,15 @@ export class TimelineService {
createPost(
timelineName: string,
request: TimelineCreatePostRequest
- ): Observable<TimelinePostInfo> {
+ ): Observable<unknown> {
const user = checkLogin();
return from(
getHttpTimelineClient()
.postPost(timelineName, request, user.token)
- .then((post) =>
- this.convertPost(post, () =>
- Promise.resolve(
- (request.content as TimelineCreatePostImageContent).data
- )
- )
- )
- .then((post) => {
+ .then(() => {
void this.syncPosts(timelineName);
- return post;
})
- ).pipe(map((post) => ({ ...post, timelineName })));
+ );
}
deletePost(timelineName: string, postId: number): Observable<unknown> {
@@ -736,12 +625,11 @@ export function usePostList(
return;
}
- const subscription = timelineService.postsHub.subscribe(
- timelineName,
- (data) => {
+ const subscription = timelineService
+ .getPosts$(timelineName)
+ .subscribe((data) => {
setState(data);
- }
- );
+ });
return () => {
subscription.unsubscribe();
};
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
index 8b9f8765..70507988 100644
--- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
+++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
@@ -24,7 +24,7 @@ import Timeline, {
import AppBar from '../common/AppBar';
import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit';
-type TimelinePostSyncState = 'loadcache' | 'syncing' | 'synced' | 'offline';
+type TimelinePostSyncState = 'cache' | 'syncing' | 'synced' | 'offline';
const TimelinePostSyncStateBadge: React.FC<{
state: TimelinePostSyncState;
@@ -37,7 +37,7 @@ const TimelinePostSyncStateBadge: React.FC<{
<div style={style} className={clsx('timeline-sync-state-badge', className)}>
{(() => {
switch (state) {
- case 'loadcache':
+ case 'cache':
case 'syncing': {
return (
<>
@@ -201,12 +201,12 @@ export default function TimelinePageTemplateUI<TManageItems>(
if (timeline != null) {
let timelineBody: React.ReactElement;
if (postListState != null) {
- if (postListState.state === 'notexist') {
+ if (postListState.type === 'notexist') {
throw new UiLogicError(
'Timeline is not null but post list state is notexist.'
);
}
- if (postListState.state === 'forbid') {
+ if (postListState.type === 'forbid') {
timelineBody = (
<p className="text-danger">{t('timeline.messageCantSee')}</p>
);
@@ -230,7 +230,7 @@ export default function TimelinePageTemplateUI<TManageItems>(
<div>
<TimelinePostSyncStateBadge
style={{ top: topHeight }}
- state={postListState.state}
+ state={postListState.type}
/>
<Timeline
containerRef={timelineRef}