diff --git a/external/lang b/external/lang index ec907eb606a3e1d5046bef503caa0585f6bcbc22..9bb62d1185f7e6f7a3821751797e30cb41e74bf8 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit ec907eb606a3e1d5046bef503caa0585f6bcbc22 +Subproject commit 9bb62d1185f7e6f7a3821751797e30cb41e74bf8 diff --git a/package.json b/package.json index a89609945ac03a43f1585f36420d845daee1d37d..595b73b7acdd6baa674907de7726b50d192c0015 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "react-router-dom": "^5.2.0", "react-scroll": "^1.8.2", "redux": "^4.1.0", - "revolt.js": "4.3.3-alpha.6", + "revolt.js": "4.3.3-alpha.7", "rimraf": "^3.0.2", "sass": "^1.35.1", "shade-blend-color": "^1.0.0", diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 9c4b3a2bd6b240808e25c17ba6a35ccf8e3a8276..8582a19370252b93174386135a652c4b66a6e9ff 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -61,6 +61,7 @@ export default styled.button<Props>` &:hover { filter: brightness(1.2); + background: var(--error); } &:disabled { diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index 32020bace5ba247b6a4e265d3342d4dab37fbbf1..bc87c5cc98ac323fc4609c5dd3f62363844a6540 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -31,6 +31,15 @@ const CheckboxBase = styled.label` background: var(--background); } } + + &[disabled] { + opacity: 0.5; + cursor: unset; + + &:hover { + background: unset; + } + } `; const CheckboxContent = styled.span` @@ -52,6 +61,7 @@ const Checkmark = styled.div<{ checked: boolean }>` width: 24px; height: 24px; display: grid; + flex-shrink: 0; border-radius: 4px; place-items: center; transition: 0.2s ease all; diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 8929806479e35ab5e067bec8e8976fc9b60fbafc..0b2929aed192b95d00fa620dad7fd4fc16db196e 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -31,7 +31,8 @@ export type Screen = { type: "create_channel", target: Servers.Server } )) | ({ id: "special_input" } & ( - { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | + { type: "create_role", server: string, callback: (id: string) => void } )) | { id: "_input"; diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx index f25b992189ef5d19fbd63d863ea467e98d42d799..d57ba8d81539a2a80e4e63217b5b0e572bc49e64 100644 --- a/src/context/intermediate/modals/Input.tsx +++ b/src/context/intermediate/modals/Input.tsx @@ -68,7 +68,8 @@ export function InputModal({ } type SpecialProps = { onClose: () => void } & ( - { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | + { type: "create_role", server: string, callback: (id: string) => void } ) export function SpecialInputModal(props: SpecialProps) { @@ -112,6 +113,17 @@ export function SpecialInputModal(props: SpecialProps) { }} />; } + case "create_role": { + return <InputModal + onClose={onClose} + question={<Text id="app.settings.permissions.create_role" />} + field={<Text id="app.settings.permissions.role_name" />} + callback={async name => { + const role = await client.servers.createRole(props.server, name); + props.callback(role.id); + }} + />; + } case "set_custom_status": { return <InputModal onClose={onClose} diff --git a/src/pages/settings/server/Panes.module.scss b/src/pages/settings/server/Panes.module.scss index fe16fd40db95b2a469f1c9e9a2b4bed33cd306dc..8c1a2091dfb5f162bbd2d69d79b28eed73ffb9b8 100644 --- a/src/pages/settings/server/Panes.module.scss +++ b/src/pages/settings/server/Panes.module.scss @@ -57,7 +57,6 @@ } .members { - .subtitle { display: flex; justify-content: space-between; @@ -84,15 +83,43 @@ .list { width: 160px; + flex-shrink: 0; overflow-y: scroll; } .permissions { flex-grow: 1; + padding: 0 8px; overflow-y: scroll; + + section { + margin-bottom: 1em; + } + } + + .title { + gap: 8px; + display: flex; + margin-bottom: 1em; + align-items: center; + + h1, h2 { + margin: 0; + min-width: 0; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + svg { + cursor: pointer; + } } - h2 { - margin: 8px 0; + .actions { + gap: 8px; + display: flex; + padding: 8px 0; } } \ No newline at end of file diff --git a/src/pages/settings/server/Roles.tsx b/src/pages/settings/server/Roles.tsx index a4118092006e690d551fe94b11daa23ce9b30b33..db3887c3e650e8d1958d461b140d1135425f0420 100644 --- a/src/pages/settings/server/Roles.tsx +++ b/src/pages/settings/server/Roles.tsx @@ -1,101 +1,119 @@ import { Text } from "preact-i18n"; import styles from './Panes.module.scss'; import Button from "../../../components/ui/Button"; +import Overline from "../../../components/ui/Overline"; import { Servers } from "revolt.js/dist/api/objects"; -import InputBox from "../../../components/ui/InputBox"; import Checkbox from "../../../components/ui/Checkbox"; import { useContext, useEffect, useState } from "preact/hooks"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { ChannelPermission, ServerPermission } from "revolt.js/dist/api/permissions"; import Tip from "../../../components/ui/Tip"; +import IconButton from "../../../components/ui/IconButton"; +import ButtonItem from "../../../components/navigation/items/ButtonItem"; +import isEqual from 'lodash.isequal'; +import InputBox from "../../../components/ui/InputBox"; +import { Plus } from "@styled-icons/boxicons-regular"; +import { useIntermediate } from "../../../context/intermediate/Intermediate"; interface Props { server: Servers.Server; } +const I32ToU32 = (arr: number[]) => arr.map(x => x >>> 0); + // ! FIXME: bad code :) export function Roles({ server }: Props) { - const [ selected, setSelected ] = useState('default'); + const [ role, setRole ] = useState('default'); + const { openScreen } = useIntermediate(); const client = useContext(AppContext); - const roles = server.roles ?? {}; - const keys = [ 'default', ...Object.keys(roles) ]; - - const defaultRole = { name: 'Default', permissions: server.default_permissions }; - const selectedRole: Servers.Role = selected === 'default' ? defaultRole : roles[selected]; - if (!selectedRole) { - useEffect(() => setSelected('default'), [ ]); - return null; + if (role !== 'default' && typeof roles[role] === 'undefined') { + useEffect(() => setRole('default')); + return; } - const [ p, setPerm ] = useState([ - selectedRole.permissions[0] >>> 0, - selectedRole.permissions[1] >>> 0, - ]); - - useEffect(() => { - setPerm([ - selectedRole.permissions[0] >>> 0, - selectedRole.permissions[1] >>> 0, - ]); - }, [ selected, selectedRole.permissions ]); + const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions) + const [ perm, setPerm ] = useState(v(role)); + useEffect(() => setPerm(v(role)), [ role, roles[role]?.permissions ]); - const [ name, setName ] = useState(''); + const modified = !isEqual(perm, v(role)); + const save = () => client.servers.setPermissions(server._id, role, { server: perm[0], channel: perm[1] }); + const deleteRole = () => { + setRole('default'); + client.servers.deleteRole(server._id, role); + }; return ( <div className={styles.roles}> - <Tip warning>This section is under construction.</Tip> <div className={styles.list}> - <h1><Text id="app.settings.server_pages.roles.title" /></h1> - { keys + <div className={styles.title}> + <h1><Text id="app.settings.server_pages.roles.title" /></h1> + <Plus size={16} onClick={() => + openScreen({ id: 'special_input', type: 'create_role', server: server._id, callback: id => setRole(id) })} /> + </div> + { [ 'default', ...Object.keys(roles) ] .map(id => { - let role: Servers.Role = id === 'default' ? defaultRole : roles[id]; - - return ( - <Checkbox checked={selected === id} onChange={selected => selected && setSelected(id)}> - { role.name } - </Checkbox> - ) + if (id === 'default') { + return ( + <ButtonItem active={role === 'default'} onClick={() => setRole('default')}> + <Text id="app.settings.permissions.default_role" /> + </ButtonItem> + ) + } else { + return ( + <ButtonItem active={role === id} onClick={() => setRole(id)}> + { roles[id].name } + </ButtonItem> + ) + } }) } - <Button disabled={selected === 'default'} error onClick={() => { - setSelected('default'); - client.servers.deleteRole(server._id, selected); - }}>delete role</Button><br/> - <InputBox placeholder="role name" value={name} onChange={e => setName(e.currentTarget.value)} /> - <Button contrast onClick={() => { - client.servers.createRole(server._id, name); - }}>create</Button> </div> <div className={styles.permissions}> - <h2>{ selectedRole.name }</h2> - { Object.keys(ServerPermission) - .map(perm => { - let value = ServerPermission[perm as keyof typeof ServerPermission]; + <div className={styles.title}> + <h2>{ role === 'default' ? <Text id="app.settings.permissions.default_role" /> : roles[role].name }</h2> + <Button contrast disabled={!modified} onClick={save}>Save</Button> + </div> + <section> + <Overline type="subtle"><Text id="app.settings.permissions.server" /></Overline> + { Object.keys(ServerPermission) + .map(key => { + if (key === 'View') return; + let value = ServerPermission[key as keyof typeof ServerPermission]; - return ( - <Checkbox checked={(p[0] & value) > 0} onChange={c => setPerm([ c ? (p[0] | value) : (p[0] ^ value), p[1] ])}> - { perm } - </Checkbox> - ) - }) - } - <h2>channel permmissions</h2> - { Object.keys(ChannelPermission) - .map(perm => { - let value = ChannelPermission[perm as keyof typeof ChannelPermission]; + return ( + <Checkbox checked={(perm[0] & value) > 0} + onChange={() => setPerm([ perm[0] ^ value, perm[1] ])} + description={<Text id={`permissions.server.${key}.d`} />}> + <Text id={`permissions.server.${key}.t`} /> + </Checkbox> + ) + }) + } + </section> + <section> + <Overline type="subtle"><Text id="app.settings.permissions.channel" /></Overline> + { Object.keys(ChannelPermission) + .map(key => { + if (key === 'ManageChannel') return; + let value = ChannelPermission[key as keyof typeof ChannelPermission]; - return ( - <Checkbox checked={((p[1] >>> 0) & value) > 0} onChange={c => setPerm([ p[0], c ? (p[1] | value) : (p[1] ^ value) ])}> - { perm } - </Checkbox> - ) - }) - } - <Button contrast onClick={() => { - client.servers.setPermissions(server._id, selected, { server: p[0], channel: p[1] }); - }}>click here to save permissions for role</Button> + return ( + <Checkbox checked={((perm[1] >>> 0) & value) > 0} + onChange={() => setPerm([ perm[0], perm[1] ^ value ])} + disabled={key === 'View'} + description={<Text id={`permissions.channel.${key}.d`} />}> + <Text id={`permissions.channel.${key}.t`} /> + </Checkbox> + ) + }) + } + </section> + <div className={styles.actions}> + <Button contrast disabled={!modified} onClick={save}>Save</Button> + { role !== 'default' && <Button contrast error onClick={deleteRole}>Delete</Button> } + </div> </div> </div> ); diff --git a/yarn.lock b/yarn.lock index 1a95434bc21aa725e1bf33fe3b815d17b8b8622e..292ec6d867db8b148850660cb26625e625efe509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3420,10 +3420,10 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -revolt.js@4.3.3-alpha.6: - version "4.3.3-alpha.6" - resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.6.tgz#054e685a5c0dac2c7ae3e2aa454d1965218cb2b0" - integrity sha512-u1/xf+YSQr8DbKsO0raym+F05R75bqYadrPWaIie3m2s2p7ZWeamHlfWIKJlmDO5AL+Lg3xoZWoLwuRHrD1K/Q== +revolt.js@4.3.3-alpha.7: + version "4.3.3-alpha.7" + resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.7.tgz#de6ecef444e8368aac3753761e2e10f516f50712" + integrity sha512-oi76A+EIxrD+tVRTU8s2LISFBpvMf0kpinw5rdukoc1VWpl0bCC6Kko26yC7lhVkWGLTZxHMOKaUkgbOgy0flA== dependencies: "@insertish/mutable" "1.1.0" axios "^0.19.2"