1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 02:12:38 +01:00

implement kick/ban logic

This commit is contained in:
Puyodead1 2023-12-18 11:01:46 -05:00
parent c048563b53
commit 9506da25c8
No known key found for this signature in database
GPG Key ID: BA5F91AAEF68E5CE
8 changed files with 212 additions and 44 deletions

View File

@ -5,14 +5,15 @@ import styled, { css } from "styled-components";
export interface Props {
readonly compact?: boolean | "icon";
palette?: "primary" | "secondary" | "success" | "warning" | "danger" | "accent" | "link";
size?: "small" | "medium" | "large";
readonly disabled?: boolean;
}
export default styled.button<Props>`
color: var(--text);
padding: 8px 16px;
padding: 2px 16px;
border-radius: 8px;
font-size: 12px;
font-size: 14px;
font-weight: var(--font-weight-medium);
outline: none;
border: none;
@ -20,6 +21,50 @@ export default styled.button<Props>`
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
opacity: ${(props) => (props.disabled ? 0.5 : 1)}
font-weight: var(--font-weight-bold);
height: ${(props) => {
switch (props.size) {
default:
case "small":
return "32px;";
case "medium":
return "40px";
case "large":
return "45px";
}
}};
min-height: ${(props) => {
switch (props.size) {
default:
case "small":
return "32px;";
case "medium":
return "40px";
case "large":
return "45px";
}
}};
width: ${(props) => {
switch (props.size) {
default:
case "small":
return "96px";
case "medium":
return "96px";
case "large":
return "130px";
}
}};
min-width: ${(props) => {
switch (props.size) {
default:
case "small":
return "96px";
case "medium":
return "96px";
case "large":
return "130px";
}
}};
${(props) => {
if (!props.palette) props.palette = "primary";

View File

@ -46,21 +46,25 @@ function UserContextMenu({ user, member }: MenuProps) {
<ContextMenuButton disabled>Mention</ContextMenuButton>
<ContextMenuButton disabled>Message</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton disabled>Change Nickname</ContextMenuButton>
{member && <ContextMenuButton disabled>Change Nickname</ContextMenuButton>}
<ContextMenuButton disabled>Add Friend</ContextMenuButton>
<ContextMenuButton disabled>Block</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton destructive onClick={kick}>
Kick {member?.nick ?? user.username}
</ContextMenuButton>
<ContextMenuButton destructive onClick={ban}>
Ban {member?.nick ?? user.username}
</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton disabled icon="mdiChevronRight">
Roles
</ContextMenuButton>
<ContextMenuDivider />
{member && (
<>
<ContextMenuButton destructive onClick={kick}>
Kick {member?.nick ?? user.username}
</ContextMenuButton>
<ContextMenuButton destructive onClick={ban}>
Ban {member?.nick ?? user.username}
</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton disabled icon="mdiChevronRight">
Roles
</ContextMenuButton>
<ContextMenuDivider />
</>
)}
<ContextMenuButton
icon="mdiIdentifier"

View File

@ -1,6 +1,7 @@
import { observer } from "mobx-react-lite";
import React from "react";
import React, { useContext } from "react";
import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { useAppStore } from "../../stores/AppStore";
import { MessageLike } from "../../stores/objects/Message";
import Floating from "../floating/Floating";
@ -23,12 +24,18 @@ interface Props {
function MessageAuthor({ message }: Props) {
const app = useAppStore();
const contextMenu = useContext(ContextMenuContext);
const [color, setColor] = React.useState<string | undefined>(undefined);
const ref = React.useRef<HTMLDivElement>(null);
const onContextMenu = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.preventDefault();
const member = await app.guilds.get(message.guild_id!)?.members.fetch(message.author.id);
contextMenu.onContextMenu(e, { user: message.author, member });
};
React.useEffect(() => {
if ("guild_id" in message && message.guild_id) {
const guild = app.guilds.get(message.guild_id);
const guild = app.guilds.get(message.guild_id!);
if (!guild) return;
const member = guild.members.get(message.author.id);
if (!member) return;
@ -46,10 +53,11 @@ function MessageAuthor({ message }: Props) {
>
<FloatingTrigger>
<Container
ref={ref}
style={{
color,
}}
ref={contextMenu.setReferenceElement}
onContextMenu={onContextMenu}
>
{message.author.username}
</Container>

View File

@ -1,8 +1,10 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
import { useForm } from "react-hook-form";
import styled from "styled-components";
import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import { Modal } from "./ModalComponents";
const DescriptionText = styled.p`
@ -12,25 +14,57 @@ const DescriptionText = styled.p`
margin-top: 8px;
`;
const TextArea = styled.textarea`
flex: 1;
padding: 8px;
border-radius: 4px;
background-color: var(--background-secondary-alt);
border: none;
color: var(--text);
font-size: 16px;
font-weight: var(--font-weight-regular);
resize: none;
outline: none;
`;
const schema = yup
.object({
reason: yup.string(),
reason: yup.string().max(512, "Reason must be less than 512 characters"),
})
.required();
export function BanMemberModal({ target, ...props }: ModalProps<"ban_member">) {
export function BanMemberModal({ target, type, ...props }: ModalProps<"ban_member">) {
const app = useAppStore();
const {
register,
control,
setError,
handleSubmit,
formState: { errors, disabled, isLoading, isSubmitting },
formState: { disabled, isLoading, isSubmitting },
} = useForm({
resolver: yupResolver(schema),
});
const isDisabled = disabled || isLoading || isSubmitting;
const onSubmit = handleSubmit((data) => {
app.rest
.put(
Routes.guildBan(target.guild.id, target.user!.id),
undefined,
undefined,
data.reason
? {
"X-Audit-Log-Reason": data.reason,
}
: undefined,
)
.then(() => {
modalController.pop("close");
})
.catch((e) => {
console.error(e);
});
});
return (
<Modal
{...props}
@ -43,17 +77,19 @@ export function BanMemberModal({ target, ...props }: ModalProps<"ban_member">) {
}
actions={[
{
onClick: () => console.log("kick"),
children: <span>Kick</span>,
onClick: onSubmit,
children: <span>Ban</span>,
palette: "danger",
confirmation: true,
disabled: isDisabled,
size: "small",
},
{
onClick: () => modalController.pop("close"),
children: <span>Cancel</span>,
palette: "link",
disabled: isDisabled,
size: "small",
},
]}
>
@ -63,10 +99,12 @@ export function BanMemberModal({ target, ...props }: ModalProps<"ban_member">) {
alt="Thanos Snap GIF"
height={300}
style={{
objectFit: "contain",
marginBottom: 20,
borderRadius: 8,
}}
/>
<span>reason form</span>
<TextArea {...register("reason")} id="reason" name="reason" placeholder="Reason" maxLength={512} />
</Modal>
);
}

View File

@ -1,8 +1,10 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
import { useForm } from "react-hook-form";
import styled from "styled-components";
import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import { Modal } from "./ModalComponents";
const DescriptionText = styled.p`
@ -12,25 +14,57 @@ const DescriptionText = styled.p`
margin-top: 8px;
`;
const TextArea = styled.textarea`
flex: 1;
padding: 8px;
border-radius: 4px;
background-color: var(--background-secondary-alt);
border: none;
color: var(--text);
font-size: 16px;
font-weight: var(--font-weight-regular);
resize: none;
outline: none;
`;
const schema = yup
.object({
reason: yup.string(),
reason: yup.string().max(512, "Reason must be less than 512 characters"),
})
.required();
export function KickMemberModal({ target, ...props }: ModalProps<"kick_member">) {
const app = useAppStore();
const {
register,
control,
setError,
handleSubmit,
formState: { errors, disabled, isLoading, isSubmitting },
formState: { disabled, isLoading, isSubmitting },
} = useForm({
resolver: yupResolver(schema),
});
const isDisabled = disabled || isLoading || isSubmitting;
const onSubmit = handleSubmit((data) => {
app.rest
.delete(
Routes.guildMember(target.guild.id, target.user!.id),
undefined,
data.reason
? {
"X-Audit-Log-Reason": data.reason,
}
: undefined,
)
.then(() => {
modalController.pop("close");
})
.catch((e) => {
console.error(e);
});
});
return (
<Modal
{...props}
@ -43,21 +77,23 @@ export function KickMemberModal({ target, ...props }: ModalProps<"kick_member">)
}
actions={[
{
onClick: () => console.log("kick"),
onClick: onSubmit,
children: <span>Kick</span>,
palette: "danger",
confirmation: true,
disabled: isDisabled,
size: "small",
},
{
onClick: () => modalController.pop("close"),
children: <span>Cancel</span>,
palette: "link",
disabled: isDisabled,
size: "small",
},
]}
>
<span>reason form</span>
<TextArea {...register("reason")} id="reason" name="reason" placeholder="Reason" maxLength={512} />
</Modal>
);
}

View File

@ -175,7 +175,7 @@ export const ModalCloseWrapper = styled.div`
}
`;
export function Modal(props: ModalProps) {
export function Modal({ title, description, ...props }: ModalProps) {
const [closing, setClosing] = useState(false);
const closeModal = useCallback(() => {
@ -212,17 +212,13 @@ export function Modal(props: ModalProps) {
</ModalCloseWrapper>
)}
</div>
{(props.title || props.description) && (
{(title || description) && (
<ModalHeader>
{props.title && typeof props.title === "string" ? (
<ModalHeaderText>{props.title}</ModalHeaderText>
{title && typeof title === "string" ? <ModalHeaderText>{title}</ModalHeaderText> : title}
{description && typeof description === "string" ? (
<ModalSubHeaderText>{description}</ModalSubHeaderText>
) : (
props.title
)}
{props.description && typeof props.description === "string" ? (
<ModalSubHeaderText>{props.description}</ModalSubHeaderText>
) : (
props.description
description
)}
</ModalHeader>
)}

View File

@ -1,6 +1,7 @@
import type { Snowflake } from "@spacebarchat/spacebar-api-types/globals";
import type { APIGuildMember } from "@spacebarchat/spacebar-api-types/v9";
import { action, computed, makeObservable, observable, ObservableMap } from "mobx";
import { type APIGuildMember } from "@spacebarchat/spacebar-api-types/v9";
import { ObservableMap, action, computed, makeObservable, observable } from "mobx";
import { APIUserProfile } from "../utils/interfaces/api";
import AppStore from "./AppStore";
import Guild from "./objects/Guild";
import GuildMember from "./objects/GuildMember";
@ -71,4 +72,16 @@ export default class GuildMemberStore {
if (!meId) return null;
return this.members.get(meId);
}
@action
async fetch(id: Snowflake): Promise<GuildMember | undefined> {
if (this.has(id)) return this.get(id);
const profile = await this.app.rest.get<APIUserProfile>(`/users/${id}/profile`, {
guild_id: this.guild.id,
});
if (!profile.guild_member) return undefined;
profile.guild_member.user = profile.user;
return this.add(profile.guild_member);
}
}

View File

@ -1,3 +1,5 @@
import { APIGuildMember, PublicUser } from "@spacebarchat/spacebar-api-types/v9";
export interface IAPILoginResponseMFARequired {
token: null;
mfa: true;
@ -119,3 +121,29 @@ export interface APIError {
}
// export type RESTAPIPostInviteResponse = {} | IAPIError;
export type UserProfile = Pick<PublicUser, "bio" | "accent_color" | "banner" | "pronouns" | "theme_colors">;
export type MutualGuild = {
id: string;
nick?: string;
};
export type PublicMemberProfile = Pick<APIGuildMember, "banner" | "bio" | "pronouns" | "theme_colors"> & {
accent_color: unknown; // TODO:
emoji: unknown; // TODO:
guild_id: string;
};
export interface APIUserProfile {
user: PublicUser;
connected_accounts: unknown[]; // TODO: type
premium_guild_since?: Date;
premium_since?: Date;
mutual_guilds: unknown[]; // TODO: type
premium_type: number;
profile_themes_experiment_bucket: number;
user_profile: UserProfile;
guild_member?: APIGuildMember;
guild_member_profile?: PublicMemberProfile;
}