Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modal): Port ServerIdentity (closes #112) #250

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 345 additions & 0 deletions components/modal/modals/ServerIdentity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
import { BiRegularX, BiSolidEditAlt, BiSolidSave } from "solid-icons/bi";
import { Show, createSignal } from "solid-js";

import { ServerMember } from "revolt.js";

import { clientController } from "@revolt/client";
import { useTranslation } from "@revolt/i18n";
import { userInformation } from "@revolt/markdown/users";
import {
Avatar,
Button,
CategoryButton,
Column,
Input,
NonBreakingText,
OverflowingText,
Preloader,
Row,
Typography,
Username,
styled,
useTheme,
} from "@revolt/ui";
import { generateTypographyCSS } from "@revolt/ui/components/design/atoms/display/Typography";

import { PropGenerator } from "../types";

const [avatarId, setAvatarId] = createSignal(null);
const [uploading, setUploading] = createSignal<boolean>();

async function uploadToAutumn(file) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use File | Blob instead of not providing a type (aka. implicit any) since the FileList type is just an array of Files (See MDN's article on FileList: https://developer.mozilla.org/en-US/docs/Web/API/FileList)

Suggested change
async function uploadToAutumn(file) {
async function uploadToAutumn(file) {

setUploading(true);
try {
const url = "https://autumn.revolt.chat/avatars";
const formData = new FormData();
formData.append("file", file);

const response = await fetch(url, {
method: "POST",
headers: {
"x-session-token": `${clientController.getCurrentClient()?.sessionId}`,
},
body: formData,
});

const json = await response.json();
setAvatarId(json["id"]);
setUploading(false);

return json;
} catch (error) {
console.error("POST request error:", error);
setUploading(false);
}
}

/**
* Modal to display server identity
*/

const ServerIdentity: PropGenerator<"server_identity"> = (props) => {
const t = useTranslation();
const [nickname, setNickname] = createSignal<string>();

setUploading(false);

let fileInputRef: HTMLInputElement | null = null;
const openFilePicker = () => {
fileInputRef?.click();
};

const handleFileSelect = async (event: Event) => {
const file = event.target?.files[0];
const json = await uploadToAutumn(file);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const json = await uploadToAutumn(file);
const handleFileSelect = async (event: Event & {target: HTMLInputElement}) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const json = await uploadToAutumn(file);
const file = event.target.files![0];


if (json !== undefined) {
props.member.edit({ avatar: avatarId() });
}
};

return {
title: (
<Column grow>
<Typography variant="legacy-settings-title">
{t("app.special.popovers.server_identity.title", {
server: props.member.server?.name as string,
})}
</Typography>
</Column>
),
children: (
<>
<Column style={{ padding: "0.75em 0em" }}>
<Typography variant="label">
{t("app.special.popovers.server_identity.nickname")}
</Typography>

<Row align="center">
<Input
type="text"
value={props.member.nickname ?? ""}
placeholder={props.member.user?.displayName}
onInput={(event) => setNickname(event.currentTarget.value)}
/>

<Button
compact="icon"
palette="plain"
disabled={nickname() === props.member.nickname || !nickname()}
onClick={() => {
props.member.edit({ nickname: nickname() });
}}
>
<BiSolidSave size={24} />
</Button>

<Button
compact="icon"
palette="plain"
disabled={!props.member.nickname}
onClick={() => {
props.member.edit({ remove: ["Nickname"] });
setNickname("");
}}
>
<BiRegularX size={24} />
</Button>
</Row>
</Column>

<Row>
<Column>
<Typography variant="label">
{t("app.special.popovers.server_identity.avatar")}
</Typography>

<Column align="center">
<AvatarEdit member={props.member}></AvatarEdit>

<div onclick={openFilePicker} style={{ cursor: "pointer" }}>
<Show
when={props.member.avatarURL === props.member.user?.avatarURL}
>
<CategoryButton onClick={() => {}}>
{t("app.settings.actions.upload")}
<input
ref={fileInputRef!}
onChange={handleFileSelect}
type="file"
accept="image/*"
multiple={false}
style={{ display: "none" }}
max={4_000_000}
/>
</CategoryButton>
</Show>

<Show
when={props.member.avatarURL != props.member.user?.avatarURL}
>
<CategoryButton
onClick={() => {
props.member.edit({ remove: ["Avatar"] });
}}
>
{t("app.settings.actions.remove")}
</CategoryButton>
</Show>
</div>

<Typography variant="small">
{t("app.settings.actions.max_filesize", {
filesize: "4.00 MB",
})}
</Typography>
</Column>
</Column>

<Column grow style={{ "padding-left": "0.75em" }}>
<Typography variant="label">
{t("app.special.modals.actions.preview")}
</Typography>

<Preview member={props.member}></Preview>
</Column>
</Row>
</>
),
};
};

export default ServerIdentity;

function AvatarEdit(props: { member: ServerMember }) {
const [isHovered, setIsHovered] = createSignal(false);

function onMouseEnter() {
setIsHovered(true);
}

function onMouseLeave() {
setIsHovered(false);
}

const user = () => userInformation(props.member.user, props.member);

let fileInputRef: HTMLInputElement | null = null;
const openFilePicker = () => {
fileInputRef?.click();
};

const handleFileSelect = async (event: Event) => {
const file = event.target?.files[0];
const json = await uploadToAutumn(file);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target property in Event isn't guaranteed to be HTMLInputElement, so typescript doesn't know whether the files property actually exists or not and throws a error.

Suggested change
const json = await uploadToAutumn(file);
const handleFileSelect = async (event: Event & {target: HTMLInputElement}) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript says that files can be null, so it's better to use the non-null assertion operator to tell it to shut up.

Suggested change
const json = await uploadToAutumn(file);
const file = event.target?.files![0];


if (json !== undefined) {
props.member.edit({ avatar: avatarId() });
}
};
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onclick={openFilePicker}
style={{
cursor: "pointer",
position: "relative",
display: "flex",
"justify-content": "center",
"align-items": "center",
...(props.member.avatarURL === props.member.user?.avatarURL && {
filter: "grayscale(100%)",
}),
...(isHovered() &&
props.member.avatarURL === props.member.user?.avatarURL && {
filter: "grayscale(100%) contrast(70%)",
}),
...(isHovered() &&
props.member.avatarURL !== props.member.user?.avatarURL && {
filter: "contrast(70%)",
}),
}}
>
<div
style={{
position: "absolute",
display: isHovered() || uploading() ? "block" : "none",
}}
>
<span>
<Show when={uploading()}>
<Preloader type="ring"></Preloader>
</Show>

<Show when={!uploading()}>
{" "}
<BiSolidEditAlt size={42} alignment-baseline="central" />
</Show>
</span>
</div>

<Avatar src={user().avatar} size={86}></Avatar>

<input
ref={fileInputRef!}
onChange={handleFileSelect}
type="file"
accept="image/*"
multiple={false}
max={4_000_000}
style={{ display: "none" }}
/>
</div>
);
}

function Preview(props: { member: ServerMember }) {
const theme = useTheme();

/**
* Right-side message content
*/
const Content = styled(Column)`
gap: 3px;
min-width: 0;
overflow: hidden;
max-height: 200vh;
padding-inline-end: ${(props) => props.theme!.gap.lg};
`;

/**
* Information text
*/
const InfoText = styled(Row)`
color: ${(props) => props.theme!.colours["foreground-400"]};
${(props) => generateTypographyCSS(props.theme!, "small")}
`;

const user = () => userInformation(props.member.user, props.member);

return (
<Column
align="center"
justify="center"
grow
style={{
background: theme.colours.background,
"border-radius": theme.borderRadius.lg,
padding: "2px, 0",
"margin-top": "12px",
}}
>
<Row style={{ "padding-left": "16px" }}>
<Avatar src={user().avatar} size={32} interactive></Avatar>

<Content align="start">
<Row
gap="sm"
align
style={{
"max-width": "250px",
overflow: "hidden",
"white-space": "nowrap",
"text-overflow": "ellipsis",
}}
>
<OverflowingText>
<Username
username={
props.member.nickname ?? props.member.user?.displayName
}
></Username>
</OverflowingText>
<NonBreakingText>
<InfoText gap="sm" align>
<Typography variant="small">Today at 19:00</Typography>
</InfoText>
</NonBreakingText>
</Row>
content
</Content>
</Row>
</Column>
);
}
2 changes: 1 addition & 1 deletion components/modal/modals/ServerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const ServerInfo: PropGenerator<"server_info"> = (props, onClose) => {
});
return true;
},
children: "Edit Identity",
children: t("app.context_menu.edit_identity"),
palette: "secondary",
},
{
Expand Down
2 changes: 2 additions & 0 deletions components/modal/modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import mfa_flow from "./MFAFlow";
import mfa_recovery from "./MFARecovery";
import onboarding from "./Onboarding";
import rename_session from "./RenameSession";
import server_identity from "./ServerIdentity";
import server_info from "./ServerInfo";
import settings from "./Settings";
import sign_out_sessions from "./SignOutSessions";
Expand Down Expand Up @@ -60,6 +61,7 @@ const Modals: Record<AllModals["type"], PropGenerator<any>> = {
onboarding,
rename_session,
server_info,
server_identity,
settings,
signed_out,
sign_out_sessions,
Expand Down
Loading