diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 7cb97202206d8c26d1be14a3b74d38e57123b18b..8929806479e35ab5e067bec8e8976fc9b60fbafc 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -53,6 +53,7 @@ export type Screen = | { id: "modify_account"; field: "username" | "email" | "password" } | { id: "profile"; user_id: string } | { id: "channel_info"; channel_id: string } +| { id: "pending_requests"; users: string[] } | { id: "user_picker"; omit?: string[]; @@ -119,7 +120,7 @@ export default function Intermediate(props: Props) { } /** By specifying a key, we reset state whenever switching screen. */ /> <Prompt - when={[ 'modify_account', 'special_prompt', 'special_input', 'image_viewer', 'profile', 'channel_info', 'user_picker' ].includes(screen.id)} + when={[ 'modify_account', 'special_prompt', 'special_input', 'image_viewer', 'profile', 'channel_info', 'pending_requests', 'user_picker' ].includes(screen.id)} message={(_, action) => { if (action === 'POP') { openScreen({ id: 'none' }); diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index 73d2337ebcff0948a47c642c721374060d958074..d0ee430584f150825c1e7860776ae46c0112b08e 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -7,6 +7,7 @@ import { SpecialPromptModal } from "./modals/Prompt"; import { UserProfile } from "./popovers/UserProfile"; import { ImageViewer } from "./popovers/ImageViewer"; import { ChannelInfo } from "./popovers/ChannelInfo"; +import { PendingRequests } from "./popovers/PendingRequests"; import { ModifyAccountModal } from "./popovers/ModifyAccount"; export default function Popovers() { @@ -24,6 +25,8 @@ export default function Popovers() { return <ImageViewer {...screen} onClose={onClose} />; case "channel_info": return <ChannelInfo {...screen} onClose={onClose} />; + case "pending_requests": + return <PendingRequests {...screen} onClose={onClose} />; case "modify_account": return <ModifyAccountModal onClose={onClose} {...screen} />; case "special_prompt": diff --git a/src/context/intermediate/popovers/PendingRequests.tsx b/src/context/intermediate/popovers/PendingRequests.tsx new file mode 100644 index 0000000000000000000000000000000000000000..710cb6b2f250a588a66a9588a2e68e8fc55376ae --- /dev/null +++ b/src/context/intermediate/popovers/PendingRequests.tsx @@ -0,0 +1,26 @@ +import styles from "./UserPicker.module.scss"; +import { useUsers } from "../../revoltjs/hooks"; +import Modal from "../../../components/ui/Modal"; +import { Friend } from "../../../pages/friends/Friend"; + +interface Props { + users: string[]; + onClose: () => void; +} + +export function PendingRequests({ users: ids, onClose }: Props) { + const users = useUsers(ids); + + return ( + <Modal + visible={true} + title={"Pending requests"} + onClose={onClose}> + <div className={styles.list}> + { users + .filter(x => typeof x !== 'undefined') + .map(x => <Friend user={x!} key={x!._id} />) } + </div> + </Modal> + ); +} diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 97e285293c7c3c515f9b89ab1c1cb62de8582f86..7252d8ba01ae1c3432da291970e4183389f93f56 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -23,7 +23,6 @@ import { At, Bell, BellOff, Check, CheckSquare, ChevronRight, Block, Square, Lef import { Cog } from "@styled-icons/boxicons-solid"; import { getNotificationState, Notifications, NotificationState } from "../redux/reducers/notifications"; import UserStatus from "../components/common/user/UserStatus"; -import { Link } from "react-router-dom"; import IconButton from "../components/ui/IconButton"; interface ContextMenuData { @@ -438,7 +437,7 @@ function ContextMenus(props: Props) { ]; break; case Users.Relationship.Incoming: - actions = ["add_friend", "block_user"]; + actions = ["add_friend", "cancel_friend", "block_user"]; break; case Users.Relationship.Outgoing: actions = ["cancel_friend", "block_user"]; diff --git a/src/pages/friends/Friend.module.scss b/src/pages/friends/Friend.module.scss index 450abee17b31be26b4faa3400c072ff63514b5a2..61cd612b83b3709766ce0be59051800e691fd996 100644 --- a/src/pages/friends/Friend.module.scss +++ b/src/pages/friends/Friend.module.scss @@ -122,6 +122,53 @@ background: var(--primary-background); } +.title { + flex-grow: 1; +} + +.pending { + gap: 12px; + padding: 1em; + display: flex; + cursor: pointer; + margin-top: 1em; + align-items: center; + flex-direction: row; + background: var(--secondary-background); + + .avatars { + display: flex; + } + + .details { + flex-grow: 1; + display: flex; + flex-direction: column; + + > div { + font-size: 1.4em; + font-weight: 800; + + span { + width: 1.5em; + height: 1.5em; + font-size: 0.8em; + + border-radius: 50%; + align-items: center; + display: inline-flex; + justify-content: center; + background: var(--error); + } + } + + > span { + font-weight: 600; + color: var(--tertiary-foreground); + } + } +} + @media only screen and (max-width: 768px) { .list { padding: 0 8px 8px 8px; @@ -131,10 +178,3 @@ display: none; } } - -//! FIXME: Move this to the Header component, do this: -// 1. Check if header has topic, if yes, flex-grow: 0 on the title. -// 2. If header has no topic (example: friends page), flex-grow 1 on the header title. -.title { - flex-grow: 1; -} \ No newline at end of file diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx index d50198cced6595a9cdb09ed6945c42c5769b0f0d..3dd598b5aa06588be4dbe5c2c6ea1f266b7b42ef 100644 --- a/src/pages/friends/Friend.tsx +++ b/src/pages/friends/Friend.tsx @@ -70,7 +70,11 @@ export function Friend({ user }: Props) { actions.push( <IconButton type="circle" className={classNames(styles.button, styles.error)} - onClick={ev => stopPropagation(ev, openScreen({ id: 'special_prompt', type: 'unfriend_user', target: user }))}> + onClick={ev => stopPropagation(ev, + user.relationship === Users.Relationship.Friend ? + openScreen({ id: 'special_prompt', type: 'unfriend_user', target: user }) + : client.users.removeFriend(user._id) + )}> <X size={24} /> </IconButton> ); diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index 7ff2a462b35dcf3c21d91703e89c5d54ee1f9278..83eba1acc81fa9f04050752da6ea503f3fbf5064 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -1,16 +1,17 @@ import { Friend } from "./Friend"; import { Text } from "preact-i18n"; import styles from "./Friend.module.scss"; -import Tooltip from "../../components/common/Tooltip"; import Header from "../../components/ui/Header"; import Overline from "../../components/ui/Overline"; +import Tooltip from "../../components/common/Tooltip"; import IconButton from "../../components/ui/IconButton"; import { useUsers } from "../../context/revoltjs/hooks"; import { User, Users } from "revolt.js/dist/api/objects"; +import UserIcon from "../../components/common/user/UserIcon"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useIntermediate } from "../../context/intermediate/Intermediate"; import { ChevronDown, ChevronRight } from "@styled-icons/boxicons-regular"; -import { UserDetail, Conversation, UserPlus, TennisBall } from "@styled-icons/boxicons-solid"; +import { UserDetail, Conversation, UserPlus } from "@styled-icons/boxicons-solid"; export default function Friends() { const { openScreen } = useIntermediate(); @@ -22,7 +23,9 @@ export default function Friends() { const lists = [ [ 'app.special.friends.pending', users.filter(x => - x.relationship === Users.Relationship.Incoming || + x.relationship === Users.Relationship.Incoming + ) ], + [ 'app.special.friends.pending', users.filter(x => x.relationship === Users.Relationship.Outgoing ) ], [ 'app.status.online', friends.filter(x => @@ -70,8 +73,23 @@ export default function Friends() { <Text id="app.special.friends.nobody" /> </> )} + + { lists[0][1].length > 0 && <div className={styles.pending} + onClick={() => openScreen({ id: 'pending_requests', users: lists[0][1].map(x => x._id) })}> + <div className={styles.avatars}> + { lists[0][1].map((x, i) => i < 3 && <UserIcon target={x} size={64} />) } + </div> + <div className={styles.details}> + {/* ! FIXME: i18n */} + <div>Pending requests <span>{ lists[0][1].length }</span></div> + <span>From { lists[0][1].map(x => x.username).join(', ') }</span> + </div> + <ChevronRight size={48} /> + </div> } + { - lists.map(([i18n, list]) => { + lists.map(([i18n, list], index) => { + if (index === 0) return; if (list.length === 0) return; return ( diff --git a/src/styles/_context-menu.scss b/src/styles/_context-menu.scss index 66df13486a0123c9bbed4436d60cda8c8325f978..8f33978b788dc889e13ff43c2448fc3ad83009d5 100644 --- a/src/styles/_context-menu.scss +++ b/src/styles/_context-menu.scss @@ -1,5 +1,5 @@ .preact-context-menu .context-menu { - z-index: 100; + z-index: 10000; min-width: 190px; padding: 6px 8px; user-select: none;