diff options
author | crupest <crupest@outlook.com> | 2021-01-30 23:35:38 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-01-30 23:35:38 +0800 |
commit | ca91b58293551cf1964e814ad01d526e5539e69b (patch) | |
tree | 7a7e2b7411a0b10b80557c2cb76bf6d56c973451 /FrontEnd/src | |
parent | f0ac87f1ea88708db0789333341d31ca33fdc808 (diff) | |
download | timeline-ca91b58293551cf1964e814ad01d526e5539e69b.tar.gz timeline-ca91b58293551cf1964e814ad01d526e5539e69b.tar.bz2 timeline-ca91b58293551cf1964e814ad01d526e5539e69b.zip |
...
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/app/locales/en/translation.json | 2 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/zh/translation.json | 2 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineItem.tsx | 19 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineMember.tsx | 257 |
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> ); }; |