aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-08-07 18:27:04 +0800
committercrupest <crupest@outlook.com>2020-08-07 18:27:04 +0800
commit00b4f38a4c6f3c1039873cc8e274e24f207d81a7 (patch)
tree32603eda8592590e869b11f6c95dad9db74ce9b8 /Timeline/ClientApp/src
parentef5f490cf3155234a12d42f1b43630e38cb49a38 (diff)
downloadtimeline-00b4f38a4c6f3c1039873cc8e274e24f207d81a7.tar.gz
timeline-00b4f38a4c6f3c1039873cc8e274e24f207d81a7.tar.bz2
timeline-00b4f38a4c6f3c1039873cc8e274e24f207d81a7.zip
Create home page for offline.
Diffstat (limited to 'Timeline/ClientApp/src')
-rw-r--r--Timeline/ClientApp/src/app/data/timeline.ts10
-rw-r--r--Timeline/ClientApp/src/app/home/BoardWithUser.tsx102
-rw-r--r--Timeline/ClientApp/src/app/home/BoardWithoutUser.tsx61
-rw-r--r--Timeline/ClientApp/src/app/home/Home.tsx88
-rw-r--r--Timeline/ClientApp/src/app/home/OfflineBoard.tsx62
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoard.tsx28
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx36
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx26
-rw-r--r--Timeline/ClientApp/src/app/locales/en/translation.ts3
-rw-r--r--Timeline/ClientApp/src/app/locales/scheme.ts2
-rw-r--r--Timeline/ClientApp/src/app/locales/zh/translation.ts3
11 files changed, 287 insertions, 134 deletions
diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts
index c0d2141f..9fc99d59 100644
--- a/Timeline/ClientApp/src/app/data/timeline.ts
+++ b/Timeline/ClientApp/src/app/data/timeline.ts
@@ -881,3 +881,13 @@ export function usePostList(
}, [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/Timeline/ClientApp/src/app/home/BoardWithUser.tsx b/Timeline/ClientApp/src/app/home/BoardWithUser.tsx
new file mode 100644
index 00000000..3830104f
--- /dev/null
+++ b/Timeline/ClientApp/src/app/home/BoardWithUser.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { Row, Col } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+
+import { UserWithToken } from '../data/user';
+import { TimelineInfo } from '../data/timeline';
+
+import { getHttpTimelineClient } from '../http/timeline';
+
+import TimelineBoard from './TimelineBoard';
+import OfflineBoard from './OfflineBoard';
+
+const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => {
+ const { t } = useTranslation();
+
+ const [ownTimelines, setOwnTimelines] = React.useState<
+ TimelineInfo[] | 'offline' | 'loading'
+ >('loading');
+ const [joinTimelines, setJoinTimelines] = React.useState<
+ TimelineInfo[] | 'offline' | 'loading'
+ >('loading');
+
+ React.useEffect(() => {
+ let subscribe = true;
+ if (ownTimelines === 'loading') {
+ void getHttpTimelineClient()
+ .listTimeline({ relate: user.username, relateType: 'own' })
+ .then(
+ (timelines) => {
+ if (subscribe) {
+ setOwnTimelines(timelines);
+ }
+ },
+ () => {
+ setOwnTimelines('offline');
+ }
+ );
+ }
+ return () => {
+ subscribe = false;
+ };
+ }, [user, ownTimelines]);
+
+ React.useEffect(() => {
+ let subscribe = true;
+ if (joinTimelines === 'loading') {
+ void getHttpTimelineClient()
+ .listTimeline({ relate: user.username, relateType: 'join' })
+ .then(
+ (timelines) => {
+ if (subscribe) {
+ setJoinTimelines(timelines);
+ }
+ },
+ () => {
+ setJoinTimelines('offline');
+ }
+ );
+ }
+ return () => {
+ subscribe = false;
+ };
+ }, [user, joinTimelines]);
+
+ return (
+ <Row className="my-2 justify-content-center">
+ {ownTimelines === 'offline' && joinTimelines === 'offline' ? (
+ <Col className="py-2" sm="8" lg="6">
+ <OfflineBoard
+ onReload={() => {
+ setOwnTimelines('loading');
+ setJoinTimelines('loading');
+ }}
+ />
+ </Col>
+ ) : (
+ <>
+ <Col sm="6" lg="5" className="py-2">
+ <TimelineBoard
+ title={t('home.ownTimeline')}
+ timelines={ownTimelines}
+ onReload={() => {
+ setOwnTimelines('loading');
+ }}
+ />
+ </Col>
+ <Col sm="6" lg="5" className="py-2">
+ <TimelineBoard
+ title={t('home.joinTimeline')}
+ timelines={joinTimelines}
+ onReload={() => {
+ setJoinTimelines('loading');
+ }}
+ />
+ </Col>
+ </>
+ )}
+ </Row>
+ );
+};
+
+export default BoardWithUser;
diff --git a/Timeline/ClientApp/src/app/home/BoardWithoutUser.tsx b/Timeline/ClientApp/src/app/home/BoardWithoutUser.tsx
new file mode 100644
index 00000000..4b30fcc4
--- /dev/null
+++ b/Timeline/ClientApp/src/app/home/BoardWithoutUser.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { Row, Col } from 'reactstrap';
+
+import { TimelineInfo } from '../data/timeline';
+
+import { getHttpTimelineClient } from '../http/timeline';
+
+import TimelineBoard from './TimelineBoard';
+import OfflineBoard from './OfflineBoard';
+
+const BoardWithoutUser: React.FC = () => {
+ const [publicTimelines, setPublicTimelines] = React.useState<
+ TimelineInfo[] | 'offline' | 'loading'
+ >('loading');
+
+ React.useEffect(() => {
+ let subscribe = true;
+ if (publicTimelines === 'loading') {
+ void getHttpTimelineClient()
+ .listTimeline({ visibility: 'Public' })
+ .then(
+ (timelines) => {
+ if (subscribe) {
+ setPublicTimelines(timelines);
+ }
+ },
+ () => {
+ setPublicTimelines('offline');
+ }
+ );
+ }
+ return () => {
+ subscribe = false;
+ };
+ }, [publicTimelines]);
+
+ return (
+ <Row className="my-2 justify-content-center">
+ {publicTimelines === 'offline' ? (
+ <Col sm="8" lg="6">
+ <OfflineBoard
+ onReload={() => {
+ setPublicTimelines('loading');
+ }}
+ />
+ </Col>
+ ) : (
+ <Col sm="8" lg="6">
+ <TimelineBoard
+ timelines={publicTimelines}
+ onReload={() => {
+ setPublicTimelines('loading');
+ }}
+ />
+ </Col>
+ )}
+ </Row>
+ );
+};
+
+export default BoardWithoutUser;
diff --git a/Timeline/ClientApp/src/app/home/Home.tsx b/Timeline/ClientApp/src/app/home/Home.tsx
index de25d5c1..b759fa50 100644
--- a/Timeline/ClientApp/src/app/home/Home.tsx
+++ b/Timeline/ClientApp/src/app/home/Home.tsx
@@ -4,16 +4,14 @@ import { Row, Container, Button, Col } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { useUser } from '../data/user';
-import { TimelineInfo } from '../data/timeline';
-import { getHttpTimelineClient } from '../http/timeline';
import AppBar from '../common/AppBar';
import SearchInput from '../common/SearchInput';
-import TimelineBoardAreaWithoutUser from './TimelineBoardAreaWithoutUser';
-import TimelineBoardAreaWithUser from './TimelineBoardAreaWithUser';
+import BoardWithoutUser from './BoardWithoutUser';
+import BoardWithUser from './BoardWithUser';
import TimelineCreateDialog from './TimelineCreateDialog';
-const Home: React.FC = (_) => {
+const Home: React.FC = () => {
const history = useHistory();
const { t } = useTranslation();
@@ -22,50 +20,6 @@ const Home: React.FC = (_) => {
const [navText, setNavText] = React.useState<string>('');
- const [publicTimelines, setPublicTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
- const [ownTimelines, setOwnTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
- const [joinTimelines, setJoinTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
-
- React.useEffect(() => {
- let subscribe = true;
- if (user == null) {
- setOwnTimelines(undefined);
- setJoinTimelines(undefined);
- void getHttpTimelineClient()
- .listTimeline({ visibility: 'Public' })
- .then((timelines) => {
- if (subscribe) {
- setPublicTimelines(timelines);
- }
- });
- } else {
- setPublicTimelines(undefined);
- void getHttpTimelineClient()
- .listTimeline({ relate: user.username, relateType: 'own' })
- .then((timelines) => {
- if (subscribe) {
- setOwnTimelines(timelines);
- }
- });
- void getHttpTimelineClient()
- .listTimeline({ relate: user.username, relateType: 'join' })
- .then((timelines) => {
- if (subscribe) {
- setJoinTimelines(timelines);
- }
- });
- }
- return () => {
- subscribe = false;
- };
- }, [user]);
-
const [dialog, setDialog] = React.useState<'create' | null>(null);
const goto = React.useCallback((): void => {
@@ -78,14 +32,6 @@ const Home: React.FC = (_) => {
}
}, [navText, history]);
- const openCreateDialog = React.useCallback(() => {
- setDialog('create');
- }, []);
-
- const closeDialog = React.useCallback(() => {
- setDialog(null);
- }, []);
-
return (
<>
<AppBar />
@@ -101,7 +47,13 @@ const Home: React.FC = (_) => {
placeholder="@crupest"
additionalButton={
user != null && (
- <Button color="success" outline onClick={openCreateDialog}>
+ <Button
+ color="success"
+ outline
+ onClick={() => {
+ setDialog('create');
+ }}
+ >
{t('home.createButton')}
</Button>
)
@@ -111,16 +63,9 @@ const Home: React.FC = (_) => {
</Row>
{(() => {
if (user == null) {
- return (
- <TimelineBoardAreaWithoutUser publicTimelines={publicTimelines} />
- );
+ return <BoardWithoutUser />;
} else {
- return (
- <TimelineBoardAreaWithUser
- ownTimelines={ownTimelines}
- joinTimelines={joinTimelines}
- />
- );
+ return <BoardWithUser user={user} />;
}
})()}
</Container>
@@ -142,7 +87,14 @@ const Home: React.FC = (_) => {
<small className="white-space-no-wrap">公安备案 42112102000124</small>
</a>
</footer>
- {dialog === 'create' && <TimelineCreateDialog open close={closeDialog} />}
+ {dialog === 'create' && (
+ <TimelineCreateDialog
+ open
+ close={() => {
+ setDialog(null);
+ }}
+ />
+ )}
</>
);
};
diff --git a/Timeline/ClientApp/src/app/home/OfflineBoard.tsx b/Timeline/ClientApp/src/app/home/OfflineBoard.tsx
new file mode 100644
index 00000000..ca6d2a26
--- /dev/null
+++ b/Timeline/ClientApp/src/app/home/OfflineBoard.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Trans } from 'react-i18next';
+
+import { getAllCachedTimelineNames } from '../data/timeline';
+
+import UserTimelineLogo from '../common/UserTimelineLogo';
+import TimelineLogo from '../common/TimelineLogo';
+
+export interface OfflineBoardProps {
+ onReload: () => void;
+}
+
+const OfflineBoard: React.FC<OfflineBoardProps> = ({ onReload }) => {
+ const [timelines, setTimelines] = React.useState<string[]>([]);
+
+ React.useEffect(() => {
+ let subscribe = true;
+ void getAllCachedTimelineNames().then((t) => {
+ if (subscribe) setTimelines(t);
+ });
+ return () => {
+ subscribe = false;
+ };
+ });
+
+ return (
+ <>
+ <Trans i18nKey="home.offlinePrompt">
+ 0
+ <a
+ href="#"
+ onClick={(e) => {
+ onReload();
+ e.preventDefault();
+ }}
+ >
+ 1
+ </a>
+ 2
+ </Trans>
+ {timelines.map((timeline) => {
+ const isPersonal = timeline.startsWith('@');
+ const url = isPersonal
+ ? `/users/${timeline.slice(1)}`
+ : `/timelines/${timeline}`;
+ return (
+ <div key={timeline} className="timeline-board-item">
+ {isPersonal ? (
+ <UserTimelineLogo className="icon" />
+ ) : (
+ <TimelineLogo className="icon" />
+ )}
+ <Link to={url}>{timeline}</Link>
+ </div>
+ );
+ })}
+ </>
+ );
+};
+
+export default OfflineBoard;
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoard.tsx b/Timeline/ClientApp/src/app/home/TimelineBoard.tsx
index 2e017bf7..8f8f6387 100644
--- a/Timeline/ClientApp/src/app/home/TimelineBoard.tsx
+++ b/Timeline/ClientApp/src/app/home/TimelineBoard.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { Link } from 'react-router-dom';
import { Spinner } from 'reactstrap';
+import { Trans } from 'react-i18next';
import { TimelineInfo } from '../data/timeline';
@@ -10,25 +11,44 @@ import UserTimelineLogo from '../common/UserTimelineLogo';
export interface TimelineBoardProps {
title?: string;
- timelines?: TimelineInfo[];
+ timelines: TimelineInfo[] | 'offline' | 'loading';
+ onReload: () => void;
className?: string;
}
-const TimelineBoard: React.FC<TimelineBoardProps> = props => {
+const TimelineBoard: React.FC<TimelineBoardProps> = (props) => {
const { title, timelines, className } = props;
return (
<div className={clsx('timeline-board', className)}>
{title != null && <h3 className="text-center">{title}</h3>}
{(() => {
- if (timelines == null) {
+ if (timelines === 'loading') {
return (
<div className="d-flex flex-grow-1 justify-content-center align-items-center">
<Spinner color="primary" />
</div>
);
+ } else if (timelines === 'offline') {
+ return (
+ <div className="d-flex flex-grow-1 justify-content-center align-items-center">
+ <Trans i18nKey="loadFailReload" parent="div">
+ 0
+ <a
+ href="#"
+ onClick={(e) => {
+ props.onReload();
+ e.preventDefault();
+ }}
+ >
+ 1
+ </a>
+ 2
+ </Trans>
+ </div>
+ );
} else {
- return timelines.map(timeline => {
+ return timelines.map((timeline) => {
const { name } = timeline;
const isPersonal = name.startsWith('@');
const url = isPersonal
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx b/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx
deleted file mode 100644
index a8603b9e..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Row, Col } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-
-import TimelineBoard from './TimelineBoard';
-import { TimelineInfo } from '../data/timeline';
-
-interface TimelineBoardAreaWithUserProps {
- ownTimelines?: TimelineInfo[];
- joinTimelines?: TimelineInfo[];
-}
-
-const TimelineBoardAreaWithUser: React.FC<TimelineBoardAreaWithUserProps> = (
- props
-) => {
- const { t } = useTranslation();
-
- return (
- <Row className="my-2 justify-content-center">
- <Col sm="6" lg="5" className="py-2">
- <TimelineBoard
- title={t('home.ownTimeline')}
- timelines={props.ownTimelines}
- />
- </Col>
- <Col sm="6" lg="5" className="py-2">
- <TimelineBoard
- title={t('home.joinTimeline')}
- timelines={props.joinTimelines}
- />
- </Col>
- </Row>
- );
-};
-
-export default TimelineBoardAreaWithUser;
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx b/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx
deleted file mode 100644
index dc05ff09..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { Row, Col } from 'reactstrap';
-
-import { TimelineInfo } from '../data/timeline';
-
-import TimelineBoard from './TimelineBoard';
-
-interface TimelineBoardAreaWithoutUserProps {
- publicTimelines?: TimelineInfo[];
-}
-
-const TimelineBoardAreaWithoutUser: React.FC<TimelineBoardAreaWithoutUserProps> = (
- props
-) => {
- const { publicTimelines } = props;
-
- return (
- <Row className="my-2 justify-content-center">
- <Col sm="8" lg="6">
- <TimelineBoard timelines={publicTimelines} />
- </Col>
- </Row>
- );
-};
-
-export default TimelineBoardAreaWithoutUser;
diff --git a/Timeline/ClientApp/src/app/locales/en/translation.ts b/Timeline/ClientApp/src/app/locales/en/translation.ts
index 2f8fb312..e7f395e1 100644
--- a/Timeline/ClientApp/src/app/locales/en/translation.ts
+++ b/Timeline/ClientApp/src/app/locales/en/translation.ts
@@ -3,6 +3,7 @@ import TranslationResource from '../scheme';
const translation: TranslationResource = {
welcome: 'Welcome!',
search: 'Search',
+ loadFailReload: 'Load failed, click <1>here</1> to reload.',
serviceWorker: {
availableOffline:
'Timeline is now cached in your computer and you can use it offline. 🎉🎉🎉',
@@ -26,6 +27,8 @@ const translation: TranslationResource = {
allTimeline: 'All Timelines',
joinTimeline: 'Joined Timelines',
ownTimeline: 'Owned Timelines',
+ offlinePrompt:
+ 'Oh oh, it seems you are offline. Here list some timelines cached locally. You can view them or click <1>here</1> to refresh.',
createButton: 'Create Timeline',
createDialog: {
title: 'Create Timeline!',
diff --git a/Timeline/ClientApp/src/app/locales/scheme.ts b/Timeline/ClientApp/src/app/locales/scheme.ts
index 7aa7e125..bbbc2686 100644
--- a/Timeline/ClientApp/src/app/locales/scheme.ts
+++ b/Timeline/ClientApp/src/app/locales/scheme.ts
@@ -3,6 +3,7 @@ export default interface TranslationResource {
search: string;
chooseImage: string;
loadImageError: string;
+ loadFailReload: string;
serviceWorker: {
availableOffline: string;
upgradePrompt: string;
@@ -21,6 +22,7 @@ export default interface TranslationResource {
allTimeline: string;
joinTimeline: string;
ownTimeline: string;
+ offlinePrompt: string;
createButton: string;
createDialog: {
title: string;
diff --git a/Timeline/ClientApp/src/app/locales/zh/translation.ts b/Timeline/ClientApp/src/app/locales/zh/translation.ts
index 35cfa38c..2e0eccd5 100644
--- a/Timeline/ClientApp/src/app/locales/zh/translation.ts
+++ b/Timeline/ClientApp/src/app/locales/zh/translation.ts
@@ -3,6 +3,7 @@ import TranslationResource from '../scheme';
const translation: TranslationResource = {
welcome: '欢迎!',
search: '搜索',
+ loadFailReload: '加载失败,<1>点击</1>重试。',
serviceWorker: {
availableOffline: 'Timeline 已经缓存在本地,你可以离线使用它。🎉🎉🎉',
upgradePrompt: 'App 有新版本!',
@@ -24,6 +25,8 @@ const translation: TranslationResource = {
allTimeline: '所有的时间线',
joinTimeline: '加入的时间线',
ownTimeline: '拥有的时间线',
+ offlinePrompt:
+ '你好像处于离线状态。以下是一些缓存在本地的时间线。你可以查看它们或者<1>点击</1>重新获取在线信息。',
createButton: '创建时间线',
createDialog: {
title: '创建时间线!',