Add permissions / roles settings.

This commit is contained in:
Paul 2021-07-01 17:36:34 +01:00
parent 471b4b0847
commit ff21d4efa8
13 changed files with 239 additions and 40 deletions

2
external/lang vendored

@ -1 +1 @@
Subproject commit 0dc9e46b376621b8af99784a7134fb8648cc3701 Subproject commit 588b882a3acca644c2f65b67cd341c88fd97989d

View file

@ -78,7 +78,7 @@
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"redux": "^4.1.0", "redux": "^4.1.0",
"revolt.js": "4.3.3-alpha.4", "revolt.js": "4.3.3-alpha.6",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0", "shade-blend-color": "^1.0.0",

View file

@ -1,8 +1,13 @@
import styled from "styled-components";
import { InfoCircle } from "@styled-icons/boxicons-regular";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import styled, { css } from "styled-components";
import { InfoCircle } from "@styled-icons/boxicons-regular";
export const TipBase = styled.div` interface Props {
warning?: boolean
error?: boolean
}
export const TipBase = styled.div<Props>`
display: flex; display: flex;
padding: 12px; padding: 12px;
overflow: hidden; overflow: hidden;
@ -24,11 +29,24 @@ export const TipBase = styled.div`
flex-shrink: 0; flex-shrink: 0;
margin-inline-end: 10px; margin-inline-end: 10px;
} }
${ props => props.warning && css`
color: var(--warning);
border: 2px solid var(--warning);
background: var(--secondary-header);
` }
${ props => props.error && css`
color: var(--error);
border: 2px solid var(--error);
background: var(--secondary-header);
` }
`; `;
export default function Tip(props: { children: Children }) { export default function Tip(props: Props & { children: Children }) {
const { children, ...tipProps } = props;
return ( return (
<TipBase> <TipBase {...tipProps}>
<InfoCircle size={20} /> <InfoCircle size={20} />
<span>{props.children}</span> <span>{props.children}</span>
</TipBase> </TipBase>

View file

@ -1,12 +1,13 @@
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { ListUl } from "@styled-icons/boxicons-regular";
import Category from "../../components/ui/Category"; import Category from "../../components/ui/Category";
import { GenericSettings } from "./GenericSettings"; import { GenericSettings } from "./GenericSettings";
import { getChannelName } from "../../context/revoltjs/util"; import { getChannelName } from "../../context/revoltjs/util";
import { Route, useHistory, useParams } from "react-router-dom"; import { Route, useHistory, useParams } from "react-router-dom";
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks"; import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
import { Overview } from "./channel/Overview"; import Overview from "./channel/Overview";
import Permissions from "./channel/Permissions";
export default function ChannelSettings() { export default function ChannelSettings() {
const { channel: cid } = useParams<{ channel: string; }>(); const { channel: cid } = useParams<{ channel: string; }>();
@ -17,10 +18,17 @@ export default function ChannelSettings() {
const history = useHistory(); const history = useHistory();
function switchPage(to?: string) { function switchPage(to?: string) {
let base_url;
switch (channel?.channel_type) {
case 'TextChannel':
case 'VoiceChannel': base_url = `/server/${channel.server}/channel/${cid}/settings`; break;
default: base_url = `/channel/${cid}/settings`;
}
if (to) { if (to) {
history.replace(`/channel/${cid}/settings/${to}`); history.replace(`${base_url}/${to}`);
} else { } else {
history.replace(`/channel/${cid}/settings`); history.replace(base_url);
} }
} }
@ -32,9 +40,17 @@ export default function ChannelSettings() {
id: 'overview', id: 'overview',
icon: <ListUl size={20} />, icon: <ListUl size={20} />,
title: <Text id="app.settings.channel_pages.overview.title" /> title: <Text id="app.settings.channel_pages.overview.title" />
},
{
id: 'permissions',
icon: <ListCheck size={20} />,
title: <Text id="app.settings.channel_pages.permissions.title" />
} }
]} ]}
children={[ children={[
<Route path="/server/:server/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
<Route path="/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
<Route path="/"><Overview channel={channel} /></Route> <Route path="/"><Overview channel={channel} /></Route>
]} ]}
category="channel_pages" category="channel_pages"

View file

@ -12,7 +12,7 @@ interface Props {
channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel; channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
} }
export function Overview({ channel }: Props) { export default function Overview({ channel }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const [name, setName] = useState(channel.name); const [name, setName] = useState(channel.name);
@ -81,9 +81,11 @@ export function Overview({ channel }: Props) {
if (!changed) setChanged(true) if (!changed) setChanged(true)
}} }}
/> />
<Button onClick={save} contrast disabled={!changed}> <p>
<Text id="app.special.modals.actions.save" /> <Button onClick={save} contrast disabled={!changed}>
</Button> <Text id="app.special.modals.actions.save" />
</Button>
</p>
</div> </div>
); );
} }

View file

@ -0,0 +1,91 @@
import Tip from "../../../components/ui/Tip";
import Button from "../../../components/ui/Button";
import { Channels } from "revolt.js/dist/api/objects";
import Checkbox from "../../../components/ui/Checkbox";
import { useServer } from "../../../context/revoltjs/hooks";
import { useContext, useEffect, useState } from "preact/hooks";
import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
// ! FIXME: export from revolt.js
const DEFAULT_PERMISSION_DM = ChannelPermission.View
+ ChannelPermission.SendMessage
+ ChannelPermission.ManageChannel
+ ChannelPermission.VoiceCall
+ ChannelPermission.InviteOthers
+ ChannelPermission.EmbedLinks
+ ChannelPermission.UploadFiles;
interface Props {
channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
}
// ! FIXME: bad code :)
export default function Permissions({ channel }: Props) {
const [ selected, setSelected ] = useState('default');
const client = useContext(AppContext);
type R = { name: string, permissions: number };
let roles: { [key: string]: R } = {};
if (channel.channel_type !== 'Group') {
const server = useServer(channel.server);
const a = server?.roles ?? {};
for (let b of Object.keys(a)) {
roles[b] = {
name: a[b].name,
permissions: a[b].permissions[1]
};
}
}
const keys = [ 'default', ...Object.keys(roles) ];
const defaultRole = { name: 'Default', permissions: (channel.channel_type === 'Group' ? channel.permissions : channel.default_permissions) ?? DEFAULT_PERMISSION_DM };
const selectedRole = selected === 'default' ? defaultRole : roles[selected];
if (!selectedRole) {
useEffect(() => setSelected('default'), [ ]);
return null;
}
const [ p, setPerm ] = useState(selectedRole.permissions >>> 0);
useEffect(() => {
setPerm(selectedRole.permissions >>> 0);
}, [ selected, selectedRole.permissions ]);
return (
<div>
<Tip warning>This section is under construction.</Tip>
<h2>select role</h2>
{ selected }
{ keys
.map(id => {
let role: R = id === 'default' ? defaultRole : roles[id];
return (
<Checkbox checked={selected === id} onChange={selected => selected && setSelected(id)}>
{ role.name }
</Checkbox>
)
})
}
<h2>channel per??issions</h2>
{ Object.keys(ChannelPermission)
.map(perm => {
let value = ChannelPermission[perm as keyof typeof ChannelPermission];
if (value & DEFAULT_PERMISSION_DM) {
return (
<Checkbox checked={(p & value) > 0} onChange={c => setPerm(c ? (p | value) : (p ^ value))}>
{ perm }
</Checkbox>
)
}
})
}
<Button contrast onClick={() => {
client.channels.setPermissions(channel._id, selected, p);
}}>click here to save permissions for role</Button>
</div>
);
}

View file

@ -69,7 +69,7 @@ export function Feedback() {
</Radio> } </Radio> }
<Radio <Radio
disabled={state === "sending"} disabled={state === "sending"}
checked={checked === "__other_option__"} checked={checked === "__other_option__" && other !== "Revite"}
onSelect={() => setChecked("__other_option__")}> onSelect={() => setChecked("__other_option__")}>
<Localizer> <Localizer>
<InputBox <InputBox
@ -96,9 +96,11 @@ export function Feedback() {
disabled={state === "sending"} disabled={state === "sending"}
onChange={ev => setDescription(ev.currentTarget.value)} onChange={ev => setDescription(ev.currentTarget.value)}
/> />
<Button type="submit" contrast> <p>
<Text id="app.settings.pages.feedback.send" /> <Button type="submit" contrast>
</Button> <Text id="app.settings.pages.feedback.send" />
</Button>
</p>
</form> </form>
); );
} }

View file

@ -113,14 +113,16 @@ export function Profile() {
intl.dictionary intl.dictionary
)} )}
/> />
<Button contrast <p>
onClick={() => { <Button contrast
setChanged(false); onClick={() => {
ctx.client.users.editUser({ profile: { content: profile?.content } }) setChanged(false);
}} ctx.client.users.editUser({ profile: { content: profile?.content } })
disabled={!changed}> }}
<Text id="app.special.modals.actions.save" /> disabled={!changed}>
</Button> <Text id="app.special.modals.actions.save" />
</Button>
</p>
</div> </div>
); );
} }

View file

@ -1,3 +1,4 @@
import Tip from "../../../components/ui/Tip";
import { Servers } from "revolt.js/dist/api/objects"; import { Servers } from "revolt.js/dist/api/objects";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
@ -17,6 +18,7 @@ export function Bans({ server }: Props) {
return ( return (
<div> <div>
<Tip warning>This section is under construction.</Tip>
{ bans?.map(x => <div>{x._id.user}: {x.reason ?? 'no reason'} <button onClick={() => client.servers.unbanUser(server._id, x._id.user)}>unban</button></div>) } { bans?.map(x => <div>{x._id.user}: {x.reason ?? 'no reason'} <button onClick={() => client.servers.unbanUser(server._id, x._id.user)}>unban</button></div>) }
</div> </div>
); );

View file

@ -1,13 +1,17 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Servers } from "revolt.js/dist/api/objects"; import { Servers } from "revolt.js/dist/api/objects";
import Checkbox from "../../../components/ui/Checkbox";
import Tip from "../../../components/ui/Tip";
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks"; import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
interface Props { interface Props {
server: Servers.Server; server: Servers.Server;
} }
// ! FIXME: bad code :)
export function Members({ server }: Props) { export function Members({ server }: Props) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined); const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
const ctx = useForceUpdate(); const ctx = useForceUpdate();
const users = useUsers(members?.map(x => x._id.user) ?? [], ctx); const users = useUsers(members?.map(x => x._id.user) ?? [], ctx);
@ -18,7 +22,36 @@ export function Members({ server }: Props) {
return ( return (
<div> <div>
{ members && members.length > 0 && users?.map(x => x && <div>@{x.username}</div>) } <Tip warning>This section is under construction.</Tip>
{ members && members.length > 0 && users?.map(x => x && <div>
<br/>
<br/>
<br/>
<span>@{x.username}</span>
{ server.roles && Object.keys(server.roles).map(id => {
let role = server.roles?.[id]!;
let member = members.find(y => x._id === y._id.user)!;
return (
<Checkbox checked={member.roles?.includes(id) ?? false} onChange={selected => {
let roles = (member.roles ?? []).filter(z => z !== id);
if (selected) roles.push(id);
ctx.client.servers.members.editMember(server._id, x._id, { roles });
setMembers(
[
...members.filter(e => e._id.user !== x._id),
{
...member,
roles
}
]
);
}}>{ role.name }</Checkbox>
)
}) }
</div>) }
</div> </div>
); );
} }

View file

@ -76,9 +76,11 @@ export function Overview({ server }: Props) {
if (!changed) setChanged(true) if (!changed) setChanged(true)
}} }}
/> />
<Button onClick={save} contrast disabled={!changed}> <p>
<Text id="app.special.modals.actions.save" /> <Button onClick={save} contrast disabled={!changed}>
</Button> <Text id="app.special.modals.actions.save" />
</Button>
</p>
<h3> <h3>
<Text id="app.main.servers.custom_banner" /> <Text id="app.main.servers.custom_banner" />

View file

@ -1,15 +1,20 @@
import { useEffect, useState } from "preact/hooks"; import Tip from "../../../components/ui/Tip";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import { Servers } from "revolt.js/dist/api/objects"; import { Servers } from "revolt.js/dist/api/objects";
import InputBox from "../../../components/ui/InputBox";
import Checkbox from "../../../components/ui/Checkbox"; 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 { ChannelPermission, ServerPermission } from "revolt.js/dist/api/permissions";
interface Props { interface Props {
server: Servers.Server; server: Servers.Server;
} }
// ! FIXME: bad code :)
export function Roles({ server }: Props) { export function Roles({ server }: Props) {
const [ selected, setSelected ] = useState('default'); const [ selected, setSelected ] = useState('default');
const client = useContext(AppContext);
const roles = server.roles ?? {}; const roles = server.roles ?? {};
const keys = [ 'default', ...Object.keys(roles) ]; const keys = [ 'default', ...Object.keys(roles) ];
@ -21,10 +26,26 @@ export function Roles({ server }: Props) {
useEffect(() => setSelected('default'), [ ]); useEffect(() => setSelected('default'), [ ]);
return null; return null;
} }
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 [ name, setName ] = useState('');
return ( return (
<div> <div>
<Tip warning>This section is under construction.</Tip>
<h2>select role</h2> <h2>select role</h2>
{ selected }
{ keys { keys
.map(id => { .map(id => {
let role: Servers.Role = id === 'default' ? defaultRole : roles[id]; let role: Servers.Role = id === 'default' ? defaultRole : roles[id];
@ -36,14 +57,21 @@ export function Roles({ server }: Props) {
) )
}) })
} }
<Button disabled={selected === 'default'} error onClick={() => {}}>delete role</Button> <Button disabled={selected === 'default'} error onClick={() => {
<h2>server permmissions</h2> 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>
<h2>serverm permmissions</h2>
{ Object.keys(ServerPermission) { Object.keys(ServerPermission)
.map(perm => { .map(perm => {
let value = ServerPermission[perm as keyof typeof ServerPermission]; let value = ServerPermission[perm as keyof typeof ServerPermission];
return ( return (
<Checkbox checked={((selectedRole.permissions[0] >>> 0) & value) > 0} onChange={() => {}}> <Checkbox checked={(p[0] & value) > 0} onChange={c => setPerm([ c ? (p[0] | value) : (p[0] ^ value), p[1] ])}>
{ perm } { perm }
</Checkbox> </Checkbox>
) )
@ -55,12 +83,15 @@ export function Roles({ server }: Props) {
let value = ChannelPermission[perm as keyof typeof ChannelPermission]; let value = ChannelPermission[perm as keyof typeof ChannelPermission];
return ( return (
<Checkbox checked={((selectedRole.permissions[1] >>> 0) & value) > 0} onChange={() => {}}> <Checkbox checked={((p[1] >>> 0) & value) > 0} onChange={c => setPerm([ p[0], c ? (p[1] | value) : (p[1] ^ value) ])}>
{ perm } { perm }
</Checkbox> </Checkbox>
) )
}) })
} }
<Button contrast onClick={() => {
client.servers.setPermissions(server._id, selected, { server: p[0], channel: p[1] });
}}>click here to save permissions for role</Button>
</div> </div>
); );
} }

View file

@ -3420,10 +3420,10 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
revolt.js@4.3.3-alpha.4: revolt.js@4.3.3-alpha.6:
version "4.3.3-alpha.4" version "4.3.3-alpha.6"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.4.tgz#7d393c9016a9d89151b1c091f6b02bc193b3a553" resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.6.tgz#054e685a5c0dac2c7ae3e2aa454d1965218cb2b0"
integrity sha512-d6SGjRKFDlWG5fEveaVf0DRGb8d0RW8iv1E0kEG0W3R138KdeWCK8zUU0H+ykUdd5OjS7ESBKaEcwSP2BXMRSA== integrity sha512-u1/xf+YSQr8DbKsO0raym+F05R75bqYadrPWaIie3m2s2p7ZWeamHlfWIKJlmDO5AL+Lg3xoZWoLwuRHrD1K/Q==
dependencies: dependencies:
"@insertish/mutable" "1.1.0" "@insertish/mutable" "1.1.0"
axios "^0.19.2" axios "^0.19.2"