mirror of
https://github.com/revoltchat/revite.git
synced 2025-02-22 00:00:56 -05:00
Port context menus.
This commit is contained in:
parent
0a0c00fe58
commit
b4bc2262ae
10 changed files with 841 additions and 18 deletions
|
@ -1,11 +1,17 @@
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
import { User } from "revolt.js";
|
import { User } from "revolt.js";
|
||||||
import Header from "../ui/Header";
|
import Header from "../ui/Header";
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
import UserStatus from './UserStatus';
|
import UserStatus from './UserStatus';
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Localizer } from 'preact-i18n';
|
import { Localizer } from 'preact-i18n';
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import IconButton from "../ui/IconButton";
|
||||||
import { Settings } from "@styled-icons/feather";
|
import { Settings } from "@styled-icons/feather";
|
||||||
|
import { openContextMenu } from "preact-context-menu";
|
||||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||||
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
const HeaderBase = styled.div`
|
const HeaderBase = styled.div`
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
@ -39,12 +45,10 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserHeader({ user }: Props) {
|
export default function UserHeader({ user }: Props) {
|
||||||
function openPresenceSelector() {
|
const { writeClipboard } = useIntermediate();
|
||||||
// openContextMenu("Status");
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeClipboard(a: string) {
|
function openPresenceSelector() {
|
||||||
alert('unimplemented');
|
openContextMenu("Status");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -57,12 +61,12 @@ export default function UserHeader({ user }: Props) {
|
||||||
/>
|
/>
|
||||||
<HeaderBase>
|
<HeaderBase>
|
||||||
<Localizer>
|
<Localizer>
|
||||||
{/*<Tooltip content={<Text id="app.special.copy_username" />}>*/}
|
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||||
<span className="username"
|
<span className="username"
|
||||||
onClick={() => writeClipboard(user.username)}>
|
onClick={() => writeClipboard(user.username)}>
|
||||||
@{user.username}
|
@{user.username}
|
||||||
</span>
|
</span>
|
||||||
{/*</Tooltip>*/}
|
</Tooltip>
|
||||||
</Localizer>
|
</Localizer>
|
||||||
<span className="status"
|
<span className="status"
|
||||||
onClick={openPresenceSelector}>
|
onClick={openPresenceSelector}>
|
||||||
|
@ -70,9 +74,11 @@ export default function UserHeader({ user }: Props) {
|
||||||
</span>
|
</span>
|
||||||
</HeaderBase>
|
</HeaderBase>
|
||||||
{ !isTouchscreenDevice && <div className="actions">
|
{ !isTouchscreenDevice && <div className="actions">
|
||||||
{/*<IconButton to="/settings">*/}
|
<Link to="/settings">
|
||||||
|
<IconButton>
|
||||||
<Settings size={24} />
|
<Settings size={24} />
|
||||||
{/*</IconButton>*/}
|
</IconButton>
|
||||||
|
</Link>
|
||||||
</div> }
|
</div> }
|
||||||
</Header>
|
</Header>
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import Header from '../../ui/Header';
|
||||||
import UserHeader from "../../common/UserHeader";
|
import UserHeader from "../../common/UserHeader";
|
||||||
import Category from '../../ui/Category';
|
import Category from '../../ui/Category';
|
||||||
import PaintCounter from "../../../lib/PaintCounter";
|
import PaintCounter from "../../../lib/PaintCounter";
|
||||||
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
type Props = WithDispatcher & {
|
type Props = WithDispatcher & {
|
||||||
unreads: Unreads;
|
unreads: Unreads;
|
||||||
|
@ -50,7 +51,7 @@ function HomeSidebar(props: Props) {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const { channel } = useParams<{ channel: string }>();
|
const { channel } = useParams<{ channel: string }>();
|
||||||
// const { openScreen, writeClipboard } = useContext(IntermediateContext);
|
const { openScreen, writeClipboard } = useIntermediate();
|
||||||
|
|
||||||
const ctx = useForceUpdate();
|
const ctx = useForceUpdate();
|
||||||
const users = useUsers(undefined, ctx);
|
const users = useUsers(undefined, ctx);
|
||||||
|
@ -119,7 +120,7 @@ function HomeSidebar(props: Props) {
|
||||||
<Text id="app.main.categories.conversations" />
|
<Text id="app.main.categories.conversations" />
|
||||||
) as any
|
) as any
|
||||||
}
|
}
|
||||||
action={() => /*openScreen({ id: "special_input", type: "create_group" })*/{}}
|
action={() => openScreen({ id: "special_input", type: "create_group" })}
|
||||||
/>
|
/>
|
||||||
</Localizer>
|
</Localizer>
|
||||||
{channelsArr.length === 0 && <img src="/assets/images/placeholder.svg" />}
|
{channelsArr.length === 0 && <img src="/assets/images/placeholder.svg" />}
|
||||||
|
|
656
src/lib/ContextMenus.tsx
Normal file
656
src/lib/ContextMenus.tsx
Normal file
|
@ -0,0 +1,656 @@
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { Attachment, Channels, Message, Servers, Users } from "revolt.js/dist/api/objects";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuWithData,
|
||||||
|
MenuItem
|
||||||
|
} from "preact-context-menu";
|
||||||
|
import { ChannelPermission, ServerPermission, UserPermission } from "revolt.js/dist/api/permissions";
|
||||||
|
import { QueuedMessage } from "../redux/reducers/queue";
|
||||||
|
import { WithDispatcher } from "../redux/reducers";
|
||||||
|
import { useIntermediate } from "../context/intermediate/Intermediate";
|
||||||
|
import { AppContext, ClientStatus, StatusContext } from "../context/revoltjs/RevoltClient";
|
||||||
|
import { takeError } from "../context/revoltjs/util";
|
||||||
|
import { useChannel, useChannelPermission, useForceUpdate, useServer, useServerPermission, useUser, useUserPermission } from "../context/revoltjs/hooks";
|
||||||
|
import { Children } from "../types/Preact";
|
||||||
|
import LineDivider from "../components/ui/LineDivider";
|
||||||
|
import { connectState } from "../redux/connector";
|
||||||
|
|
||||||
|
interface ContextMenuData {
|
||||||
|
user?: string;
|
||||||
|
server?: string;
|
||||||
|
server_list?: string;
|
||||||
|
channel?: string;
|
||||||
|
message?: Message;
|
||||||
|
|
||||||
|
unread?: boolean;
|
||||||
|
queued?: QueuedMessage;
|
||||||
|
contextualChannel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| { action: "copy_id"; id: string }
|
||||||
|
| { action: "copy_selection" }
|
||||||
|
| { action: "copy_text"; content: string }
|
||||||
|
| { action: "mark_as_read"; channel: Channels.Channel }
|
||||||
|
| { action: "retry_message"; message: QueuedMessage }
|
||||||
|
| { action: "cancel_message"; message: QueuedMessage }
|
||||||
|
| { action: "mention"; user: string }
|
||||||
|
| { action: "quote_message"; content: string }
|
||||||
|
| { action: "edit_message"; id: string }
|
||||||
|
| { action: "delete_message"; target: Channels.Message }
|
||||||
|
| { action: "open_file"; attachment: Attachment }
|
||||||
|
| { action: "save_file"; attachment: Attachment }
|
||||||
|
| { action: "copy_file_link"; attachment: Attachment }
|
||||||
|
| { action: "open_link"; link: string }
|
||||||
|
| { action: "copy_link"; link: string }
|
||||||
|
| { action: "remove_member"; channel: string; user: string }
|
||||||
|
| { action: "kick_member"; target: Servers.Server; user: string }
|
||||||
|
| { action: "ban_member"; target: Servers.Server; user: string }
|
||||||
|
| { action: "view_profile"; user: string }
|
||||||
|
| { action: "message_user"; user: string }
|
||||||
|
| { action: "block_user"; user: string }
|
||||||
|
| { action: "unblock_user"; user: string }
|
||||||
|
| { action: "add_friend"; user: string }
|
||||||
|
| { action: "remove_friend"; user: string }
|
||||||
|
| { action: "cancel_friend"; user: string }
|
||||||
|
| { action: "set_presence"; presence: Users.Presence }
|
||||||
|
| { action: "set_status" }
|
||||||
|
| { action: "clear_status" }
|
||||||
|
| { action: "create_channel"; server: string }
|
||||||
|
| { action: "create_invite"; target: Channels.GroupChannel | Channels.TextChannel }
|
||||||
|
| { action: "leave_group"; target: Channels.GroupChannel }
|
||||||
|
| { action: "delete_channel"; target: Channels.TextChannel }
|
||||||
|
| { action: "close_dm"; target: Channels.DirectMessageChannel }
|
||||||
|
| { action: "leave_server"; target: Servers.Server }
|
||||||
|
| { action: "delete_server"; target: Servers.Server }
|
||||||
|
| { action: "open_channel_settings", id: string }
|
||||||
|
| { action: "open_server_settings", id: string }
|
||||||
|
| { action: "open_server_channel_settings", server: string, id: string };
|
||||||
|
|
||||||
|
function ContextMenus(props: WithDispatcher) {
|
||||||
|
const { openScreen, writeClipboard } = useIntermediate();
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
const userId = client.user!._id;
|
||||||
|
const status = useContext(StatusContext);
|
||||||
|
const isOnline = status === ClientStatus.ONLINE;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
function contextClick(data?: Action) {
|
||||||
|
if (typeof data === "undefined") return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
switch (data.action) {
|
||||||
|
case "copy_id":
|
||||||
|
writeClipboard(data.id);
|
||||||
|
break;
|
||||||
|
case "copy_selection":
|
||||||
|
writeClipboard(document.getSelection()?.toString() ?? '');
|
||||||
|
break;
|
||||||
|
case "mark_as_read":
|
||||||
|
if (data.channel.channel_type === 'SavedMessages') return;
|
||||||
|
props.dispatcher({
|
||||||
|
type: "UNREADS_MARK_READ",
|
||||||
|
channel: data.channel._id,
|
||||||
|
message: data.channel.channel_type === 'TextChannel' ? data.channel.last_message : data.channel.last_message._id,
|
||||||
|
request: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "retry_message":
|
||||||
|
{
|
||||||
|
const nonce = data.message.id;
|
||||||
|
const fail = (error: any) =>
|
||||||
|
props.dispatcher({
|
||||||
|
type: "QUEUE_FAIL",
|
||||||
|
nonce,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
|
||||||
|
client.channels
|
||||||
|
.sendMessage(
|
||||||
|
data.message.channel,
|
||||||
|
{
|
||||||
|
content: data.message.data.content as string,
|
||||||
|
nonce
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(fail);
|
||||||
|
|
||||||
|
props.dispatcher({
|
||||||
|
type: "QUEUE_START",
|
||||||
|
nonce
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "cancel_message":
|
||||||
|
{
|
||||||
|
props.dispatcher({
|
||||||
|
type: "QUEUE_REMOVE",
|
||||||
|
nonce: data.message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "mention":
|
||||||
|
{
|
||||||
|
// edit draft
|
||||||
|
/*InternalEventEmitter.emit(
|
||||||
|
"append_messagebox",
|
||||||
|
`<@${data.user}>`,
|
||||||
|
"mention"
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "copy_text":
|
||||||
|
writeClipboard(data.content);
|
||||||
|
break;
|
||||||
|
case "quote_message":
|
||||||
|
{
|
||||||
|
// edit draft
|
||||||
|
/*InternalEventEmitter.emit(
|
||||||
|
"append_messagebox",
|
||||||
|
data.content,
|
||||||
|
"quote"
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "edit_message":
|
||||||
|
{
|
||||||
|
// InternalEventEmitter.emit("edit_message", data.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "open_file":
|
||||||
|
{
|
||||||
|
window
|
||||||
|
.open(
|
||||||
|
client.generateFileURL(data.attachment),
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "save_file":
|
||||||
|
{
|
||||||
|
window.open(
|
||||||
|
// ! FIXME: do this from revolt.js
|
||||||
|
client.generateFileURL(data.attachment)?.replace('attachments', 'attachments/download'),
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "copy_file_link":
|
||||||
|
{
|
||||||
|
const { _id, filename } = data.attachment;
|
||||||
|
writeClipboard(
|
||||||
|
// ! FIXME: do from r.js
|
||||||
|
client.generateFileURL(data.attachment) + '/${encodeURI(filename)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "open_link":
|
||||||
|
{
|
||||||
|
window.open(data.link, "_blank")?.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "copy_link":
|
||||||
|
{
|
||||||
|
writeClipboard(data.link);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "remove_member":
|
||||||
|
{
|
||||||
|
client.channels.removeMember(data.channel, data.user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "view_profile":
|
||||||
|
openScreen({ id: 'profile', user_id: data.user });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "message_user":
|
||||||
|
{
|
||||||
|
const channel = await client.users.openDM(data.user);
|
||||||
|
if (channel) {
|
||||||
|
history.push(`/channel/${channel._id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "add_friend":
|
||||||
|
{
|
||||||
|
let user = client.users.get(data.user);
|
||||||
|
if (user) {
|
||||||
|
await client.users.addFriend(user.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "block_user":
|
||||||
|
await client.users.blockUser(data.user);
|
||||||
|
break;
|
||||||
|
case "unblock_user":
|
||||||
|
await client.users.unblockUser(data.user);
|
||||||
|
break;
|
||||||
|
case "remove_friend":
|
||||||
|
case "cancel_friend":
|
||||||
|
await client.users.removeFriend(data.user);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set_presence":
|
||||||
|
{
|
||||||
|
await client.users.editUser({
|
||||||
|
status: {
|
||||||
|
...client.user?.status,
|
||||||
|
presence: data.presence
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set_status": openScreen({ id: "special_input", type: "set_custom_status" }); break;
|
||||||
|
|
||||||
|
case "clear_status":
|
||||||
|
{
|
||||||
|
let { text, ...status } = client.user?.status ?? {};
|
||||||
|
await client.users.editUser({ status });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "leave_group":
|
||||||
|
case "close_dm":
|
||||||
|
case "leave_server":
|
||||||
|
case "delete_channel":
|
||||||
|
case "delete_server":
|
||||||
|
case "delete_message":
|
||||||
|
// @ts-expect-error
|
||||||
|
case "create_invite": openScreen({ id: "special_prompt", type: data.action, target: data.target }); break;
|
||||||
|
|
||||||
|
case "ban_member":
|
||||||
|
case "kick_member": openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user }); break;
|
||||||
|
|
||||||
|
case "create_channel": openScreen({ id: "special_input", type: "create_channel", server: data.server }); break;
|
||||||
|
|
||||||
|
case "open_channel_settings": history.push(`/channel/${data.id}/settings`); break;
|
||||||
|
case "open_server_channel_settings": history.push(`/server/${data.server}/channel/${data.id}/settings`); break;
|
||||||
|
case "open_server_settings": history.push(`/server/${data.id}/settings`); break;
|
||||||
|
}
|
||||||
|
})().catch(err => {
|
||||||
|
openScreen({ id: "error", error: takeError(err) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ContextMenuWithData id="Menu" onClose={contextClick}>
|
||||||
|
{({
|
||||||
|
user: uid,
|
||||||
|
channel: cid,
|
||||||
|
server: sid,
|
||||||
|
message,
|
||||||
|
server_list,
|
||||||
|
queued,
|
||||||
|
unread,
|
||||||
|
contextualChannel: cxid
|
||||||
|
}: ContextMenuData) => {
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
const elements: Children[] = [];
|
||||||
|
var lastDivider = false;
|
||||||
|
|
||||||
|
function generateAction(
|
||||||
|
action: Action,
|
||||||
|
locale?: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
tip?: Children
|
||||||
|
) {
|
||||||
|
lastDivider = false;
|
||||||
|
elements.push(
|
||||||
|
<MenuItem data={action} disabled={disabled}>
|
||||||
|
<Text
|
||||||
|
id={`app.context_menu.${locale ??
|
||||||
|
action.action}`}
|
||||||
|
/>
|
||||||
|
{ tip && <div className="tip">
|
||||||
|
{ tip }
|
||||||
|
</div> }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushDivider() {
|
||||||
|
if (lastDivider || elements.length === 0) return;
|
||||||
|
lastDivider = true;
|
||||||
|
elements.push(<LineDivider />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server_list) {
|
||||||
|
let permissions = useServerPermission(server_list, forceUpdate);
|
||||||
|
if (permissions & ServerPermission.ManageChannels) generateAction({ action: 'create_channel', server: server_list });
|
||||||
|
if (permissions & ServerPermission.ManageServer) generateAction({ action: 'open_server_settings', id: server_list });
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getSelection()?.toString().length ?? 0 > 0) {
|
||||||
|
generateAction({ action: "copy_selection" }, undefined, undefined, <Text id="shortcuts.ctrlc" />);
|
||||||
|
pushDivider();
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = useChannel(cid, forceUpdate);
|
||||||
|
const contextualChannel = useChannel(cxid, forceUpdate);
|
||||||
|
const targetChannel = channel ?? contextualChannel;
|
||||||
|
|
||||||
|
const user = useUser(uid, forceUpdate);
|
||||||
|
const server = useServer(targetChannel?.channel_type === 'TextChannel' ? targetChannel.server : sid, forceUpdate);
|
||||||
|
|
||||||
|
const channelPermissions = targetChannel ? useChannelPermission(targetChannel._id, forceUpdate) : 0;
|
||||||
|
const serverPermissions = server ? useServerPermission(server._id, forceUpdate) : (
|
||||||
|
targetChannel?.channel_type === 'TextChannel' ? useServerPermission(targetChannel.server, forceUpdate) : 0
|
||||||
|
);
|
||||||
|
const userPermissions = user ? useUserPermission(user._id, forceUpdate) : 0;
|
||||||
|
|
||||||
|
if (channel && unread) {
|
||||||
|
generateAction(
|
||||||
|
{ action: "mark_as_read", channel },
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextualChannel) {
|
||||||
|
if (user && user._id !== userId) {
|
||||||
|
generateAction({
|
||||||
|
action: "mention",
|
||||||
|
user: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
pushDivider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
let actions: string[];
|
||||||
|
switch (user.relationship) {
|
||||||
|
case Users.Relationship.User: actions = []; break;
|
||||||
|
case Users.Relationship.Friend:
|
||||||
|
actions = [
|
||||||
|
"remove_friend",
|
||||||
|
"block_user"
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case Users.Relationship.Incoming:
|
||||||
|
actions = ["add_friend", "block_user"];
|
||||||
|
break;
|
||||||
|
case Users.Relationship.Outgoing:
|
||||||
|
actions = ["cancel_friend", "block_user"];
|
||||||
|
break;
|
||||||
|
case Users.Relationship.Blocked:
|
||||||
|
actions = ["unblock_user"];
|
||||||
|
break;
|
||||||
|
case Users.Relationship.BlockedOther:
|
||||||
|
actions = ["block_user"];
|
||||||
|
break;
|
||||||
|
case Users.Relationship.None:
|
||||||
|
default:
|
||||||
|
actions = ["add_friend", "block_user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPermissions & UserPermission.ViewProfile) {
|
||||||
|
generateAction({ action: 'view_profile', user: user._id });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user._id !== userId && userPermissions & UserPermission.SendMessage) {
|
||||||
|
generateAction({ action: 'message_user', user: user._id });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
generateAction({
|
||||||
|
action: action as any,
|
||||||
|
user: user._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextualChannel) {
|
||||||
|
if (contextualChannel.channel_type === "Group" && uid) {
|
||||||
|
if (
|
||||||
|
contextualChannel.owner === userId &&
|
||||||
|
userId !== uid
|
||||||
|
) {
|
||||||
|
generateAction({
|
||||||
|
action: "remove_member",
|
||||||
|
channel: contextualChannel._id,
|
||||||
|
user: uid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server && uid && userId !== uid && uid !== server.owner) {
|
||||||
|
if (serverPermissions & ServerPermission.KickMembers)
|
||||||
|
generateAction({ action: "kick_member", target: server, user: uid });
|
||||||
|
|
||||||
|
if (serverPermissions & ServerPermission.BanMembers)
|
||||||
|
generateAction({ action: "ban_member", target: server, user: uid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queued) {
|
||||||
|
generateAction({
|
||||||
|
action: "retry_message",
|
||||||
|
message: queued
|
||||||
|
});
|
||||||
|
|
||||||
|
generateAction({
|
||||||
|
action: "cancel_message",
|
||||||
|
message: queued
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message && !queued) {
|
||||||
|
if (
|
||||||
|
typeof message.content === "string" &&
|
||||||
|
message.content.length > 0
|
||||||
|
) {
|
||||||
|
generateAction({
|
||||||
|
action: "quote_message",
|
||||||
|
content: message.content
|
||||||
|
});
|
||||||
|
generateAction({
|
||||||
|
action: "copy_text",
|
||||||
|
content: message.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.author === userId) {
|
||||||
|
generateAction({
|
||||||
|
action: "edit_message",
|
||||||
|
id: message._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.author === userId ||
|
||||||
|
channelPermissions & ChannelPermission.ManageMessages) {
|
||||||
|
generateAction({
|
||||||
|
action: "delete_message",
|
||||||
|
target: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.attachments) {
|
||||||
|
pushDivider();
|
||||||
|
const { metadata } = message.attachments[0];
|
||||||
|
const { type } = metadata;
|
||||||
|
|
||||||
|
generateAction(
|
||||||
|
{
|
||||||
|
action: "open_file",
|
||||||
|
attachment: message.attachments[0]
|
||||||
|
},
|
||||||
|
type === "Image"
|
||||||
|
? "open_image"
|
||||||
|
: type === "Video"
|
||||||
|
? "open_video"
|
||||||
|
: "open_file"
|
||||||
|
);
|
||||||
|
|
||||||
|
generateAction(
|
||||||
|
{
|
||||||
|
action: "save_file",
|
||||||
|
attachment: message.attachments[0]
|
||||||
|
},
|
||||||
|
type === "Image"
|
||||||
|
? "save_image"
|
||||||
|
: type === "Video"
|
||||||
|
? "save_video"
|
||||||
|
: "save_file"
|
||||||
|
);
|
||||||
|
|
||||||
|
generateAction(
|
||||||
|
{
|
||||||
|
action: "copy_file_link",
|
||||||
|
attachment: message.attachments[0]
|
||||||
|
},
|
||||||
|
"copy_link"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.activeElement?.tagName === "A") {
|
||||||
|
let link = document.activeElement.getAttribute(
|
||||||
|
"href"
|
||||||
|
);
|
||||||
|
if (link) {
|
||||||
|
pushDivider();
|
||||||
|
generateAction({ action: "open_link", link });
|
||||||
|
generateAction({ action: "copy_link", link });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = server?._id ?? channel?._id ?? user?._id ?? message?._id;
|
||||||
|
if (id) {
|
||||||
|
pushDivider();
|
||||||
|
|
||||||
|
if (channel) {
|
||||||
|
switch (channel.channel_type) {
|
||||||
|
case 'Group':
|
||||||
|
// ! generateAction({ action: "create_invite", target: channel }); FIXME: add support for group invites
|
||||||
|
generateAction({ action: "open_channel_settings", id: channel._id }, "open_group_settings");
|
||||||
|
generateAction({ action: "leave_group", target: channel }, "leave_group");
|
||||||
|
break;
|
||||||
|
case 'DirectMessage':
|
||||||
|
generateAction({ action: "close_dm", target: channel });
|
||||||
|
break;
|
||||||
|
case 'TextChannel':
|
||||||
|
// ! FIXME: add permission for invites
|
||||||
|
generateAction({ action: "create_invite", target: channel });
|
||||||
|
|
||||||
|
if (serverPermissions & ServerPermission.ManageServer)
|
||||||
|
generateAction({ action: "open_server_channel_settings", server: channel.server, id: channel._id }, "open_channel_settings");
|
||||||
|
|
||||||
|
if (serverPermissions & ServerPermission.ManageChannels)
|
||||||
|
generateAction({ action: "delete_channel", target: channel });
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sid && server) {
|
||||||
|
if (serverPermissions & ServerPermission.ManageServer)
|
||||||
|
generateAction({ action: "open_server_settings", id: server._id }, "open_server_settings");
|
||||||
|
|
||||||
|
if (userId === server.owner) {
|
||||||
|
generateAction({ action: "delete_server", target: server }, "delete_server");
|
||||||
|
} else {
|
||||||
|
generateAction({ action: "leave_server", target: server }, "leave_server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateAction(
|
||||||
|
{ action: "copy_id", id },
|
||||||
|
sid ? "copy_sid" :
|
||||||
|
cid ? "copy_cid" :
|
||||||
|
message ? "copy_mid" : "copy_uid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}}
|
||||||
|
</ContextMenuWithData>
|
||||||
|
<ContextMenu id="Status" onClose={contextClick}>
|
||||||
|
<span data-disabled={true}>@{client.user?.username}</span>
|
||||||
|
<LineDivider />
|
||||||
|
<MenuItem
|
||||||
|
data={{
|
||||||
|
action: "set_presence",
|
||||||
|
presence: Users.Presence.Online
|
||||||
|
}}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
<div className="indicator online" />
|
||||||
|
<Text id={`app.status.online`} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
data={{
|
||||||
|
action: "set_presence",
|
||||||
|
presence: Users.Presence.Idle
|
||||||
|
}}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
<div className="indicator idle" />
|
||||||
|
<Text id={`app.status.idle`} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
data={{
|
||||||
|
action: "set_presence",
|
||||||
|
presence: Users.Presence.Busy
|
||||||
|
}}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
<div className="indicator busy" />
|
||||||
|
<Text id={`app.status.busy`} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
data={{
|
||||||
|
action: "set_presence",
|
||||||
|
presence: Users.Presence.Invisible
|
||||||
|
}}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
<div className="indicator invisible" />
|
||||||
|
<Text id={`app.status.invisible`} />
|
||||||
|
</MenuItem>
|
||||||
|
<LineDivider />
|
||||||
|
<MenuItem data={{ action: "set_status" }} disabled={!isOnline}>
|
||||||
|
<Text id={`app.context_menu.custom_status`} />
|
||||||
|
</MenuItem>
|
||||||
|
{client.user?.status?.text && (
|
||||||
|
<MenuItem
|
||||||
|
data={{ action: "clear_status" }}
|
||||||
|
disabled={!isOnline}
|
||||||
|
>
|
||||||
|
<Text id={`app.context_menu.clear_status`} />
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectState(
|
||||||
|
ContextMenus,
|
||||||
|
() => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
|
@ -2,8 +2,8 @@ import { useState } from "preact/hooks";
|
||||||
|
|
||||||
const counts: { [key: string]: number } = {};
|
const counts: { [key: string]: number } = {};
|
||||||
|
|
||||||
export default function PaintCounter({ small }: { small?: boolean }) {
|
export default function PaintCounter({ small, always }: { small?: boolean, always?: boolean }) {
|
||||||
if (import.meta.env.PROD) return null;
|
if (import.meta.env.PROD && !always) return null;
|
||||||
|
|
||||||
const [uniqueId] = useState('' + Math.random());
|
const [uniqueId] = useState('' + Math.random());
|
||||||
const count = counts[uniqueId] ?? 0;
|
const count = counts[uniqueId] ?? 0;
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { Docked, OverlappingPanels } from "react-overlapping-panels";
|
import { Docked, OverlappingPanels } from "react-overlapping-panels";
|
||||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||||
import Popovers from "../context/intermediate/Popovers";
|
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import Popovers from "../context/intermediate/Popovers";
|
||||||
|
import ContextMenus from "../lib/ContextMenus";
|
||||||
|
|
||||||
import LeftSidebar from "../components/navigation/LeftSidebar";
|
import LeftSidebar from "../components/navigation/LeftSidebar";
|
||||||
import RightSidebar from "../components/navigation/RightSidebar";
|
import RightSidebar from "../components/navigation/RightSidebar";
|
||||||
|
|
||||||
import Home from './home/Home';
|
import Home from './home/Home';
|
||||||
import Friends from "./friends/Friends";
|
import Friends from "./friends/Friends";
|
||||||
|
import Developer from "./developer/Developer";
|
||||||
|
|
||||||
const Routes = styled.div`
|
const Routes = styled.div`
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -28,6 +31,10 @@ export default function App() {
|
||||||
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route path="/dev">
|
||||||
|
<Developer />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/friends">
|
<Route path="/friends">
|
||||||
<Friends />
|
<Friends />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -37,6 +44,7 @@ export default function App() {
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<ContextMenus />
|
||||||
<Popovers />
|
<Popovers />
|
||||||
</OverlappingPanels>
|
</OverlappingPanels>
|
||||||
);
|
);
|
||||||
|
|
39
src/pages/developer/Developer.tsx
Normal file
39
src/pages/developer/Developer.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import Header from "../../components/ui/Header";
|
||||||
|
import PaintCounter from "../../lib/PaintCounter";
|
||||||
|
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||||
|
import { useUserPermission } from "../../context/revoltjs/hooks";
|
||||||
|
|
||||||
|
export default function Developer() {
|
||||||
|
// const voice = useContext(VoiceContext);
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
const userPermission = useUserPermission(client.user!._id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header placement="primary">Developer Tab</Header>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
<PaintCounter always />
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
<b>User ID:</b> {client.user!._id} <br/>
|
||||||
|
<b>Permission against self:</b> {userPermission} <br/>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
{/*<span>
|
||||||
|
<b>Voice Status:</b> {VoiceStatus[voice.status]}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
<b>Voice Room ID:</b> {voice.roomId || "undefined"}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
<b>Voice Participants:</b> [
|
||||||
|
{Array.from(voice.participants.keys()).join(", ")}]
|
||||||
|
</span>
|
||||||
|
<br />*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
28
src/pages/home/Home.module.scss
Normal file
28
src/pages/home/Home.module.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.home {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 1em 0;
|
||||||
|
font-size: 48px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: lower-greek;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-light="true"] .home svg {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
|
@ -1,9 +1,35 @@
|
||||||
import PaintCounter from "../../lib/PaintCounter";
|
import styles from "./Home.module.scss";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import Header from "../../components/ui/Header";
|
||||||
|
// import WideLogo from "../../../../../assets/wide.svg";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.home}>
|
||||||
<PaintCounter />
|
<Header placement="primary"><Text id="app.navigation.tabs.home" /></Header>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.special.modals.onboarding.welcome" /> {/*<WideLogo />*/}
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Go to your <Link to="/friends">friends list</Link>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Give <Link to="/settings/feedback">feedback</Link>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Join <Link to="/invite/Testers">testers server</Link>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
View{" "}
|
||||||
|
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||||
|
source code
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
58
src/styles/_context-menu.scss
Normal file
58
src/styles/_context-menu.scss
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
.preact-context-menu .context-menu {
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
background: var(--primary-background) !important;
|
||||||
|
box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
> span {
|
||||||
|
gap: 6px;
|
||||||
|
margin: 2px 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: .875rem;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:not([data-disabled="true"]) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: .650rem;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
background: var(--status-online);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.idle {
|
||||||
|
background: var(--status-away);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.busy {
|
||||||
|
background: var(--status-busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invisible {
|
||||||
|
background: var(--status-invisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
@import "context-menu";
|
||||||
@import "elements";
|
@import "elements";
|
||||||
@import "fonts";
|
@import "fonts";
|
||||||
@import "page";
|
@import "page";
|
||||||
|
|
Loading…
Add table
Reference in a new issue