mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
feat: port UserProfile, Onboarding, CreateBot to legacy
This commit is contained in:
@@ -5,6 +5,8 @@ import { state } from "../../mobx/State";
|
||||
|
||||
import { __thisIsAHack } from "../../context/intermediate/Intermediate";
|
||||
|
||||
import { modalController } from "../modals/ModalController";
|
||||
|
||||
/**
|
||||
* Current lifecycle state
|
||||
*/
|
||||
@@ -189,8 +191,8 @@ export default class Session {
|
||||
);
|
||||
|
||||
if (onboarding) {
|
||||
__thisIsAHack({
|
||||
id: "onboarding",
|
||||
modalController.push({
|
||||
type: "onboarding",
|
||||
callback: async (username: string) =>
|
||||
this.client!.completeOnboarding(
|
||||
{ username },
|
||||
|
||||
@@ -16,7 +16,6 @@ import { getApplicationState } from "../../mobx/State";
|
||||
import { history } from "../../context/history";
|
||||
import { __thisIsAHack } from "../../context/intermediate/Intermediate";
|
||||
|
||||
// import { determineLink } from "../../lib/links";
|
||||
import Changelog from "./components/Changelog";
|
||||
import ChannelInfo from "./components/ChannelInfo";
|
||||
import Clipboard from "./components/Clipboard";
|
||||
@@ -34,6 +33,10 @@ import ServerInfo from "./components/ServerInfo";
|
||||
import ShowToken from "./components/ShowToken";
|
||||
import SignOutSessions from "./components/SignOutSessions";
|
||||
import SignedOut from "./components/SignedOut";
|
||||
import { UserPicker } from "./components/UserPicker";
|
||||
import { CreateBotModal } from "./components/legacy/CreateBot";
|
||||
import { OnboardingModal } from "./components/legacy/Onboarding";
|
||||
import { UserProfile } from "./components/legacy/UserProfile";
|
||||
import { Modal } from "./types";
|
||||
|
||||
type Components = Record<string, React.FC<any>>;
|
||||
@@ -191,7 +194,7 @@ class ModalControllerExtended extends ModalController<Modal> {
|
||||
|
||||
switch (link.type) {
|
||||
case "profile": {
|
||||
__thisIsAHack({ id: "profile", user_id: link.id });
|
||||
this.push({ type: "user_profile", user_id: link.id });
|
||||
break;
|
||||
}
|
||||
case "navigate": {
|
||||
@@ -222,6 +225,7 @@ export const modalController = new ModalControllerExtended({
|
||||
changelog: Changelog,
|
||||
channel_info: ChannelInfo,
|
||||
clipboard: Clipboard,
|
||||
create_bot: CreateBotModal,
|
||||
error: Error,
|
||||
image_viewer: ImageViewer,
|
||||
link_warning: LinkWarning,
|
||||
@@ -229,6 +233,7 @@ export const modalController = new ModalControllerExtended({
|
||||
mfa_recovery: MFARecovery,
|
||||
mfa_enable_totp: MFAEnableTOTP,
|
||||
modify_account: ModifyAccount,
|
||||
onboarding: OnboardingModal,
|
||||
out_of_date: OutOfDate,
|
||||
pending_friend_requests: PendingFriendRequests,
|
||||
server_identity: ServerIdentity,
|
||||
@@ -236,4 +241,6 @@ export const modalController = new ModalControllerExtended({
|
||||
show_token: ShowToken,
|
||||
signed_out: SignedOut,
|
||||
sign_out_sessions: SignOutSessions,
|
||||
user_picker: UserPicker,
|
||||
user_profile: UserProfile,
|
||||
});
|
||||
|
||||
68
src/controllers/modals/components/UserPicker.tsx
Normal file
68
src/controllers/modals/components/UserPicker.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
||||
import { useClient } from "../../client/ClientController";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
const List = styled.div`
|
||||
max-width: 100%;
|
||||
max-height: 360px;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
export function UserPicker({
|
||||
callback,
|
||||
omit,
|
||||
...props
|
||||
}: ModalProps<"user_picker">) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const omitted = useMemo(
|
||||
() => new Set([...(omit || []), "00000000000000000000000000"]),
|
||||
[omit],
|
||||
);
|
||||
|
||||
const client = useClient();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id="app.special.popovers.user_picker.select" />}
|
||||
actions={[
|
||||
{
|
||||
children: <Text id="app.special.modals.actions.ok" />,
|
||||
onClick: () => callback(selected).then(() => true),
|
||||
},
|
||||
]}>
|
||||
<List>
|
||||
{[...client.users.values()]
|
||||
.filter(
|
||||
(x) =>
|
||||
x &&
|
||||
x.relationship === "Friend" &&
|
||||
!omitted.has(x._id),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserCheckbox
|
||||
key={x._id}
|
||||
user={x}
|
||||
value={selected.includes(x._id)}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
setSelected([...selected, x._id]);
|
||||
} else {
|
||||
setSelected(
|
||||
selected.filter((y) => y !== x._id),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
86
src/controllers/modals/components/legacy/CreateBot.tsx
Normal file
86
src/controllers/modals/components/legacy/CreateBot.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { API } from "revolt.js";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { Category, Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../../lib/js";
|
||||
|
||||
import { I18nError } from "../../../../context/Locale";
|
||||
import { takeError } from "../../../../context/revoltjs/util";
|
||||
|
||||
import FormField from "../../../../pages/login/FormField";
|
||||
import { useClient } from "../../../client/ClientController";
|
||||
import { modalController } from "../../ModalController";
|
||||
import { ModalProps } from "../../types";
|
||||
|
||||
interface FormInputs {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function CreateBotModal({
|
||||
onCreate,
|
||||
...props
|
||||
}: ModalProps<"create_bot">) {
|
||||
const client = useClient();
|
||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSubmit: SubmitHandler<FormInputs> = async ({ name }) => {
|
||||
try {
|
||||
const { bot } = await client.bots.create({ name });
|
||||
onCreate(bot);
|
||||
modalController.close();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id="app.special.popovers.create_bot.title" />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
palette: "accent",
|
||||
onClick: async () => {
|
||||
await handleSubmit(onSubmit)();
|
||||
return true;
|
||||
},
|
||||
children: <Text id="app.special.modals.actions.create" />,
|
||||
},
|
||||
{
|
||||
palette: "plain",
|
||||
onClick: noopTrue,
|
||||
children: <Text id="app.special.modals.actions.cancel" />,
|
||||
},
|
||||
]}>
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
)(e as any);
|
||||
}}>
|
||||
<FormField
|
||||
type="username"
|
||||
name="name"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.name?.message}
|
||||
/>
|
||||
{error && (
|
||||
<Category>
|
||||
<Text id="app.special.popovers.create_bot.failed" />{" "}
|
||||
· <I18nError error={error} />
|
||||
</Category>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.onboarding {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: var(--background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
|
||||
&.header {
|
||||
gap: 8px;
|
||||
padding: 3em;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
&.form {
|
||||
flex-grow: 1;
|
||||
max-width: 420px;
|
||||
|
||||
img {
|
||||
margin: auto;
|
||||
display: block;
|
||||
max-height: 420px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
margin: 24px 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/controllers/modals/components/legacy/Onboarding.tsx
Normal file
79
src/controllers/modals/components/legacy/Onboarding.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
import styles from "./Onboarding.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { Button, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { takeError } from "../../../../context/revoltjs/util";
|
||||
|
||||
import wideSVG from "/assets/wide.svg";
|
||||
|
||||
import FormField from "../../../../pages/login/FormField";
|
||||
import { ModalProps } from "../../types";
|
||||
|
||||
interface FormInputs {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export function OnboardingModal({
|
||||
callback,
|
||||
...props
|
||||
}: ModalProps<"onboarding">) {
|
||||
const { handleSubmit, register } = useForm<FormInputs>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
||||
setLoading(true);
|
||||
callback(username, true)
|
||||
.then(() => props.onClose())
|
||||
.catch((err: unknown) => {
|
||||
setError(takeError(err));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.onboarding}>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
<Text id="app.special.modals.onboarding.welcome" />
|
||||
<br />
|
||||
<img src={wideSVG} loading="eager" />
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
{loading ? (
|
||||
<Preloader type="spinner" />
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<Text id="app.special.modals.onboarding.pick" />
|
||||
</p>
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
) as unknown as JSX.GenericEventHandler<HTMLFormElement>
|
||||
}>
|
||||
<div>
|
||||
<FormField
|
||||
type="username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">
|
||||
<Text id="app.special.modals.actions.continue" />
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
177
src/controllers/modals/components/legacy/UserProfile.module.scss
Normal file
177
src/controllers/modals/components/legacy/UserProfile.module.scss
Normal file
@@ -0,0 +1,177 @@
|
||||
.modal {
|
||||
height: 460px;
|
||||
display: flex;
|
||||
padding: 0 !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-size: cover;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
background-position: center;
|
||||
background-color: var(--secondary-background);
|
||||
|
||||
&[data-force="light"] {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[data-force="dark"] {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.profile {
|
||||
gap: 16px;
|
||||
width: 560px;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
> svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
* {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 13px;
|
||||
|
||||
> div {
|
||||
display: inline !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
padding: 0 1.5em;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
||||
> div {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-bottom 0.3s;
|
||||
|
||||
&[data-active="true"] {
|
||||
border-bottom: 2px solid var(--foreground);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover:not([data-active="true"]) {
|
||||
border-bottom: 2px solid var(--tertiary-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: 8px;
|
||||
min-height: 240px;
|
||||
max-height: 240px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 1em 1.5em;
|
||||
|
||||
max-width: 560px;
|
||||
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
background: var(--primary-background);
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
|
||||
.markdown {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--tertiary-foreground);
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
> span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entries {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
gap: 12px;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s;
|
||||
color: var(--foreground);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--secondary-background);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-half);
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
443
src/controllers/modals/components/legacy/UserProfile.tsx
Normal file
443
src/controllers/modals/components/legacy/UserProfile.tsx
Normal file
@@ -0,0 +1,443 @@
|
||||
import { ListUl } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Envelope,
|
||||
Edit,
|
||||
UserPlus,
|
||||
UserX,
|
||||
Group,
|
||||
InfoCircle,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { UserPermission, API } from "revolt.js";
|
||||
|
||||
import styles from "./UserProfile.module.scss";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Category,
|
||||
Error,
|
||||
IconButton,
|
||||
Modal,
|
||||
Preloader,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import { noop } from "../../../../lib/js";
|
||||
|
||||
import ChannelIcon from "../../../../components/common/ChannelIcon";
|
||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||
import Tooltip from "../../../../components/common/Tooltip";
|
||||
import UserBadges from "../../../../components/common/user/UserBadges";
|
||||
import UserIcon from "../../../../components/common/user/UserIcon";
|
||||
import { Username } from "../../../../components/common/user/UserShort";
|
||||
import UserStatus from "../../../../components/common/user/UserStatus";
|
||||
import Markdown from "../../../../components/markdown/Markdown";
|
||||
import { useSession } from "../../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
import { ModalProps } from "../../types";
|
||||
|
||||
export const UserProfile = observer(
|
||||
({
|
||||
user_id,
|
||||
dummy,
|
||||
dummyProfile,
|
||||
...props
|
||||
}: ModalProps<"user_profile">) => {
|
||||
const [profile, setProfile] = useState<
|
||||
undefined | null | API.UserProfile
|
||||
>(undefined);
|
||||
const [mutual, setMutual] = useState<
|
||||
undefined | null | API.MutualResponse
|
||||
>(undefined);
|
||||
const [isPublicBot, setIsPublicBot] = useState<
|
||||
undefined | null | boolean
|
||||
>();
|
||||
|
||||
const history = useHistory();
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
const [tab, setTab] = useState("profile");
|
||||
|
||||
const user = client.users.get(user_id);
|
||||
if (!user) {
|
||||
if (props.onClose) useEffect(props.onClose, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
const users = mutual?.users.map((id) => client.users.get(id));
|
||||
|
||||
const mutualGroups = [...client.channels.values()].filter(
|
||||
(channel) =>
|
||||
channel?.channel_type === "Group" &&
|
||||
channel.recipient_ids!.includes(user_id),
|
||||
);
|
||||
|
||||
const mutualServers = mutual?.servers.map((id) =>
|
||||
client.servers.get(id),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!user_id) return;
|
||||
if (typeof profile !== "undefined") setProfile(undefined);
|
||||
if (typeof mutual !== "undefined") setMutual(undefined);
|
||||
if (typeof isPublicBot !== "undefined") setIsPublicBot(undefined);
|
||||
// eslint-disable-next-line
|
||||
}, [user_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dummy) {
|
||||
setProfile(dummyProfile);
|
||||
}
|
||||
}, [dummy, dummyProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (session.state === "Online" && typeof mutual === "undefined") {
|
||||
setMutual(null);
|
||||
user.fetchMutual().then(setMutual);
|
||||
}
|
||||
}, [mutual, session.state, dummy, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (session.state === "Online" && typeof profile === "undefined") {
|
||||
setProfile(null);
|
||||
|
||||
if (user.permission & UserPermission.ViewProfile) {
|
||||
user.fetchProfile().then(setProfile).catch(noop);
|
||||
}
|
||||
}
|
||||
}, [profile, session.state, dummy, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
session.state === "Online" &&
|
||||
user.bot &&
|
||||
typeof isPublicBot === "undefined"
|
||||
) {
|
||||
setIsPublicBot(null);
|
||||
client.bots
|
||||
.fetchPublic(user._id)
|
||||
.then(() => setIsPublicBot(true))
|
||||
.catch(noop);
|
||||
}
|
||||
}, [isPublicBot, session.state, user, client.bots]);
|
||||
|
||||
const backgroundURL =
|
||||
profile &&
|
||||
client.generateFileURL(
|
||||
profile.background as any,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
);
|
||||
|
||||
const badges = user.badges ?? 0;
|
||||
const flags = user.flags ?? 0;
|
||||
|
||||
const children = (
|
||||
<>
|
||||
<div
|
||||
className={styles.header}
|
||||
data-force={profile?.background ? "light" : undefined}
|
||||
style={{
|
||||
backgroundImage:
|
||||
backgroundURL &&
|
||||
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
|
||||
paddingBottom: "1px",
|
||||
}}>
|
||||
<div className={styles.profile}>
|
||||
<UserIcon
|
||||
size={80}
|
||||
target={user}
|
||||
status
|
||||
animate
|
||||
hover={typeof user.avatar !== "undefined"}
|
||||
onClick={() =>
|
||||
user.avatar &&
|
||||
modalController.push({
|
||||
type: "image_viewer",
|
||||
attachment: user.avatar,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className={styles.details}>
|
||||
<Localizer>
|
||||
<span
|
||||
className={styles.username}
|
||||
onClick={() =>
|
||||
modalController.writeText(user.username)
|
||||
}>
|
||||
@{user.username}
|
||||
</span>
|
||||
</Localizer>
|
||||
{user.status?.text && (
|
||||
<span className={styles.status}>
|
||||
<UserStatus user={user} tooltip />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isPublicBot && (
|
||||
<Link to={`/bot/${user._id}`}>
|
||||
<Button
|
||||
palette="accent"
|
||||
compact
|
||||
onClick={props.onClose}>
|
||||
Add to server
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{user.relationship === "Friend" && (
|
||||
<Localizer>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.context_menu.message_user" />
|
||||
}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
props.onClose?.();
|
||||
history.push(`/open/${user_id}`);
|
||||
}}>
|
||||
<Envelope size={30} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Localizer>
|
||||
)}
|
||||
{user.relationship === "User" && !dummy && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
props.onClose?.();
|
||||
history.push(`/settings/profile`);
|
||||
}}>
|
||||
<Edit size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
{!user.bot &&
|
||||
flags != 2 &&
|
||||
flags != 4 &&
|
||||
(user.relationship === "Incoming" ||
|
||||
user.relationship === "None" ||
|
||||
user.relationship === null) && (
|
||||
<IconButton onClick={() => user.addFriend()}>
|
||||
<UserPlus size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
{user.relationship === "Outgoing" && (
|
||||
<IconButton onClick={() => user.removeFriend()}>
|
||||
<UserX size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.tabs}>
|
||||
<div
|
||||
data-active={tab === "profile"}
|
||||
onClick={() => setTab("profile")}>
|
||||
<Text id="app.special.popovers.user_profile.profile" />
|
||||
</div>
|
||||
{user.relationship !== "User" && (
|
||||
<>
|
||||
<div
|
||||
data-active={tab === "friends"}
|
||||
onClick={() => setTab("friends")}>
|
||||
<Text id="app.special.popovers.user_profile.mutual_friends" />
|
||||
</div>
|
||||
<div
|
||||
data-active={tab === "groups"}
|
||||
onClick={() => setTab("groups")}>
|
||||
<Text id="app.special.popovers.user_profile.mutual_groups" />
|
||||
</div>
|
||||
<div
|
||||
data-active={tab === "servers"}
|
||||
onClick={() => setTab("servers")}>
|
||||
<Text id="app.special.popovers.user_profile.mutual_servers" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{tab === "profile" &&
|
||||
(profile?.content ||
|
||||
badges > 0 ||
|
||||
flags > 0 ||
|
||||
user.bot ? (
|
||||
<div>
|
||||
{flags & 1 ? (
|
||||
/** ! FIXME: i18n this area */
|
||||
<Category>
|
||||
<Error error="User is suspended" />
|
||||
</Category>
|
||||
) : undefined}
|
||||
{flags & 2 ? (
|
||||
<Category>
|
||||
<Error error="User deleted their account" />
|
||||
</Category>
|
||||
) : undefined}
|
||||
{flags & 4 ? (
|
||||
<Category>
|
||||
<Error error="User is banned" />
|
||||
</Category>
|
||||
) : undefined}
|
||||
{user.bot ? (
|
||||
<>
|
||||
<div className={styles.category}>
|
||||
bot owner
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
user.bot &&
|
||||
modalController.push({
|
||||
type: "user_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 && (
|
||||
<div className={styles.category}>
|
||||
<Text id="app.special.popovers.user_profile.sub.badges" />
|
||||
</div>
|
||||
)}
|
||||
{badges > 0 && (
|
||||
<UserBadges
|
||||
badges={badges}
|
||||
uid={user._id}
|
||||
/>
|
||||
)}
|
||||
{profile?.content && (
|
||||
<div className={styles.category}>
|
||||
<Text id="app.special.popovers.user_profile.sub.information" />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.markdown}>
|
||||
<Markdown content={profile?.content} />
|
||||
</div>
|
||||
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<InfoCircle size={72} />
|
||||
<Text id="app.special.popovers.user_profile.empty" />
|
||||
</div>
|
||||
))}
|
||||
{tab === "friends" &&
|
||||
(users ? (
|
||||
users.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<UserPlus size={72} />
|
||||
<Text id="app.special.popovers.user_profile.no_users" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.entries}>
|
||||
{users.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div
|
||||
onClick={() =>
|
||||
modalController.push({
|
||||
type: "user_profile",
|
||||
user_id: x._id,
|
||||
})
|
||||
}
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<UserIcon
|
||||
size={32}
|
||||
target={x}
|
||||
status
|
||||
/>
|
||||
<span>{x.username}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Preloader type="ring" />
|
||||
))}
|
||||
{tab === "groups" &&
|
||||
(mutualGroups.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<Group size="72" />
|
||||
<Text id="app.special.popovers.user_profile.no_groups" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.entries}>
|
||||
{mutualGroups.map(
|
||||
(x) =>
|
||||
x?.channel_type === "Group" && (
|
||||
<Link to={`/channel/${x._id}`}>
|
||||
<div
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<ChannelIcon
|
||||
target={x}
|
||||
size={32}
|
||||
/>
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{tab === "servers" &&
|
||||
(!mutualServers || mutualServers.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<ListUl size="72" />
|
||||
<Text id="app.special.popovers.user_profile.no_servers" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.entries}>
|
||||
{mutualServers.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<Link to={`/server/${x._id}`}>
|
||||
<div
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<ServerIcon
|
||||
target={x}
|
||||
size={32}
|
||||
/>
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (dummy) return <div>{children}</div>;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
nonDismissable={dummy}
|
||||
transparent
|
||||
maxWidth="560px">
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -85,6 +85,28 @@ export type Modal = {
|
||||
embed?: API.Image;
|
||||
attachment?: API.File;
|
||||
}
|
||||
| {
|
||||
type: "user_picker";
|
||||
omit?: string[];
|
||||
callback: (users: string[]) => Promise<void>;
|
||||
}
|
||||
| {
|
||||
type: "user_profile";
|
||||
user_id: string;
|
||||
dummy?: boolean;
|
||||
dummyProfile?: API.UserProfile;
|
||||
}
|
||||
| {
|
||||
type: "create_bot";
|
||||
onCreate: (bot: API.Bot) => void;
|
||||
}
|
||||
| {
|
||||
type: "onboarding";
|
||||
callback: (
|
||||
username: string,
|
||||
loginAfterSuccess?: true,
|
||||
) => Promise<void>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
||||
|
||||
Reference in New Issue
Block a user