diff --git a/src/components/common/ServerHeader.tsx b/src/components/common/ServerHeader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e2a89df811d231061a88ad7758987369de2a2b64 --- /dev/null +++ b/src/components/common/ServerHeader.tsx @@ -0,0 +1,39 @@ +import Header from "../ui/Header"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; +import IconButton from "../ui/IconButton"; +import { Settings } from "@styled-icons/feather"; +import { Server } from "revolt.js/dist/api/objects"; +import { ServerPermission } from "revolt.js/dist/api/permissions"; +import { HookContext, useServerPermission } from "../../context/revoltjs/hooks"; + +interface Props { + server: Server, + ctx: HookContext +} + +const ServerName = styled.div` + flex-grow: 1; +`; + +export default function ServerHeader({ server, ctx }: Props) { + const permissions = useServerPermission(server._id, ctx); + const bannerURL = ctx.client.servers.getBannerURL(server._id, { width: 480 }, true); + + return ( + <Header placement="secondary" + background={typeof bannerURL !== 'undefined'} + style={{ background: bannerURL ? `linear-gradient(to bottom, transparent 50%, #000e), url('${bannerURL}')` : undefined }}> + <ServerName> + { server.name } + </ServerName> + { (permissions & ServerPermission.ManageServer) > 0 && <div className="actions"> + <Link to={`/server/${server._id}/settings`}> + <IconButton> + <Settings size={24} /> + </IconButton> + </Link> + </div> } + </Header> + ) +} diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index f4cae5fdd538bd417361c486b106ad7043b357ae..b84bb69c4f7a7b90f7f59c8ba11858fd4da44fdd 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -14,6 +14,7 @@ import { connectState } from "../../../redux/connector"; import PaintCounter from "../../../lib/PaintCounter"; import styled from "styled-components"; import { attachContextMenu } from 'preact-context-menu'; +import ServerHeader from "../../common/ServerHeader"; interface Props { unreads: Unreads; @@ -45,7 +46,6 @@ function ServerSidebar(props: Props & WithDispatcher) { const server = useServer(server_id, ctx); if (!server) return <Redirect to="/" />; - const permissions = useServerPermission(server._id, ctx); const channels = (useChannels(server.channels, ctx) .filter(entry => typeof entry !== 'undefined') as Readonly<Channels.TextChannel>[]) .map(x => mapChannelWithUnread(x, props.unreads)); @@ -55,16 +55,7 @@ function ServerSidebar(props: Props & WithDispatcher) { return ( <ServerBase> - <Header placement="secondary" background style={{ background: `url('${ctx.client.servers.getBannerURL(server._id, { width: 480 }, true)}')` }}> - <div> - { server.name } - </div> - { (permissions & ServerPermission.ManageServer) > 0 && <div className="actions"> - {/*<IconButton to={`/server/${server._id}/settings`}>*/} - <Settings size={24} /> - {/*</IconButton>*/} - </div> } - </Header> + <ServerHeader server={server} ctx={ctx} /> <ConnectionStatus /> <ServerList onContextMenu={attachContextMenu('Menu', { server_list: server._id })}> {channels.map(entry => { diff --git a/src/pages/Open.tsx b/src/pages/Open.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7ee8120cfee956098ff60e9606faf0f4c9216f03 --- /dev/null +++ b/src/pages/Open.tsx @@ -0,0 +1,72 @@ +import { Text } from "preact-i18n"; +import Header from "../components/ui/Header"; +import { useContext, useEffect } from "preact/hooks"; +import { useHistory, useParams } from "react-router-dom"; +import { useIntermediate } from "../context/intermediate/Intermediate"; +import { useChannels, useForceUpdate, useUser } from "../context/revoltjs/hooks"; +import { AppContext, ClientStatus, StatusContext } from "../context/revoltjs/RevoltClient"; + +export default function Open() { + const history = useHistory(); + const client = useContext(AppContext); + const status = useContext(StatusContext); + const { id } = useParams<{ id: string }>(); + const { openScreen } = useIntermediate(); + + if (status !== ClientStatus.ONLINE) { + return ( + <Header placement="primary"> + <Text id="general.loading" /> + </Header> + ); + } + + const ctx = useForceUpdate(); + const channels = useChannels(undefined, ctx); + const user = useUser(id, ctx); + + useEffect(() => { + if (id === "saved") { + for (const channel of channels) { + if (channel?.channel_type === "SavedMessages") { + history.push(`/channel/${channel._id}`); + return; + } + } + + client.users + .openDM(client.user?._id as string) + .then(channel => history.push(`/channel/${channel?._id}`)) + .catch(error => openScreen({ id: "error", error })); + + return; + } + + if (user) { + const channel: string | undefined = channels.find( + channel => + channel?.channel_type === "DirectMessage" && + channel.recipients.includes(id) + )?._id; + + if (channel) { + history.push(`/channel/${channel}`); + } else { + client.users + .openDM(id) + .then(channel => history.push(`/channel/${channel?._id}`)) + .catch(error => openScreen({ id: "error", error })); + } + + return; + } + + history.push("/"); + }, []); + + return ( + <Header placement="primary"> + <Text id="general.loading" /> + </Header> + ); +} diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index f20c20317ea1fb5b0fefd4901f0cb735a1cdb2ae..53400ce80482a278e8fc260cd32f18eb6693a070 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -13,7 +13,9 @@ import LeftSidebar from "../components/navigation/LeftSidebar"; import RightSidebar from "../components/navigation/RightSidebar"; import BottomNavigation from "../components/navigation/BottomNavigation"; +import Open from "./Open"; import Home from './home/Home'; +import Invite from "./invite/Invite"; import Friends from "./friends/Friends"; import Channel from "./channels/Channel"; import Settings from './settings/Settings'; @@ -64,6 +66,8 @@ export default function App() { <Route path="/dev" component={Developer} /> <Route path="/friends" component={Friends} /> + <Route path="/open/:id" component={Open} /> + <Route path="/invite/:code" component={Invite} /> <Route path="/" component={Home} /> </Switch> </Routes> @@ -75,15 +79,3 @@ export default function App() { </OverlappingPanels> ); }; - -/** - * - * - - <Route path="/open/:id"> - <Open /> - </Route> - {/*<Route path="/invite/:code"> - <OpenInvite /> - </Route> - */ diff --git a/src/pages/invite/Invite.module.scss b/src/pages/invite/Invite.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..58814516539e8c91e091675a8f89885d2f3c4b8b --- /dev/null +++ b/src/pages/invite/Invite.module.scss @@ -0,0 +1,75 @@ +.invite { + height: 100%; + display: flex; + color: white; + user-select: none; + align-items: center; + flex-direction: column; + background-size: cover; + justify-content: center; + background-position: center; + + * { + overflow: visible; + } + + .icon { + width: 64px; + z-index: 100; + text-align: left; + position: relative; + + > * { + top: -32px; + position: absolute; + } + } + + .leave { + top: 8px; + left: 8px; + cursor: pointer; + position: fixed; + } + + .details { + text-align: center; + border-radius: 3px; + align-self: center; + padding: 32px 16px 16px 16px; + background: rgba(0, 0, 0, 0.6); + + h1 { + margin: 0; + font-weight: 500; + } + + h2 { + margin: 4px; + opacity: 0.7; + font-size: 0.8em; + font-weight: 400; + } + + h3 { + gap: 8px; + display: flex; + font-size: 1em; + font-weight: 400; + flex-direction: row; + justify-content: center; + } + + button { + margin: auto; + display: block; + background: rgba(0, 0, 0, 0.8); + } + } +} + +.preloader { + height: 100%; + display: grid; + place-items: center; +} diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb01eaf116983b8bdbcaa962436a69318a74098e --- /dev/null +++ b/src/pages/invite/Invite.tsx @@ -0,0 +1,87 @@ +import styles from './Invite.module.scss'; +import Button from '../../components/ui/Button'; +import { ArrowLeft } from "@styled-icons/feather"; +import Overline from '../../components/ui/Overline'; +import { Invites } from "revolt.js/dist/api/objects"; +import Preloader from '../../components/ui/Preloader'; +import { takeError } from "../../context/revoltjs/util"; +import { useHistory, useParams } from "react-router-dom"; +import ServerIcon from '../../components/common/ServerIcon'; +import UserIcon from '../../components/common/user/UserIcon'; +import { useContext, useEffect, useState } from "preact/hooks"; +import RequiresOnline from '../../context/revoltjs/RequiresOnline'; +import { AppContext, ClientStatus, StatusContext } from "../../context/revoltjs/RevoltClient"; + +export default function Invite() { + const history = useHistory(); + const client = useContext(AppContext); + const status = useContext(StatusContext); + const { code } = useParams<{ code: string }>(); + const [ processing, setProcessing ] = useState(false); + const [ error, setError ] = useState<string | undefined>(undefined); + const [ invite, setInvite ] = useState<Invites.RetrievedInvite | undefined>(undefined); + + useEffect(() => { + if (typeof invite === 'undefined' && (status === ClientStatus.ONLINE || status === ClientStatus.READY)) { + client.fetchInvite(code) + .then(data => setInvite(data)) + .catch(err => setError(takeError(err))) + } + }, [ status ]); + + if (typeof invite === 'undefined') { + return ( + <div className={styles.preloader}> + <RequiresOnline> + { error ? <Overline type="error" error={error} /> + : <Preloader /> } + </RequiresOnline> + </div> + ) + } + + return ( + <div className={styles.invite} style={{ backgroundImage: invite.server_banner ? `url('${client.generateFileURL(invite.server_banner)}')` : undefined }}> + <div className={styles.leave}> + <ArrowLeft size={32} onClick={() => history.push('/')} /> + </div> + + { !processing && + <div className={styles.icon}> + <ServerIcon attachment={invite.server_icon} server_name={invite.server_name} size={64} /> + </div> } + + <div className={styles.details}> + { processing ? <Preloader /> : + <> + <h1>{ invite.server_name }</h1> + <h2>#{invite.channel_name}</h2> + <h3>Invited by <UserIcon size={24} attachment={invite.user_avatar} /> { invite.user_name }</h3> + <Overline type="error" error={error} /> + <Button contrast + onClick={ + async () => { + if (status === ClientStatus.READY) { + return history.push('/'); + } + + try { + setProcessing(true); + + let result = await client.joinInvite(code); + if (result.type === 'Server') { + history.push(`/server/${result.server._id}/channel/${result.channel._id}`); + } + } catch (err) { + setError(takeError(err)); + setProcessing(false); + } + } + } + >{ status === ClientStatus.READY ? 'Login to REVOLT' : 'Accept Invite' }</Button> + </> + } + </div> + </div> + ); +}