diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/timeline')
5 files changed, 143 insertions, 94 deletions
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<TimelineProps> = (props) => { return (
<div
ref={props.containerRef}
- className={clsx(
- 'container-fluid d-flex flex-column position-relative',
- props.className
- )}
+ className={clsx('container-fluid timeline', props.className)}
>
<div className="timeline-enter-animation-mask" />
{(() => {
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx index 9be7f305..a68d08c6 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,9 +11,10 @@ import { timelineService,
TimelineInfo,
TimelineNotExistError,
+ 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';
@@ -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<TimelineInfo | undefined>(
undefined
);
- const [posts, setPosts] = React.useState<
- TimelinePostInfoEx[] | 'forbid' | undefined
- >(undefined);
+
+ const postListState = usePostList(timeline?.name);
+
const [error, setError] = React.useState<string | undefined>(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();
};
@@ -207,41 +182,19 @@ export default function TimelinePageTemplate< const onDelete: TimelineDeleteCallback = React.useCallback(
(index, id) => {
- service.deletePost(name, id).subscribe(
- () => {
- 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) => {
- setPosts((oldPosts) =>
- concat(oldPosts as TimelinePostInfoEx[], {
- ...newPost,
- deletable: true,
- })
- );
- })
- )
- .toPromise();
+ return service.createPost(name, req).toPromise().then();
},
[service, name]
);
@@ -268,7 +221,7 @@ export default function TimelinePageTemplate< <UiComponent
error={error}
timeline={timeline}
- posts={posts}
+ postListState={postListState}
onDelete={onDelete}
onPost={
timeline != null && service.hasPostPermission(user, timeline)
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx index 4b3b3096..dc5bfda8 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx @@ -1,13 +1,21 @@ -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';
import { getAlertHost } from '../common/alert-service';
+import { useEventEmiiter, UiLogicError } from '../common';
+import {
+ TimelineInfo,
+ TimelinePostListState,
+ timelineService,
+} from '../data/timeline';
+import { userService } from '../data/user';
import Timeline, {
TimelinePostInfoEx,
@@ -15,8 +23,55 @@ import Timeline, { } from './Timeline';
import AppBar from '../common/AppBar';
import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit';
-import { useEventEmiiter } from '../common';
-import { TimelineInfo } from '../data/timeline';
+
+const TimelinePostSyncStateBadge: React.FC<{
+ state: 'syncing' | 'synced' | 'offline';
+ style?: CSSProperties;
+ className?: string;
+}> = ({ state, style, className }) => {
+ const { t } = useTranslation();
+
+ return (
+ <div style={style} className={clsx('timeline-sync-state-badge', className)}>
+ {(() => {
+ switch (state) {
+ case 'syncing': {
+ return (
+ <>
+ <span className="timeline-sync-state-badge-pin bg-warning" />
+ <span className="text-warning">
+ {t('timeline.postSyncState.syncing')}
+ </span>
+ </>
+ );
+ }
+ case 'synced': {
+ return (
+ <>
+ <span className="timeline-sync-state-badge-pin bg-success" />
+ <span className="text-success">
+ {t('timeline.postSyncState.synced')}
+ </span>
+ </>
+ );
+ }
+ case 'offline': {
+ return (
+ <>
+ <span className="timeline-sync-state-badge-pin bg-danger" />
+ <span className="text-danger">
+ {t('timeline.postSyncState.offline')}
+ </span>
+ </>
+ );
+ }
+ default:
+ throw new UiLogicError('Unknown sync state.');
+ }
+ })()}
+ </div>
+ );
+};
export interface TimelineCardComponentProps<TManageItems> {
timeline: TimelineInfo;
@@ -29,7 +84,7 @@ export interface TimelineCardComponentProps<TManageItems> { export interface TimelinePageTemplateUIProps<TManageItems> {
avatarKey?: string | number;
timeline?: TimelineInfo;
- posts?: TimelinePostInfoEx[] | 'forbid';
+ postListState?: TimelinePostListState;
CardComponent: React.ComponentType<TimelineCardComponentProps<TManageItems>>;
onMember: () => void;
onManage?: (item: TManageItems | 'property') => void;
@@ -41,7 +96,7 @@ export interface TimelinePageTemplateUIProps<TManageItems> { export default function TimelinePageTemplateUI<TManageItems>(
props: TimelinePageTemplateUIProps<TManageItems>
): React.ReactElement | null {
- const { timeline } = props;
+ const { timeline, postListState } = props;
const { t } = useTranslation();
@@ -116,7 +171,7 @@ export default function TimelinePageTemplateUI<TManageItems>( subscriptions.forEach((s) => s.unsubscribe());
};
}
- }, [getResizeEvent, triggerResizeEvent, timeline, props.posts]);
+ }, [getResizeEvent, triggerResizeEvent, timeline, postListState]);
const [cardHeight, setCardHeight] = React.useState<number>(0);
@@ -142,19 +197,40 @@ export default function TimelinePageTemplateUI<TManageItems>( } else {
if (timeline != null) {
let timelineBody: React.ReactElement;
- if (props.posts != null) {
- if (props.posts === 'forbid') {
+ if (postListState != null && postListState.state !== 'loading') {
+ if (postListState.state === 'forbid') {
timelineBody = (
<p className="text-danger">{t('timeline.messageCantSee')}</p>
);
} else {
+ const posts: TimelinePostInfoEx[] = postListState.posts.map(
+ (post) => ({
+ ...post,
+ deletable: timelineService.hasModifyPostPermission(
+ userService.currentUser,
+ timeline,
+ post
+ ),
+ })
+ );
+
+ const topHeight: string = infoCardCollapse
+ ? 'calc(68px + 1.5em)'
+ : `${cardHeight + 60}px`;
+
timelineBody = (
- <Timeline
- containerRef={timelineRef}
- posts={props.posts}
- onDelete={props.onDelete}
- onResize={triggerResizeEvent}
- />
+ <div>
+ <TimelinePostSyncStateBadge
+ style={{ top: topHeight }}
+ state={postListState.state}
+ />
+ <Timeline
+ containerRef={timelineRef}
+ posts={posts}
+ onDelete={props.onDelete}
+ onResize={triggerResizeEvent}
+ />
+ </div>
);
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..667c1da9 100644 --- a/Timeline/ClientApp/src/app/timeline/timeline-ui.sass +++ b/Timeline/ClientApp/src/app/timeline/timeline-ui.sass @@ -16,3 +16,20 @@ .timeline-page-top-space
transition: height 0.5s
+.timeline-sync-state-badge
+ position: fixed
+ top: 0
+ right: 0
+ z-index: 1
+ font-size: 0.8em
+ 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
|