diff --git a/src/components/common/UserHeader.tsx b/src/components/common/UserHeader.tsx
index eae71db9..6a8265e3 100644
--- a/src/components/common/UserHeader.tsx
+++ b/src/components/common/UserHeader.tsx
@@ -1,11 +1,17 @@
+import Tooltip from "./Tooltip";
import { User } from "revolt.js";
import Header from "../ui/Header";
import UserIcon from "./UserIcon";
+import { Text } from "preact-i18n";
import UserStatus from './UserStatus';
import styled from "styled-components";
import { Localizer } from 'preact-i18n';
+import { Link } from "react-router-dom";
+import IconButton from "../ui/IconButton";
import { Settings } from "@styled-icons/feather";
+import { openContextMenu } from "preact-context-menu";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+import { useIntermediate } from "../../context/intermediate/Intermediate";
const HeaderBase = styled.div`
gap: 0;
@@ -39,12 +45,10 @@ interface Props {
}
export default function UserHeader({ user }: Props) {
- function openPresenceSelector() {
- // openContextMenu("Status");
- }
+ const { writeClipboard } = useIntermediate();
- function writeClipboard(a: string) {
- alert('unimplemented');
+ function openPresenceSelector() {
+ openContextMenu("Status");
}
return (
@@ -57,12 +61,12 @@ export default function UserHeader({ user }: Props) {
/>
- {/*}>*/}
+ }>
writeClipboard(user.username)}>
@{user.username}
- {/**/}
+
@@ -70,9 +74,11 @@ export default function UserHeader({ user }: Props) {
{ !isTouchscreenDevice &&
- {/**/}
-
- {/**/}
+
+
+
+
+
}
)
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
index cc4d6dcb..bf1b99ce 100644
--- a/src/components/navigation/left/HomeSidebar.tsx
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -22,6 +22,7 @@ import Header from '../../ui/Header';
import UserHeader from "../../common/UserHeader";
import Category from '../../ui/Category';
import PaintCounter from "../../../lib/PaintCounter";
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
type Props = WithDispatcher & {
unreads: Unreads;
@@ -50,7 +51,7 @@ function HomeSidebar(props: Props) {
const { pathname } = useLocation();
const client = useContext(AppContext);
const { channel } = useParams<{ channel: string }>();
- // const { openScreen, writeClipboard } = useContext(IntermediateContext);
+ const { openScreen, writeClipboard } = useIntermediate();
const ctx = useForceUpdate();
const users = useUsers(undefined, ctx);
@@ -119,7 +120,7 @@ function HomeSidebar(props: Props) {
) as any
}
- action={() => /*openScreen({ id: "special_input", type: "create_group" })*/{}}
+ action={() => openScreen({ id: "special_input", type: "create_group" })}
/>
{channelsArr.length === 0 && }
diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx
new file mode 100644
index 00000000..c1d35ba0
--- /dev/null
+++ b/src/lib/ContextMenus.tsx
@@ -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 (
+ <>
+
+
+ @{client.user?.username}
+
+
+
+
+
+
+
+ {client.user?.status?.text && (
+
+ )}
+
+ >
+ );
+}
+
+export default connectState(
+ ContextMenus,
+ () => {
+ return {};
+ },
+ true
+);
diff --git a/src/lib/PaintCounter.tsx b/src/lib/PaintCounter.tsx
index 668ce682..487a015c 100644
--- a/src/lib/PaintCounter.tsx
+++ b/src/lib/PaintCounter.tsx
@@ -2,8 +2,8 @@ import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {};
-export default function PaintCounter({ small }: { small?: boolean }) {
- if (import.meta.env.PROD) return null;
+export default function PaintCounter({ small, always }: { small?: boolean, always?: boolean }) {
+ if (import.meta.env.PROD && !always) return null;
const [uniqueId] = useState('' + Math.random());
const count = counts[uniqueId] ?? 0;
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
index 4648649f..f0ff58d3 100644
--- a/src/pages/App.tsx
+++ b/src/pages/App.tsx
@@ -1,14 +1,17 @@
import { Docked, OverlappingPanels } from "react-overlapping-panels";
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
-import Popovers from "../context/intermediate/Popovers";
import { Switch, Route } from "react-router-dom";
import styled from "styled-components";
+import Popovers from "../context/intermediate/Popovers";
+import ContextMenus from "../lib/ContextMenus";
+
import LeftSidebar from "../components/navigation/LeftSidebar";
import RightSidebar from "../components/navigation/RightSidebar";
import Home from './home/Home';
import Friends from "./friends/Friends";
+import Developer from "./developer/Developer";
const Routes = styled.div`
min-width: 0;
@@ -28,6 +31,10 @@ export default function App() {
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
+
+
+
+
@@ -37,6 +44,7 @@ export default function App() {
+
);
diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx
new file mode 100644
index 00000000..e7a5cbc0
--- /dev/null
+++ b/src/pages/developer/Developer.tsx
@@ -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 (
+
+
+
+
+ User ID: {client.user!._id}
+ Permission against self: {userPermission}
+
+
+ {/*
+ Voice Status: {VoiceStatus[voice.status]}
+
+
+
+ Voice Room ID: {voice.roomId || "undefined"}
+
+
+
+ Voice Participants: [
+ {Array.from(voice.participants.keys()).join(", ")}]
+
+
*/}
+
+
+ );
+}
diff --git a/src/pages/home/Home.module.scss b/src/pages/home/Home.module.scss
new file mode 100644
index 00000000..1210ee80
--- /dev/null
+++ b/src/pages/home/Home.module.scss
@@ -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%);
+}
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
index 9ccb04f0..78e0348c 100644
--- a/src/pages/home/Home.tsx
+++ b/src/pages/home/Home.tsx
@@ -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() {
return (
-
-
+
+
+
+ {/**/}
+
+
+ -
+ Go to your friends list.
+
+ -
+ Give feedback.
+
+ -
+ Join testers server.
+
+ -
+ View{" "}
+
+ source code
+
+ .
+
+
);
}
diff --git a/src/styles/_context-menu.scss b/src/styles/_context-menu.scss
new file mode 100644
index 00000000..159efac9
--- /dev/null
+++ b/src/styles/_context-menu.scss
@@ -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);
+ }
+ }
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 7869c3d3..4508966e 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -1,3 +1,4 @@
+@import "context-menu";
@import "elements";
@import "fonts";
@import "page";