From 5a90a7d0de9ae8410ef8c23a6994fdba7657666d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jul 2020 23:57:13 +0800 Subject: ... --- .../src/app/timeline/TimelinePageTemplate.tsx | 67 +++++++--------------- 1 file changed, 22 insertions(+), 45 deletions(-) (limited to 'Timeline/ClientApp/src/app/timeline') diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx index 9be7f305..88066b76 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { concat, without } from 'lodash'; import { of } from 'rxjs'; -import { catchError, switchMap, map } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { ExcludeKey } from '../utilities/type'; import { pushAlert } from '../common/alert-service'; @@ -11,6 +11,7 @@ import { timelineService, TimelineInfo, TimelineNotExistError, + usePostList, } from '../data/timeline'; import { TimelinePostInfoEx, TimelineDeleteCallback } from './Timeline'; @@ -22,7 +23,7 @@ import { UiLogicError } from '../common'; export interface TimelinePageTemplateProps< TManageItem, - TTimeline extends TimelineInfo + TTimeline extends TimelineInfo // TODO: Remove this. > { name: string; onManage: (item: TManageItem) => void; @@ -53,53 +54,27 @@ export default function TimelinePageTemplate< const [timeline, setTimeline] = React.useState( undefined ); - const [posts, setPosts] = React.useState< - TimelinePostInfoEx[] | 'forbid' | undefined - >(undefined); + + const rawPosts = usePostList(timeline?.name); + const [error, setError] = React.useState(undefined); React.useEffect(() => { - const subscription = service - .getTimeline(name) - .pipe( - switchMap((ti) => { - setTimeline(ti); - if (!service.hasReadPermission(user, ti)) { - setPosts('forbid'); - return of(null); - } else { - return service - .getPosts(name) - .pipe(map((ps) => ({ timeline: ti, posts: ps }))); - } - }) - ) - .subscribe( - (data) => { - if (data != null) { - setPosts( - data.posts.map((post) => ({ - ...post, - deletable: service.hasModifyPostPermission( - user, - data.timeline, - post - ), - })) - ); - } - }, - (error) => { - if (error instanceof TimelineNotExistError) { - setError(t(props.notFoundI18nKey)); - } else { - setError( - // TODO: Convert this to a function. - (error as { message?: string })?.message ?? 'Unknown error' - ); - } + const subscription = service.getTimeline(name).subscribe( + (ti) => { + setTimeline(ti); + }, + (error) => { + if (error instanceof TimelineNotExistError) { + setError(t(props.notFoundI18nKey)); + } else { + setError( + // TODO: Convert this to a function. + (error as { message?: string })?.message ?? 'Unknown error' + ); } - ); + } + ); return () => { subscription.unsubscribe(); }; @@ -209,6 +184,7 @@ export default function TimelinePageTemplate< (index, id) => { service.deletePost(name, id).subscribe( () => { + // TODO: Remove this. setPosts((oldPosts) => without( oldPosts as TimelinePostInfoEx[], @@ -233,6 +209,7 @@ export default function TimelinePageTemplate< .createPost(name, req) .pipe( map((newPost) => { + // TODO: Remove this. setPosts((oldPosts) => concat(oldPosts as TimelinePostInfoEx[], { ...newPost, -- cgit v1.2.3 From 9e500f240a76bd0e16c8c63b764dd81c01f46f78 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jul 2020 00:14:32 +0800 Subject: Update post list when create or delete post. --- Timeline/ClientApp/src/app/data/timeline.ts | 23 ++++++++++- .../src/app/timeline/TimelinePageTemplate.tsx | 44 +++++----------------- .../src/app/timeline/TimelinePageTemplateUI.tsx | 32 ++++++++++++---- 3 files changed, 55 insertions(+), 44 deletions(-) (limited to 'Timeline/ClientApp/src/app/timeline') diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts index 88a13381..84eb3764 100644 --- a/Timeline/ClientApp/src/app/data/timeline.ts +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -450,14 +450,33 @@ export class TimelineService { ): Observable { const user = checkLogin(); return from( - getHttpTimelineClient().postPost(timelineName, request, user.token) + getHttpTimelineClient() + .postPost(timelineName, request, user.token) + .then((res) => { + this._postListSubscriptionHub.update(timelineName, (_, old) => { + return Promise.resolve({ + ...old, + posts: [...old.posts, { ...res, timelineName }], + }); + }); + return res; + }) ).pipe(map((post) => ({ ...post, timelineName }))); } deletePost(timelineName: string, postId: number): Observable { const user = checkLogin(); return from( - getHttpTimelineClient().deletePost(timelineName, postId, user.token) + getHttpTimelineClient() + .deletePost(timelineName, postId, user.token) + .then(() => { + this._postListSubscriptionHub.update(timelineName, (_, old) => { + return Promise.resolve({ + ...old, + posts: old.posts.filter((post) => post.id != postId), + }); + }); + }) ); } diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx index 88066b76..a68d08c6 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx @@ -14,7 +14,7 @@ import { usePostList, } from '../data/timeline'; -import { TimelinePostInfoEx, TimelineDeleteCallback } from './Timeline'; +import { TimelineDeleteCallback } from './Timeline'; import { TimelineMemberDialog } from './TimelineMember'; import TimelinePropertyChangeDialog from './TimelinePropertyChangeDialog'; import { TimelinePageTemplateUIProps } from './TimelinePageTemplateUI'; @@ -55,7 +55,7 @@ export default function TimelinePageTemplate< undefined ); - const rawPosts = usePostList(timeline?.name); + const postListState = usePostList(timeline?.name); const [error, setError] = React.useState(undefined); @@ -182,43 +182,19 @@ export default function TimelinePageTemplate< const onDelete: TimelineDeleteCallback = React.useCallback( (index, id) => { - service.deletePost(name, id).subscribe( - () => { - // TODO: Remove this. - setPosts((oldPosts) => - without( - oldPosts as TimelinePostInfoEx[], - (oldPosts as TimelinePostInfoEx[])[index] - ) - ); - }, - () => { - pushAlert({ - type: 'danger', - message: t('timeline.deletePostFailed'), - }); - } - ); + service.deletePost(name, id).subscribe(null, () => { + pushAlert({ + type: 'danger', + message: t('timeline.deletePostFailed'), + }); + }); }, [service, name, t] ); const onPost: TimelinePostSendCallback = React.useCallback( (req) => { - return service - .createPost(name, req) - .pipe( - map((newPost) => { - // TODO: Remove this. - setPosts((oldPosts) => - concat(oldPosts as TimelinePostInfoEx[], { - ...newPost, - deletable: true, - }) - ); - }) - ) - .toPromise(); + return service.createPost(name, req).toPromise().then(); }, [service, name] ); @@ -245,7 +221,7 @@ export default function TimelinePageTemplate< { timeline: TimelineInfo; @@ -29,7 +34,7 @@ export interface TimelineCardComponentProps { export interface TimelinePageTemplateUIProps { avatarKey?: string | number; timeline?: TimelineInfo; - posts?: TimelinePostInfoEx[] | 'forbid'; + postListState?: TimelinePostListState; CardComponent: React.ComponentType>; onMember: () => void; onManage?: (item: TManageItems | 'property') => void; @@ -41,7 +46,7 @@ export interface TimelinePageTemplateUIProps { export default function TimelinePageTemplateUI( props: TimelinePageTemplateUIProps ): React.ReactElement | null { - const { timeline } = props; + const { timeline, postListState } = props; const { t } = useTranslation(); @@ -116,7 +121,7 @@ export default function TimelinePageTemplateUI( subscriptions.forEach((s) => s.unsubscribe()); }; } - }, [getResizeEvent, triggerResizeEvent, timeline, props.posts]); + }, [getResizeEvent, triggerResizeEvent, timeline, postListState]); const [cardHeight, setCardHeight] = React.useState(0); @@ -142,16 +147,27 @@ export default function TimelinePageTemplateUI( } else { if (timeline != null) { let timelineBody: React.ReactElement; - if (props.posts != null) { - if (props.posts === 'forbid') { + if (postListState != null) { + if (postListState.state === 'forbid') { timelineBody = (

{t('timeline.messageCantSee')}

); } else { + const posts: TimelinePostInfoEx[] = postListState.posts.map( + (post) => ({ + ...post, + deletable: timelineService.hasModifyPostPermission( + userService.currentUser, + timeline, + post + ), + }) + ); + timelineBody = ( -- cgit v1.2.3 From 4d17746be0daff8f566ec102d4d119321cda8c53 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jul 2020 00:57:22 +0800 Subject: Add sync state badge. --- .../ClientApp/src/app/locales/en/translation.ts | 5 ++ Timeline/ClientApp/src/app/locales/scheme.ts | 5 ++ .../ClientApp/src/app/locales/zh/translation.ts | 5 ++ Timeline/ClientApp/src/app/timeline/Timeline.tsx | 5 +- .../src/app/timeline/TimelinePageTemplateUI.tsx | 66 +++++++++++++++++++--- .../ClientApp/src/app/timeline/timeline-ui.sass | 18 ++++++ Timeline/ClientApp/src/app/timeline/timeline.sass | 6 ++ 7 files changed, 98 insertions(+), 12 deletions(-) (limited to 'Timeline/ClientApp/src/app/timeline') diff --git a/Timeline/ClientApp/src/app/locales/en/translation.ts b/Timeline/ClientApp/src/app/locales/en/translation.ts index 6abe910e..2f8fb312 100644 --- a/Timeline/ClientApp/src/app/locales/en/translation.ts +++ b/Timeline/ClientApp/src/app/locales/en/translation.ts @@ -92,6 +92,11 @@ const translation: TranslationResource = { 'This is a dangerous action. If you are sure to delete timeline<1>{{name}}, please input its name below and click confirm button.', notMatch: 'Name does not match.', }, + postSyncState: { + syncing: 'Syncing', + synced: 'Synced', + offline: 'Offline', + }, post: { deleteDialog: { title: 'Confirm Delete', diff --git a/Timeline/ClientApp/src/app/locales/scheme.ts b/Timeline/ClientApp/src/app/locales/scheme.ts index 19ac6c31..7aa7e125 100644 --- a/Timeline/ClientApp/src/app/locales/scheme.ts +++ b/Timeline/ClientApp/src/app/locales/scheme.ts @@ -83,6 +83,11 @@ export default interface TranslationResource { inputPrompt: string; notMatch: string; }; + postSyncState: { + syncing: string; + synced: string; + offline: string; + }; post: { deleteDialog: { title: string; diff --git a/Timeline/ClientApp/src/app/locales/zh/translation.ts b/Timeline/ClientApp/src/app/locales/zh/translation.ts index 372979c0..35cfa38c 100644 --- a/Timeline/ClientApp/src/app/locales/zh/translation.ts +++ b/Timeline/ClientApp/src/app/locales/zh/translation.ts @@ -88,6 +88,11 @@ const translation: TranslationResource = { '这是一个危险的操作。如果您确认要删除时间线<1>{{name}},请在下面输入它的名字并点击确认。', notMatch: '名字不匹配', }, + postSyncState: { + syncing: '同步中', + synced: '同步成功', + offline: '离线', + }, post: { deleteDialog: { title: '确认删除', diff --git a/Timeline/ClientApp/src/app/timeline/Timeline.tsx b/Timeline/ClientApp/src/app/timeline/Timeline.tsx index 849933cf..7c3a93fb 100644 --- a/Timeline/ClientApp/src/app/timeline/Timeline.tsx +++ b/Timeline/ClientApp/src/app/timeline/Timeline.tsx @@ -53,10 +53,7 @@ const Timeline: React.FC = (props) => { return (
{(() => { diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx index 3c8e312c..0d8ad278 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx @@ -8,7 +8,7 @@ import arrowsAngleContractIcon from 'bootstrap-icons/icons/arrows-angle-contract import arrowsAngleExpandIcon from 'bootstrap-icons/icons/arrows-angle-expand.svg'; import { getAlertHost } from '../common/alert-service'; -import { useEventEmiiter } from '../common'; +import { useEventEmiiter, UiLogicError } from '../common'; import { TimelineInfo, TimelinePostListState, @@ -23,6 +23,53 @@ import Timeline, { import AppBar from '../common/AppBar'; import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit'; +const TimelinePostSyncStateBadge: React.FC<{ + state: 'syncing' | 'synced' | 'offline'; +}> = ({ state }) => { + const { t } = useTranslation(); + + return ( +
+ {(() => { + switch (state) { + case 'syncing': { + return ( + <> + + + {t('timeline.postSyncState.syncing')} + + + ); + } + case 'synced': { + return ( + <> + + + {t('timeline.postSyncState.synced')} + + + ); + } + case 'offline': { + return ( + <> + + + {t('timeline.postSyncState.offline')} + + + ); + } + default: + throw new UiLogicError('Unknown sync state.'); + } + })()} +
+ ); +}; + export interface TimelineCardComponentProps { timeline: TimelineInfo; onManage?: (item: TManageItems | 'property') => void; @@ -147,7 +194,7 @@ export default function TimelinePageTemplateUI( } else { if (timeline != null) { let timelineBody: React.ReactElement; - if (postListState != null) { + if (postListState != null && postListState.state !== 'loading') { if (postListState.state === 'forbid') { timelineBody = (

{t('timeline.messageCantSee')}

@@ -165,12 +212,15 @@ export default function TimelinePageTemplateUI( ); timelineBody = ( - +
+ + +
); if (props.onPost != null) { timelineBody = ( diff --git a/Timeline/ClientApp/src/app/timeline/timeline-ui.sass b/Timeline/ClientApp/src/app/timeline/timeline-ui.sass index b92327bd..952e4659 100644 --- a/Timeline/ClientApp/src/app/timeline/timeline-ui.sass +++ b/Timeline/ClientApp/src/app/timeline/timeline-ui.sass @@ -16,3 +16,21 @@ .timeline-page-top-space transition: height 0.5s +.timeline-sync-state-badge + position: absolute + top: 0 + right: 0 + z-index: 1 + font-size: 0.8em + margin-top: 4px + padding: 3px 8px + border-radius: 5px + background: #e8fbff + +.timeline-sync-state-badge-pin + display: inline-block + width: 0.4em + height: 0.4em + border-radius: 50% + vertical-align: middle + margin-right: 0.6em diff --git a/Timeline/ClientApp/src/app/timeline/timeline.sass b/Timeline/ClientApp/src/app/timeline/timeline.sass index 4f69295b..b224e973 100644 --- a/Timeline/ClientApp/src/app/timeline/timeline.sass +++ b/Timeline/ClientApp/src/app/timeline/timeline.sass @@ -1,5 +1,11 @@ @use 'sass:color' +.timeline + display: flex + flex-direction: column + z-index: 0 + position: relative + @keyframes timeline-enter-animation-mask-animation to height: 0 -- cgit v1.2.3 From d48ca755bae9f24378eda3c0a25285ec4b97a761 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jul 2020 01:11:13 +0800 Subject: Make sync state badge fixed. --- .../src/app/timeline/TimelinePageTemplateUI.tsx | 20 +++++++++++++++----- Timeline/ClientApp/src/app/timeline/timeline-ui.sass | 3 +-- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'Timeline/ClientApp/src/app/timeline') diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx index 0d8ad278..dc5bfda8 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { CSSProperties } from 'react'; import { Spinner } from 'reactstrap'; import { useTranslation } from 'react-i18next'; import { fromEvent } from 'rxjs'; import Svg from 'react-inlinesvg'; +import clsx from 'clsx'; import arrowsAngleContractIcon from 'bootstrap-icons/icons/arrows-angle-contract.svg'; import arrowsAngleExpandIcon from 'bootstrap-icons/icons/arrows-angle-expand.svg'; @@ -25,11 +26,13 @@ import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit'; const TimelinePostSyncStateBadge: React.FC<{ state: 'syncing' | 'synced' | 'offline'; -}> = ({ state }) => { + style?: CSSProperties; + className?: string; +}> = ({ state, style, className }) => { const { t } = useTranslation(); return ( -
+
{(() => { switch (state) { case 'syncing': { @@ -211,9 +214,16 @@ export default function TimelinePageTemplateUI( }) ); + const topHeight: string = infoCardCollapse + ? 'calc(68px + 1.5em)' + : `${cardHeight + 60}px`; + timelineBody = ( -
- +
+