Merge branch 'master' of https://github.com/revoltchat/revite into pr-5

This commit is contained in:
janderedev 2021-08-12 17:52:38 +02:00
commit 3d707a64da
No known key found for this signature in database
GPG key ID: 5D5E18ACB990F57A
12 changed files with 437 additions and 91 deletions

2
external/lang vendored

@ -1 +1 @@
Subproject commit 7be90cf44ba08d235ae52d7dc6073d8f9347232b Subproject commit b27044c332ca419d22edbe9e7bf465a665398999

View file

@ -115,8 +115,8 @@
"react-virtualized-auto-sizer": "^1.0.5", "react-virtualized-auto-sizer": "^1.0.5",
"react-virtuoso": "^1.10.4", "react-virtuoso": "^1.10.4",
"redux": "^4.1.0", "redux": "^4.1.0",
"revolt-api": "^0.5.2-alpha.0", "revolt-api": "0.5.2-alpha.1",
"revolt.js": "5.0.0-alpha.21", "revolt.js": "5.0.1-alpha.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0", "shade-blend-color": "^1.0.0",

View file

@ -1,6 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -9,6 +10,21 @@ import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "./UserIcon"; import UserIcon from "./UserIcon";
const BotBadge = styled.div`
display: inline-block;
height: 1.4em;
padding: 0 4px;
font-size: 0.6em;
user-select: none;
margin-inline-start: 2px;
text-transform: uppercase;
color: var(--foreground);
background: var(--accent);
border-radius: calc(var(--border-radius) / 2);
`;
export const Username = observer( export const Username = observer(
({ ({
user, user,
@ -51,6 +67,21 @@ export const Username = observer(
} }
} }
if (user?.bot) {
return (
<>
<span {...otherProps} style={{ color }}>
{username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<BotBadge>
<Text id="app.main.channel.bot" />
</BotBadge>
</>
);
}
return ( return (
<span {...otherProps} style={{ color }}> <span {...otherProps} style={{ color }}>
{username ?? <Text id="app.main.channel.unknown_user" />} {username ?? <Text id="app.main.channel.unknown_user" />}

View file

@ -13,7 +13,7 @@ import InputBox from "../../ui/InputBox";
import Overline from "../../ui/Overline"; import Overline from "../../ui/Overline";
import Preloader from "../../ui/Preloader"; import Preloader from "../../ui/Preloader";
import { GenericSidebarBase } from "../SidebarBase"; import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
type SearchState = type SearchState =
| { | {
@ -100,57 +100,59 @@ export function SearchSidebar({ close }: Props) {
return ( return (
<GenericSidebarBase> <GenericSidebarBase>
<SearchBase> <GenericSidebarList>
<Overline type="error" onClick={close} block> <SearchBase>
« back to members <Overline type="error" onClick={close} block>
</Overline> « back to members
<Overline type="subtle" block> </Overline>
<Text id="app.main.channel.search.title" /> <Overline type="subtle" block>
</Overline> <Text id="app.main.channel.search.title" />
<InputBox </Overline>
value={query} <InputBox
onKeyDown={(e) => e.key === "Enter" && search()} value={query}
onChange={(e) => setQuery(e.currentTarget.value)} onKeyDown={(e) => e.key === "Enter" && search()}
/> onChange={(e) => setQuery(e.currentTarget.value)}
<div class="sort"> />
{["Latest", "Oldest", "Relevance"].map((key) => ( <div class="sort">
<Button {["Latest", "Oldest", "Relevance"].map((key) => (
key={key} <Button
compact key={key}
error={sort === key} compact
onClick={() => setSort(key as Sort)}> error={sort === key}
<Text onClick={() => setSort(key as Sort)}>
id={`app.main.channel.search.sort.${key.toLowerCase()}`} <Text
/> id={`app.main.channel.search.sort.${key.toLowerCase()}`}
</Button> />
))} </Button>
</div> ))}
{state.type === "loading" && <Preloader type="ring" />}
{state.type === "results" && (
<div class="list">
{state.results.map((message) => {
let href = "";
if (channel?.channel_type === "TextChannel") {
href += `/server/${channel.server_id}`;
}
href += `/channel/${message.channel_id}/${message._id}`;
return (
<Link to={href} key={message._id}>
<div class="message">
<Message
message={message}
head
hideReply
/>
</div>
</Link>
);
})}
</div> </div>
)} {state.type === "loading" && <Preloader type="ring" />}
</SearchBase> {state.type === "results" && (
<div class="list">
{state.results.map((message) => {
let href = "";
if (channel?.channel_type === "TextChannel") {
href += `/server/${channel.server_id}`;
}
href += `/channel/${message.channel_id}/${message._id}`;
return (
<Link to={href} key={message._id}>
<div class="message">
<Message
message={message}
head
hideReply
/>
</div>
</Link>
);
})}
</div>
)}
</SearchBase>
</GenericSidebarList>
</GenericSidebarBase> </GenericSidebarBase>
); );
} }

View file

@ -7,6 +7,7 @@ import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import Overline from "../../../components/ui/Overline"; import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
import { FileUploader } from "../../revoltjs/FileUploads"; import { FileUploader } from "../../revoltjs/FileUploads";
import { useClient } from "../../revoltjs/RevoltClient"; import { useClient } from "../../revoltjs/RevoltClient";
@ -30,6 +31,9 @@ export const ServerIdentityModal = observer(({ server, onClose }: Props) => {
return ( return (
<Modal visible={true} onClose={onClose}> <Modal visible={true} onClose={onClose}>
<Tip warning hideSeparator>
This section is under construction.
</Tip>
<Overline type="subtle">Nickname</Overline> <Overline type="subtle">Nickname</Overline>
<p> <p>
<InputBox <InputBox

View file

@ -136,34 +136,34 @@
a { a {
min-width: 0; min-width: 0;
} }
}
.entry { .entry {
gap: 8px; gap: 8px;
min-width: 0;
padding: 12px;
display: flex;
cursor: pointer;
align-items: center;
transition: background-color 0.1s;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background);
&:hover {
background-color: var(--primary-background);
}
img {
width: 32px;
height: 32px;
border-radius: 50%;
}
span {
min-width: 0; min-width: 0;
padding: 12px; overflow: hidden;
display: flex; white-space: nowrap;
cursor: pointer; text-overflow: ellipsis;
align-items: center;
transition: background-color 0.1s;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background);
&:hover {
background-color: var(--primary-background);
}
img {
width: 32px;
height: 32px;
border-radius: 50%;
}
span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
} }
} }

View file

@ -14,9 +14,11 @@ import ChannelIcon from "../../../components/common/ChannelIcon";
import ServerIcon from "../../../components/common/ServerIcon"; import ServerIcon from "../../../components/common/ServerIcon";
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import { Username } from "../../../components/common/user/UserShort";
import UserStatus from "../../../components/common/user/UserStatus"; import UserStatus from "../../../components/common/user/UserStatus";
import IconButton from "../../../components/ui/IconButton"; import IconButton from "../../../components/ui/IconButton";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import Overline from "../../../components/ui/Overline";
import Preloader from "../../../components/ui/Preloader"; import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
@ -118,7 +120,9 @@ export const UserProfile = observer(
const backgroundURL = const backgroundURL =
profile && profile &&
client.generateFileURL(profile.background, { width: 1000 }, true); client.generateFileURL(profile.background, { width: 1000 }, true);
const badges = user.badges ?? 0; const badges = user.badges ?? 0;
const flags = user.flags ?? 0;
return ( return (
<Modal <Modal
@ -229,11 +233,63 @@ export const UserProfile = observer(
<div className={styles.content}> <div className={styles.content}>
{tab === "profile" && ( {tab === "profile" && (
<div> <div>
{!(profile?.content || badges > 0) && ( {!(
profile?.content ||
badges > 0 ||
flags > 0 ||
user.bot
) && (
<div className={styles.empty}> <div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" /> <Text id="app.special.popovers.user_profile.empty" />
</div> </div>
)} )}
{flags & 1 ? (
/** ! FIXME: i18n this area */
<Overline type="error" block>
User is suspended
</Overline>
) : undefined}
{flags & 2 ? (
<Overline type="error" block>
User deleted their account
</Overline>
) : undefined}
{flags & 4 ? (
<Overline type="error" block>
User is banned
</Overline>
) : undefined}
{user.bot ? (
<>
<div className={styles.category}>
bot owner
</div>
<div
onClick={() =>
user.bot &&
openScreen({
id: "profile",
user_id: user.bot.owner,
})
}
className={styles.entry}
key={user.bot.owner}>
<UserIcon
size={32}
target={client.users.get(
user.bot.owner,
)}
/>
<span>
<Username
user={client.users.get(
user.bot.owner,
)}
/>
</span>
</div>
</>
) : undefined}
{badges > 0 && ( {badges > 0 && (
<div className={styles.category}> <div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.badges" /> <Text id="app.special.popovers.user_profile.sub.badges" />

View file

@ -20,6 +20,7 @@ import Developer from "./developer/Developer";
import Friends from "./friends/Friends"; import Friends from "./friends/Friends";
import Home from "./home/Home"; import Home from "./home/Home";
import Invite from "./invite/Invite"; import Invite from "./invite/Invite";
import InviteBot from "./invite/InviteBot";
import ChannelSettings from "./settings/ChannelSettings"; import ChannelSettings from "./settings/ChannelSettings";
import ServerSettings from "./settings/ServerSettings"; import ServerSettings from "./settings/ServerSettings";
import Settings from "./settings/Settings"; import Settings from "./settings/Settings";
@ -119,6 +120,7 @@ export default function App() {
<Route path="/dev" component={Developer} /> <Route path="/dev" component={Developer} />
<Route path="/friends" component={Friends} /> <Route path="/friends" component={Friends} />
<Route path="/open/:id" component={Open} /> <Route path="/open/:id" component={Open} />
<Route path="/bot/:id" component={InviteBot} />
<Route path="/invite/:code" component={Invite} /> <Route path="/invite/:code" component={Invite} />
<Route path="/" component={Home} /> <Route path="/" component={Home} />
</Switch> </Switch>

View file

@ -0,0 +1,80 @@
import { useParams } from "react-router-dom";
import { Route } from "revolt.js/dist/api/routes";
import { useEffect, useState } from "preact/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon";
import Button from "../../components/ui/Button";
import ComboBox from "../../components/ui/ComboBox";
import Overline from "../../components/ui/Overline";
import Preloader from "../../components/ui/Preloader";
export default function InviteBot() {
const { id } = useParams<{ id: string }>();
const client = useClient();
const [data, setData] =
useState<Route<"GET", "/bots/id/invite">["response"]>();
useEffect(() => {
client.bots.fetchPublic(id).then(setData);
// eslint-disable-next-line
}, []);
const [server, setServer] = useState("none");
const [group, setGroup] = useState("none");
return (
<div style={{ padding: "6em" }}>
{typeof data === "undefined" && <Preloader type="spinner" />}
{data && (
<>
<UserIcon size={64} attachment={data.avatar} />
<h1>{data.username}</h1>
{data.description && <p>{data.description}</p>}
<Overline type="subtle">Add to server</Overline>
<ComboBox
value={server}
onChange={(e) => setServer(e.currentTarget.value)}>
<option value="none">un selected</option>
{[...client.servers.values()].map((server) => (
<option value={server._id} key={server._id}>
{server.name}
</option>
))}
</ComboBox>
<Button
contrast
onClick={() =>
group !== "none" &&
client.bots.invite(data._id, { server })
}>
add
</Button>
<Overline type="subtle">Add to group</Overline>
<ComboBox
value={group}
onChange={(e) => setGroup(e.currentTarget.value)}>
<option value="none">un selected</option>
{[...client.channels.values()]
.filter((x) => x.channel_type === "Group")
.map((channel) => (
<option value={channel._id} key={channel._id}>
{channel.name}
</option>
))}
</ComboBox>
<Button
contrast
onClick={() =>
group !== "none" &&
client.bots.invite(data._id, { group })
}>
add
</Button>
</>
)}
</div>
);
}

View file

@ -4,6 +4,7 @@ import {
Globe, Globe,
LogOut, LogOut,
Desktop, Desktop,
Bot,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { import {
Bell, Bell,
@ -39,6 +40,7 @@ import { Appearance } from "./panes/Appearance";
import { ExperimentsPage } from "./panes/Experiments"; import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback"; import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages"; import { Languages } from "./panes/Languages";
import { MyBots } from "./panes/MyBots";
import { Native } from "./panes/Native"; import { Native } from "./panes/Native";
import { Notifications } from "./panes/Notifications"; import { Notifications } from "./panes/Notifications";
import { Profile } from "./panes/Profile"; import { Profile } from "./panes/Profile";
@ -109,11 +111,17 @@ export default function Settings() {
title: <Text id="app.settings.pages.native.title" />, title: <Text id="app.settings.pages.native.title" />,
}, },
{ {
divider: true,
id: "experiments", id: "experiments",
icon: <Flask size={20} />, icon: <Flask size={20} />,
title: <Text id="app.settings.pages.experiments.title" />, title: <Text id="app.settings.pages.experiments.title" />,
}, },
{
divider: true,
category: "revolt",
id: "bots",
icon: <Bot size={20} />,
title: <Text id="app.settings.pages.bots.title" />,
},
{ {
id: "feedback", id: "feedback",
icon: <Megaphone size={20} />, icon: <Megaphone size={20} />,
@ -148,6 +156,9 @@ export default function Settings() {
<Route path="/settings/experiments"> <Route path="/settings/experiments">
<ExperimentsPage /> <ExperimentsPage />
</Route> </Route>
<Route path="/settings/bots">
<MyBots />
</Route>
<Route path="/settings/feedback"> <Route path="/settings/feedback">
<Feedback /> <Feedback />
</Route> </Route>

View file

@ -0,0 +1,160 @@
import { observer } from "mobx-react-lite";
import { Bot } from "revolt-api/types/Bots";
import { useEffect, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserShort from "../../../components/common/user/UserShort";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
interface Data {
_id: string;
username: string;
public: boolean;
interactions_url?: string;
}
function BotEditor({ bot }: { bot: Data }) {
const client = useClient();
const [data, setData] = useState<Data>(bot);
function save() {
const changes: Record<string, string | boolean | undefined> = {};
if (data.username !== bot.username) changes.name = data.username;
if (data.public !== bot.public) changes.public = data.public;
if (data.interactions_url !== bot.interactions_url)
changes.interactions_url = data.interactions_url;
client.bots.edit(bot._id, changes);
}
return (
<div>
<p>
<InputBox
value={data.username}
onChange={(e) =>
setData({ ...data, username: e.currentTarget.value })
}
/>
</p>
<p>
<Checkbox
checked={data.public}
onChange={(v) => setData({ ...data, public: v })}>
is public
</Checkbox>
</p>
<p>interactions url: (reserved for the future)</p>
<p>
<InputBox
value={data.interactions_url}
onChange={(e) =>
setData({
...data,
interactions_url: e.currentTarget.value,
})
}
/>
</p>
<Button onClick={save}>save</Button>
</div>
);
}
export const MyBots = observer(() => {
const client = useClient();
const [bots, setBots] = useState<Bot[] | undefined>(undefined);
useEffect(() => {
client.bots.fetchOwned().then(({ bots }) => setBots(bots));
// eslint-disable-next-line
}, []);
const [name, setName] = useState("");
const { writeClipboard } = useIntermediate();
return (
<div>
<Tip warning hideSeparator>
This section is under construction.
</Tip>
<Overline>create a new bot</Overline>
<p>
<InputBox
value={name}
contrast
onChange={(e) => setName(e.currentTarget.value)}
/>
</p>
<p>
<Button
contrast
onClick={() =>
name.length > 0 &&
client.bots
.create({ name })
.then(({ bot }) => setBots([...(bots ?? []), bot]))
}>
create
</Button>
</p>
<Overline>my bots</Overline>
{bots?.map((bot) => {
const user = client.users.get(bot._id);
return (
<div
key={bot._id}
style={{
background: "var(--secondary-background)",
margin: "8px",
padding: "12px",
}}>
<UserShort user={user} />
<p>
token:{" "}
<code style={{ userSelect: "all" }}>
{bot.token}
</code>
</p>
<BotEditor
bot={{
...bot,
username: user!.username,
}}
/>
<Button
error
onClick={() =>
client.bots
.delete(bot._id)
.then(() =>
setBots(
bots.filter(
(x) => x._id !== bot._id,
),
),
)
}>
delete
</Button>
<Button
onClick={() =>
writeClipboard(
`${window.origin}/bot/${bot._id}`,
)
}>
copy invite link
</Button>
</div>
);
})}
</div>
);
});

View file

@ -3600,15 +3600,15 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
revolt-api@^0.5.2-alpha.0: revolt-api@0.5.2-alpha.1:
version "0.5.2-alpha.0" version "0.5.2-alpha.1"
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.2-alpha.0.tgz#a41f44ee38622636c9b5b5843f9e2798a79f00d3" resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.2-alpha.1.tgz#2164d04cd5581267ce59142557666bd386bc85c4"
integrity sha512-VI/o4nQTPXrDCVdFpZFfZfj7Q4nunj62gftdmYJtuSmXx+6eN2Nve7QQZjNt6UIH6Dc/IDgiFDcBdafBF9YXug== integrity sha512-3OrjYCDNPkJ+yO9d87NJvuUDAbungEbUfrfHlvFwV8hJze/RMkuYUTFWe1HyBMwBC7F/yWQK+2V7IoifC5STmw==
revolt.js@5.0.0-alpha.21: revolt.js@5.0.1-alpha.3:
version "5.0.0-alpha.21" version "5.0.1-alpha.3"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.0.0-alpha.21.tgz#24e01dbcb2887dadcb480732a1b9b8f167c557b5" resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.0.1-alpha.3.tgz#986d2ec21d751067d95c4f444f81b922df566cde"
integrity sha512-UNRJRCyKoOFKULRYIWFZ3QN4th6s/sgMpQXtqaitVMtVBo6BJJvUT9wUM3WV08pN1acr3EPwnVre6sOtKM7khg== integrity sha512-h1xlaBvKyTS+wF9Oe4rtjuTe5plrOpYMp9qskqxMeNIoVu9VuJjHU+n9YUWANbgn7Ji9sxPHZrco5+0+bLOCcg==
dependencies: dependencies:
axios "^0.19.2" axios "^0.19.2"
eventemitter3 "^4.0.7" eventemitter3 "^4.0.7"