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,
TimelinePostsWithSyncState,
timelineService,
} from '../data/timeline';
import { userService } from '../data/user';
import Timeline, {
TimelinePostInfoEx,
TimelineDeleteCallback,
} from './Timeline';
import AppBar from '../common/AppBar';
import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit';
type TimelinePostSyncState = 'loadcache' | 'syncing' | 'synced' | 'offline';
const TimelinePostSyncStateBadge: React.FC<{
state: TimelinePostSyncState;
style?: CSSProperties;
className?: string;
}> = ({ state, style, className }) => {
const { t } = useTranslation();
return (
{(() => {
switch (state) {
case 'loadcache':
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;
onMember: () => void;
className?: string;
onHeight?: (height: number) => void;
}
export interface TimelinePageTemplateUIProps {
avatarKey?: string | number;
timeline?: TimelineInfo;
postListState?: TimelinePostsWithSyncState;
CardComponent: React.ComponentType>;
onMember: () => void;
onManage?: (item: TManageItems | 'property') => void;
onPost?: TimelinePostSendCallback;
onDelete: TimelineDeleteCallback;
error?: string;
}
export default function TimelinePageTemplateUI(
props: TimelinePageTemplateUIProps
): React.ReactElement | null {
const { timeline, postListState } = props;
const { t } = useTranslation();
const bottomSpaceRef = React.useRef(null);
const onPostEditHeightChange = React.useCallback((height: number): void => {
const { current: bottomSpaceDiv } = bottomSpaceRef;
if (bottomSpaceDiv != null) {
bottomSpaceDiv.style.height = `${height}px`;
}
if (height === 0) {
const alertHost = getAlertHost();
if (alertHost != null) {
alertHost.style.removeProperty('margin-bottom');
}
} else {
const alertHost = getAlertHost();
if (alertHost != null) {
alertHost.style.marginBottom = `${height}px`;
}
}
}, []);
const timelineRef = React.useRef(null);
const [getResizeEvent, triggerResizeEvent] = useEventEmiiter();
React.useEffect(() => {
const { current: timelineElement } = timelineRef;
if (timelineElement != null) {
let loadingScrollToBottom = true;
let pinBottom = false;
const isAtBottom = (): boolean =>
window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight;
const disableLoadingScrollToBottom = (): void => {
loadingScrollToBottom = false;
if (isAtBottom()) pinBottom = true;
};
const checkAndScrollToBottom = (): void => {
if (loadingScrollToBottom || pinBottom) {
window.scrollTo(0, document.body.scrollHeight);
}
};
const subscriptions = [
fromEvent(timelineElement, 'wheel').subscribe(
disableLoadingScrollToBottom
),
fromEvent(timelineElement, 'pointerdown').subscribe(
disableLoadingScrollToBottom
),
fromEvent(timelineElement, 'keydown').subscribe(
disableLoadingScrollToBottom
),
fromEvent(window, 'scroll').subscribe(() => {
if (loadingScrollToBottom) return;
if (isAtBottom()) {
pinBottom = true;
} else {
pinBottom = false;
}
}),
fromEvent(window, 'resize').subscribe(checkAndScrollToBottom),
getResizeEvent().subscribe(checkAndScrollToBottom),
];
return () => {
subscriptions.forEach((s) => s.unsubscribe());
};
}
}, [getResizeEvent, triggerResizeEvent, timeline, postListState]);
const [cardHeight, setCardHeight] = React.useState(0);
const genCardCollapseLocalStorageKey = (uniqueId: string): string =>
`timeline.${uniqueId}.cardCollapse`;
const cardCollapseLocalStorageKey =
timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null;
const [infoCardCollapse, setInfoCardCollapse] = React.useState(true);
React.useEffect(() => {
if (cardCollapseLocalStorageKey != null) {
const savedCollapse =
window.localStorage.getItem(cardCollapseLocalStorageKey) === 'true';
setInfoCardCollapse(savedCollapse);
}
}, [cardCollapseLocalStorageKey]);
let body: React.ReactElement;
if (props.error != null) {
body = {t(props.error)}
;
} else {
if (timeline != null) {
let timelineBody: React.ReactElement;
if (postListState != null) {
if (postListState.posts == null) {
throw new UiLogicError(
"Timeline is not null but postListState is 'timeline-notexist or 'timeline-offline'."
);
}
if (postListState.state === 'forbid') {
timelineBody = (
{t('timeline.messageCantSee')}
);
} 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 = (
);
if (props.onPost != null) {
timelineBody = (
<>
{timelineBody}
>
);
}
}
} else {
timelineBody = (
);
}
const { CardComponent } = props;
body = (
<>
{
const newState = !infoCardCollapse;
setInfoCardCollapse(newState);
window.localStorage.setItem(
genCardCollapseLocalStorageKey(timeline.uniqueId),
newState.toString()
);
}}
className="float-right m-1 info-card-collapse-button text-primary icon-button"
/>
{timelineBody}
>
);
} else {
body = (
);
}
}
return (
<>
>
);
}