450 lines
13 KiB
TypeScript
450 lines
13 KiB
TypeScript
import { JSX, Match, Show, Switch } from "solid-js";
|
|
|
|
import { Trans } from "@lingui-solid/solid/macro";
|
|
import { useNavigate } from "@solidjs/router";
|
|
import { Channel, Message, ServerMember, User } from "stoat.js";
|
|
|
|
import { useClient } from "@revolt/client";
|
|
import { useModals } from "@revolt/modal";
|
|
import { useSmartParams } from "@revolt/routing";
|
|
import { useState } from "@revolt/state";
|
|
import { Slider, Text } from "@revolt/ui";
|
|
|
|
import MdAddCircleOutline from "@material-design-icons/svg/outlined/add_circle_outline.svg?component-solid";
|
|
import MdAdminPanelSettings from "@material-design-icons/svg/outlined/admin_panel_settings.svg?component-solid";
|
|
import MdAlternateEmail from "@material-design-icons/svg/outlined/alternate_email.svg?component-solid";
|
|
import MdAssignmentInd from "@material-design-icons/svg/outlined/assignment_ind.svg?component-solid";
|
|
import MdBadge from "@material-design-icons/svg/outlined/badge.svg?component-solid";
|
|
import MdBlock from "@material-design-icons/svg/outlined/block.svg?component-solid";
|
|
import MdCancel from "@material-design-icons/svg/outlined/cancel.svg?component-solid";
|
|
import MdChat from "@material-design-icons/svg/outlined/chat.svg?component-solid";
|
|
import MdClose from "@material-design-icons/svg/outlined/close.svg?component-solid";
|
|
import MdDoNotDisturbOn from "@material-design-icons/svg/outlined/do_not_disturb_on.svg?component-solid";
|
|
import MdFace from "@material-design-icons/svg/outlined/face.svg?component-solid";
|
|
import MdMicOff from "@material-design-icons/svg/outlined/mic_off.svg?component-solid";
|
|
import MdPersonAddAlt from "@material-design-icons/svg/outlined/person_add_alt.svg?component-solid";
|
|
import MdPersonRemove from "@material-design-icons/svg/outlined/person_remove.svg?component-solid";
|
|
import MdReport from "@material-design-icons/svg/outlined/report.svg?component-solid";
|
|
|
|
import MdChecked from "@material-symbols/svg-400/outlined/check_box.svg?component-solid";
|
|
import MdUnchecked from "@material-symbols/svg-400/outlined/check_box_outline_blank.svg?component-solid";
|
|
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuButton,
|
|
ContextMenuDivider,
|
|
} from "./ContextMenu";
|
|
import { NotificationContextMenu } from "./shared/NotificationContextMenu";
|
|
|
|
/**
|
|
* Context menu for users
|
|
*/
|
|
export function UserContextMenu(props: {
|
|
user: User;
|
|
channel?: Channel;
|
|
member?: ServerMember;
|
|
contextMessage?: Message;
|
|
inVoice?: boolean;
|
|
}) {
|
|
// TODO: if we take serverId instead, we could dynamically fetch server member here
|
|
// same for the floating menu I guess?
|
|
const state = useState();
|
|
const client = useClient();
|
|
const navigate = useNavigate();
|
|
const { openModal } = useModals();
|
|
|
|
// server context
|
|
const params = useSmartParams();
|
|
|
|
/**
|
|
* Open direct message channel
|
|
*/
|
|
function openDm() {
|
|
props.user.openDM().then((channel) => navigate(channel.url));
|
|
}
|
|
|
|
/**
|
|
* Delete channel
|
|
*/
|
|
function closeDm() {
|
|
openModal({
|
|
type: "delete_channel",
|
|
channel: props.channel!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mention the user
|
|
*/
|
|
function mention() {
|
|
if (!state.draft._setNodeReplacement) return;
|
|
state.draft._setNodeReplacement([props.user.toString()]);
|
|
}
|
|
|
|
/**
|
|
* Edit server identity for user
|
|
*/
|
|
function editIdentity() {
|
|
openModal({
|
|
type: "server_identity",
|
|
member: props.member!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Report the user
|
|
*/
|
|
function reportUser() {
|
|
openModal({
|
|
type: "report_content",
|
|
target: props.user!,
|
|
client: client(),
|
|
contextMessage: props.contextMessage,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Edit this user's roles
|
|
*/
|
|
function editRoles() {
|
|
openModal({
|
|
type: "user_profile_roles",
|
|
member: props.member!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Kick the member
|
|
*/
|
|
function kickMember() {
|
|
openModal({
|
|
type: "kick_member",
|
|
member: props.member!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ban the member
|
|
*/
|
|
function banMember() {
|
|
openModal({
|
|
type: "ban_member",
|
|
member: props.member!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ban the user
|
|
*/
|
|
function banUser() {
|
|
openModal({
|
|
type: "ban_non_member",
|
|
user: props.user!,
|
|
server: client().servers.get(params().serverId!)!,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add friend
|
|
*/
|
|
function addFriend() {
|
|
props.user.addFriend();
|
|
}
|
|
|
|
/**
|
|
* Remove friend
|
|
*/
|
|
function removeFriend() {
|
|
props.user.removeFriend();
|
|
}
|
|
|
|
/**
|
|
* Block user
|
|
*/
|
|
function blockUser() {
|
|
props.user.blockUser();
|
|
}
|
|
|
|
/**
|
|
* Unblock user
|
|
*/
|
|
function unblockUser() {
|
|
props.user.unblockUser();
|
|
}
|
|
|
|
/**
|
|
* Open user in Stoat Admin Panel
|
|
*/
|
|
function openAdminPanel() {
|
|
window.open(
|
|
`https://old-admin.stoatinternal.com/panel/inspect/user/${props.user.id}`,
|
|
"_blank",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Copy user id to clipboard
|
|
*/
|
|
function copyId() {
|
|
navigator.clipboard.writeText(props.user.id);
|
|
}
|
|
|
|
return (
|
|
<ContextMenu class="UserContextMenu">
|
|
<Show when={props.inVoice && !props.user.self}>
|
|
<ContextMenuButton
|
|
onMouseDown={(e) => e.stopImmediatePropagation()}
|
|
onClick={(e) => e.stopImmediatePropagation()}
|
|
>
|
|
<Text class="label">
|
|
<Trans>Volume</Trans>
|
|
</Text>
|
|
<Slider
|
|
min={0}
|
|
max={3}
|
|
step={0.1}
|
|
value={state.voice.getUserVolume(props.user.id)}
|
|
onInput={(event) =>
|
|
state.voice.setUserVolume(
|
|
props.user.id,
|
|
event.currentTarget.value,
|
|
)
|
|
}
|
|
labelFormatter={(label) => (label * 100).toFixed(0) + "%"}
|
|
/>
|
|
</ContextMenuButton>
|
|
<ContextMenuButton
|
|
icon={MdMicOff}
|
|
onClick={() =>
|
|
state.voice.setUserMuted(
|
|
props.user.id,
|
|
!state.voice.getUserMuted(props.user.id),
|
|
)
|
|
}
|
|
actionSymbol={
|
|
state.voice.getUserMuted(props.user.id) ? MdChecked : MdUnchecked
|
|
}
|
|
>
|
|
<Trans>Mute</Trans>
|
|
</ContextMenuButton>
|
|
|
|
<ContextMenuDivider />
|
|
</Show>
|
|
|
|
<Show when={props.channel?.type === "DirectMessage"}>
|
|
<ContextMenuButton icon={MdClose} onClick={closeDm}>
|
|
<Trans>Close chat</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.channel?.type === "TextChannel"}>
|
|
<ContextMenuButton icon={MdAlternateEmail} onClick={mention}>
|
|
<Trans>Mention</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Friend"}>
|
|
<ContextMenuButton icon={MdChat} onClick={openDm}>
|
|
<Trans>Message</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
|
|
<Show
|
|
when={
|
|
props.user.relationship === "Friend" ||
|
|
(props.channel &&
|
|
(props.channel.type === "DirectMessage" ||
|
|
props.channel.type === "TextChannel"))
|
|
}
|
|
>
|
|
<ContextMenuDivider />
|
|
</Show>
|
|
|
|
<Show when={props.channel?.type === "DirectMessage"}>
|
|
<NotificationContextMenu channel={props.channel!} />
|
|
<ContextMenuDivider />
|
|
</Show>
|
|
|
|
<Show
|
|
when={
|
|
props.member &&
|
|
(props.user.self
|
|
? props.member!.server!.havePermission("ChangeNickname") ||
|
|
props.member!.server!.havePermission("ChangeAvatar")
|
|
: (props.member!.server!.havePermission("ManageNicknames") ||
|
|
props.member!.server!.havePermission("RemoveAvatars")) &&
|
|
props.member!.inferiorTo(props.member!.server!.member!))
|
|
}
|
|
>
|
|
<ContextMenuButton icon={MdFace} onClick={editIdentity}>
|
|
<Switch fallback={<Trans>Edit identity</Trans>}>
|
|
<Match when={props.user.self}>
|
|
<Trans>Edit your identity</Trans>
|
|
</Match>
|
|
</Switch>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
|
|
<Show when={props.member}>
|
|
<Show
|
|
when={
|
|
props.member?.server?.owner?.self ||
|
|
(props.member?.server?.havePermission("AssignRoles") &&
|
|
props.member.inferiorTo(props.member.server.member!))
|
|
}
|
|
>
|
|
<ContextMenuButton icon={MdAssignmentInd} onClick={editRoles}>
|
|
<Trans>Edit roles</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
{/** TODO: #287 timeout users */}
|
|
<Show
|
|
when={
|
|
!props.user.self &&
|
|
props.member?.server?.havePermission("KickMembers") &&
|
|
props.member.inferiorTo(props.member.server.member!)
|
|
}
|
|
>
|
|
<ContextMenuButton
|
|
icon={MdPersonRemove}
|
|
onClick={kickMember}
|
|
destructive
|
|
>
|
|
<Trans>Kick member</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show
|
|
when={
|
|
!props.user.self &&
|
|
props.member?.server?.havePermission("BanMembers") &&
|
|
props.member.inferiorTo(props.member.server.member!)
|
|
}
|
|
>
|
|
<ContextMenuButton
|
|
icon={MdDoNotDisturbOn}
|
|
onClick={banMember}
|
|
destructive
|
|
>
|
|
<Trans>Ban member</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
</Show>
|
|
|
|
<Show
|
|
when={
|
|
!props.user.self &&
|
|
props.member?.server?.havePermission("BanMembers") &&
|
|
params().serverId &&
|
|
!props.member
|
|
}
|
|
>
|
|
<ContextMenuButton
|
|
icon={MdDoNotDisturbOn}
|
|
onClick={banUser}
|
|
destructive
|
|
>
|
|
<Trans>Ban user</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
|
|
<Show when={!props.user.self}>
|
|
<ContextMenuButton icon={MdReport} onClick={reportUser} destructive>
|
|
<Trans>Report user</Trans>
|
|
</ContextMenuButton>
|
|
{/* TODO: #286 show profile / message */}
|
|
<Show when={props.user.relationship === "None" && !props.user.bot}>
|
|
<ContextMenuButton icon={MdPersonAddAlt} onClick={addFriend}>
|
|
<Trans>Add friend</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Friend"}>
|
|
<ContextMenuButton icon={MdPersonRemove} onClick={removeFriend}>
|
|
<Trans>Remove friend</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Incoming"}>
|
|
<ContextMenuButton icon={MdPersonAddAlt} onClick={addFriend}>
|
|
<Trans>Accept friend request</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Incoming"}>
|
|
<ContextMenuButton icon={MdCancel} onClick={removeFriend}>
|
|
<Trans>Reject friend request</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Outgoing"}>
|
|
<ContextMenuButton icon={MdCancel} onClick={removeFriend}>
|
|
<Trans>Cancel friend request</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship !== "Blocked"}>
|
|
<ContextMenuButton icon={MdBlock} onClick={blockUser}>
|
|
<Trans>Block user</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={props.user.relationship === "Blocked"}>
|
|
<ContextMenuButton icon={MdAddCircleOutline} onClick={unblockUser}>
|
|
<Trans>Unblock user</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
</Show>
|
|
|
|
<Show
|
|
when={
|
|
state.settings.getValue("advanced:admin_panel") ||
|
|
state.settings.getValue("advanced:copy_id")
|
|
}
|
|
>
|
|
<ContextMenuDivider />
|
|
</Show>
|
|
|
|
<Show when={state.settings.getValue("advanced:admin_panel")}>
|
|
<ContextMenuButton icon={MdAdminPanelSettings} onClick={openAdminPanel}>
|
|
<Trans>Admin Panel</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
<Show when={state.settings.getValue("advanced:copy_id")}>
|
|
<ContextMenuButton icon={MdBadge} onClick={copyId}>
|
|
<Trans>Copy user ID</Trans>
|
|
</ContextMenuButton>
|
|
</Show>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Provide floating user menus on this element
|
|
* @param user User
|
|
* @param member Server Member
|
|
*/
|
|
export function floatingUserMenus(
|
|
user: User,
|
|
member?: ServerMember,
|
|
contextMessage?: Message,
|
|
): JSX.Directives["floating"] & object {
|
|
return {
|
|
userCard: {
|
|
user,
|
|
member,
|
|
// we could use message to display masquerade info in user card
|
|
},
|
|
/**
|
|
* Build user context menu
|
|
*/
|
|
contextMenu() {
|
|
return (
|
|
<UserContextMenu
|
|
user={user}
|
|
member={member}
|
|
contextMessage={contextMessage}
|
|
channel={contextMessage?.channel}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
export function floatingUserMenusFromMessage(message: Message) {
|
|
return message.author
|
|
? floatingUserMenus(message.author!, message.member, message)
|
|
: {}; // TODO: webhook menu
|
|
}
|