mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
Use tabWidth 4 without actual tabs.
This commit is contained in:
@@ -5,79 +5,79 @@ import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { useIntermediate } from "../context/intermediate/Intermediate";
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../context/revoltjs/RevoltClient";
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUser,
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUser,
|
||||
} from "../context/revoltjs/hooks";
|
||||
|
||||
import Header from "../components/ui/Header";
|
||||
|
||||
export default function Open() {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
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>
|
||||
);
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 }));
|
||||
client.users
|
||||
.openDM(client.user?._id as string)
|
||||
.then((channel) => history.push(`/channel/${channel?._id}`))
|
||||
.catch((error) => openScreen({ id: "error", error }));
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const channel: string | undefined = channels.find(
|
||||
(channel) =>
|
||||
channel?.channel_type === "DirectMessage" &&
|
||||
channel.recipients.includes(id),
|
||||
)?._id;
|
||||
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 }));
|
||||
}
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
history.push("/");
|
||||
}, []);
|
||||
history.push("/");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Header placement="primary">
|
||||
<Text id="general.loading" />
|
||||
</Header>
|
||||
);
|
||||
return (
|
||||
<Header placement="primary">
|
||||
<Text id="general.loading" />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,97 +24,97 @@ import ServerSettings from "./settings/ServerSettings";
|
||||
import Settings from "./settings/Settings";
|
||||
|
||||
const Routes = styled.div`
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: var(--primary-background);
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
background: var(--primary-background);
|
||||
`;
|
||||
|
||||
export default function App() {
|
||||
const path = useLocation().pathname;
|
||||
const fixedBottomNav =
|
||||
path === "/" || path === "/settings" || path.startsWith("/friends");
|
||||
const inSettings = path.includes("/settings");
|
||||
const inChannel = path.includes("/channel");
|
||||
const inSpecial =
|
||||
(path.startsWith("/friends") && isTouchscreenDevice) ||
|
||||
path.startsWith("/invite") ||
|
||||
path.startsWith("/settings");
|
||||
const path = useLocation().pathname;
|
||||
const fixedBottomNav =
|
||||
path === "/" || path === "/settings" || path.startsWith("/friends");
|
||||
const inSettings = path.includes("/settings");
|
||||
const inChannel = path.includes("/channel");
|
||||
const inSpecial =
|
||||
(path.startsWith("/friends") && isTouchscreenDevice) ||
|
||||
path.startsWith("/invite") ||
|
||||
path.startsWith("/settings");
|
||||
|
||||
return (
|
||||
<OverlappingPanels
|
||||
width="100vw"
|
||||
height="var(--app-height)"
|
||||
leftPanel={
|
||||
inSpecial
|
||||
? undefined
|
||||
: { width: 292, component: <LeftSidebar /> }
|
||||
}
|
||||
rightPanel={
|
||||
!inSettings && inChannel
|
||||
? { width: 240, component: <RightSidebar /> }
|
||||
: undefined
|
||||
}
|
||||
bottomNav={{
|
||||
component: <BottomNavigation />,
|
||||
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
|
||||
height: 50,
|
||||
}}
|
||||
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
||||
<Routes>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel/settings/:page"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel/settings"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/settings/:page"
|
||||
component={ServerSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/settings"
|
||||
component={ServerSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/channel/:channel/settings/:page"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/channel/:channel/settings"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
return (
|
||||
<OverlappingPanels
|
||||
width="100vw"
|
||||
height="var(--app-height)"
|
||||
leftPanel={
|
||||
inSpecial
|
||||
? undefined
|
||||
: { width: 292, component: <LeftSidebar /> }
|
||||
}
|
||||
rightPanel={
|
||||
!inSettings && inChannel
|
||||
? { width: 240, component: <RightSidebar /> }
|
||||
: undefined
|
||||
}
|
||||
bottomNav={{
|
||||
component: <BottomNavigation />,
|
||||
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
|
||||
height: 50,
|
||||
}}
|
||||
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
||||
<Routes>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel/settings/:page"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel/settings"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/settings/:page"
|
||||
component={ServerSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/settings"
|
||||
component={ServerSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/channel/:channel/settings/:page"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
<Route
|
||||
path="/channel/:channel/settings"
|
||||
component={ChannelSettings}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/channel/:channel/message/:message"
|
||||
component={Channel}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel"
|
||||
component={Channel}
|
||||
/>
|
||||
<Route path="/server/:server" />
|
||||
<Route path="/channel/:channel" component={Channel} />
|
||||
<Route
|
||||
path="/channel/:channel/message/:message"
|
||||
component={Channel}
|
||||
/>
|
||||
<Route
|
||||
path="/server/:server/channel/:channel"
|
||||
component={Channel}
|
||||
/>
|
||||
<Route path="/server/:server" />
|
||||
<Route path="/channel/:channel" component={Channel} />
|
||||
|
||||
<Route path="/settings/:page" component={Settings} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/settings/:page" component={Settings} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
|
||||
<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>
|
||||
<ContextMenus />
|
||||
<Popovers />
|
||||
<Notifications />
|
||||
<StateMonitor />
|
||||
<SyncManager />
|
||||
</OverlappingPanels>
|
||||
);
|
||||
<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>
|
||||
<ContextMenus />
|
||||
<Popovers />
|
||||
<Notifications />
|
||||
<StateMonitor />
|
||||
<SyncManager />
|
||||
</OverlappingPanels>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,25 +12,25 @@ const Login = lazy(() => import("./login/Login"));
|
||||
const RevoltApp = lazy(() => import("./RevoltApp"));
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Context>
|
||||
<Masks />
|
||||
{/*
|
||||
return (
|
||||
<Context>
|
||||
<Masks />
|
||||
{/*
|
||||
// @ts-expect-error */}
|
||||
<Suspense fallback={<Preloader type="spinner" />}>
|
||||
<Switch>
|
||||
<Route path="/login">
|
||||
<CheckAuth>
|
||||
<Login />
|
||||
</CheckAuth>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<CheckAuth auth>
|
||||
<RevoltApp />
|
||||
</CheckAuth>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Context>
|
||||
);
|
||||
<Suspense fallback={<Preloader type="spinner" />}>
|
||||
<Switch>
|
||||
<Route path="/login">
|
||||
<CheckAuth>
|
||||
<Login />
|
||||
</CheckAuth>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<CheckAuth auth>
|
||||
<RevoltApp />
|
||||
</CheckAuth>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Context>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,134 +20,134 @@ import { MessageArea } from "./messaging/MessageArea";
|
||||
import VoiceHeader from "./voice/VoiceHeader";
|
||||
|
||||
const ChannelMain = styled.div`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const ChannelContent = styled.div`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const AgeGate = styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
padding: 12px;
|
||||
|
||||
img {
|
||||
height: 150px;
|
||||
}
|
||||
img {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
color: var(--secondary-foreground);
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.subtext {
|
||||
color: var(--secondary-foreground);
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Channel({ id }: { id: string }) {
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(id, ctx);
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(id, ctx);
|
||||
|
||||
if (!channel) return null;
|
||||
if (!channel) return null;
|
||||
|
||||
if (channel.channel_type === "VoiceChannel") {
|
||||
return <VoiceChannel channel={channel} />;
|
||||
} else {
|
||||
return <TextChannel channel={channel} />;
|
||||
}
|
||||
if (channel.channel_type === "VoiceChannel") {
|
||||
return <VoiceChannel channel={channel} />;
|
||||
} else {
|
||||
return <TextChannel channel={channel} />;
|
||||
}
|
||||
}
|
||||
|
||||
function TextChannel({ channel }: { channel: Channels.Channel }) {
|
||||
const [showMembers, setMembers] = useState(true);
|
||||
const [showMembers, setMembers] = useState(true);
|
||||
|
||||
if (
|
||||
(channel.channel_type === "TextChannel" ||
|
||||
channel.channel_type === "Group") &&
|
||||
channel.name.includes("nsfw")
|
||||
) {
|
||||
const goBack = useHistory();
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [ageGate, setAgeGate] = useState(false);
|
||||
if (!ageGate) {
|
||||
return (
|
||||
<AgeGate>
|
||||
<img
|
||||
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
|
||||
draggable={false}
|
||||
/>
|
||||
<h2>{channel.name}</h2>
|
||||
<span className="subtext">
|
||||
This channel is marked as NSFW.{" "}
|
||||
<a href="#">Learn more</a>
|
||||
</span>
|
||||
if (
|
||||
(channel.channel_type === "TextChannel" ||
|
||||
channel.channel_type === "Group") &&
|
||||
channel.name.includes("nsfw")
|
||||
) {
|
||||
const goBack = useHistory();
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [ageGate, setAgeGate] = useState(false);
|
||||
if (!ageGate) {
|
||||
return (
|
||||
<AgeGate>
|
||||
<img
|
||||
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
|
||||
draggable={false}
|
||||
/>
|
||||
<h2>{channel.name}</h2>
|
||||
<span className="subtext">
|
||||
This channel is marked as NSFW.{" "}
|
||||
<a href="#">Learn more</a>
|
||||
</span>
|
||||
|
||||
<Checkbox checked={consent} onChange={(v) => setConsent(v)}>
|
||||
I confirm that I am at least 18 years old.
|
||||
</Checkbox>
|
||||
<div className="actions">
|
||||
<Button contrast onClick={() => goBack}>
|
||||
Go back
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => consent && setAgeGate(true)}>
|
||||
Enter Channel
|
||||
</Button>
|
||||
</div>
|
||||
</AgeGate>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Checkbox checked={consent} onChange={(v) => setConsent(v)}>
|
||||
I confirm that I am at least 18 years old.
|
||||
</Checkbox>
|
||||
<div className="actions">
|
||||
<Button contrast onClick={() => goBack}>
|
||||
Go back
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => consent && setAgeGate(true)}>
|
||||
Enter Channel
|
||||
</Button>
|
||||
</div>
|
||||
</AgeGate>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let id = channel._id;
|
||||
return (
|
||||
<>
|
||||
<ChannelHeader
|
||||
channel={channel}
|
||||
toggleSidebar={() => setMembers(!showMembers)}
|
||||
/>
|
||||
<ChannelMain>
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={id} />
|
||||
<MessageArea id={id} />
|
||||
<TypingIndicator id={id} />
|
||||
<JumpToBottom id={id} />
|
||||
<MessageBox channel={channel} />
|
||||
</ChannelContent>
|
||||
{!isTouchscreenDevice && showMembers && (
|
||||
<MemberSidebar channel={channel} />
|
||||
)}
|
||||
</ChannelMain>
|
||||
</>
|
||||
);
|
||||
let id = channel._id;
|
||||
return (
|
||||
<>
|
||||
<ChannelHeader
|
||||
channel={channel}
|
||||
toggleSidebar={() => setMembers(!showMembers)}
|
||||
/>
|
||||
<ChannelMain>
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={id} />
|
||||
<MessageArea id={id} />
|
||||
<TypingIndicator id={id} />
|
||||
<JumpToBottom id={id} />
|
||||
<MessageBox channel={channel} />
|
||||
</ChannelContent>
|
||||
{!isTouchscreenDevice && showMembers && (
|
||||
<MemberSidebar channel={channel} />
|
||||
)}
|
||||
</ChannelMain>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceChannel({ channel }: { channel: Channels.Channel }) {
|
||||
return (
|
||||
<>
|
||||
<ChannelHeader channel={channel} />
|
||||
<VoiceHeader id={channel._id} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ChannelHeader channel={channel} />
|
||||
<VoiceHeader id={channel._id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
return <Channel id={channel} key={channel} />;
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
return <Channel id={channel} key={channel} />;
|
||||
}
|
||||
|
||||
@@ -19,121 +19,121 @@ import Markdown from "../../components/markdown/Markdown";
|
||||
import HeaderActions from "./actions/HeaderActions";
|
||||
|
||||
export interface ChannelHeaderProps {
|
||||
channel: Channel;
|
||||
toggleSidebar?: () => void;
|
||||
channel: Channel;
|
||||
toggleSidebar?: () => void;
|
||||
}
|
||||
|
||||
const Info = styled.div`
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
}
|
||||
* {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 20px;
|
||||
margin: 0 5px;
|
||||
padding-left: 1px;
|
||||
background-color: var(--tertiary-background);
|
||||
}
|
||||
.divider {
|
||||
height: 20px;
|
||||
margin: 0 5px;
|
||||
padding-left: 1px;
|
||||
background-color: var(--tertiary-background);
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
.status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
cursor: pointer;
|
||||
margin-top: 2px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
color: var(--secondary-foreground);
|
||||
}
|
||||
.desc {
|
||||
cursor: pointer;
|
||||
margin-top: 2px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
color: var(--secondary-foreground);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ChannelHeader({
|
||||
channel,
|
||||
toggleSidebar,
|
||||
channel,
|
||||
toggleSidebar,
|
||||
}: ChannelHeaderProps) {
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const name = getChannelName(client, channel);
|
||||
let icon, recipient;
|
||||
switch (channel.channel_type) {
|
||||
case "SavedMessages":
|
||||
icon = <Notepad size={24} />;
|
||||
break;
|
||||
case "DirectMessage":
|
||||
icon = <At size={24} />;
|
||||
const uid = client.channels.getRecipient(channel._id);
|
||||
recipient = client.users.get(uid);
|
||||
break;
|
||||
case "Group":
|
||||
icon = <Group size={24} />;
|
||||
break;
|
||||
case "TextChannel":
|
||||
icon = <Hash size={24} />;
|
||||
break;
|
||||
}
|
||||
const name = getChannelName(client, channel);
|
||||
let icon, recipient;
|
||||
switch (channel.channel_type) {
|
||||
case "SavedMessages":
|
||||
icon = <Notepad size={24} />;
|
||||
break;
|
||||
case "DirectMessage":
|
||||
icon = <At size={24} />;
|
||||
const uid = client.channels.getRecipient(channel._id);
|
||||
recipient = client.users.get(uid);
|
||||
break;
|
||||
case "Group":
|
||||
icon = <Group size={24} />;
|
||||
break;
|
||||
case "TextChannel":
|
||||
icon = <Hash size={24} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Header placement="primary">
|
||||
{icon}
|
||||
<Info>
|
||||
<span className="name">{name}</span>
|
||||
{isTouchscreenDevice &&
|
||||
channel.channel_type === "DirectMessage" && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<span className="desc">
|
||||
<div
|
||||
className="status"
|
||||
style={{
|
||||
backgroundColor: useStatusColour(
|
||||
recipient as User,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<UserStatus user={recipient as User} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!isTouchscreenDevice &&
|
||||
(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "TextChannel") &&
|
||||
channel.description && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<span
|
||||
className="desc"
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "channel_info",
|
||||
channel_id: channel._id,
|
||||
})
|
||||
}>
|
||||
<Markdown
|
||||
content={
|
||||
channel.description.split("\n")[0] ?? ""
|
||||
}
|
||||
disallowBigEmoji
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Info>
|
||||
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
|
||||
</Header>
|
||||
);
|
||||
return (
|
||||
<Header placement="primary">
|
||||
{icon}
|
||||
<Info>
|
||||
<span className="name">{name}</span>
|
||||
{isTouchscreenDevice &&
|
||||
channel.channel_type === "DirectMessage" && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<span className="desc">
|
||||
<div
|
||||
className="status"
|
||||
style={{
|
||||
backgroundColor: useStatusColour(
|
||||
recipient as User,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<UserStatus user={recipient as User} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!isTouchscreenDevice &&
|
||||
(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "TextChannel") &&
|
||||
channel.description && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<span
|
||||
className="desc"
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "channel_info",
|
||||
channel_id: channel._id,
|
||||
})
|
||||
}>
|
||||
<Markdown
|
||||
content={
|
||||
channel.description.split("\n")[0] ?? ""
|
||||
}
|
||||
disallowBigEmoji
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Info>
|
||||
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Sidebar as SidebarIcon } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
UserPlus,
|
||||
Cog,
|
||||
PhoneCall,
|
||||
PhoneOutgoing,
|
||||
UserPlus,
|
||||
Cog,
|
||||
PhoneCall,
|
||||
PhoneOutgoing,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
@@ -12,9 +12,9 @@ import { useContext } from "preact/hooks";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import {
|
||||
VoiceContext,
|
||||
VoiceOperationsContext,
|
||||
VoiceStatus,
|
||||
VoiceContext,
|
||||
VoiceOperationsContext,
|
||||
VoiceStatus,
|
||||
} from "../../../context/Voice";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
@@ -25,88 +25,88 @@ import IconButton from "../../../components/ui/IconButton";
|
||||
import { ChannelHeaderProps } from "../ChannelHeader";
|
||||
|
||||
export default function HeaderActions({
|
||||
channel,
|
||||
toggleSidebar,
|
||||
channel,
|
||||
toggleSidebar,
|
||||
}: ChannelHeaderProps) {
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateIndicator />
|
||||
{channel.channel_type === "Group" && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "user_picker",
|
||||
omit: channel.recipients,
|
||||
callback: async (users) => {
|
||||
for (const user of users) {
|
||||
await client.channels.addMember(
|
||||
channel._id,
|
||||
user,
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
}>
|
||||
<UserPlus size={27} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
history.push(`/channel/${channel._id}/settings`)
|
||||
}>
|
||||
<Cog size={24} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<VoiceActions channel={channel} />
|
||||
{(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "TextChannel") &&
|
||||
!isTouchscreenDevice && (
|
||||
<IconButton onClick={toggleSidebar}>
|
||||
<SidebarIcon size={22} />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<UpdateIndicator />
|
||||
{channel.channel_type === "Group" && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "user_picker",
|
||||
omit: channel.recipients,
|
||||
callback: async (users) => {
|
||||
for (const user of users) {
|
||||
await client.channels.addMember(
|
||||
channel._id,
|
||||
user,
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
}>
|
||||
<UserPlus size={27} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
history.push(`/channel/${channel._id}/settings`)
|
||||
}>
|
||||
<Cog size={24} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<VoiceActions channel={channel} />
|
||||
{(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "TextChannel") &&
|
||||
!isTouchscreenDevice && (
|
||||
<IconButton onClick={toggleSidebar}>
|
||||
<SidebarIcon size={22} />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "TextChannel"
|
||||
)
|
||||
return null;
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "TextChannel"
|
||||
)
|
||||
return null;
|
||||
|
||||
const voice = useContext(VoiceContext);
|
||||
const { connect, disconnect } = useContext(VoiceOperationsContext);
|
||||
const voice = useContext(VoiceContext);
|
||||
const { connect, disconnect } = useContext(VoiceOperationsContext);
|
||||
|
||||
if (voice.status >= VoiceStatus.READY) {
|
||||
if (voice.roomId === channel._id) {
|
||||
return (
|
||||
<IconButton onClick={disconnect}>
|
||||
<PhoneOutgoing size={22} />
|
||||
</IconButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
disconnect();
|
||||
connect(channel._id);
|
||||
}}>
|
||||
<PhoneCall size={24} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<IconButton>
|
||||
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
if (voice.status >= VoiceStatus.READY) {
|
||||
if (voice.roomId === channel._id) {
|
||||
return (
|
||||
<IconButton onClick={disconnect}>
|
||||
<PhoneOutgoing size={22} />
|
||||
</IconButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
disconnect();
|
||||
connect(channel._id);
|
||||
}}>
|
||||
<PhoneCall size={24} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<IconButton>
|
||||
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,35 +6,35 @@ import { useChannel, useForceUpdate } from "../../../context/revoltjs/hooks";
|
||||
import { getChannelName } from "../../../context/revoltjs/util";
|
||||
|
||||
const StartBase = styled.div`
|
||||
margin: 18px 16px 10px 16px;
|
||||
margin: 18px 16px 10px 16px;
|
||||
|
||||
h1 {
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function ConversationStart({ id }: Props) {
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(id, ctx);
|
||||
if (!channel) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(id, ctx);
|
||||
if (!channel) return null;
|
||||
|
||||
return (
|
||||
<StartBase>
|
||||
<h1>{getChannelName(ctx.client, channel, true)}</h1>
|
||||
<h4>
|
||||
<Text id="app.main.channel.start.group" />
|
||||
</h4>
|
||||
</StartBase>
|
||||
);
|
||||
return (
|
||||
<StartBase>
|
||||
<h1>{getChannelName(ctx.client, channel, true)}</h1>
|
||||
<h4>
|
||||
<Text id="app.main.channel.start.group" />
|
||||
</h4>
|
||||
</StartBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import useResizeObserver from "use-resize-observer";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "preact/hooks";
|
||||
|
||||
import { defer } from "../../../lib/defer";
|
||||
@@ -19,8 +19,8 @@ import { RenderState, ScrollState } from "../../../lib/renderer/types";
|
||||
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
@@ -29,231 +29,231 @@ import ConversationStart from "./ConversationStart";
|
||||
import MessageRenderer from "./MessageRenderer";
|
||||
|
||||
const Area = styled.div`
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-break: break-word;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
word-break: break-word;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
padding-bottom: 20px;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
> div {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
padding-bottom: 20px;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const MessageAreaWidthContext = createContext(0);
|
||||
export const MESSAGE_AREA_PADDING = 82;
|
||||
|
||||
export function MessageArea({ id }: Props) {
|
||||
const status = useContext(StatusContext);
|
||||
const { focusTaken } = useContext(IntermediateContext);
|
||||
const status = useContext(StatusContext);
|
||||
const { focusTaken } = useContext(IntermediateContext);
|
||||
|
||||
// ? This is the scroll container.
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
|
||||
// ? This is the scroll container.
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
|
||||
|
||||
// ? Current channel state.
|
||||
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
||||
// ? Current channel state.
|
||||
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
||||
|
||||
// ? useRef to avoid re-renders
|
||||
const scrollState = useRef<ScrollState>({ type: "Free" });
|
||||
// ? useRef to avoid re-renders
|
||||
const scrollState = useRef<ScrollState>({ type: "Free" });
|
||||
|
||||
const setScrollState = (v: ScrollState) => {
|
||||
if (v.type === "StayAtBottom") {
|
||||
if (scrollState.current.type === "Bottom" || atBottom()) {
|
||||
scrollState.current = {
|
||||
type: "ScrollToBottom",
|
||||
smooth: v.smooth,
|
||||
};
|
||||
} else {
|
||||
scrollState.current = { type: "Free" };
|
||||
}
|
||||
} else {
|
||||
scrollState.current = v;
|
||||
}
|
||||
const setScrollState = (v: ScrollState) => {
|
||||
if (v.type === "StayAtBottom") {
|
||||
if (scrollState.current.type === "Bottom" || atBottom()) {
|
||||
scrollState.current = {
|
||||
type: "ScrollToBottom",
|
||||
smooth: v.smooth,
|
||||
};
|
||||
} else {
|
||||
scrollState.current = { type: "Free" };
|
||||
}
|
||||
} else {
|
||||
scrollState.current = v;
|
||||
}
|
||||
|
||||
defer(() => {
|
||||
if (scrollState.current.type === "ScrollToBottom") {
|
||||
setScrollState({
|
||||
type: "Bottom",
|
||||
scrollingUntil: +new Date() + 150,
|
||||
});
|
||||
defer(() => {
|
||||
if (scrollState.current.type === "ScrollToBottom") {
|
||||
setScrollState({
|
||||
type: "Bottom",
|
||||
scrollingUntil: +new Date() + 150,
|
||||
});
|
||||
|
||||
animateScroll.scrollToBottom({
|
||||
container: ref.current,
|
||||
duration: scrollState.current.smooth ? 150 : 0,
|
||||
});
|
||||
} else if (scrollState.current.type === "OffsetTop") {
|
||||
animateScroll.scrollTo(
|
||||
Math.max(
|
||||
101,
|
||||
ref.current.scrollTop +
|
||||
(ref.current.scrollHeight -
|
||||
scrollState.current.previousHeight),
|
||||
),
|
||||
{
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
animateScroll.scrollToBottom({
|
||||
container: ref.current,
|
||||
duration: scrollState.current.smooth ? 150 : 0,
|
||||
});
|
||||
} else if (scrollState.current.type === "OffsetTop") {
|
||||
animateScroll.scrollTo(
|
||||
Math.max(
|
||||
101,
|
||||
ref.current.scrollTop +
|
||||
(ref.current.scrollHeight -
|
||||
scrollState.current.previousHeight),
|
||||
),
|
||||
{
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
|
||||
setScrollState({ type: "Free" });
|
||||
} else if (scrollState.current.type === "ScrollTop") {
|
||||
animateScroll.scrollTo(scrollState.current.y, {
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
});
|
||||
setScrollState({ type: "Free" });
|
||||
} else if (scrollState.current.type === "ScrollTop") {
|
||||
animateScroll.scrollTo(scrollState.current.y, {
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
setScrollState({ type: "Free" });
|
||||
}
|
||||
});
|
||||
};
|
||||
setScrollState({ type: "Free" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ? Determine if we are at the bottom of the scroll container.
|
||||
// -> https://stackoverflow.com/a/44893438
|
||||
// By default, we assume we are at the bottom, i.e. when we first load.
|
||||
const atBottom = (offset = 0) =>
|
||||
ref.current
|
||||
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
|
||||
offset <=
|
||||
ref.current.clientHeight
|
||||
: true;
|
||||
// ? Determine if we are at the bottom of the scroll container.
|
||||
// -> https://stackoverflow.com/a/44893438
|
||||
// By default, we assume we are at the bottom, i.e. when we first load.
|
||||
const atBottom = (offset = 0) =>
|
||||
ref.current
|
||||
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
|
||||
offset <=
|
||||
ref.current.clientHeight
|
||||
: true;
|
||||
|
||||
const atTop = (offset = 0) => ref.current.scrollTop <= offset;
|
||||
const atTop = (offset = 0) => ref.current.scrollTop <= offset;
|
||||
|
||||
// ? Handle events from renderer.
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.addListener("state", setState);
|
||||
return () => SingletonMessageRenderer.removeListener("state", setState);
|
||||
}, []);
|
||||
// ? Handle events from renderer.
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.addListener("state", setState);
|
||||
return () => SingletonMessageRenderer.removeListener("state", setState);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.addListener("scroll", setScrollState);
|
||||
return () =>
|
||||
SingletonMessageRenderer.removeListener("scroll", setScrollState);
|
||||
}, [scrollState]);
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.addListener("scroll", setScrollState);
|
||||
return () =>
|
||||
SingletonMessageRenderer.removeListener("scroll", setScrollState);
|
||||
}, [scrollState]);
|
||||
|
||||
// ? Load channel initially.
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.init(id);
|
||||
}, [id]);
|
||||
// ? Load channel initially.
|
||||
useEffect(() => {
|
||||
SingletonMessageRenderer.init(id);
|
||||
}, [id]);
|
||||
|
||||
// ? If we are waiting for network, try again.
|
||||
useEffect(() => {
|
||||
switch (status) {
|
||||
case ClientStatus.ONLINE:
|
||||
if (state.type === "WAITING_FOR_NETWORK") {
|
||||
SingletonMessageRenderer.init(id);
|
||||
} else {
|
||||
SingletonMessageRenderer.reloadStale(id);
|
||||
}
|
||||
// ? If we are waiting for network, try again.
|
||||
useEffect(() => {
|
||||
switch (status) {
|
||||
case ClientStatus.ONLINE:
|
||||
if (state.type === "WAITING_FOR_NETWORK") {
|
||||
SingletonMessageRenderer.init(id);
|
||||
} else {
|
||||
SingletonMessageRenderer.reloadStale(id);
|
||||
}
|
||||
|
||||
break;
|
||||
case ClientStatus.OFFLINE:
|
||||
case ClientStatus.DISCONNECTED:
|
||||
case ClientStatus.CONNECTING:
|
||||
SingletonMessageRenderer.markStale();
|
||||
break;
|
||||
}
|
||||
}, [status, state]);
|
||||
break;
|
||||
case ClientStatus.OFFLINE:
|
||||
case ClientStatus.DISCONNECTED:
|
||||
case ClientStatus.CONNECTING:
|
||||
SingletonMessageRenderer.markStale();
|
||||
break;
|
||||
}
|
||||
}, [status, state]);
|
||||
|
||||
// ? When the container is scrolled.
|
||||
// ? Also handle StayAtBottom
|
||||
useEffect(() => {
|
||||
async function onScroll() {
|
||||
if (scrollState.current.type === "Free" && atBottom()) {
|
||||
setScrollState({ type: "Bottom" });
|
||||
} else if (scrollState.current.type === "Bottom" && !atBottom()) {
|
||||
if (
|
||||
scrollState.current.scrollingUntil &&
|
||||
scrollState.current.scrollingUntil > +new Date()
|
||||
)
|
||||
return;
|
||||
setScrollState({ type: "Free" });
|
||||
}
|
||||
}
|
||||
// ? When the container is scrolled.
|
||||
// ? Also handle StayAtBottom
|
||||
useEffect(() => {
|
||||
async function onScroll() {
|
||||
if (scrollState.current.type === "Free" && atBottom()) {
|
||||
setScrollState({ type: "Bottom" });
|
||||
} else if (scrollState.current.type === "Bottom" && !atBottom()) {
|
||||
if (
|
||||
scrollState.current.scrollingUntil &&
|
||||
scrollState.current.scrollingUntil > +new Date()
|
||||
)
|
||||
return;
|
||||
setScrollState({ type: "Free" });
|
||||
}
|
||||
}
|
||||
|
||||
ref.current.addEventListener("scroll", onScroll);
|
||||
return () => ref.current.removeEventListener("scroll", onScroll);
|
||||
}, [ref, scrollState]);
|
||||
ref.current.addEventListener("scroll", onScroll);
|
||||
return () => ref.current.removeEventListener("scroll", onScroll);
|
||||
}, [ref, scrollState]);
|
||||
|
||||
// ? Top and bottom loaders.
|
||||
useEffect(() => {
|
||||
async function onScroll() {
|
||||
if (atTop(100)) {
|
||||
SingletonMessageRenderer.loadTop(ref.current);
|
||||
}
|
||||
// ? Top and bottom loaders.
|
||||
useEffect(() => {
|
||||
async function onScroll() {
|
||||
if (atTop(100)) {
|
||||
SingletonMessageRenderer.loadTop(ref.current);
|
||||
}
|
||||
|
||||
if (atBottom(100)) {
|
||||
SingletonMessageRenderer.loadBottom(ref.current);
|
||||
}
|
||||
}
|
||||
if (atBottom(100)) {
|
||||
SingletonMessageRenderer.loadBottom(ref.current);
|
||||
}
|
||||
}
|
||||
|
||||
ref.current.addEventListener("scroll", onScroll);
|
||||
return () => ref.current.removeEventListener("scroll", onScroll);
|
||||
}, [ref]);
|
||||
ref.current.addEventListener("scroll", onScroll);
|
||||
return () => ref.current.removeEventListener("scroll", onScroll);
|
||||
}, [ref]);
|
||||
|
||||
// ? Scroll down whenever the message area resizes.
|
||||
function stbOnResize() {
|
||||
if (!atBottom() && scrollState.current.type === "Bottom") {
|
||||
animateScroll.scrollToBottom({
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
});
|
||||
// ? Scroll down whenever the message area resizes.
|
||||
function stbOnResize() {
|
||||
if (!atBottom() && scrollState.current.type === "Bottom") {
|
||||
animateScroll.scrollToBottom({
|
||||
container: ref.current,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
setScrollState({ type: "Bottom" });
|
||||
}
|
||||
}
|
||||
setScrollState({ type: "Bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
// ? Scroll down when container resized.
|
||||
useLayoutEffect(() => {
|
||||
stbOnResize();
|
||||
}, [height]);
|
||||
// ? Scroll down when container resized.
|
||||
useLayoutEffect(() => {
|
||||
stbOnResize();
|
||||
}, [height]);
|
||||
|
||||
// ? Scroll down whenever the window resizes.
|
||||
useLayoutEffect(() => {
|
||||
document.addEventListener("resize", stbOnResize);
|
||||
return () => document.removeEventListener("resize", stbOnResize);
|
||||
}, [ref, scrollState]);
|
||||
// ? Scroll down whenever the window resizes.
|
||||
useLayoutEffect(() => {
|
||||
document.addEventListener("resize", stbOnResize);
|
||||
return () => document.removeEventListener("resize", stbOnResize);
|
||||
}, [ref, scrollState]);
|
||||
|
||||
// ? Scroll to bottom when pressing 'Escape'.
|
||||
useEffect(() => {
|
||||
function keyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && !focusTaken) {
|
||||
SingletonMessageRenderer.jumpToBottom(id, true);
|
||||
internalEmit("TextArea", "focus", "message");
|
||||
}
|
||||
}
|
||||
// ? Scroll to bottom when pressing 'Escape'.
|
||||
useEffect(() => {
|
||||
function keyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && !focusTaken) {
|
||||
SingletonMessageRenderer.jumpToBottom(id, true);
|
||||
internalEmit("TextArea", "focus", "message");
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keyup", keyUp);
|
||||
return () => document.body.removeEventListener("keyup", keyUp);
|
||||
}, [ref, focusTaken]);
|
||||
document.body.addEventListener("keyup", keyUp);
|
||||
return () => document.body.removeEventListener("keyup", keyUp);
|
||||
}, [ref, focusTaken]);
|
||||
|
||||
return (
|
||||
<MessageAreaWidthContext.Provider
|
||||
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
||||
<Area ref={ref}>
|
||||
<div>
|
||||
{state.type === "LOADING" && <Preloader type="ring" />}
|
||||
{state.type === "WAITING_FOR_NETWORK" && (
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>
|
||||
)}
|
||||
{state.type === "RENDER" && (
|
||||
<MessageRenderer id={id} state={state} />
|
||||
)}
|
||||
{state.type === "EMPTY" && <ConversationStart id={id} />}
|
||||
</div>
|
||||
</Area>
|
||||
</MessageAreaWidthContext.Provider>
|
||||
);
|
||||
return (
|
||||
<MessageAreaWidthContext.Provider
|
||||
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
||||
<Area ref={ref}>
|
||||
<div>
|
||||
{state.type === "LOADING" && <Preloader type="ring" />}
|
||||
{state.type === "WAITING_FOR_NETWORK" && (
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>
|
||||
)}
|
||||
{state.type === "RENDER" && (
|
||||
<MessageRenderer id={id} state={state} />
|
||||
)}
|
||||
{state.type === "EMPTY" && <ConversationStart id={id} />}
|
||||
</div>
|
||||
</Area>
|
||||
</MessageAreaWidthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,128 +6,128 @@ import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import {
|
||||
IntermediateContext,
|
||||
useIntermediate,
|
||||
IntermediateContext,
|
||||
useIntermediate,
|
||||
} from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { MessageObject } from "../../../context/revoltjs/util";
|
||||
|
||||
import AutoComplete, {
|
||||
useAutoComplete,
|
||||
useAutoComplete,
|
||||
} from "../../../components/common/AutoComplete";
|
||||
|
||||
const EditorBase = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
background: var(--secondary-header);
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
background: var(--secondary-header);
|
||||
}
|
||||
|
||||
.caption {
|
||||
padding: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--tertiary-foreground);
|
||||
.caption {
|
||||
padding: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--tertiary-foreground);
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
message: MessageObject;
|
||||
finish: () => void;
|
||||
message: MessageObject;
|
||||
finish: () => void;
|
||||
}
|
||||
|
||||
export default function MessageEditor({ message, finish }: Props) {
|
||||
const [content, setContent] = useState((message.content as string) ?? "");
|
||||
const { focusTaken } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const [content, setContent] = useState((message.content as string) ?? "");
|
||||
const { focusTaken } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
|
||||
async function save() {
|
||||
finish();
|
||||
async function save() {
|
||||
finish();
|
||||
|
||||
if (content.length === 0) {
|
||||
openScreen({
|
||||
id: "special_prompt",
|
||||
// @ts-expect-error
|
||||
type: "delete_message",
|
||||
// @ts-expect-error
|
||||
target: message,
|
||||
});
|
||||
} else if (content !== message.content) {
|
||||
await client.channels.editMessage(message.channel, message._id, {
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (content.length === 0) {
|
||||
openScreen({
|
||||
id: "special_prompt",
|
||||
// @ts-expect-error
|
||||
type: "delete_message",
|
||||
// @ts-expect-error
|
||||
target: message,
|
||||
});
|
||||
} else if (content !== message.content) {
|
||||
await client.channels.editMessage(message.channel, message._id, {
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ? Stop editing when pressing ESC.
|
||||
useEffect(() => {
|
||||
function keyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && !focusTaken) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
// ? Stop editing when pressing ESC.
|
||||
useEffect(() => {
|
||||
function keyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && !focusTaken) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keyup", keyUp);
|
||||
return () => document.body.removeEventListener("keyup", keyUp);
|
||||
}, [focusTaken]);
|
||||
document.body.addEventListener("keyup", keyUp);
|
||||
return () => document.body.removeEventListener("keyup", keyUp);
|
||||
}, [focusTaken]);
|
||||
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete((v) => setContent(v ?? ""), {
|
||||
users: { type: "all" },
|
||||
});
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete((v) => setContent(v ?? ""), {
|
||||
users: { type: "all" },
|
||||
});
|
||||
|
||||
return (
|
||||
<EditorBase>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
forceFocus
|
||||
maxRows={3}
|
||||
padding={12}
|
||||
value={content}
|
||||
maxLength={2000}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (onKeyDown(e)) return;
|
||||
return (
|
||||
<EditorBase>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
forceFocus
|
||||
maxRows={3}
|
||||
padding={12}
|
||||
value={content}
|
||||
maxLength={2000}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (onKeyDown(e)) return;
|
||||
|
||||
if (
|
||||
!e.shiftKey &&
|
||||
e.key === "Enter" &&
|
||||
!isTouchscreenDevice
|
||||
) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
}
|
||||
}}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<span className="caption">
|
||||
escape to <a onClick={finish}>cancel</a> · enter to{" "}
|
||||
<a onClick={save}>save</a>
|
||||
</span>
|
||||
</EditorBase>
|
||||
);
|
||||
if (
|
||||
!e.shiftKey &&
|
||||
e.key === "Enter" &&
|
||||
!isTouchscreenDevice
|
||||
) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
}
|
||||
}}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<span className="caption">
|
||||
escape to <a onClick={finish}>cancel</a> · enter to{" "}
|
||||
<a onClick={save}>save</a>
|
||||
</span>
|
||||
</EditorBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,190 +26,190 @@ import ConversationStart from "./ConversationStart";
|
||||
import MessageEditor from "./MessageEditor";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
state: RenderState;
|
||||
queue: QueuedMessage[];
|
||||
id: string;
|
||||
state: RenderState;
|
||||
queue: QueuedMessage[];
|
||||
}
|
||||
|
||||
const BlockedMessage = styled.div`
|
||||
font-size: 0.8em;
|
||||
margin-top: 6px;
|
||||
padding: 4px 64px;
|
||||
color: var(--tertiary-foreground);
|
||||
font-size: 0.8em;
|
||||
margin-top: 6px;
|
||||
padding: 4px 64px;
|
||||
color: var(--tertiary-foreground);
|
||||
|
||||
&:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
&:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
`;
|
||||
|
||||
function MessageRenderer({ id, state, queue }: Props) {
|
||||
if (state.type !== "RENDER") return null;
|
||||
if (state.type !== "RENDER") return null;
|
||||
|
||||
const client = useContext(AppContext);
|
||||
const userId = client.user!._id;
|
||||
const client = useContext(AppContext);
|
||||
const userId = client.user!._id;
|
||||
|
||||
const [editing, setEditing] = useState<string | undefined>(undefined);
|
||||
const stopEditing = () => {
|
||||
setEditing(undefined);
|
||||
internalEmit("TextArea", "focus", "message");
|
||||
};
|
||||
const [editing, setEditing] = useState<string | undefined>(undefined);
|
||||
const stopEditing = () => {
|
||||
setEditing(undefined);
|
||||
internalEmit("TextArea", "focus", "message");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function editLast() {
|
||||
if (state.type !== "RENDER") return;
|
||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||
if (state.messages[i].author === userId) {
|
||||
setEditing(state.messages[i]._id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
function editLast() {
|
||||
if (state.type !== "RENDER") return;
|
||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||
if (state.messages[i].author === userId) {
|
||||
setEditing(state.messages[i]._id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subs = [
|
||||
internalSubscribe("MessageRenderer", "edit_last", editLast),
|
||||
internalSubscribe("MessageRenderer", "edit_message", setEditing),
|
||||
];
|
||||
const subs = [
|
||||
internalSubscribe("MessageRenderer", "edit_last", editLast),
|
||||
internalSubscribe("MessageRenderer", "edit_message", setEditing),
|
||||
];
|
||||
|
||||
return () => subs.forEach((unsub) => unsub());
|
||||
}, [state.messages]);
|
||||
return () => subs.forEach((unsub) => unsub());
|
||||
}, [state.messages]);
|
||||
|
||||
let render: Children[] = [],
|
||||
previous: MessageObject | undefined;
|
||||
let render: Children[] = [],
|
||||
previous: MessageObject | undefined;
|
||||
|
||||
if (state.atTop) {
|
||||
render.push(<ConversationStart id={id} />);
|
||||
} else {
|
||||
render.push(
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>,
|
||||
);
|
||||
}
|
||||
if (state.atTop) {
|
||||
render.push(<ConversationStart id={id} />);
|
||||
} else {
|
||||
render.push(
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>,
|
||||
);
|
||||
}
|
||||
|
||||
let head = true;
|
||||
function compare(
|
||||
current: string,
|
||||
curAuthor: string,
|
||||
previous: string,
|
||||
prevAuthor: string,
|
||||
) {
|
||||
const atime = decodeTime(current),
|
||||
adate = new Date(atime),
|
||||
btime = decodeTime(previous),
|
||||
bdate = new Date(btime);
|
||||
let head = true;
|
||||
function compare(
|
||||
current: string,
|
||||
curAuthor: string,
|
||||
previous: string,
|
||||
prevAuthor: string,
|
||||
) {
|
||||
const atime = decodeTime(current),
|
||||
adate = new Date(atime),
|
||||
btime = decodeTime(previous),
|
||||
bdate = new Date(btime);
|
||||
|
||||
if (
|
||||
adate.getFullYear() !== bdate.getFullYear() ||
|
||||
adate.getMonth() !== bdate.getMonth() ||
|
||||
adate.getDate() !== bdate.getDate()
|
||||
) {
|
||||
render.push(<DateDivider date={adate} />);
|
||||
head = true;
|
||||
}
|
||||
if (
|
||||
adate.getFullYear() !== bdate.getFullYear() ||
|
||||
adate.getMonth() !== bdate.getMonth() ||
|
||||
adate.getDate() !== bdate.getDate()
|
||||
) {
|
||||
render.push(<DateDivider date={adate} />);
|
||||
head = true;
|
||||
}
|
||||
|
||||
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
|
||||
}
|
||||
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
|
||||
}
|
||||
|
||||
let blocked = 0;
|
||||
function pushBlocked() {
|
||||
render.push(
|
||||
<BlockedMessage>
|
||||
<X size={16} /> {blocked} blocked messages
|
||||
</BlockedMessage>,
|
||||
);
|
||||
blocked = 0;
|
||||
}
|
||||
let blocked = 0;
|
||||
function pushBlocked() {
|
||||
render.push(
|
||||
<BlockedMessage>
|
||||
<X size={16} /> {blocked} blocked messages
|
||||
</BlockedMessage>,
|
||||
);
|
||||
blocked = 0;
|
||||
}
|
||||
|
||||
for (const message of state.messages) {
|
||||
if (previous) {
|
||||
compare(message._id, message.author, previous._id, previous.author);
|
||||
}
|
||||
for (const message of state.messages) {
|
||||
if (previous) {
|
||||
compare(message._id, message.author, previous._id, previous.author);
|
||||
}
|
||||
|
||||
if (message.author === "00000000000000000000000000") {
|
||||
render.push(
|
||||
<SystemMessage
|
||||
key={message._id}
|
||||
message={message}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
// ! FIXME: temp solution
|
||||
if (
|
||||
client.users.get(message.author)?.relationship ===
|
||||
Users.Relationship.Blocked
|
||||
) {
|
||||
blocked++;
|
||||
} else {
|
||||
if (blocked > 0) pushBlocked();
|
||||
if (message.author === "00000000000000000000000000") {
|
||||
render.push(
|
||||
<SystemMessage
|
||||
key={message._id}
|
||||
message={message}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
// ! FIXME: temp solution
|
||||
if (
|
||||
client.users.get(message.author)?.relationship ===
|
||||
Users.Relationship.Blocked
|
||||
) {
|
||||
blocked++;
|
||||
} else {
|
||||
if (blocked > 0) pushBlocked();
|
||||
|
||||
render.push(
|
||||
<Message
|
||||
message={message}
|
||||
key={message._id}
|
||||
head={head}
|
||||
content={
|
||||
editing === message._id ? (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
finish={stopEditing}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
render.push(
|
||||
<Message
|
||||
message={message}
|
||||
key={message._id}
|
||||
head={head}
|
||||
content={
|
||||
editing === message._id ? (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
finish={stopEditing}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
previous = message;
|
||||
}
|
||||
previous = message;
|
||||
}
|
||||
|
||||
if (blocked > 0) pushBlocked();
|
||||
if (blocked > 0) pushBlocked();
|
||||
|
||||
const nonces = state.messages.map((x) => x.nonce);
|
||||
if (state.atBottom) {
|
||||
for (const msg of queue) {
|
||||
if (msg.channel !== id) continue;
|
||||
if (nonces.includes(msg.id)) continue;
|
||||
const nonces = state.messages.map((x) => x.nonce);
|
||||
if (state.atBottom) {
|
||||
for (const msg of queue) {
|
||||
if (msg.channel !== id) continue;
|
||||
if (nonces.includes(msg.id)) continue;
|
||||
|
||||
if (previous) {
|
||||
compare(msg.id, userId!, previous._id, previous.author);
|
||||
if (previous) {
|
||||
compare(msg.id, userId!, previous._id, previous.author);
|
||||
|
||||
previous = {
|
||||
_id: msg.id,
|
||||
data: { author: userId! },
|
||||
} as any;
|
||||
}
|
||||
previous = {
|
||||
_id: msg.id,
|
||||
data: { author: userId! },
|
||||
} as any;
|
||||
}
|
||||
|
||||
render.push(
|
||||
<Message
|
||||
message={{
|
||||
...msg.data,
|
||||
replies: msg.data.replies.map((x) => x.id),
|
||||
}}
|
||||
key={msg.id}
|
||||
queued={msg}
|
||||
head={head}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
render.push(
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>,
|
||||
);
|
||||
}
|
||||
render.push(
|
||||
<Message
|
||||
message={{
|
||||
...msg.data,
|
||||
replies: msg.data.replies.map((x) => x.id),
|
||||
}}
|
||||
key={msg.id}
|
||||
queued={msg}
|
||||
head={head}
|
||||
attachContext
|
||||
/>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
render.push(
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>,
|
||||
);
|
||||
}
|
||||
|
||||
return <>{render}</>;
|
||||
return <>{render}</>;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
|
||||
return {
|
||||
queue: state.queue,
|
||||
};
|
||||
}),
|
||||
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
|
||||
return {
|
||||
queue: state.queue,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,134 +5,134 @@ import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import {
|
||||
VoiceContext,
|
||||
VoiceOperationsContext,
|
||||
VoiceStatus,
|
||||
VoiceContext,
|
||||
VoiceOperationsContext,
|
||||
VoiceStatus,
|
||||
} from "../../../context/Voice";
|
||||
import {
|
||||
useForceUpdate,
|
||||
useSelf,
|
||||
useUsers,
|
||||
useForceUpdate,
|
||||
useSelf,
|
||||
useUsers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import Button from "../../../components/ui/Button";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const VoiceBase = styled.div`
|
||||
padding: 20px;
|
||||
background: var(--secondary-background);
|
||||
padding: 20px;
|
||||
background: var(--secondary-background);
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
color: var(--success);
|
||||
background: var(--primary-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 7px;
|
||||
flex: 1 0;
|
||||
user-select: none;
|
||||
.status {
|
||||
position: absolute;
|
||||
color: var(--success);
|
||||
background: var(--primary-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 7px;
|
||||
flex: 1 0;
|
||||
user-select: none;
|
||||
|
||||
svg {
|
||||
margin-inline-end: 4px;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
margin-inline-end: 4px;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.participants {
|
||||
margin: 20px 0;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.participants {
|
||||
margin: 20px 0;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.disconnected {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.disconnected {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function VoiceHeader({ id }: Props) {
|
||||
const { status, participants, roomId } = useContext(VoiceContext);
|
||||
if (roomId !== id) return null;
|
||||
const { status, participants, roomId } = useContext(VoiceContext);
|
||||
if (roomId !== id) return null;
|
||||
|
||||
const { isProducing, startProducing, stopProducing, disconnect } =
|
||||
useContext(VoiceOperationsContext);
|
||||
const { isProducing, startProducing, stopProducing, disconnect } =
|
||||
useContext(VoiceOperationsContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const self = useSelf(ctx);
|
||||
const keys = participants ? Array.from(participants.keys()) : undefined;
|
||||
const users = keys ? useUsers(keys, ctx) : undefined;
|
||||
const ctx = useForceUpdate();
|
||||
const self = useSelf(ctx);
|
||||
const keys = participants ? Array.from(participants.keys()) : undefined;
|
||||
const users = keys ? useUsers(keys, ctx) : undefined;
|
||||
|
||||
return (
|
||||
<VoiceBase>
|
||||
<div className="participants">
|
||||
{users && users.length !== 0
|
||||
? users.map((user, index) => {
|
||||
const id = keys![index];
|
||||
return (
|
||||
<div key={id}>
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={user}
|
||||
status={false}
|
||||
voice={
|
||||
participants!.get(id)?.audio
|
||||
? undefined
|
||||
: "muted"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: self !== undefined && (
|
||||
<div key={self._id} className="disconnected">
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={self}
|
||||
status={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="status">
|
||||
<BarChart size={20} />
|
||||
{status === VoiceStatus.CONNECTED && (
|
||||
<Text id="app.main.channel.voice.connected" />
|
||||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<Button error onClick={disconnect}>
|
||||
<Text id="app.main.channel.voice.leave" />
|
||||
</Button>
|
||||
{isProducing("audio") ? (
|
||||
<Button onClick={() => stopProducing("audio")}>
|
||||
<Text id="app.main.channel.voice.mute" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => startProducing("audio")}>
|
||||
<Text id="app.main.channel.voice.unmute" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</VoiceBase>
|
||||
);
|
||||
return (
|
||||
<VoiceBase>
|
||||
<div className="participants">
|
||||
{users && users.length !== 0
|
||||
? users.map((user, index) => {
|
||||
const id = keys![index];
|
||||
return (
|
||||
<div key={id}>
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={user}
|
||||
status={false}
|
||||
voice={
|
||||
participants!.get(id)?.audio
|
||||
? undefined
|
||||
: "muted"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: self !== undefined && (
|
||||
<div key={self._id} className="disconnected">
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={self}
|
||||
status={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="status">
|
||||
<BarChart size={20} />
|
||||
{status === VoiceStatus.CONNECTED && (
|
||||
<Text id="app.main.channel.voice.connected" />
|
||||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<Button error onClick={disconnect}>
|
||||
<Text id="app.main.channel.voice.leave" />
|
||||
</Button>
|
||||
{isProducing("audio") ? (
|
||||
<Button onClick={() => stopProducing("audio")}>
|
||||
<Text id="app.main.channel.voice.mute" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => startProducing("audio")}>
|
||||
<Text id="app.main.channel.voice.unmute" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</VoiceBase>
|
||||
);
|
||||
}
|
||||
|
||||
/**{voice.roomId === id && (
|
||||
|
||||
@@ -11,31 +11,31 @@ import { useUserPermission } from "../../context/revoltjs/hooks";
|
||||
import Header from "../../components/ui/Header";
|
||||
|
||||
export default function Developer() {
|
||||
// const voice = useContext(VoiceContext);
|
||||
const client = useContext(AppContext);
|
||||
const userPermission = useUserPermission(client.user!._id);
|
||||
// const voice = useContext(VoiceContext);
|
||||
const client = useContext(AppContext);
|
||||
const userPermission = useUserPermission(client.user!._id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header placement="primary">
|
||||
<Wrench size="24" />
|
||||
Developer Tab
|
||||
</Header>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<PaintCounter always />
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<b>User ID:</b> {client.user!._id} <br />
|
||||
<b>Permission against self:</b> {userPermission} <br />
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<TextReact
|
||||
id="login.open_mail_provider"
|
||||
fields={{ provider: <b>GAMING!</b> }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
{/*<span>
|
||||
return (
|
||||
<div>
|
||||
<Header placement="primary">
|
||||
<Wrench size="24" />
|
||||
Developer Tab
|
||||
</Header>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<PaintCounter always />
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<b>User ID:</b> {client.user!._id} <br />
|
||||
<b>Permission against self:</b> {userPermission} <br />
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<TextReact
|
||||
id="login.open_mail_provider"
|
||||
fields={{ provider: <b>GAMING!</b> }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
{/*<span>
|
||||
<b>Voice Status:</b> {VoiceStatus[voice.status]}
|
||||
</span>
|
||||
<br />
|
||||
@@ -48,7 +48,7 @@ export default function Developer() {
|
||||
{Array.from(voice.participants.keys()).join(", ")}]
|
||||
</span>
|
||||
<br />*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { stopPropagation } from "../../lib/stopPropagation";
|
||||
import { VoiceOperationsContext } from "../../context/Voice";
|
||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||
import {
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
} from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import UserIcon from "../../components/common/user/UserIcon";
|
||||
@@ -24,113 +24,113 @@ import IconButton from "../../components/ui/IconButton";
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export function Friend({ user }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const { openDM } = useContext(OperationsContext);
|
||||
const { connect } = useContext(VoiceOperationsContext);
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const { openDM } = useContext(OperationsContext);
|
||||
const { connect } = useContext(VoiceOperationsContext);
|
||||
|
||||
const actions: Children[] = [];
|
||||
let subtext: Children = null;
|
||||
const actions: Children[] = [];
|
||||
let subtext: Children = null;
|
||||
|
||||
if (user.relationship === Users.Relationship.Friend) {
|
||||
subtext = <UserStatus user={user} />;
|
||||
actions.push(
|
||||
<>
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(
|
||||
styles.button,
|
||||
styles.call,
|
||||
styles.success,
|
||||
)}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, openDM(user._id).then(connect))
|
||||
}>
|
||||
<PhoneCall size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={styles.button}
|
||||
onClick={(ev) => stopPropagation(ev, openDM(user._id))}>
|
||||
<Envelope size={20} />
|
||||
</IconButton>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
if (user.relationship === Users.Relationship.Friend) {
|
||||
subtext = <UserStatus user={user} />;
|
||||
actions.push(
|
||||
<>
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(
|
||||
styles.button,
|
||||
styles.call,
|
||||
styles.success,
|
||||
)}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, openDM(user._id).then(connect))
|
||||
}>
|
||||
<PhoneCall size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={styles.button}
|
||||
onClick={(ev) => stopPropagation(ev, openDM(user._id))}>
|
||||
<Envelope size={20} />
|
||||
</IconButton>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
if (user.relationship === Users.Relationship.Incoming) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={styles.button}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, client.users.addFriend(user.username))
|
||||
}>
|
||||
<Plus size={24} />
|
||||
</IconButton>,
|
||||
);
|
||||
if (user.relationship === Users.Relationship.Incoming) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={styles.button}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, client.users.addFriend(user.username))
|
||||
}>
|
||||
<Plus size={24} />
|
||||
</IconButton>,
|
||||
);
|
||||
|
||||
subtext = <Text id="app.special.friends.incoming" />;
|
||||
}
|
||||
subtext = <Text id="app.special.friends.incoming" />;
|
||||
}
|
||||
|
||||
if (user.relationship === Users.Relationship.Outgoing) {
|
||||
subtext = <Text id="app.special.friends.outgoing" />;
|
||||
}
|
||||
if (user.relationship === Users.Relationship.Outgoing) {
|
||||
subtext = <Text id="app.special.friends.outgoing" />;
|
||||
}
|
||||
|
||||
if (
|
||||
user.relationship === Users.Relationship.Friend ||
|
||||
user.relationship === Users.Relationship.Outgoing ||
|
||||
user.relationship === Users.Relationship.Incoming
|
||||
) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(styles.button, styles.error)}
|
||||
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>,
|
||||
);
|
||||
}
|
||||
if (
|
||||
user.relationship === Users.Relationship.Friend ||
|
||||
user.relationship === Users.Relationship.Outgoing ||
|
||||
user.relationship === Users.Relationship.Incoming
|
||||
) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(styles.button, styles.error)}
|
||||
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>,
|
||||
);
|
||||
}
|
||||
|
||||
if (user.relationship === Users.Relationship.Blocked) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(styles.button, styles.error)}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, client.users.unblockUser(user._id))
|
||||
}>
|
||||
<X size={24} />
|
||||
</IconButton>,
|
||||
);
|
||||
}
|
||||
if (user.relationship === Users.Relationship.Blocked) {
|
||||
actions.push(
|
||||
<IconButton
|
||||
type="circle"
|
||||
className={classNames(styles.button, styles.error)}
|
||||
onClick={(ev) =>
|
||||
stopPropagation(ev, client.users.unblockUser(user._id))
|
||||
}>
|
||||
<X size={24} />
|
||||
</IconButton>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.friend}
|
||||
onClick={() => openScreen({ id: "profile", user_id: user._id })}
|
||||
onContextMenu={attachContextMenu("Menu", { user: user._id })}>
|
||||
<UserIcon target={user} size={36} status />
|
||||
<div className={styles.name}>
|
||||
<span>@{user.username}</span>
|
||||
{subtext && <span className={styles.subtext}>{subtext}</span>}
|
||||
</div>
|
||||
<div className={styles.actions}>{actions}</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={styles.friend}
|
||||
onClick={() => openScreen({ id: "profile", user_id: user._id })}
|
||||
onContextMenu={attachContextMenu("Menu", { user: user._id })}>
|
||||
<UserIcon target={user} size={36} status />
|
||||
<div className={styles.name}>
|
||||
<span>@{user.username}</span>
|
||||
{subtext && <span className={styles.subtext}>{subtext}</span>}
|
||||
</div>
|
||||
<div className={styles.actions}>{actions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ListPlus,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ListPlus,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
|
||||
import { User, Users } from "revolt.js/dist/api/objects";
|
||||
@@ -27,91 +27,91 @@ import { Children } from "../../types/Preact";
|
||||
import { Friend } from "./Friend";
|
||||
|
||||
export default function Friends() {
|
||||
const { openScreen } = useIntermediate();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const users = useUsers() as User[];
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
const users = useUsers() as User[];
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
const friends = users.filter(
|
||||
(x) => x.relationship === Users.Relationship.Friend,
|
||||
);
|
||||
const friends = users.filter(
|
||||
(x) => x.relationship === Users.Relationship.Friend,
|
||||
);
|
||||
|
||||
const lists = [
|
||||
[
|
||||
"",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Incoming),
|
||||
],
|
||||
[
|
||||
"app.special.friends.sent",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Outgoing),
|
||||
"outgoing",
|
||||
],
|
||||
[
|
||||
"app.status.online",
|
||||
friends.filter(
|
||||
(x) =>
|
||||
x.online && x.status?.presence !== Users.Presence.Invisible,
|
||||
),
|
||||
"online",
|
||||
],
|
||||
[
|
||||
"app.status.offline",
|
||||
friends.filter(
|
||||
(x) =>
|
||||
!x.online ||
|
||||
x.status?.presence === Users.Presence.Invisible,
|
||||
),
|
||||
"offline",
|
||||
],
|
||||
[
|
||||
"app.special.friends.blocked",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Blocked),
|
||||
"blocked",
|
||||
],
|
||||
] as [string, User[], string][];
|
||||
const lists = [
|
||||
[
|
||||
"",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Incoming),
|
||||
],
|
||||
[
|
||||
"app.special.friends.sent",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Outgoing),
|
||||
"outgoing",
|
||||
],
|
||||
[
|
||||
"app.status.online",
|
||||
friends.filter(
|
||||
(x) =>
|
||||
x.online && x.status?.presence !== Users.Presence.Invisible,
|
||||
),
|
||||
"online",
|
||||
],
|
||||
[
|
||||
"app.status.offline",
|
||||
friends.filter(
|
||||
(x) =>
|
||||
!x.online ||
|
||||
x.status?.presence === Users.Presence.Invisible,
|
||||
),
|
||||
"offline",
|
||||
],
|
||||
[
|
||||
"app.special.friends.blocked",
|
||||
users.filter((x) => x.relationship === Users.Relationship.Blocked),
|
||||
"blocked",
|
||||
],
|
||||
] as [string, User[], string][];
|
||||
|
||||
const incoming = lists[0][1];
|
||||
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>);
|
||||
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
|
||||
const incoming = lists[0][1];
|
||||
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>);
|
||||
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
|
||||
|
||||
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
|
||||
return (
|
||||
<>
|
||||
<Header placement="primary">
|
||||
{!isTouchscreenDevice && <UserDetail size={24} />}
|
||||
<div className={styles.title}>
|
||||
<Text id="app.navigation.tabs.friends" />
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{/*<Tooltip content={"Create Category"} placement="bottom">
|
||||
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
|
||||
return (
|
||||
<>
|
||||
<Header placement="primary">
|
||||
{!isTouchscreenDevice && <UserDetail size={24} />}
|
||||
<div className={styles.title}>
|
||||
<Text id="app.navigation.tabs.friends" />
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{/*<Tooltip content={"Create Category"} placement="bottom">
|
||||
<IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_group' })}>
|
||||
<ListPlus size={28} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={styles.divider} />*/}
|
||||
<Tooltip content={"Create Group"} placement="bottom">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_group",
|
||||
})
|
||||
}>
|
||||
<MessageAdd size={24} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={"Add Friend"} placement="bottom">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "add_friend",
|
||||
})
|
||||
}>
|
||||
<UserPlus size={27} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/*
|
||||
<Tooltip content={"Create Group"} placement="bottom">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_group",
|
||||
})
|
||||
}>
|
||||
<MessageAdd size={24} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={"Add Friend"} placement="bottom">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "add_friend",
|
||||
})
|
||||
}>
|
||||
<UserPlus size={27} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/*
|
||||
<div className={styles.divider} />
|
||||
<Tooltip content={"Friend Activity"} placement="bottom">
|
||||
<IconButton>
|
||||
@@ -119,98 +119,98 @@ export default function Friends() {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
</div>
|
||||
</Header>
|
||||
<div className={styles.list} data-empty={isEmpty}>
|
||||
{isEmpty && (
|
||||
<>
|
||||
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
|
||||
<Text id="app.special.friends.nobody" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Header>
|
||||
<div className={styles.list} data-empty={isEmpty}>
|
||||
{isEmpty && (
|
||||
<>
|
||||
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
|
||||
<Text id="app.special.friends.nobody" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{incoming.length > 0 && (
|
||||
<div
|
||||
className={styles.pending}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "pending_requests",
|
||||
users: incoming.map((x) => x._id),
|
||||
})
|
||||
}>
|
||||
<div className={styles.avatars}>
|
||||
{incoming.map(
|
||||
(x, i) =>
|
||||
i < 3 && (
|
||||
<UserIcon
|
||||
target={x}
|
||||
size={64}
|
||||
mask={
|
||||
i <
|
||||
Math.min(incoming.length - 1, 2)
|
||||
? "url(#overlap)"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<Text id="app.special.friends.pending" />{" "}
|
||||
<span>{incoming.length}</span>
|
||||
</div>
|
||||
<span>
|
||||
{incoming.length > 3 ? (
|
||||
<TextReact
|
||||
id="app.special.friends.from.several"
|
||||
fields={{
|
||||
userlist: userlist.slice(0, 6),
|
||||
count: incoming.length - 3,
|
||||
}}
|
||||
/>
|
||||
) : incoming.length > 1 ? (
|
||||
<TextReact
|
||||
id="app.special.friends.from.multiple"
|
||||
fields={{
|
||||
user: userlist.shift()!,
|
||||
userlist: userlist.slice(1),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextReact
|
||||
id="app.special.friends.from.single"
|
||||
fields={{ user: userlist[0] }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight size={28} />
|
||||
</div>
|
||||
)}
|
||||
{incoming.length > 0 && (
|
||||
<div
|
||||
className={styles.pending}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "pending_requests",
|
||||
users: incoming.map((x) => x._id),
|
||||
})
|
||||
}>
|
||||
<div className={styles.avatars}>
|
||||
{incoming.map(
|
||||
(x, i) =>
|
||||
i < 3 && (
|
||||
<UserIcon
|
||||
target={x}
|
||||
size={64}
|
||||
mask={
|
||||
i <
|
||||
Math.min(incoming.length - 1, 2)
|
||||
? "url(#overlap)"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<Text id="app.special.friends.pending" />{" "}
|
||||
<span>{incoming.length}</span>
|
||||
</div>
|
||||
<span>
|
||||
{incoming.length > 3 ? (
|
||||
<TextReact
|
||||
id="app.special.friends.from.several"
|
||||
fields={{
|
||||
userlist: userlist.slice(0, 6),
|
||||
count: incoming.length - 3,
|
||||
}}
|
||||
/>
|
||||
) : incoming.length > 1 ? (
|
||||
<TextReact
|
||||
id="app.special.friends.from.multiple"
|
||||
fields={{
|
||||
user: userlist.shift()!,
|
||||
userlist: userlist.slice(1),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextReact
|
||||
id="app.special.friends.from.single"
|
||||
fields={{ user: userlist[0] }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight size={28} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lists.map(([i18n, list, section_id], index) => {
|
||||
if (index === 0) return;
|
||||
if (list.length === 0) return;
|
||||
{lists.map(([i18n, list, section_id], index) => {
|
||||
if (index === 0) return;
|
||||
if (list.length === 0) return;
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
id={`friends_${section_id}`}
|
||||
defaultValue={true}
|
||||
sticky
|
||||
large
|
||||
summary={
|
||||
<div class="title">
|
||||
<Text id={i18n} /> — {list.length}
|
||||
</div>
|
||||
}>
|
||||
{list.map((x) => (
|
||||
<Friend key={x._id} user={x} />
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<CollapsibleSection
|
||||
id={`friends_${section_id}`}
|
||||
defaultValue={true}
|
||||
sticky
|
||||
large
|
||||
summary={
|
||||
<div class="title">
|
||||
<Text id={i18n} /> — {list.length}
|
||||
</div>
|
||||
}>
|
||||
{list.map((x) => (
|
||||
<Friend key={x._id} user={x} />
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,34 +8,34 @@ import wideSVG from "../../assets/wide.svg";
|
||||
import Header from "../../components/ui/Header";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<Header placement="primary">
|
||||
<HomeIcon size={24} />
|
||||
<Text id="app.navigation.tabs.home" />
|
||||
</Header>
|
||||
<h3>
|
||||
<Text id="app.special.modals.onboarding.welcome" />{" "}
|
||||
<img src={wideSVG} />
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Go to your <Link to="/friends">friends list</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Give <Link to="/settings/feedback">feedback</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Join <Link to="/invite/Testers">testers server</Link>.
|
||||
</li>
|
||||
<li>
|
||||
View{" "}
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
source code
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<Header placement="primary">
|
||||
<HomeIcon size={24} />
|
||||
<Text id="app.navigation.tabs.home" />
|
||||
</Header>
|
||||
<h3>
|
||||
<Text id="app.special.modals.onboarding.welcome" />{" "}
|
||||
<img src={wideSVG} />
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Go to your <Link to="/friends">friends list</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Give <Link to="/settings/feedback">feedback</Link>.
|
||||
</li>
|
||||
<li>
|
||||
Join <Link to="/invite/Testers">testers server</Link>.
|
||||
</li>
|
||||
<li>
|
||||
View{" "}
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
source code
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../context/revoltjs/util";
|
||||
|
||||
@@ -20,109 +20,109 @@ import Overline from "../../components/ui/Overline";
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
|
||||
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,
|
||||
);
|
||||
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]);
|
||||
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 type="spinner" />
|
||||
)}
|
||||
</RequiresOnline>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (typeof invite === "undefined") {
|
||||
return (
|
||||
<div className={styles.preloader}>
|
||||
<RequiresOnline>
|
||||
{error ? (
|
||||
<Overline type="error" error={error} />
|
||||
) : (
|
||||
<Preloader type="spinner" />
|
||||
)}
|
||||
</RequiresOnline>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ! FIXME: add i18n translations
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
style={{
|
||||
backgroundImage: invite.server_banner
|
||||
? `url('${client.generateFileURL(invite.server_banner)}')`
|
||||
: undefined,
|
||||
}}>
|
||||
<div className={styles.leave}>
|
||||
<ArrowBack size={32} onClick={() => history.push("/")} />
|
||||
</div>
|
||||
// ! FIXME: add i18n translations
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
style={{
|
||||
backgroundImage: invite.server_banner
|
||||
? `url('${client.generateFileURL(invite.server_banner)}')`
|
||||
: undefined,
|
||||
}}>
|
||||
<div className={styles.leave}>
|
||||
<ArrowBack size={32} onClick={() => history.push("/")} />
|
||||
</div>
|
||||
|
||||
{!processing && (
|
||||
<div className={styles.icon}>
|
||||
<ServerIcon
|
||||
attachment={invite.server_icon}
|
||||
server_name={invite.server_name}
|
||||
size={64}
|
||||
/>
|
||||
</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 type="ring" />
|
||||
) : (
|
||||
<>
|
||||
<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("/");
|
||||
}
|
||||
<div className={styles.details}>
|
||||
{processing ? (
|
||||
<Preloader type="ring" />
|
||||
) : (
|
||||
<>
|
||||
<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);
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,68 +4,68 @@ import InputBox from "../../components/ui/InputBox";
|
||||
import Overline from "../../components/ui/Overline";
|
||||
|
||||
interface Props {
|
||||
type: "email" | "username" | "password" | "invite" | "current_password";
|
||||
showOverline?: boolean;
|
||||
register: Function;
|
||||
error?: string;
|
||||
name?: string;
|
||||
type: "email" | "username" | "password" | "invite" | "current_password";
|
||||
showOverline?: boolean;
|
||||
register: Function;
|
||||
error?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function FormField({
|
||||
type,
|
||||
register,
|
||||
showOverline,
|
||||
error,
|
||||
name,
|
||||
type,
|
||||
register,
|
||||
showOverline,
|
||||
error,
|
||||
name,
|
||||
}: Props) {
|
||||
return (
|
||||
<>
|
||||
{showOverline && (
|
||||
<Overline error={error}>
|
||||
<Text id={`login.${type}`} />
|
||||
</Overline>
|
||||
)}
|
||||
<Localizer>
|
||||
<InputBox
|
||||
// Styled uses React typing while we use Preact
|
||||
// this leads to inconsistances where things need to be typed oddly
|
||||
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
||||
name={
|
||||
type === "current_password" ? "password" : name ?? type
|
||||
}
|
||||
type={
|
||||
type === "invite" || type === "username"
|
||||
? "text"
|
||||
: type === "current_password"
|
||||
? "password"
|
||||
: type
|
||||
}
|
||||
ref={register(
|
||||
type === "password" || type === "current_password"
|
||||
? {
|
||||
validate: (value: string) =>
|
||||
value.length === 0
|
||||
? "RequiredField"
|
||||
: value.length < 8
|
||||
? "TooShort"
|
||||
: value.length > 1024
|
||||
? "TooLong"
|
||||
: undefined,
|
||||
}
|
||||
: type === "email"
|
||||
? {
|
||||
required: "RequiredField",
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: "InvalidEmail",
|
||||
},
|
||||
}
|
||||
: type === "username"
|
||||
? { required: "RequiredField" }
|
||||
: { required: "RequiredField" },
|
||||
)}
|
||||
/>
|
||||
</Localizer>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{showOverline && (
|
||||
<Overline error={error}>
|
||||
<Text id={`login.${type}`} />
|
||||
</Overline>
|
||||
)}
|
||||
<Localizer>
|
||||
<InputBox
|
||||
// Styled uses React typing while we use Preact
|
||||
// this leads to inconsistances where things need to be typed oddly
|
||||
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
||||
name={
|
||||
type === "current_password" ? "password" : name ?? type
|
||||
}
|
||||
type={
|
||||
type === "invite" || type === "username"
|
||||
? "text"
|
||||
: type === "current_password"
|
||||
? "password"
|
||||
: type
|
||||
}
|
||||
ref={register(
|
||||
type === "password" || type === "current_password"
|
||||
? {
|
||||
validate: (value: string) =>
|
||||
value.length === 0
|
||||
? "RequiredField"
|
||||
: value.length < 8
|
||||
? "TooShort"
|
||||
: value.length > 1024
|
||||
? "TooLong"
|
||||
: undefined,
|
||||
}
|
||||
: type === "email"
|
||||
? {
|
||||
required: "RequiredField",
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: "InvalidEmail",
|
||||
},
|
||||
}
|
||||
: type === "username"
|
||||
? { required: "RequiredField" }
|
||||
: { required: "RequiredField" },
|
||||
)}
|
||||
/>
|
||||
</Localizer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,56 +19,56 @@ import { FormResend } from "./forms/FormResend";
|
||||
import { FormReset, FormSendReset } from "./forms/FormReset";
|
||||
|
||||
export default function Login() {
|
||||
const theme = useContext(ThemeContext);
|
||||
const client = useContext(AppContext);
|
||||
const theme = useContext(ThemeContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<div className={styles.login}>
|
||||
<Helmet>
|
||||
<meta name="theme-color" content={theme.background} />
|
||||
</Helmet>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.attribution}>
|
||||
<span>
|
||||
API:{" "}
|
||||
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
|
||||
· revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
|
||||
· App: <code>{APP_VERSION}</code>
|
||||
</span>
|
||||
<span>
|
||||
<LocaleSelector />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.modal}>
|
||||
<Switch>
|
||||
<Route path="/login/create">
|
||||
<FormCreate />
|
||||
</Route>
|
||||
<Route path="/login/resend">
|
||||
<FormResend />
|
||||
</Route>
|
||||
<Route path="/login/reset/:token">
|
||||
<FormReset />
|
||||
</Route>
|
||||
<Route path="/login/reset">
|
||||
<FormSendReset />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<FormLogin />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className={styles.attribution}>
|
||||
<span>
|
||||
<Text id="general.image_by" /> ‎@lorenzoherrera
|
||||
‏· unsplash.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.bg}
|
||||
style={{ background: `url('${background}')` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.login}>
|
||||
<Helmet>
|
||||
<meta name="theme-color" content={theme.background} />
|
||||
</Helmet>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.attribution}>
|
||||
<span>
|
||||
API:{" "}
|
||||
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
|
||||
· revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
|
||||
· App: <code>{APP_VERSION}</code>
|
||||
</span>
|
||||
<span>
|
||||
<LocaleSelector />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.modal}>
|
||||
<Switch>
|
||||
<Route path="/login/create">
|
||||
<FormCreate />
|
||||
</Route>
|
||||
<Route path="/login/resend">
|
||||
<FormResend />
|
||||
</Route>
|
||||
<Route path="/login/reset/:token">
|
||||
<FormReset />
|
||||
</Route>
|
||||
<Route path="/login/reset">
|
||||
<FormSendReset />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<FormLogin />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className={styles.attribution}>
|
||||
<span>
|
||||
<Text id="general.image_by" /> ‎@lorenzoherrera
|
||||
‏· unsplash.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.bg}
|
||||
style={{ background: `url('${background}')` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,33 +9,33 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
export interface CaptchaProps {
|
||||
onSuccess: (token?: string) => void;
|
||||
onCancel: () => void;
|
||||
onSuccess: (token?: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function CaptchaBlock(props: CaptchaProps) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!client.configuration?.features.captcha.enabled) {
|
||||
props.onSuccess();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!client.configuration?.features.captcha.enabled) {
|
||||
props.onSuccess();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!client.configuration?.features.captcha.enabled)
|
||||
return <Preloader type="spinner" />;
|
||||
if (!client.configuration?.features.captcha.enabled)
|
||||
return <Preloader type="spinner" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HCaptcha
|
||||
sitekey={client.configuration.features.captcha.key}
|
||||
onVerify={(token) => props.onSuccess(token)}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
<a onClick={props.onCancel}>
|
||||
<Text id="login.cancel" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<HCaptcha
|
||||
sitekey={client.configuration.features.captcha.key}
|
||||
onVerify={(token) => props.onSuccess(token)}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
<a onClick={props.onCancel}>
|
||||
<Text id="login.cancel" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,232 +20,232 @@ import { Legal } from "./Legal";
|
||||
import { MailProvider } from "./MailProvider";
|
||||
|
||||
interface Props {
|
||||
page: "create" | "login" | "send_reset" | "reset" | "resend";
|
||||
callback: (fields: {
|
||||
email: string;
|
||||
password: string;
|
||||
invite: string;
|
||||
captcha?: string;
|
||||
}) => Promise<void>;
|
||||
page: "create" | "login" | "send_reset" | "reset" | "resend";
|
||||
callback: (fields: {
|
||||
email: string;
|
||||
password: string;
|
||||
invite: string;
|
||||
captcha?: string;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
function getInviteCode() {
|
||||
if (typeof window === "undefined") return "";
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
return code ?? "";
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
return code ?? "";
|
||||
}
|
||||
|
||||
interface FormInputs {
|
||||
email: string;
|
||||
password: string;
|
||||
invite: string;
|
||||
email: string;
|
||||
password: string;
|
||||
invite: string;
|
||||
}
|
||||
|
||||
export function Form({ page, callback }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<string | undefined>(undefined);
|
||||
const [error, setGlobalError] = useState<string | undefined>(undefined);
|
||||
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<string | undefined>(undefined);
|
||||
const [error, setGlobalError] = useState<string | undefined>(undefined);
|
||||
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
|
||||
|
||||
const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
invite: getInviteCode(),
|
||||
},
|
||||
});
|
||||
const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
invite: getInviteCode(),
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: FormInputs) {
|
||||
setGlobalError(undefined);
|
||||
setLoading(true);
|
||||
async function onSubmit(data: FormInputs) {
|
||||
setGlobalError(undefined);
|
||||
setLoading(true);
|
||||
|
||||
function onError(err: any) {
|
||||
setLoading(false);
|
||||
function onError(err: any) {
|
||||
setLoading(false);
|
||||
|
||||
const error = takeError(err);
|
||||
switch (error) {
|
||||
case "email_in_use":
|
||||
return setError("email", { type: "", message: error });
|
||||
case "unknown_user":
|
||||
return setError("email", { type: "", message: error });
|
||||
case "invalid_invite":
|
||||
return setError("invite", { type: "", message: error });
|
||||
}
|
||||
const error = takeError(err);
|
||||
switch (error) {
|
||||
case "email_in_use":
|
||||
return setError("email", { type: "", message: error });
|
||||
case "unknown_user":
|
||||
return setError("email", { type: "", message: error });
|
||||
case "invalid_invite":
|
||||
return setError("invite", { type: "", message: error });
|
||||
}
|
||||
|
||||
setGlobalError(error);
|
||||
}
|
||||
setGlobalError(error);
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
client.configuration?.features.captcha.enabled &&
|
||||
page !== "reset"
|
||||
) {
|
||||
setCaptcha({
|
||||
onSuccess: async (captcha) => {
|
||||
setCaptcha(undefined);
|
||||
try {
|
||||
await callback({ ...data, captcha });
|
||||
setSuccess(data.email);
|
||||
} catch (err) {
|
||||
onError(err);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
setCaptcha(undefined);
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await callback(data);
|
||||
setSuccess(data.email);
|
||||
}
|
||||
} catch (err) {
|
||||
onError(err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (
|
||||
client.configuration?.features.captcha.enabled &&
|
||||
page !== "reset"
|
||||
) {
|
||||
setCaptcha({
|
||||
onSuccess: async (captcha) => {
|
||||
setCaptcha(undefined);
|
||||
try {
|
||||
await callback({ ...data, captcha });
|
||||
setSuccess(data.email);
|
||||
} catch (err) {
|
||||
onError(err);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
setCaptcha(undefined);
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await callback(data);
|
||||
setSuccess(data.email);
|
||||
}
|
||||
} catch (err) {
|
||||
onError(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof success !== "undefined") {
|
||||
return (
|
||||
<div className={styles.success}>
|
||||
{client.configuration?.features.email ? (
|
||||
<>
|
||||
<Envelope size={72} />
|
||||
<h2>
|
||||
<Text id="login.check_mail" />
|
||||
</h2>
|
||||
<p className={styles.note}>
|
||||
<Text id="login.email_delay" />
|
||||
</p>
|
||||
<MailProvider email={success} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle size={72} />
|
||||
<h1>
|
||||
<Text id="login.successful_registration" />
|
||||
</h1>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.footer}>
|
||||
<Link to="/login">
|
||||
<a>
|
||||
<Text id="login.remembered" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (typeof success !== "undefined") {
|
||||
return (
|
||||
<div className={styles.success}>
|
||||
{client.configuration?.features.email ? (
|
||||
<>
|
||||
<Envelope size={72} />
|
||||
<h2>
|
||||
<Text id="login.check_mail" />
|
||||
</h2>
|
||||
<p className={styles.note}>
|
||||
<Text id="login.email_delay" />
|
||||
</p>
|
||||
<MailProvider email={success} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle size={72} />
|
||||
<h1>
|
||||
<Text id="login.successful_registration" />
|
||||
</h1>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.footer}>
|
||||
<Link to="/login">
|
||||
<a>
|
||||
<Text id="login.remembered" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (captcha) return <CaptchaBlock {...captcha} />;
|
||||
if (loading) return <Preloader type="spinner" />;
|
||||
if (captcha) return <CaptchaBlock {...captcha} />;
|
||||
if (loading) return <Preloader type="spinner" />;
|
||||
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<img src={wideSVG} />
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
) as JSX.GenericEventHandler<HTMLFormElement>
|
||||
}>
|
||||
{page !== "reset" && (
|
||||
<FormField
|
||||
type="email"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
)}
|
||||
{(page === "login" ||
|
||||
page === "create" ||
|
||||
page === "reset") && (
|
||||
<FormField
|
||||
type="password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.password?.message}
|
||||
/>
|
||||
)}
|
||||
{client.configuration?.features.invite_only &&
|
||||
page === "create" && (
|
||||
<FormField
|
||||
type="invite"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.invite?.message}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Overline type="error" error={error}>
|
||||
<Text id={`login.error.${page}`} />
|
||||
</Overline>
|
||||
)}
|
||||
<Button>
|
||||
<Text
|
||||
id={
|
||||
page === "create"
|
||||
? "login.register"
|
||||
: page === "login"
|
||||
? "login.title"
|
||||
: page === "reset"
|
||||
? "login.set_password"
|
||||
: page === "resend"
|
||||
? "login.resend"
|
||||
: "login.reset"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</form>
|
||||
{page === "create" && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.existing" />
|
||||
<Link to="/login">
|
||||
<Text id="login.title" />
|
||||
</Link>
|
||||
</span>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.missing_verification" />
|
||||
<Link to="/login/resend">
|
||||
<Text id="login.resend" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{page === "login" && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.new" />
|
||||
<Link to="/login/create">
|
||||
<Text id="login.create" />
|
||||
</Link>
|
||||
</span>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.forgot" />
|
||||
<Link to="/login/reset">
|
||||
<Text id="login.reset" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{(page === "reset" ||
|
||||
page === "resend" ||
|
||||
page === "send_reset") && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Link to="/login">
|
||||
<Text id="login.remembered" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<Legal />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<img src={wideSVG} />
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
) as JSX.GenericEventHandler<HTMLFormElement>
|
||||
}>
|
||||
{page !== "reset" && (
|
||||
<FormField
|
||||
type="email"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
)}
|
||||
{(page === "login" ||
|
||||
page === "create" ||
|
||||
page === "reset") && (
|
||||
<FormField
|
||||
type="password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.password?.message}
|
||||
/>
|
||||
)}
|
||||
{client.configuration?.features.invite_only &&
|
||||
page === "create" && (
|
||||
<FormField
|
||||
type="invite"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.invite?.message}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Overline type="error" error={error}>
|
||||
<Text id={`login.error.${page}`} />
|
||||
</Overline>
|
||||
)}
|
||||
<Button>
|
||||
<Text
|
||||
id={
|
||||
page === "create"
|
||||
? "login.register"
|
||||
: page === "login"
|
||||
? "login.title"
|
||||
: page === "reset"
|
||||
? "login.set_password"
|
||||
: page === "resend"
|
||||
? "login.resend"
|
||||
: "login.reset"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</form>
|
||||
{page === "create" && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.existing" />
|
||||
<Link to="/login">
|
||||
<Text id="login.title" />
|
||||
</Link>
|
||||
</span>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.missing_verification" />
|
||||
<Link to="/login/resend">
|
||||
<Text id="login.resend" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{page === "login" && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.new" />
|
||||
<Link to="/login/create">
|
||||
<Text id="login.create" />
|
||||
</Link>
|
||||
</span>
|
||||
<span className={styles.create}>
|
||||
<Text id="login.forgot" />
|
||||
<Link to="/login/reset">
|
||||
<Text id="login.reset" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{(page === "reset" ||
|
||||
page === "resend" ||
|
||||
page === "send_reset") && (
|
||||
<>
|
||||
<span className={styles.create}>
|
||||
<Link to="/login">
|
||||
<Text id="login.remembered" />
|
||||
</Link>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<Legal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormCreate() {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Form
|
||||
page="create"
|
||||
callback={async (data) => {
|
||||
await client.register(import.meta.env.VITE_API_URL, data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Form
|
||||
page="create"
|
||||
callback={async (data) => {
|
||||
await client.register(import.meta.env.VITE_API_URL, data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@ import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormLogin() {
|
||||
const { login } = useContext(OperationsContext);
|
||||
const history = useHistory();
|
||||
const { login } = useContext(OperationsContext);
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Form
|
||||
page="login"
|
||||
callback={async (data) => {
|
||||
const browser = detect();
|
||||
let device_name;
|
||||
if (browser) {
|
||||
const { name, os } = browser;
|
||||
device_name = `${name} on ${os}`;
|
||||
} else {
|
||||
device_name = "Unknown Device";
|
||||
}
|
||||
return (
|
||||
<Form
|
||||
page="login"
|
||||
callback={async (data) => {
|
||||
const browser = detect();
|
||||
let device_name;
|
||||
if (browser) {
|
||||
const { name, os } = browser;
|
||||
device_name = `${name} on ${os}`;
|
||||
} else {
|
||||
device_name = "Unknown Device";
|
||||
}
|
||||
|
||||
await login({ ...data, device_name });
|
||||
history.push("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await login({ ...data, device_name });
|
||||
history.push("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormResend() {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Form
|
||||
page="resend"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/resend", data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Form
|
||||
page="resend"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/resend", data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,33 +7,33 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormSendReset() {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Form
|
||||
page="send_reset"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/send_reset", data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Form
|
||||
page="send_reset"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/send_reset", data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormReset() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const client = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const client = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Form
|
||||
page="reset"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/reset", {
|
||||
token,
|
||||
...data,
|
||||
});
|
||||
history.push("/login");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Form
|
||||
page="reset"
|
||||
callback={async (data) => {
|
||||
await client.req("POST", "/auth/reset", {
|
||||
token,
|
||||
...data,
|
||||
});
|
||||
history.push("/login");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ import styles from "../Login.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
export function Legal() {
|
||||
return (
|
||||
<span className={styles.footer}>
|
||||
<a href="https://revolt.chat/about" target="_blank">
|
||||
<Text id="general.about" />
|
||||
</a>
|
||||
·
|
||||
<a href="https://revolt.chat/terms" target="_blank">
|
||||
<Text id="general.tos" />
|
||||
</a>
|
||||
·
|
||||
<a href="https://revolt.chat/privacy" target="_blank">
|
||||
<Text id="general.privacy" />
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className={styles.footer}>
|
||||
<a href="https://revolt.chat/about" target="_blank">
|
||||
<Text id="general.about" />
|
||||
</a>
|
||||
·
|
||||
<a href="https://revolt.chat/terms" target="_blank">
|
||||
<Text id="general.tos" />
|
||||
</a>
|
||||
·
|
||||
<a href="https://revolt.chat/privacy" target="_blank">
|
||||
<Text id="general.privacy" />
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,53 +4,53 @@ import { Text } from "preact-i18n";
|
||||
import Button from "../../../components/ui/Button";
|
||||
|
||||
interface Props {
|
||||
email?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
function mapMailProvider(email?: string): [string, string] | undefined {
|
||||
if (!email) return;
|
||||
if (!email) return;
|
||||
|
||||
const match = /@(.+)/.exec(email);
|
||||
if (match === null) return;
|
||||
const match = /@(.+)/.exec(email);
|
||||
if (match === null) return;
|
||||
|
||||
const domain = match[1];
|
||||
switch (domain) {
|
||||
case "gmail.com":
|
||||
return ["Gmail", "https://gmail.com"];
|
||||
case "tuta.io":
|
||||
return ["Tutanota", "https://mail.tutanota.com"];
|
||||
case "outlook.com":
|
||||
return ["Outlook", "https://outlook.live.com"];
|
||||
case "yahoo.com":
|
||||
return ["Yahoo", "https://mail.yahoo.com"];
|
||||
case "wp.pl":
|
||||
return ["WP Poczta", "https://poczta.wp.pl"];
|
||||
case "protonmail.com":
|
||||
case "protonmail.ch":
|
||||
return ["ProtonMail", "https://mail.protonmail.com"];
|
||||
case "seznam.cz":
|
||||
case "email.cz":
|
||||
case "post.cz":
|
||||
return ["Seznam", "https://email.seznam.cz"];
|
||||
default:
|
||||
return [domain, `https://${domain}`];
|
||||
}
|
||||
const domain = match[1];
|
||||
switch (domain) {
|
||||
case "gmail.com":
|
||||
return ["Gmail", "https://gmail.com"];
|
||||
case "tuta.io":
|
||||
return ["Tutanota", "https://mail.tutanota.com"];
|
||||
case "outlook.com":
|
||||
return ["Outlook", "https://outlook.live.com"];
|
||||
case "yahoo.com":
|
||||
return ["Yahoo", "https://mail.yahoo.com"];
|
||||
case "wp.pl":
|
||||
return ["WP Poczta", "https://poczta.wp.pl"];
|
||||
case "protonmail.com":
|
||||
case "protonmail.ch":
|
||||
return ["ProtonMail", "https://mail.protonmail.com"];
|
||||
case "seznam.cz":
|
||||
case "email.cz":
|
||||
case "post.cz":
|
||||
return ["Seznam", "https://email.seznam.cz"];
|
||||
default:
|
||||
return [domain, `https://${domain}`];
|
||||
}
|
||||
}
|
||||
|
||||
export function MailProvider({ email }: Props) {
|
||||
const provider = mapMailProvider(email);
|
||||
if (!provider) return null;
|
||||
const provider = mapMailProvider(email);
|
||||
if (!provider) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.mailProvider}>
|
||||
<a href={provider[1]} target="_blank">
|
||||
<Button>
|
||||
<Text
|
||||
id="login.open_mail_provider"
|
||||
fields={{ provider: provider[0] }}
|
||||
/>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.mailProvider}>
|
||||
<a href={provider[1]} target="_blank">
|
||||
<Button>
|
||||
<Text
|
||||
id="login.open_mail_provider"
|
||||
fields={{ provider: provider[0] }}
|
||||
/>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,75 +13,75 @@ import Overview from "./channel/Overview";
|
||||
import Permissions from "./channel/Permissions";
|
||||
|
||||
export default function ChannelSettings() {
|
||||
const { channel: cid } = useParams<{ channel: string }>();
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(cid, ctx);
|
||||
if (!channel) return null;
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "DirectMessage"
|
||||
)
|
||||
return null;
|
||||
const { channel: cid } = useParams<{ channel: string }>();
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(cid, ctx);
|
||||
if (!channel) return null;
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "DirectMessage"
|
||||
)
|
||||
return null;
|
||||
|
||||
const history = useHistory();
|
||||
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`;
|
||||
}
|
||||
const history = useHistory();
|
||||
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) {
|
||||
history.replace(`${base_url}/${to}`);
|
||||
} else {
|
||||
history.replace(base_url);
|
||||
}
|
||||
}
|
||||
if (to) {
|
||||
history.replace(`${base_url}/${to}`);
|
||||
} else {
|
||||
history.replace(base_url);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={getChannelName(ctx.client, channel, true)}
|
||||
/>
|
||||
),
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
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={[
|
||||
<Route path="/server/:server/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
<Route path="/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={getChannelName(ctx.client, channel, true)}
|
||||
/>
|
||||
),
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
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={[
|
||||
<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>,
|
||||
]}
|
||||
category="channel_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
<Route path="/">
|
||||
<Overview channel={channel} />
|
||||
</Route>,
|
||||
]}
|
||||
category="channel_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,140 +19,140 @@ import ButtonItem from "../../components/navigation/items/ButtonItem";
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
interface Props {
|
||||
pages: {
|
||||
category?: Children;
|
||||
divider?: boolean;
|
||||
id: string;
|
||||
icon: Children;
|
||||
title: Children;
|
||||
hideTitle?: boolean;
|
||||
}[];
|
||||
custom?: Children;
|
||||
children: Children;
|
||||
defaultPage: string;
|
||||
showExitButton?: boolean;
|
||||
switchPage: (to?: string) => void;
|
||||
category: "pages" | "channel_pages" | "server_pages";
|
||||
pages: {
|
||||
category?: Children;
|
||||
divider?: boolean;
|
||||
id: string;
|
||||
icon: Children;
|
||||
title: Children;
|
||||
hideTitle?: boolean;
|
||||
}[];
|
||||
custom?: Children;
|
||||
children: Children;
|
||||
defaultPage: string;
|
||||
showExitButton?: boolean;
|
||||
switchPage: (to?: string) => void;
|
||||
category: "pages" | "channel_pages" | "server_pages";
|
||||
}
|
||||
|
||||
export function GenericSettings({
|
||||
pages,
|
||||
switchPage,
|
||||
category,
|
||||
custom,
|
||||
children,
|
||||
defaultPage,
|
||||
showExitButton,
|
||||
pages,
|
||||
switchPage,
|
||||
category,
|
||||
custom,
|
||||
children,
|
||||
defaultPage,
|
||||
showExitButton,
|
||||
}: Props) {
|
||||
const history = useHistory();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { page } = useParams<{ page: string }>();
|
||||
const history = useHistory();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { page } = useParams<{ page: string }>();
|
||||
|
||||
function exitSettings() {
|
||||
if (history.length > 0) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
}
|
||||
function exitSettings() {
|
||||
if (history.length > 0) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
exitSettings();
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
exitSettings();
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, []);
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["secondary-background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
{isTouchscreenDevice && (
|
||||
<Header placement="primary">
|
||||
{typeof page === "undefined" ? (
|
||||
<>
|
||||
{showExitButton && (
|
||||
<IconButton onClick={exitSettings}>
|
||||
<X size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Text id="app.settings.title" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={() => switchPage()}>
|
||||
<ArrowBack size={24} />
|
||||
</IconButton>
|
||||
<Text
|
||||
id={`app.settings.${category}.${page}.title`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "undefined") && (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.container}>
|
||||
{pages.map((entry, i) => (
|
||||
<>
|
||||
{entry.category && (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={entry.category}
|
||||
/>
|
||||
)}
|
||||
<ButtonItem
|
||||
active={
|
||||
page === entry.id ||
|
||||
(i === 0 &&
|
||||
!isTouchscreenDevice &&
|
||||
typeof page === "undefined")
|
||||
}
|
||||
onClick={() => switchPage(entry.id)}
|
||||
compact>
|
||||
{entry.icon} {entry.title}
|
||||
</ButtonItem>
|
||||
{entry.divider && <LineDivider />}
|
||||
</>
|
||||
))}
|
||||
{custom}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "string") && (
|
||||
<div className={styles.content}>
|
||||
{!isTouchscreenDevice &&
|
||||
!pages.find((x) => x.id === page && x.hideTitle) && (
|
||||
<h1>
|
||||
<Text
|
||||
id={`app.settings.${category}.${
|
||||
page ?? defaultPage
|
||||
}.title`}
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
<Switch>{children}</Switch>
|
||||
</div>
|
||||
)}
|
||||
{!isTouchscreenDevice && (
|
||||
<div className={styles.action}>
|
||||
<IconButton onClick={exitSettings}>
|
||||
<XCircle size={48} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["secondary-background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
{isTouchscreenDevice && (
|
||||
<Header placement="primary">
|
||||
{typeof page === "undefined" ? (
|
||||
<>
|
||||
{showExitButton && (
|
||||
<IconButton onClick={exitSettings}>
|
||||
<X size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Text id="app.settings.title" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={() => switchPage()}>
|
||||
<ArrowBack size={24} />
|
||||
</IconButton>
|
||||
<Text
|
||||
id={`app.settings.${category}.${page}.title`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "undefined") && (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.container}>
|
||||
{pages.map((entry, i) => (
|
||||
<>
|
||||
{entry.category && (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={entry.category}
|
||||
/>
|
||||
)}
|
||||
<ButtonItem
|
||||
active={
|
||||
page === entry.id ||
|
||||
(i === 0 &&
|
||||
!isTouchscreenDevice &&
|
||||
typeof page === "undefined")
|
||||
}
|
||||
onClick={() => switchPage(entry.id)}
|
||||
compact>
|
||||
{entry.icon} {entry.title}
|
||||
</ButtonItem>
|
||||
{entry.divider && <LineDivider />}
|
||||
</>
|
||||
))}
|
||||
{custom}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "string") && (
|
||||
<div className={styles.content}>
|
||||
{!isTouchscreenDevice &&
|
||||
!pages.find((x) => x.id === page && x.hideTitle) && (
|
||||
<h1>
|
||||
<Text
|
||||
id={`app.settings.${category}.${
|
||||
page ?? defaultPage
|
||||
}.title`}
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
<Switch>{children}</Switch>
|
||||
</div>
|
||||
)}
|
||||
{!isTouchscreenDevice && (
|
||||
<div className={styles.action}>
|
||||
<IconButton onClick={exitSettings}>
|
||||
<XCircle size={48} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
ListUl,
|
||||
Share,
|
||||
Group,
|
||||
ListCheck,
|
||||
ListUl,
|
||||
Share,
|
||||
Group,
|
||||
ListCheck,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { XSquare } from "@styled-icons/boxicons-solid";
|
||||
import { Route, useHistory, useParams } from "react-router-dom";
|
||||
@@ -22,85 +22,85 @@ import { Overview } from "./server/Overview";
|
||||
import { Roles } from "./server/Roles";
|
||||
|
||||
export default function ServerSettings() {
|
||||
const { server: sid } = useParams<{ server: string }>();
|
||||
const server = useServer(sid);
|
||||
if (!server) return null;
|
||||
const { server: sid } = useParams<{ server: string }>();
|
||||
const server = useServer(sid);
|
||||
if (!server) return null;
|
||||
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/server/${sid}/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/server/${sid}/settings`);
|
||||
}
|
||||
}
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/server/${sid}/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/server/${sid}/settings`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
icon: <Group size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.members.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "invites",
|
||||
icon: <Share size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.invites.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bans",
|
||||
icon: <XSquare size={20} />,
|
||||
title: <Text id="app.settings.server_pages.bans.title" />,
|
||||
},
|
||||
{
|
||||
id: "roles",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: <Text id="app.settings.server_pages.roles.title" />,
|
||||
hideTitle: true,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/settings/members">
|
||||
<RequiresOnline>
|
||||
<Members server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/invites">
|
||||
<RequiresOnline>
|
||||
<Invites server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/bans">
|
||||
<RequiresOnline>
|
||||
<Bans server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/roles">
|
||||
<RequiresOnline>
|
||||
<Roles server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Overview server={server} />
|
||||
</Route>,
|
||||
]}
|
||||
category="server_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
icon: <Group size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.members.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "invites",
|
||||
icon: <Share size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.invites.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bans",
|
||||
icon: <XSquare size={20} />,
|
||||
title: <Text id="app.settings.server_pages.bans.title" />,
|
||||
},
|
||||
{
|
||||
id: "roles",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: <Text id="app.settings.server_pages.roles.title" />,
|
||||
hideTitle: true,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/settings/members">
|
||||
<RequiresOnline>
|
||||
<Members server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/invites">
|
||||
<RequiresOnline>
|
||||
<Invites server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/bans">
|
||||
<RequiresOnline>
|
||||
<Bans server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/roles">
|
||||
<RequiresOnline>
|
||||
<Roles server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Overview server={server} />
|
||||
</Route>,
|
||||
]}
|
||||
category="server_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Gitlab } from "@styled-icons/boxicons-logos";
|
||||
import {
|
||||
Sync as SyncIcon,
|
||||
Globe,
|
||||
LogOut,
|
||||
Sync as SyncIcon,
|
||||
Globe,
|
||||
LogOut,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Bell,
|
||||
Palette,
|
||||
Coffee,
|
||||
IdCard,
|
||||
CheckShield,
|
||||
Flask,
|
||||
User,
|
||||
Megaphone,
|
||||
Bell,
|
||||
Palette,
|
||||
Coffee,
|
||||
IdCard,
|
||||
CheckShield,
|
||||
Flask,
|
||||
User,
|
||||
Megaphone,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { Route, useHistory } from "react-router-dom";
|
||||
import { LIBRARY_VERSION } from "revolt.js";
|
||||
@@ -23,8 +23,8 @@ import { useContext } from "preact/hooks";
|
||||
|
||||
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
} from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import LineDivider from "../../components/ui/LineDivider";
|
||||
@@ -44,159 +44,159 @@ import { Sessions } from "./panes/Sessions";
|
||||
import { Sync } from "./panes/Sync";
|
||||
|
||||
export default function Settings() {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/settings`);
|
||||
}
|
||||
}
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/settings`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.user_settings" />
|
||||
),
|
||||
id: "account",
|
||||
icon: <User size={20} />,
|
||||
title: <Text id="app.settings.pages.account.title" />,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
icon: <IdCard size={20} />,
|
||||
title: <Text id="app.settings.pages.profile.title" />,
|
||||
},
|
||||
{
|
||||
id: "sessions",
|
||||
icon: <CheckShield size={20} />,
|
||||
title: <Text id="app.settings.pages.sessions.title" />,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.client_settings" />
|
||||
),
|
||||
id: "appearance",
|
||||
icon: <Palette size={20} />,
|
||||
title: <Text id="app.settings.pages.appearance.title" />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
icon: <Bell size={20} />,
|
||||
title: <Text id="app.settings.pages.notifications.title" />,
|
||||
},
|
||||
{
|
||||
id: "language",
|
||||
icon: <Globe size={20} />,
|
||||
title: <Text id="app.settings.pages.language.title" />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
icon: <SyncIcon size={20} />,
|
||||
title: <Text id="app.settings.pages.sync.title" />,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
id: "experiments",
|
||||
icon: <Flask size={20} />,
|
||||
title: <Text id="app.settings.pages.experiments.title" />,
|
||||
},
|
||||
{
|
||||
id: "feedback",
|
||||
icon: <Megaphone size={20} />,
|
||||
title: <Text id="app.settings.pages.feedback.title" />,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/settings/profile">
|
||||
<Profile />
|
||||
</Route>,
|
||||
<Route path="/settings/sessions">
|
||||
<RequiresOnline>
|
||||
<Sessions />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/settings/appearance">
|
||||
<Appearance />
|
||||
</Route>,
|
||||
<Route path="/settings/notifications">
|
||||
<Notifications />
|
||||
</Route>,
|
||||
<Route path="/settings/language">
|
||||
<Languages />
|
||||
</Route>,
|
||||
<Route path="/settings/sync">
|
||||
<Sync />
|
||||
</Route>,
|
||||
<Route path="/settings/experiments">
|
||||
<ExperimentsPage />
|
||||
</Route>,
|
||||
<Route path="/settings/feedback">
|
||||
<Feedback />
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Account />
|
||||
</Route>,
|
||||
]}
|
||||
defaultPage="account"
|
||||
switchPage={switchPage}
|
||||
category="pages"
|
||||
custom={[
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
<ButtonItem compact>
|
||||
<Gitlab size={20} />
|
||||
<Text id="app.settings.pages.source_code" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<a href="https://ko-fi.com/insertish" target="_blank">
|
||||
<ButtonItem className={styles.donate} compact>
|
||||
<Coffee size={20} />
|
||||
<Text id="app.settings.pages.donate.title" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<LineDivider />,
|
||||
<ButtonItem
|
||||
onClick={() => operations.logout()}
|
||||
className={styles.logOut}
|
||||
compact>
|
||||
<LogOut size={20} />
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</ButtonItem>,
|
||||
<div className={styles.version}>
|
||||
<div>
|
||||
<span className={styles.revision}>
|
||||
<a
|
||||
href={`${REPO_URL}/${GIT_REVISION}`}
|
||||
target="_blank">
|
||||
{GIT_REVISION.substr(0, 7)}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
href={
|
||||
GIT_BRANCH !== "DETACHED"
|
||||
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
||||
: undefined
|
||||
}
|
||||
target="_blank">
|
||||
({GIT_BRANCH})
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
||||
{APP_VERSION}
|
||||
</span>
|
||||
<span>
|
||||
API: {client.configuration?.revolt ?? "N/A"}
|
||||
</span>
|
||||
<span>revolt.js: {LIBRARY_VERSION}</span>
|
||||
</div>
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.user_settings" />
|
||||
),
|
||||
id: "account",
|
||||
icon: <User size={20} />,
|
||||
title: <Text id="app.settings.pages.account.title" />,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
icon: <IdCard size={20} />,
|
||||
title: <Text id="app.settings.pages.profile.title" />,
|
||||
},
|
||||
{
|
||||
id: "sessions",
|
||||
icon: <CheckShield size={20} />,
|
||||
title: <Text id="app.settings.pages.sessions.title" />,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.client_settings" />
|
||||
),
|
||||
id: "appearance",
|
||||
icon: <Palette size={20} />,
|
||||
title: <Text id="app.settings.pages.appearance.title" />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
icon: <Bell size={20} />,
|
||||
title: <Text id="app.settings.pages.notifications.title" />,
|
||||
},
|
||||
{
|
||||
id: "language",
|
||||
icon: <Globe size={20} />,
|
||||
title: <Text id="app.settings.pages.language.title" />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
icon: <SyncIcon size={20} />,
|
||||
title: <Text id="app.settings.pages.sync.title" />,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
id: "experiments",
|
||||
icon: <Flask size={20} />,
|
||||
title: <Text id="app.settings.pages.experiments.title" />,
|
||||
},
|
||||
{
|
||||
id: "feedback",
|
||||
icon: <Megaphone size={20} />,
|
||||
title: <Text id="app.settings.pages.feedback.title" />,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/settings/profile">
|
||||
<Profile />
|
||||
</Route>,
|
||||
<Route path="/settings/sessions">
|
||||
<RequiresOnline>
|
||||
<Sessions />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/settings/appearance">
|
||||
<Appearance />
|
||||
</Route>,
|
||||
<Route path="/settings/notifications">
|
||||
<Notifications />
|
||||
</Route>,
|
||||
<Route path="/settings/language">
|
||||
<Languages />
|
||||
</Route>,
|
||||
<Route path="/settings/sync">
|
||||
<Sync />
|
||||
</Route>,
|
||||
<Route path="/settings/experiments">
|
||||
<ExperimentsPage />
|
||||
</Route>,
|
||||
<Route path="/settings/feedback">
|
||||
<Feedback />
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Account />
|
||||
</Route>,
|
||||
]}
|
||||
defaultPage="account"
|
||||
switchPage={switchPage}
|
||||
category="pages"
|
||||
custom={[
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
<ButtonItem compact>
|
||||
<Gitlab size={20} />
|
||||
<Text id="app.settings.pages.source_code" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<a href="https://ko-fi.com/insertish" target="_blank">
|
||||
<ButtonItem className={styles.donate} compact>
|
||||
<Coffee size={20} />
|
||||
<Text id="app.settings.pages.donate.title" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<LineDivider />,
|
||||
<ButtonItem
|
||||
onClick={() => operations.logout()}
|
||||
className={styles.logOut}
|
||||
compact>
|
||||
<LogOut size={20} />
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</ButtonItem>,
|
||||
<div className={styles.version}>
|
||||
<div>
|
||||
<span className={styles.revision}>
|
||||
<a
|
||||
href={`${REPO_URL}/${GIT_REVISION}`}
|
||||
target="_blank">
|
||||
{GIT_REVISION.substr(0, 7)}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
href={
|
||||
GIT_BRANCH !== "DETACHED"
|
||||
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
||||
: undefined
|
||||
}
|
||||
target="_blank">
|
||||
({GIT_BRANCH})
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
||||
{APP_VERSION}
|
||||
</span>
|
||||
<span>
|
||||
API: {client.configuration?.revolt ?? "N/A"}
|
||||
</span>
|
||||
<span>revolt.js: {LIBRARY_VERSION}</span>
|
||||
</div>
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,105 +13,105 @@ import Button from "../../../components/ui/Button";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
|
||||
interface Props {
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
}
|
||||
|
||||
export default function Overview({ channel }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [name, setName] = useState(channel.name);
|
||||
const [description, setDescription] = useState(channel.description ?? "");
|
||||
const [name, setName] = useState(channel.name);
|
||||
const [description, setDescription] = useState(channel.description ?? "");
|
||||
|
||||
useEffect(() => setName(channel.name), [channel.name]);
|
||||
useEffect(
|
||||
() => setDescription(channel.description ?? ""),
|
||||
[channel.description],
|
||||
);
|
||||
useEffect(() => setName(channel.name), [channel.name]);
|
||||
useEffect(
|
||||
() => setDescription(channel.description ?? ""),
|
||||
[channel.description],
|
||||
);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: any = {};
|
||||
if (name !== channel.name) changes.name = name;
|
||||
if (description !== channel.description)
|
||||
changes.description = description;
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: any = {};
|
||||
if (name !== channel.name) changes.name = name;
|
||||
if (description !== channel.description)
|
||||
changes.description = description;
|
||||
|
||||
client.channels.edit(channel._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
client.channels.edit(channel._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.channels.edit(channel._id, { icon })
|
||||
}
|
||||
previewURL={client.channels.getIconURL(
|
||||
channel._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.channels.edit(channel._id, { remove: "Icon" })
|
||||
}
|
||||
defaultPreview={
|
||||
channel.channel_type === "Group"
|
||||
? "/assets/group.png"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.name" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_name" />
|
||||
)}
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.channels.edit(channel._id, { icon })
|
||||
}
|
||||
previewURL={client.channels.getIconURL(
|
||||
channel._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.channels.edit(channel._id, { remove: "Icon" })
|
||||
}
|
||||
defaultPreview={
|
||||
channel.channel_type === "Group"
|
||||
? "/assets/group.png"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.name" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_name" />
|
||||
)}
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.description" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_description" />
|
||||
)}
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a description..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.description" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_description" />
|
||||
)}
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a description..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,100 +12,100 @@ import Tip from "../../../components/ui/Tip";
|
||||
|
||||
// ! FIXME: export from revolt.js
|
||||
const DEFAULT_PERMISSION_DM =
|
||||
ChannelPermission.View +
|
||||
ChannelPermission.SendMessage +
|
||||
ChannelPermission.ManageChannel +
|
||||
ChannelPermission.VoiceCall +
|
||||
ChannelPermission.InviteOthers +
|
||||
ChannelPermission.EmbedLinks +
|
||||
ChannelPermission.UploadFiles;
|
||||
ChannelPermission.View +
|
||||
ChannelPermission.SendMessage +
|
||||
ChannelPermission.ManageChannel +
|
||||
ChannelPermission.VoiceCall +
|
||||
ChannelPermission.InviteOthers +
|
||||
ChannelPermission.EmbedLinks +
|
||||
ChannelPermission.UploadFiles;
|
||||
|
||||
interface Props {
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
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);
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
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 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];
|
||||
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;
|
||||
}
|
||||
if (!selectedRole) {
|
||||
useEffect(() => setSelected("default"), []);
|
||||
return null;
|
||||
}
|
||||
|
||||
const [p, setPerm] = useState(selectedRole.permissions >>> 0);
|
||||
const [p, setPerm] = useState(selectedRole.permissions >>> 0);
|
||||
|
||||
useEffect(() => {
|
||||
setPerm(selectedRole.permissions >>> 0);
|
||||
}, [selected, selectedRole.permissions]);
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
|
||||
@@ -20,87 +20,87 @@ import Overline from "../../../components/ui/Overline";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
export function Account() {
|
||||
const { openScreen } = useIntermediate();
|
||||
const status = useContext(StatusContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf(ctx);
|
||||
if (!user) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf(ctx);
|
||||
if (!user) return null;
|
||||
|
||||
const [email, setEmail] = useState("...");
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const history = useHistory();
|
||||
const [email, setEmail] = useState("...");
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const history = useHistory();
|
||||
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||
ctx.client
|
||||
.req("GET", "/auth/user")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
useEffect(() => {
|
||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||
ctx.client
|
||||
.req("GET", "/auth/user")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
ctx.client.users
|
||||
.fetchProfile(user._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
}, [status]);
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
ctx.client.users
|
||||
.fetchProfile(user._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<div className={styles.banner}>
|
||||
<UserIcon
|
||||
className={styles.avatar}
|
||||
target={user}
|
||||
size={72}
|
||||
onClick={() => switchPage("profile")}
|
||||
/>
|
||||
<div className={styles.username}>@{user.username}</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
{(
|
||||
[
|
||||
["username", user.username, <At size={24} />],
|
||||
["email", email, <Envelope size={24} />],
|
||||
["password", "*****", <Key size={24} />],
|
||||
] as const
|
||||
).map(([field, value, icon]) => (
|
||||
<div>
|
||||
{icon}
|
||||
<div className={styles.detail}>
|
||||
<Overline>
|
||||
<Text id={`login.${field}`} />
|
||||
</Overline>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "modify_account",
|
||||
field: field,
|
||||
})
|
||||
}
|
||||
contrast>
|
||||
<Text id="app.settings.pages.account.change_field" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.account.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("profile")}>
|
||||
<Text id="app.settings.tips.account.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<div className={styles.banner}>
|
||||
<UserIcon
|
||||
className={styles.avatar}
|
||||
target={user}
|
||||
size={72}
|
||||
onClick={() => switchPage("profile")}
|
||||
/>
|
||||
<div className={styles.username}>@{user.username}</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
{(
|
||||
[
|
||||
["username", user.username, <At size={24} />],
|
||||
["email", email, <Envelope size={24} />],
|
||||
["password", "*****", <Key size={24} />],
|
||||
] as const
|
||||
).map(([field, value, icon]) => (
|
||||
<div>
|
||||
{icon}
|
||||
<div className={styles.detail}>
|
||||
<Overline>
|
||||
<Text id={`login.${field}`} />
|
||||
</Overline>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "modify_account",
|
||||
field: field,
|
||||
})
|
||||
}
|
||||
contrast>
|
||||
<Text id="app.settings.pages.account.change_field" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.account.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("profile")}>
|
||||
<Text id="app.settings.tips.account.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ import { connectState } from "../../../redux/connector";
|
||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT,
|
||||
DEFAULT_MONO_FONT,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MONOSCAPE_FONTS,
|
||||
MONOSCAPE_FONT_KEYS,
|
||||
Theme,
|
||||
ThemeContext,
|
||||
ThemeOptions,
|
||||
DEFAULT_FONT,
|
||||
DEFAULT_MONO_FONT,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MONOSCAPE_FONTS,
|
||||
MONOSCAPE_FONT_KEYS,
|
||||
Theme,
|
||||
ThemeContext,
|
||||
ThemeOptions,
|
||||
} from "../../../context/Theme";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
@@ -39,92 +39,92 @@ import openmojiSVG from "../assets/openmoji_emoji.svg";
|
||||
import twemojiSVG from "../assets/twemoji_emoji.svg";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
// ! FIXME: code needs to be rewritten to fix jittering
|
||||
export function Component(props: Props) {
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
function pushOverride(custom: Partial<Theme>) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}
|
||||
function pushOverride(custom: Partial<Theme>) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}
|
||||
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent),
|
||||
});
|
||||
}
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent),
|
||||
});
|
||||
}
|
||||
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_APPEARANCE",
|
||||
options: {
|
||||
emojiPack,
|
||||
},
|
||||
});
|
||||
}
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_APPEARANCE",
|
||||
options: {
|
||||
emojiPack,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
|
||||
custom: Partial<Theme>,
|
||||
) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
|
||||
custom: Partial<Theme>,
|
||||
) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
|
||||
useEffect(() => setOverride({ css }), [css]);
|
||||
useEffect(() => setOverride({ css }), [css]);
|
||||
|
||||
const selected = props.settings.theme?.preset ?? "dark";
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.theme" />
|
||||
</h3>
|
||||
<div className={styles.themes}>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={lightSVG}
|
||||
data-active={selected === "light"}
|
||||
onClick={() =>
|
||||
selected !== "light" &&
|
||||
setTheme({ preset: "light" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.light" />
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={darkSVG}
|
||||
data-active={selected === "dark"}
|
||||
onClick={() =>
|
||||
selected !== "dark" && setTheme({ preset: "dark" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.dark" />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
const selected = props.settings.theme?.preset ?? "dark";
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.theme" />
|
||||
</h3>
|
||||
<div className={styles.themes}>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={lightSVG}
|
||||
data-active={selected === "light"}
|
||||
onClick={() =>
|
||||
selected !== "light" &&
|
||||
setTheme({ preset: "light" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.light" />
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={darkSVG}
|
||||
data-active={selected === "dark"}
|
||||
onClick={() =>
|
||||
selected !== "dark" && setTheme({ preset: "dark" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.dark" />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches value={theme.accent} onChange={setAccent} />
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches value={theme.accent} onChange={setAccent} />
|
||||
|
||||
{/*<h3>
|
||||
{/*<h3>
|
||||
<Text id="app.settings.pages.appearance.message_display" />
|
||||
</h3>
|
||||
<div className={styles.display}>
|
||||
@@ -146,229 +146,229 @@ export function Component(props: Props) {
|
||||
</Radio>
|
||||
</div>*/}
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.font ?? DEFAULT_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({ custom: { font: e.currentTarget.value as any } })
|
||||
}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<p>
|
||||
<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.ligatures" />
|
||||
</Checkbox>
|
||||
</p>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.font ?? DEFAULT_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({ custom: { font: e.currentTarget.value as any } })
|
||||
}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<p>
|
||||
<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.ligatures" />
|
||||
</Checkbox>
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||
</h3>
|
||||
<div className={styles.emojiPack}>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("mutant")}
|
||||
data-active={emojiPack === "mutant"}>
|
||||
<img src={mutantSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>
|
||||
Mutant Remix{" "}
|
||||
<a
|
||||
href="https://mutant.revolt.chat"
|
||||
target="_blank">
|
||||
(by Revolt)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("twemoji")}
|
||||
data-active={emojiPack === "twemoji"}>
|
||||
<img src={twemojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("openmoji")}
|
||||
data-active={emojiPack === "openmoji"}>
|
||||
<img src={openmojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("noto")}
|
||||
data-active={emojiPack === "noto"}>
|
||||
<img src={notoSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||
</h3>
|
||||
<div className={styles.emojiPack}>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("mutant")}
|
||||
data-active={emojiPack === "mutant"}>
|
||||
<img src={mutantSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>
|
||||
Mutant Remix{" "}
|
||||
<a
|
||||
href="https://mutant.revolt.chat"
|
||||
target="_blank">
|
||||
(by Revolt)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("twemoji")}
|
||||
data-active={emojiPack === "twemoji"}>
|
||||
<img src={twemojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("openmoji")}
|
||||
data-active={emojiPack === "openmoji"}>
|
||||
<img src={openmojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("noto")}
|
||||
data-active={emojiPack === "noto"}>
|
||||
<img src={notoSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
defaultValue={false}
|
||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.overrides" />
|
||||
</h3>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast onClick={() => setTheme({ custom: {} })}>
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
<Text id="app.settings.pages.appearance.export_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
setOverride(JSON.parse(text));
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
openScreen({
|
||||
id: "_input",
|
||||
question: (
|
||||
<Text id="app.settings.pages.appearance.import_theme" />
|
||||
),
|
||||
field: (
|
||||
<Text id="app.settings.pages.appearance.theme_data" />
|
||||
),
|
||||
callback: async (string) =>
|
||||
setOverride(JSON.parse(string)),
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_manual" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.overrides}>
|
||||
{(
|
||||
[
|
||||
"accent",
|
||||
"background",
|
||||
"foreground",
|
||||
"primary-background",
|
||||
"primary-header",
|
||||
"secondary-background",
|
||||
"secondary-foreground",
|
||||
"secondary-header",
|
||||
"tertiary-background",
|
||||
"tertiary-foreground",
|
||||
"block",
|
||||
"message-box",
|
||||
"mention",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover",
|
||||
] as const
|
||||
).map((x) => (
|
||||
<div className={styles.entry} key={x}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
style={{ backgroundColor: theme[x] }}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme[x]}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputBox
|
||||
className={styles.text}
|
||||
value={theme[x]}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
defaultValue={false}
|
||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.overrides" />
|
||||
</h3>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast onClick={() => setTheme({ custom: {} })}>
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
<Text id="app.settings.pages.appearance.export_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
setOverride(JSON.parse(text));
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
openScreen({
|
||||
id: "_input",
|
||||
question: (
|
||||
<Text id="app.settings.pages.appearance.import_theme" />
|
||||
),
|
||||
field: (
|
||||
<Text id="app.settings.pages.appearance.theme_data" />
|
||||
),
|
||||
callback: async (string) =>
|
||||
setOverride(JSON.parse(string)),
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_manual" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.overrides}>
|
||||
{(
|
||||
[
|
||||
"accent",
|
||||
"background",
|
||||
"foreground",
|
||||
"primary-background",
|
||||
"primary-header",
|
||||
"secondary-background",
|
||||
"secondary-foreground",
|
||||
"secondary-header",
|
||||
"tertiary-background",
|
||||
"tertiary-foreground",
|
||||
"block",
|
||||
"message-box",
|
||||
"mention",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover",
|
||||
] as const
|
||||
).map((x) => (
|
||||
<div className={styles.entry} key={x}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
style={{ backgroundColor: theme[x] }}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme[x]}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputBox
|
||||
className={styles.text}
|
||||
value={theme[x]}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.monoscapeFont ?? DEFAULT_MONO_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({
|
||||
custom: {
|
||||
monoscapeFont: e.currentTarget.value as any,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{MONOSCAPE_FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{
|
||||
MONOSCAPE_FONTS[
|
||||
key as keyof typeof MONOSCAPE_FONTS
|
||||
].name
|
||||
}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.monoscapeFont ?? DEFAULT_MONO_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({
|
||||
custom: {
|
||||
monoscapeFont: e.currentTarget.value as any,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{MONOSCAPE_FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{
|
||||
MONOSCAPE_FONTS[
|
||||
key as keyof typeof MONOSCAPE_FONTS
|
||||
].name
|
||||
}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
code
|
||||
value={css}
|
||||
onChange={(ev) => setCSS(ev.currentTarget.value)}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
code
|
||||
value={css}
|
||||
onChange={(ev) => setCSS(ev.currentTarget.value)}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Appearance = connectState(Component, (state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,52 +4,52 @@ import { Text } from "preact-i18n";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import {
|
||||
AVAILABLE_EXPERIMENTS,
|
||||
ExperimentOptions,
|
||||
AVAILABLE_EXPERIMENTS,
|
||||
ExperimentOptions,
|
||||
} from "../../../redux/reducers/experiments";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: ExperimentOptions;
|
||||
options?: ExperimentOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.experiments}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.experiments.features" />
|
||||
</h3>
|
||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||
<Checkbox
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "EXPERIMENTS_ENABLE"
|
||||
: "EXPERIMENTS_DISABLE",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.experiments.titles.${key}`} />
|
||||
<p>
|
||||
<Text
|
||||
id={`app.settings.pages.experiments.descriptions.${key}`}
|
||||
/>
|
||||
</p>
|
||||
</Checkbox>
|
||||
))}
|
||||
{AVAILABLE_EXPERIMENTS.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.settings.pages.experiments.not_available" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.experiments}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.experiments.features" />
|
||||
</h3>
|
||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||
<Checkbox
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "EXPERIMENTS_ENABLE"
|
||||
: "EXPERIMENTS_DISABLE",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.experiments.titles.${key}`} />
|
||||
<p>
|
||||
<Text
|
||||
id={`app.settings.pages.experiments.descriptions.${key}`}
|
||||
/>
|
||||
</p>
|
||||
</Checkbox>
|
||||
))}
|
||||
{AVAILABLE_EXPERIMENTS.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.settings.pages.experiments.not_available" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ExperimentsPage = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.experiments,
|
||||
};
|
||||
return {
|
||||
options: state.experiments,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,101 +10,101 @@ import Radio from "../../../components/ui/Radio";
|
||||
import TextArea from "../../../components/ui/TextArea";
|
||||
|
||||
export function Feedback() {
|
||||
const user = useSelf();
|
||||
const [other, setOther] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
|
||||
const [checked, setChecked] = useState<
|
||||
"Bug" | "Feature Request" | "__other_option__"
|
||||
>("Bug");
|
||||
const user = useSelf();
|
||||
const [other, setOther] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
|
||||
const [checked, setChecked] = useState<
|
||||
"Bug" | "Feature Request" | "__other_option__"
|
||||
>("Bug");
|
||||
|
||||
async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
|
||||
ev.preventDefault();
|
||||
setState("sending");
|
||||
async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
|
||||
ev.preventDefault();
|
||||
setState("sending");
|
||||
|
||||
await fetch(`https://workers.revolt.chat/feedback`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
checked,
|
||||
other,
|
||||
description,
|
||||
name: user?.username ?? "Unknown User",
|
||||
}),
|
||||
mode: "no-cors",
|
||||
});
|
||||
await fetch(`https://workers.revolt.chat/feedback`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
checked,
|
||||
other,
|
||||
description,
|
||||
name: user?.username ?? "Unknown User",
|
||||
}),
|
||||
mode: "no-cors",
|
||||
});
|
||||
|
||||
setState("sent");
|
||||
setChecked("Bug");
|
||||
setDescription("");
|
||||
setOther("");
|
||||
}
|
||||
setState("sent");
|
||||
setChecked("Bug");
|
||||
setDescription("");
|
||||
setOther("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className={styles.feedback} onSubmit={onSubmit}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.report" />
|
||||
</h3>
|
||||
<div className={styles.options}>
|
||||
<Radio
|
||||
checked={checked === "Bug"}
|
||||
disabled={state === "sending"}
|
||||
onSelect={() => setChecked("Bug")}>
|
||||
<Text id="app.settings.pages.feedback.bug" />
|
||||
</Radio>
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={checked === "Feature Request"}
|
||||
onSelect={() => setChecked("Feature Request")}>
|
||||
<Text id="app.settings.pages.feedback.feature" />
|
||||
</Radio>
|
||||
{(location.hostname === "vite.revolt.chat" ||
|
||||
location.hostname === "local.revolt.chat") && (
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={other === "Revite"}
|
||||
onSelect={() => {
|
||||
setChecked("__other_option__");
|
||||
setOther("Revite");
|
||||
}}>
|
||||
Issues with Revite
|
||||
</Radio>
|
||||
)}
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={
|
||||
checked === "__other_option__" && other !== "Revite"
|
||||
}
|
||||
onSelect={() => setChecked("__other_option__")}>
|
||||
<Localizer>
|
||||
<InputBox
|
||||
value={other}
|
||||
disabled={state === "sending"}
|
||||
name="entry.1151440373.other_option_response"
|
||||
onChange={(e) => setOther(e.currentTarget.value)}
|
||||
placeholder={
|
||||
(
|
||||
<Text id="app.settings.pages.feedback.other" />
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</Localizer>
|
||||
</Radio>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.describe" />
|
||||
</h3>
|
||||
<TextArea
|
||||
// maxRows={10}
|
||||
value={description}
|
||||
id="entry.685672624"
|
||||
disabled={state === "sending"}
|
||||
onChange={(ev) => setDescription(ev.currentTarget.value)}
|
||||
/>
|
||||
<p>
|
||||
<Button type="submit" contrast>
|
||||
<Text id="app.settings.pages.feedback.send" />
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<form className={styles.feedback} onSubmit={onSubmit}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.report" />
|
||||
</h3>
|
||||
<div className={styles.options}>
|
||||
<Radio
|
||||
checked={checked === "Bug"}
|
||||
disabled={state === "sending"}
|
||||
onSelect={() => setChecked("Bug")}>
|
||||
<Text id="app.settings.pages.feedback.bug" />
|
||||
</Radio>
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={checked === "Feature Request"}
|
||||
onSelect={() => setChecked("Feature Request")}>
|
||||
<Text id="app.settings.pages.feedback.feature" />
|
||||
</Radio>
|
||||
{(location.hostname === "vite.revolt.chat" ||
|
||||
location.hostname === "local.revolt.chat") && (
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={other === "Revite"}
|
||||
onSelect={() => {
|
||||
setChecked("__other_option__");
|
||||
setOther("Revite");
|
||||
}}>
|
||||
Issues with Revite
|
||||
</Radio>
|
||||
)}
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={
|
||||
checked === "__other_option__" && other !== "Revite"
|
||||
}
|
||||
onSelect={() => setChecked("__other_option__")}>
|
||||
<Localizer>
|
||||
<InputBox
|
||||
value={other}
|
||||
disabled={state === "sending"}
|
||||
name="entry.1151440373.other_option_response"
|
||||
onChange={(e) => setOther(e.currentTarget.value)}
|
||||
placeholder={
|
||||
(
|
||||
<Text id="app.settings.pages.feedback.other" />
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</Localizer>
|
||||
</Radio>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.describe" />
|
||||
</h3>
|
||||
<TextArea
|
||||
// maxRows={10}
|
||||
value={description}
|
||||
id="entry.685672624"
|
||||
disabled={state === "sending"}
|
||||
onChange={(ev) => setDescription(ev.currentTarget.value)}
|
||||
/>
|
||||
<p>
|
||||
<Button type="submit" contrast>
|
||||
<Text id="app.settings.pages.feedback.send" />
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import {
|
||||
Language,
|
||||
LanguageEntry,
|
||||
Languages as Langs,
|
||||
Language,
|
||||
LanguageEntry,
|
||||
Languages as Langs,
|
||||
} from "../../../context/Locale";
|
||||
|
||||
import Emoji from "../../../components/common/Emoji";
|
||||
@@ -15,77 +15,77 @@ import Checkbox from "../../../components/ui/Checkbox";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
type Props = {
|
||||
locale: Language;
|
||||
locale: Language;
|
||||
};
|
||||
|
||||
type Key = [string, LanguageEntry];
|
||||
|
||||
function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div className={styles.flag}>
|
||||
<Emoji size={42} emoji={lang.emoji} />
|
||||
</div>
|
||||
<span className={styles.description}>{lang.display}</span>
|
||||
</Checkbox>
|
||||
);
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div className={styles.flag}>
|
||||
<Emoji size={42} emoji={lang.emoji} />
|
||||
</div>
|
||||
<span className={styles.description}>{lang.display}</span>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.languages.a" />
|
||||
</span>{" "}
|
||||
<a
|
||||
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
|
||||
target="_blank">
|
||||
<Text id="app.settings.tips.languages.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.languages.a" />
|
||||
</span>{" "}
|
||||
<a
|
||||
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
|
||||
target="_blank">
|
||||
<Text id="app.settings.tips.languages.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Languages = connectState(Component, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,9 +9,9 @@ import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import {
|
||||
DEFAULT_SOUNDS,
|
||||
NotificationOptions,
|
||||
SoundOptions,
|
||||
DEFAULT_SOUNDS,
|
||||
NotificationOptions,
|
||||
SoundOptions,
|
||||
} from "../../../redux/reducers/settings";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
@@ -22,133 +22,133 @@ import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
options?: NotificationOptions;
|
||||
}
|
||||
|
||||
export function Component({ options }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Load current state of pushManager.
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker
|
||||
?.getRegistration()
|
||||
.then(async (registration) => {
|
||||
const sub = await registration?.pushManager?.getSubscription();
|
||||
setPushEnabled(sub !== null && sub !== undefined);
|
||||
});
|
||||
}, []);
|
||||
// Load current state of pushManager.
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker
|
||||
?.getRegistration()
|
||||
.then(async (registration) => {
|
||||
const sub = await registration?.pushManager?.getSubscription();
|
||||
setPushEnabled(sub !== null && sub !== undefined);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const enabledSounds: SoundOptions = defaultsDeep(
|
||||
options?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.push_notifications" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
onChange={async (desktopEnabled) => {
|
||||
if (desktopEnabled) {
|
||||
let permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
error: "DeniedNotification",
|
||||
});
|
||||
}
|
||||
}
|
||||
const enabledSounds: SoundOptions = defaultsDeep(
|
||||
options?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.push_notifications" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
onChange={async (desktopEnabled) => {
|
||||
if (desktopEnabled) {
|
||||
let permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
error: "DeniedNotification",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
disabled={typeof pushEnabled === "undefined"}
|
||||
checked={pushEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
|
||||
}
|
||||
onChange={async (pushEnabled) => {
|
||||
try {
|
||||
const reg =
|
||||
await navigator.serviceWorker?.getRegistration();
|
||||
if (reg) {
|
||||
if (pushEnabled) {
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
client.configuration!.vapid,
|
||||
),
|
||||
});
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
disabled={typeof pushEnabled === "undefined"}
|
||||
checked={pushEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
|
||||
}
|
||||
onChange={async (pushEnabled) => {
|
||||
try {
|
||||
const reg =
|
||||
await navigator.serviceWorker?.getRegistration();
|
||||
if (reg) {
|
||||
if (pushEnabled) {
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
client.configuration!.vapid,
|
||||
),
|
||||
});
|
||||
|
||||
// tell the server we just subscribed
|
||||
const json = sub.toJSON();
|
||||
if (json.keys) {
|
||||
client.req("POST", "/push/subscribe", {
|
||||
endpoint: sub.endpoint,
|
||||
...(json.keys as {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
}),
|
||||
});
|
||||
setPushEnabled(true);
|
||||
}
|
||||
} else {
|
||||
const sub =
|
||||
await reg.pushManager.getSubscription();
|
||||
sub?.unsubscribe();
|
||||
setPushEnabled(false);
|
||||
// tell the server we just subscribed
|
||||
const json = sub.toJSON();
|
||||
if (json.keys) {
|
||||
client.req("POST", "/push/subscribe", {
|
||||
endpoint: sub.endpoint,
|
||||
...(json.keys as {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
}),
|
||||
});
|
||||
setPushEnabled(true);
|
||||
}
|
||||
} else {
|
||||
const sub =
|
||||
await reg.pushManager.getSubscription();
|
||||
sub?.unsubscribe();
|
||||
setPushEnabled(false);
|
||||
|
||||
client.req("POST", "/push/unsubscribe");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to enable push!", err);
|
||||
}
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_push" />
|
||||
</Checkbox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
<Checkbox
|
||||
checked={enabledSounds[key] ? true : false}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: {
|
||||
sounds: {
|
||||
...options?.sounds,
|
||||
[key]: enabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Text
|
||||
id={`app.settings.pages.notifications.sound.${key}`}
|
||||
/>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
client.req("POST", "/push/unsubscribe");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to enable push!", err);
|
||||
}
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_push" />
|
||||
</Checkbox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
<Checkbox
|
||||
checked={enabledSounds[key] ? true : false}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: {
|
||||
sounds: {
|
||||
...options?.sounds,
|
||||
[key]: enabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Text
|
||||
id={`app.settings.pages.notifications.sound.${key}`}
|
||||
/>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Notifications = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,178 +9,178 @@ import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
|
||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
|
||||
import AutoComplete, {
|
||||
useAutoComplete,
|
||||
useAutoComplete,
|
||||
} from "../../../components/common/AutoComplete";
|
||||
import Button from "../../../components/ui/Button";
|
||||
|
||||
export function Profile() {
|
||||
const { intl } = useContext(IntlContext);
|
||||
const status = useContext(StatusContext);
|
||||
const { intl } = useContext(IntlContext);
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf();
|
||||
if (!user) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf();
|
||||
if (!user) return null;
|
||||
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// ! FIXME: temporary solution
|
||||
// ! we should just announce profile changes through WS
|
||||
function refreshProfile() {
|
||||
ctx.client.users
|
||||
.fetchProfile(user!._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
// ! FIXME: temporary solution
|
||||
// ! we should just announce profile changes through WS
|
||||
function refreshProfile() {
|
||||
ctx.client.users
|
||||
.fetchProfile(user!._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
refreshProfile();
|
||||
}
|
||||
}, [status]);
|
||||
useEffect(() => {
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
refreshProfile();
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function setContent(content?: string) {
|
||||
setProfile({ ...profile, content });
|
||||
if (!changed) setChanged(true);
|
||||
}
|
||||
const [changed, setChanged] = useState(false);
|
||||
function setContent(content?: string) {
|
||||
setProfile({ ...profile, content });
|
||||
if (!changed) setChanged(true);
|
||||
}
|
||||
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete(setContent, {
|
||||
users: { type: "all" },
|
||||
});
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete(setContent, {
|
||||
users: { type: "all" },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<h3>
|
||||
<Text id="app.special.modals.actions.preview" />
|
||||
</h3>
|
||||
<div className={styles.preview}>
|
||||
<UserProfile
|
||||
user_id={user._id}
|
||||
dummy={true}
|
||||
dummyProfile={profile}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.pfp}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.profile_picture" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="avatars"
|
||||
behaviour="upload"
|
||||
maxFileSize={4_000_000}
|
||||
onUpload={(avatar) =>
|
||||
ctx.client.users.editUser({ avatar })
|
||||
}
|
||||
remove={() =>
|
||||
ctx.client.users.editUser({ remove: "Avatar" })
|
||||
}
|
||||
defaultPreview={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
previewURL={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.background}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.custom_background" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={92}
|
||||
style="banner"
|
||||
behaviour="upload"
|
||||
fileType="backgrounds"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={async (background) => {
|
||||
await ctx.client.users.editUser({
|
||||
profile: { background },
|
||||
});
|
||||
refreshProfile();
|
||||
}}
|
||||
remove={async () => {
|
||||
await ctx.client.users.editUser({
|
||||
remove: "ProfileBackground",
|
||||
});
|
||||
setProfile({ ...profile, background: undefined });
|
||||
}}
|
||||
previewURL={
|
||||
profile?.background
|
||||
? ctx.client.users.getBackgroundURL(
|
||||
profile,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.info" />
|
||||
</h3>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={200}
|
||||
maxLength={2000}
|
||||
value={profile?.content ?? ""}
|
||||
disabled={typeof profile === "undefined"}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
placeholder={translate(
|
||||
`app.settings.pages.profile.${
|
||||
typeof profile === "undefined"
|
||||
? "fetching"
|
||||
: "placeholder"
|
||||
}`,
|
||||
"",
|
||||
(intl as any).dictionary as Record<string, unknown>,
|
||||
)}
|
||||
onKeyUp={onKeyUp}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<p>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
setChanged(false);
|
||||
ctx.client.users.editUser({
|
||||
profile: { content: profile?.content },
|
||||
});
|
||||
}}
|
||||
disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<h3>
|
||||
<Text id="app.special.modals.actions.preview" />
|
||||
</h3>
|
||||
<div className={styles.preview}>
|
||||
<UserProfile
|
||||
user_id={user._id}
|
||||
dummy={true}
|
||||
dummyProfile={profile}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.pfp}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.profile_picture" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="avatars"
|
||||
behaviour="upload"
|
||||
maxFileSize={4_000_000}
|
||||
onUpload={(avatar) =>
|
||||
ctx.client.users.editUser({ avatar })
|
||||
}
|
||||
remove={() =>
|
||||
ctx.client.users.editUser({ remove: "Avatar" })
|
||||
}
|
||||
defaultPreview={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
previewURL={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.background}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.custom_background" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={92}
|
||||
style="banner"
|
||||
behaviour="upload"
|
||||
fileType="backgrounds"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={async (background) => {
|
||||
await ctx.client.users.editUser({
|
||||
profile: { background },
|
||||
});
|
||||
refreshProfile();
|
||||
}}
|
||||
remove={async () => {
|
||||
await ctx.client.users.editUser({
|
||||
remove: "ProfileBackground",
|
||||
});
|
||||
setProfile({ ...profile, background: undefined });
|
||||
}}
|
||||
previewURL={
|
||||
profile?.background
|
||||
? ctx.client.users.getBackgroundURL(
|
||||
profile,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.info" />
|
||||
</h3>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={200}
|
||||
maxLength={2000}
|
||||
value={profile?.content ?? ""}
|
||||
disabled={typeof profile === "undefined"}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
placeholder={translate(
|
||||
`app.settings.pages.profile.${
|
||||
typeof profile === "undefined"
|
||||
? "fetching"
|
||||
: "placeholder"
|
||||
}`,
|
||||
"",
|
||||
(intl as any).dictionary as Record<string, unknown>,
|
||||
)}
|
||||
onKeyUp={onKeyUp}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<p>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
setChanged(false);
|
||||
ctx.client.users.editUser({
|
||||
profile: { content: profile?.content },
|
||||
});
|
||||
}}
|
||||
disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { HelpCircle } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Android,
|
||||
Firefoxbrowser,
|
||||
Googlechrome,
|
||||
Ios,
|
||||
Linux,
|
||||
Macos,
|
||||
Microsoftedge,
|
||||
Safari,
|
||||
Windows,
|
||||
Android,
|
||||
Firefoxbrowser,
|
||||
Googlechrome,
|
||||
Ios,
|
||||
Linux,
|
||||
Macos,
|
||||
Microsoftedge,
|
||||
Safari,
|
||||
Windows,
|
||||
} from "@styled-icons/simple-icons";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
@@ -28,159 +28,159 @@ import Tip from "../../../components/ui/Tip";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
friendly_name: string;
|
||||
id: string;
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export function Sessions() {
|
||||
const client = useContext(AppContext);
|
||||
const deviceId = client.session?.id;
|
||||
const client = useContext(AppContext);
|
||||
const deviceId = client.session?.id;
|
||||
|
||||
const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
|
||||
const [attemptingDelete, setDelete] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
|
||||
const [attemptingDelete, setDelete] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
client.req("GET", "/auth/sessions").then((data) => {
|
||||
data.sort(
|
||||
(a, b) =>
|
||||
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
|
||||
);
|
||||
setSessions(data);
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
client.req("GET", "/auth/sessions").then((data) => {
|
||||
data.sort(
|
||||
(a, b) =>
|
||||
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
|
||||
);
|
||||
setSessions(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (typeof sessions === "undefined") {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (typeof sessions === "undefined") {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /firefox/i.test(name):
|
||||
return <Firefoxbrowser />;
|
||||
case /chrome/i.test(name):
|
||||
return <Googlechrome />;
|
||||
case /safari/i.test(name):
|
||||
return <Safari />;
|
||||
case /edge/i.test(name):
|
||||
return <Microsoftedge />;
|
||||
default:
|
||||
return <HelpCircle />;
|
||||
}
|
||||
}
|
||||
function getIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /firefox/i.test(name):
|
||||
return <Firefoxbrowser />;
|
||||
case /chrome/i.test(name):
|
||||
return <Googlechrome />;
|
||||
case /safari/i.test(name):
|
||||
return <Safari />;
|
||||
case /edge/i.test(name):
|
||||
return <Microsoftedge />;
|
||||
default:
|
||||
return <HelpCircle />;
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /linux/i.test(name):
|
||||
return <Linux />;
|
||||
case /android/i.test(name):
|
||||
return <Android />;
|
||||
case /mac.*os/i.test(name):
|
||||
return <Macos />;
|
||||
case /ios/i.test(name):
|
||||
return <Ios />;
|
||||
case /windows/i.test(name):
|
||||
return <Windows />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getSystemIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /linux/i.test(name):
|
||||
return <Linux />;
|
||||
case /android/i.test(name):
|
||||
return <Android />;
|
||||
case /mac.*os/i.test(name):
|
||||
return <Macos />;
|
||||
case /ios/i.test(name):
|
||||
return <Ios />;
|
||||
case /windows/i.test(name):
|
||||
return <Windows />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = sessions.map((session) => {
|
||||
return {
|
||||
...session,
|
||||
timestamp: decodeTime(session.id),
|
||||
};
|
||||
});
|
||||
const mapped = sessions.map((session) => {
|
||||
return {
|
||||
...session,
|
||||
timestamp: decodeTime(session.id),
|
||||
};
|
||||
});
|
||||
|
||||
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
||||
let id = mapped.findIndex((x) => x.id === deviceId);
|
||||
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
||||
let id = mapped.findIndex((x) => x.id === deviceId);
|
||||
|
||||
const render = [
|
||||
mapped[id],
|
||||
...mapped.slice(0, id),
|
||||
...mapped.slice(id + 1, mapped.length),
|
||||
];
|
||||
const render = [
|
||||
mapped[id],
|
||||
...mapped.slice(0, id),
|
||||
...mapped.slice(id + 1, mapped.length),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.sessions}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sessions.active_sessions" />
|
||||
</h3>
|
||||
{render.map((session) => (
|
||||
<div
|
||||
className={styles.entry}
|
||||
data-active={session.id === deviceId}
|
||||
data-deleting={attemptingDelete.indexOf(session.id) > -1}>
|
||||
{deviceId === session.id && (
|
||||
<span className={styles.label}>
|
||||
<Text id="app.settings.pages.sessions.this_device" />{" "}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.session}>
|
||||
<div className={styles.icon}>
|
||||
{getIcon(session)}
|
||||
<div>{getSystemIcon(session)}</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.name}>
|
||||
{session.friendly_name}
|
||||
</span>
|
||||
<span className={styles.time}>
|
||||
<Text
|
||||
id="app.settings.pages.sessions.created"
|
||||
fields={{
|
||||
time_ago: dayjs(
|
||||
session.timestamp,
|
||||
).fromNow(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{deviceId !== session.id && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDelete([
|
||||
...attemptingDelete,
|
||||
session.id,
|
||||
]);
|
||||
await client.req(
|
||||
"DELETE",
|
||||
`/auth/sessions/${session.id}` as "/auth/sessions",
|
||||
);
|
||||
setSessions(
|
||||
sessions?.filter(
|
||||
(x) => x.id !== session.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
attemptingDelete.indexOf(session.id) > -1
|
||||
}>
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.sessions.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("account")}>
|
||||
<Text id="app.settings.tips.sessions.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.sessions}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sessions.active_sessions" />
|
||||
</h3>
|
||||
{render.map((session) => (
|
||||
<div
|
||||
className={styles.entry}
|
||||
data-active={session.id === deviceId}
|
||||
data-deleting={attemptingDelete.indexOf(session.id) > -1}>
|
||||
{deviceId === session.id && (
|
||||
<span className={styles.label}>
|
||||
<Text id="app.settings.pages.sessions.this_device" />{" "}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.session}>
|
||||
<div className={styles.icon}>
|
||||
{getIcon(session)}
|
||||
<div>{getSystemIcon(session)}</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.name}>
|
||||
{session.friendly_name}
|
||||
</span>
|
||||
<span className={styles.time}>
|
||||
<Text
|
||||
id="app.settings.pages.sessions.created"
|
||||
fields={{
|
||||
time_ago: dayjs(
|
||||
session.timestamp,
|
||||
).fromNow(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{deviceId !== session.id && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDelete([
|
||||
...attemptingDelete,
|
||||
session.id,
|
||||
]);
|
||||
await client.req(
|
||||
"DELETE",
|
||||
`/auth/sessions/${session.id}` as "/auth/sessions",
|
||||
);
|
||||
setSessions(
|
||||
sessions?.filter(
|
||||
(x) => x.id !== session.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
attemptingDelete.indexOf(session.id) > -1
|
||||
}>
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.sessions.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("account")}>
|
||||
<Text id="app.settings.tips.sessions.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,49 +8,49 @@ import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: SyncOptions;
|
||||
options?: SyncOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sync.categories" />
|
||||
</h3>
|
||||
{(
|
||||
[
|
||||
["appearance", "appearance.title"],
|
||||
["theme", "appearance.theme"],
|
||||
["locale", "language.title"],
|
||||
// notifications sync is always-on
|
||||
] as [SyncKeys, string][]
|
||||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sync.categories" />
|
||||
</h3>
|
||||
{(
|
||||
[
|
||||
["appearance", "appearance.title"],
|
||||
["theme", "appearance.theme"],
|
||||
["locale", "language.title"],
|
||||
// notifications sync is always-on
|
||||
] as [SyncKeys, string][]
|
||||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sync = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,31 +7,31 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Bans({ server }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
|
||||
const client = useContext(AppContext);
|
||||
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
client.servers.fetchBans(server._id).then((bans) => setBans(bans));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
client.servers.fetchBans(server._id).then((bans) => setBans(bans));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
import { getChannelName } from "../../../context/revoltjs/util";
|
||||
|
||||
@@ -17,70 +17,70 @@ import IconButton from "../../../components/ui/IconButton";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Invites({ server }: Props) {
|
||||
const [invites, setInvites] = useState<
|
||||
InvitesNS.ServerInvite[] | undefined
|
||||
>(undefined);
|
||||
const [invites, setInvites] = useState<
|
||||
InvitesNS.ServerInvite[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx);
|
||||
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
|
||||
const ctx = useForceUpdate();
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx);
|
||||
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.servers
|
||||
.fetchInvites(server._id)
|
||||
.then((invites) => setInvites(invites));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
ctx.client.servers
|
||||
.fetchInvites(server._id)
|
||||
.then((invites) => setInvites(invites));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.invites}>
|
||||
<div className={styles.subtitle}>
|
||||
<span>Invite Code</span>
|
||||
<span>Invitor</span>
|
||||
<span>Channel</span>
|
||||
<span>Revoke</span>
|
||||
</div>
|
||||
{typeof invites === "undefined" && <Preloader type="ring" />}
|
||||
{invites?.map((invite) => {
|
||||
let creator = users.find((x) => x?._id === invite.creator);
|
||||
let channel = channels.find((x) => x?._id === invite.channel);
|
||||
return (
|
||||
<div className={styles.invites}>
|
||||
<div className={styles.subtitle}>
|
||||
<span>Invite Code</span>
|
||||
<span>Invitor</span>
|
||||
<span>Channel</span>
|
||||
<span>Revoke</span>
|
||||
</div>
|
||||
{typeof invites === "undefined" && <Preloader type="ring" />}
|
||||
{invites?.map((invite) => {
|
||||
let creator = users.find((x) => x?._id === invite.creator);
|
||||
let channel = channels.find((x) => x?._id === invite.channel);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={creator} size={24} />{" "}
|
||||
{creator?.username ?? "unknown"}
|
||||
</span>
|
||||
<span>
|
||||
{channel && creator
|
||||
? getChannelName(ctx.client, channel, true)
|
||||
: "#unknown"}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, invite._id]);
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={creator} size={24} />{" "}
|
||||
{creator?.username ?? "unknown"}
|
||||
</span>
|
||||
<span>
|
||||
{channel && creator
|
||||
? getChannelName(ctx.client, channel, true)
|
||||
: "#unknown"}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, invite._id]);
|
||||
|
||||
await ctx.client.deleteInvite(invite._id);
|
||||
await ctx.client.deleteInvite(invite._id);
|
||||
|
||||
setInvites(
|
||||
invites?.filter(
|
||||
(x) => x._id !== invite._id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={deleting.indexOf(invite._id) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
setInvites(
|
||||
invites?.filter(
|
||||
(x) => x._id !== invite._id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={deleting.indexOf(invite._id) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,39 +6,39 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
// ! FIXME: bad code :)
|
||||
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 users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
|
||||
const ctx = useForceUpdate();
|
||||
const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.servers.members
|
||||
.fetchMembers(server._id)
|
||||
.then((members) => setMembers(members));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
ctx.client.servers.members
|
||||
.fetchMembers(server._id)
|
||||
.then((members) => setMembers(members));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.members}>
|
||||
<div className={styles.subtitle}>
|
||||
{members?.length ?? 0} Members
|
||||
</div>
|
||||
{members &&
|
||||
members.length > 0 &&
|
||||
users?.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div className={styles.member}>
|
||||
<div>@{x.username}</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.members}>
|
||||
<div className={styles.subtitle}>
|
||||
{members?.length ?? 0} Members
|
||||
</div>
|
||||
{members &&
|
||||
members.length > 0 &&
|
||||
users?.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div className={styles.member}>
|
||||
<div>@{x.username}</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,178 +16,178 @@ import ComboBox from "../../../components/ui/ComboBox";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Overview({ server }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [name, setName] = useState(server.name);
|
||||
const [description, setDescription] = useState(server.description ?? "");
|
||||
const [systemMessages, setSystemMessages] = useState(
|
||||
server.system_messages,
|
||||
);
|
||||
const [name, setName] = useState(server.name);
|
||||
const [description, setDescription] = useState(server.description ?? "");
|
||||
const [systemMessages, setSystemMessages] = useState(
|
||||
server.system_messages,
|
||||
);
|
||||
|
||||
useEffect(() => setName(server.name), [server.name]);
|
||||
useEffect(
|
||||
() => setDescription(server.description ?? ""),
|
||||
[server.description],
|
||||
);
|
||||
useEffect(
|
||||
() => setSystemMessages(server.system_messages),
|
||||
[server.system_messages],
|
||||
);
|
||||
useEffect(() => setName(server.name), [server.name]);
|
||||
useEffect(
|
||||
() => setDescription(server.description ?? ""),
|
||||
[server.description],
|
||||
);
|
||||
useEffect(
|
||||
() => setSystemMessages(server.system_messages),
|
||||
[server.system_messages],
|
||||
);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: Partial<
|
||||
Pick<Servers.Server, "name" | "description" | "system_messages">
|
||||
> = {};
|
||||
if (name !== server.name) changes.name = name;
|
||||
if (description !== server.description)
|
||||
changes.description = description;
|
||||
if (!isEqual(systemMessages, server.system_messages))
|
||||
changes.system_messages = systemMessages;
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: Partial<
|
||||
Pick<Servers.Server, "name" | "description" | "system_messages">
|
||||
> = {};
|
||||
if (name !== server.name) changes.name = name;
|
||||
if (description !== server.description)
|
||||
changes.description = description;
|
||||
if (!isEqual(systemMessages, server.system_messages))
|
||||
changes.system_messages = systemMessages;
|
||||
|
||||
client.servers.edit(server._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
client.servers.edit(server._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.servers.edit(server._id, { icon })
|
||||
}
|
||||
previewURL={client.servers.getIconURL(
|
||||
server._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.servers.edit(server._id, { remove: "Icon" })
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
<Text id="app.main.servers.name" />
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.servers.edit(server._id, { icon })
|
||||
}
|
||||
previewURL={client.servers.getIconURL(
|
||||
server._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.servers.edit(server._id, { remove: "Icon" })
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
<Text id="app.main.servers.name" />
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.main.servers.description" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a topic..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<h3>
|
||||
<Text id="app.main.servers.description" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a topic..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<h3>
|
||||
<Text id="app.main.servers.custom_banner" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={160}
|
||||
style="banner"
|
||||
fileType="banners"
|
||||
behaviour="upload"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={(banner) =>
|
||||
client.servers.edit(server._id, { banner })
|
||||
}
|
||||
previewURL={client.servers.getBannerURL(
|
||||
server._id,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.servers.edit(server._id, { remove: "Banner" })
|
||||
}
|
||||
/>
|
||||
<h3>
|
||||
<Text id="app.main.servers.custom_banner" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={160}
|
||||
style="banner"
|
||||
fileType="banners"
|
||||
behaviour="upload"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={(banner) =>
|
||||
client.servers.edit(server._id, { banner })
|
||||
}
|
||||
previewURL={client.servers.getBannerURL(
|
||||
server._id,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.servers.edit(server._id, { remove: "Banner" })
|
||||
}
|
||||
/>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.server_pages.overview.system_messages" />
|
||||
</h3>
|
||||
{[
|
||||
["User Joined", "user_joined"],
|
||||
["User Left", "user_left"],
|
||||
["User Kicked", "user_kicked"],
|
||||
["User Banned", "user_banned"],
|
||||
].map(([i18n, key]) => (
|
||||
// ! FIXME: temporary code just so we can expose the options
|
||||
<p
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
|
||||
<ComboBox
|
||||
value={
|
||||
systemMessages?.[
|
||||
key as keyof typeof systemMessages
|
||||
] ?? "disabled"
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (!changed) setChanged(true);
|
||||
const v = e.currentTarget.value;
|
||||
if (v === "disabled") {
|
||||
const {
|
||||
[key as keyof typeof systemMessages]: _,
|
||||
...other
|
||||
} = systemMessages;
|
||||
setSystemMessages(other);
|
||||
} else {
|
||||
setSystemMessages({
|
||||
...systemMessages,
|
||||
[key]: v,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<option value="disabled">
|
||||
<Text id="general.disabled" />
|
||||
</option>
|
||||
{server.channels.map((id) => {
|
||||
const channel = client.channels.get(id);
|
||||
if (!channel) return null;
|
||||
return (
|
||||
<option value={id}>
|
||||
{getChannelName(client, channel, true)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
</p>
|
||||
))}
|
||||
<h3>
|
||||
<Text id="app.settings.server_pages.overview.system_messages" />
|
||||
</h3>
|
||||
{[
|
||||
["User Joined", "user_joined"],
|
||||
["User Left", "user_left"],
|
||||
["User Kicked", "user_kicked"],
|
||||
["User Banned", "user_banned"],
|
||||
].map(([i18n, key]) => (
|
||||
// ! FIXME: temporary code just so we can expose the options
|
||||
<p
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
|
||||
<ComboBox
|
||||
value={
|
||||
systemMessages?.[
|
||||
key as keyof typeof systemMessages
|
||||
] ?? "disabled"
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (!changed) setChanged(true);
|
||||
const v = e.currentTarget.value;
|
||||
if (v === "disabled") {
|
||||
const {
|
||||
[key as keyof typeof systemMessages]: _,
|
||||
...other
|
||||
} = systemMessages;
|
||||
setSystemMessages(other);
|
||||
} else {
|
||||
setSystemMessages({
|
||||
...systemMessages,
|
||||
[key]: v,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<option value="disabled">
|
||||
<Text id="general.disabled" />
|
||||
</option>
|
||||
{server.channels.map((id) => {
|
||||
const channel = client.channels.get(id);
|
||||
if (!channel) return null;
|
||||
return (
|
||||
<option value={id}>
|
||||
{getChannelName(client, channel, true)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
</p>
|
||||
))}
|
||||
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { Servers } from "revolt.js/dist/api/objects";
|
||||
import {
|
||||
ChannelPermission,
|
||||
ServerPermission,
|
||||
ChannelPermission,
|
||||
ServerPermission,
|
||||
} from "revolt.js/dist/api/permissions";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
@@ -23,157 +23,157 @@ import Tip from "../../../components/ui/Tip";
|
||||
import ButtonItem from "../../../components/navigation/items/ButtonItem";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
|
||||
|
||||
// ! FIXME: bad code :)
|
||||
export function Roles({ server }: Props) {
|
||||
const [role, setRole] = useState("default");
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const roles = server.roles ?? {};
|
||||
const [role, setRole] = useState("default");
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const roles = server.roles ?? {};
|
||||
|
||||
if (role !== "default" && typeof roles[role] === "undefined") {
|
||||
useEffect(() => setRole("default"));
|
||||
return null;
|
||||
}
|
||||
if (role !== "default" && typeof roles[role] === "undefined") {
|
||||
useEffect(() => setRole("default"));
|
||||
return null;
|
||||
}
|
||||
|
||||
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 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 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);
|
||||
};
|
||||
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}>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.title}>
|
||||
<h1>
|
||||
<Text id="app.settings.server_pages.roles.title" />
|
||||
</h1>
|
||||
<Plus
|
||||
size={22}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_role",
|
||||
server: server._id,
|
||||
callback: (id) => setRole(id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{["default", ...Object.keys(roles)].map((id) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.permissions}>
|
||||
<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 (
|
||||
<div className={styles.roles}>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.title}>
|
||||
<h1>
|
||||
<Text id="app.settings.server_pages.roles.title" />
|
||||
</h1>
|
||||
<Plus
|
||||
size={22}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_role",
|
||||
server: server._id,
|
||||
callback: (id) => setRole(id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{["default", ...Object.keys(roles)].map((id) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.permissions}>
|
||||
<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={(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={(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={((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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user