aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-01-30 23:35:38 +0800
committercrupest <crupest@outlook.com>2021-01-30 23:35:38 +0800
commitca91b58293551cf1964e814ad01d526e5539e69b (patch)
tree7a7e2b7411a0b10b80557c2cb76bf6d56c973451 /FrontEnd/src
parentf0ac87f1ea88708db0789333341d31ca33fdc808 (diff)
downloadtimeline-ca91b58293551cf1964e814ad01d526e5539e69b.tar.gz
timeline-ca91b58293551cf1964e814ad01d526e5539e69b.tar.bz2
timeline-ca91b58293551cf1964e814ad01d526e5539e69b.zip
...
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/app/locales/en/translation.json2
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json2
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineItem.tsx19
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineMember.tsx257
4 files changed, 121 insertions, 159 deletions
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index ebf9552a..0aafb2ae 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -80,7 +80,7 @@
"description": "Description"
},
"member": {
- "alreadyMember": "The user is already a member.",
+ "noUserAvailableToAdd": "Sorry, no user available to be a member in search result.",
"add": "Add",
"remove": "Remove"
},
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index 0d063b4e..a12a6296 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -80,7 +80,7 @@
"description": "描述"
},
"member": {
- "alreadyMember": "该用户已经是一个成员。",
+ "noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。",
"add": "添加",
"remove": "移除"
},
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
index c096f890..a5b6d04a 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx
@@ -2,10 +2,10 @@ import React from "react";
import clsx from "clsx";
import { Link } from "react-router-dom";
-import { useAvatar } from "@/services/user";
import { TimelinePostInfo } from "@/services/timeline";
import BlobImage from "../common/BlobImage";
+import UserAvatar from "../common/user/UserAvatar";
import TimelineLine from "./TimelineLine";
import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog";
@@ -25,9 +25,7 @@ export interface TimelineItemProps {
const TimelineItem: React.FC<TimelineItemProps> = (props) => {
const current = props.current === true;
- const { more } = props;
-
- const avatar = useAvatar(props.post.author.username);
+ const { post, more } = props;
const [deleteDialog, setDeleteDialog] = React.useState<boolean>(false);
@@ -52,20 +50,21 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
<span className="mr-2">
<span>
<Link to={"/users/" + props.post.author.username}>
- <BlobImage blob={avatar} className="timeline-avatar mr-1" />
+ <UserAvatar
+ username={post.author.username}
+ className="timeline-avatar mr-1"
+ />
</Link>
- <small className="text-dark mr-2">
- {props.post.author.nickname}
- </small>
+ <small className="text-dark mr-2">{post.author.nickname}</small>
<small className="text-secondary white-space-no-wrap">
- {props.post.time.toLocaleTimeString()}
+ {post.time.toLocaleTimeString()}
</small>
</span>
</span>
</div>
<div className="timeline-content">
{(() => {
- const { content } = props.post;
+ const { content } = post;
if (content.type === "text") {
return content.text;
} else {
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx
index efa7e971..7bc81e7f 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx
@@ -2,27 +2,27 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap";
-import { User, useAvatar } from "@/services/user";
+import { getHttpSearchClient } from "@/http/search";
+
+import { User } from "@/services/user";
import { TimelineInfo, timelineService } from "@/services/timeline";
-import { getHttpUserClient, HttpUserNotExistError } from "@/http/user";
import SearchInput from "../common/SearchInput";
-import BlobImage from "../common/BlobImage";
+import UserAvatar from "../common/user/UserAvatar";
+import { convertI18nText, I18nText } from "@/common";
const TimelineMemberItem: React.FC<{
user: User;
- owner: boolean;
- onRemove?: (username: string) => void;
-}> = ({ user, owner, onRemove }) => {
+ add?: boolean;
+ onAction?: (username: string) => void;
+}> = ({ user, add, onAction }) => {
const { t } = useTranslation();
- const avatar = useAvatar(user.username);
-
return (
<ListGroup.Item className="container">
<Row>
<Col xs="auto">
- <BlobImage blob={avatar} className="avatar small" />
+ <UserAvatar username={user.username} className="avatar small" />
</Col>
<Col>
<Row>{user.nickname}</Row>
@@ -30,55 +30,122 @@ const TimelineMemberItem: React.FC<{
<small>{"@" + user.username}</small>
</Row>
</Col>
- {(() => {
- if (owner) {
- return null;
- }
- if (onRemove == null) {
- return null;
- }
- return (
- <Button
- className="align-self-center"
- variant="danger"
- onClick={() => {
- onRemove(user.username);
- }}
- >
- {t("timeline.member.remove")}
- </Button>
- );
- })()}
+ {onAction ? (
+ <Button
+ className="align-self-center"
+ variant="danger"
+ onClick={() => {
+ onAction(user.username);
+ }}
+ >
+ {t(`timeline.member.${add ? "add" : "remove"}`)}
+ </Button>
+ ) : null}
</Row>
</ListGroup.Item>
);
};
-export interface TimelineMemberProps {
- timeline: TimelineInfo;
- editable: boolean;
-}
-
-const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
+const TimelineMemberUserSearch: React.FC<{ timeline: TimelineInfo }> = ({
+ timeline,
+}) => {
const { t } = useTranslation();
const [userSearchText, setUserSearchText] = useState<string>("");
const [userSearchState, setUserSearchState] = useState<
| {
- type: "user";
- data: User;
+ type: "users";
+ data: User[];
}
- | { type: "error"; data: string }
+ | { type: "error"; data: I18nText }
| { type: "loading" }
| { type: "init" }
>({ type: "init" });
- const userSearchAvatar = useAvatar(
- userSearchState.type === "user" ? userSearchState.data.username : undefined
+ return (
+ <>
+ <SearchInput
+ className="mt-3"
+ value={userSearchText}
+ onChange={(v) => {
+ setUserSearchText(v);
+ }}
+ loading={userSearchState.type === "loading"}
+ onButtonClick={() => {
+ if (userSearchText === "") {
+ setUserSearchState({
+ type: "error",
+ data: "login.emptyUsername",
+ });
+ return;
+ }
+ setUserSearchState({ type: "loading" });
+ getHttpSearchClient()
+ .searchUsers(userSearchText)
+ .then(
+ (users) => {
+ users = users.filter(
+ (user) =>
+ timeline.members.findIndex(
+ (m) => m.username === user.username
+ ) === -1
+ );
+ setUserSearchState({ type: "users", data: users });
+ },
+ (e) => {
+ setUserSearchState({
+ type: "error",
+ data: { type: "custom", value: String(e) },
+ });
+ }
+ );
+ }}
+ />
+ {(() => {
+ if (userSearchState.type === "users") {
+ const users = userSearchState.data;
+ if (users.length === 0) {
+ return <div>{t("timeline.member.noUserAvailableToAdd")}</div>;
+ } else {
+ return (
+ <ListGroup>
+ {users.map((user) => {
+ <TimelineMemberItem
+ key={user.username}
+ user={user}
+ add
+ onAction={() => {
+ void timelineService
+ .addMember(timeline.name, user.username)
+ .then(() => {
+ setUserSearchText("");
+ setUserSearchState({ type: "init" });
+ });
+ }}
+ />;
+ })}
+ </ListGroup>
+ );
+ }
+ } else if (userSearchState.type === "error") {
+ return (
+ <div className="text-danger">
+ {convertI18nText(userSearchState.data, t)}
+ </div>
+ );
+ }
+ })()}
+ </>
);
+};
- const { timeline } = props;
+export interface TimelineMemberProps {
+ timeline: TimelineInfo;
+ editable: boolean;
+}
+const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
+ const { timeline, editable } = props;
const members = [timeline.owner, ...timeline.members];
return (
@@ -88,9 +155,8 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
<TimelineMemberItem
key={member.username}
user={member}
- owner={index === 0}
- onRemove={
- props.editable
+ onAction={
+ editable && index !== 0
? () => {
void timelineService.removeMember(
timeline.name,
@@ -102,110 +168,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
/>
))}
</ListGroup>
- {(() => {
- if (props.editable) {
- return (
- <>
- <SearchInput
- className="mt-3"
- value={userSearchText}
- onChange={(v) => {
- setUserSearchText(v);
- }}
- loading={userSearchState.type === "loading"}
- onButtonClick={() => {
- if (userSearchText === "") {
- setUserSearchState({
- type: "error",
- data: "login.emptyUsername",
- });
- return;
- }
- setUserSearchState({ type: "loading" });
- getHttpUserClient()
- .get(userSearchText)
- .catch((e) => {
- if (e instanceof HttpUserNotExistError) {
- return null;
- } else {
- throw e;
- }
- })
- .then(
- (u) => {
- if (u == null) {
- setUserSearchState({
- type: "error",
- data: "timeline.userNotExist",
- });
- } else {
- setUserSearchState({ type: "user", data: u });
- }
- },
- (e) => {
- setUserSearchState({
- type: "error",
- data: `${e as string}`,
- });
- }
- );
- }}
- />
- {(() => {
- if (userSearchState.type === "user") {
- const u = userSearchState.data;
- const addable =
- members.findIndex((m) => m.username === u.username) === -1;
- return (
- <>
- {!addable ? (
- <p>{t("timeline.member.alreadyMember")}</p>
- ) : null}
- <Container className="mb-3">
- <Row>
- <Col className="col-auto">
- <BlobImage
- blob={userSearchAvatar}
- className="avatar small"
- />
- </Col>
- <Col>
- <Row>{u.nickname}</Row>
- <Row>
- <small>{"@" + u.username}</small>
- </Row>
- </Col>
- <Button
- variant="primary"
- className="align-self-center"
- disabled={!addable}
- onClick={() => {
- void timelineService
- .addMember(timeline.name, u.username)
- .then(() => {
- setUserSearchText("");
- setUserSearchState({ type: "init" });
- });
- }}
- >
- {t("timeline.member.add")}
- </Button>
- </Row>
- </Container>
- </>
- );
- } else if (userSearchState.type === "error") {
- return (
- <p className="text-danger">{t(userSearchState.data)}</p>
- );
- }
- })()}
- </>
- );
- } else {
- return null;
- }
- })()}
+ {editable ? <TimelineMemberUserSearch timeline={timeline} /> : null}
</Container>
);
};