-
-
Notifications
You must be signed in to change notification settings - Fork 60
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||||||||||
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); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
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); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typescript says that
Suggested change
|
||||||||||
|
||||||||||
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> | ||||||||||
); | ||||||||||
} |
There was a problem hiding this comment.
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. implicitany
) since theFileList
type is just an array ofFile
s (See MDN's article on FileList: https://developer.mozilla.org/en-US/docs/Web/API/FileList)