From babb53c7940cb8505b1a0fc65d63ed0152e4f532 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Wed, 23 Jun 2021 13:52:16 +0100
Subject: [PATCH] Add VoiceChannel support.

---
 external/lang                               |  2 +-
 package.json                                |  2 +-
 src/components/common/ChannelIcon.tsx       | 18 ++++---
 src/components/common/messaging/Message.tsx |  3 +-
 src/components/navigation/left/common.ts    |  3 +-
 src/context/intermediate/Intermediate.tsx   |  6 +--
 src/context/intermediate/modals/Input.tsx   | 21 +-------
 src/context/intermediate/modals/Prompt.tsx  | 59 ++++++++++++++++++++-
 src/lib/ContextMenus.tsx                    | 15 +++---
 src/pages/channels/Channel.tsx              | 43 ++++++++++-----
 src/pages/settings/channel/Overview.tsx     |  2 +-
 yarn.lock                                   |  8 +--
 12 files changed, 123 insertions(+), 59 deletions(-)

diff --git a/external/lang b/external/lang
index 332cc2d..f3d13c0 160000
--- a/external/lang
+++ b/external/lang
@@ -1 +1 @@
-Subproject commit 332cc2d7125b9cfb26ce211a9cb0fbf29301946c
+Subproject commit f3d13c09b6fa2f28f027ce32643caffadbb63cf1
diff --git a/package.json b/package.json
index bce943b..a1a8deb 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
     "react-scroll": "^1.8.2",
     "react-tippy": "^1.4.0",
     "redux": "^4.1.0",
-    "revolt.js": "4.3.1-alpha.0",
+    "revolt.js": "4.3.2",
     "rimraf": "^3.0.2",
     "sass": "^1.35.1",
     "shade-blend-color": "^1.0.0",
diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx
index a793462..fb4d1a5 100644
--- a/src/components/common/ChannelIcon.tsx
+++ b/src/components/common/ChannelIcon.tsx
@@ -1,10 +1,10 @@
 import { useContext } from "preact/hooks";
-import { Hash } from "@styled-icons/feather";
 import { Channels } from "revolt.js/dist/api/objects";
+import { Hash, Volume2 } from "@styled-icons/feather";
 import { ImageIconBase, IconBaseProps } from "./IconBase";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
-interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel> {
+interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel> {
     isServerChannel?: boolean;
 }
 
@@ -15,13 +15,19 @@ export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLI
 
     const { size, target, attachment, isServerChannel: server, animate, children, as, ...imgProps } = props;
     const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
-    const isServerChannel = server || target?.channel_type === 'TextChannel';
+    const isServerChannel = server || (target && (target.channel_type === 'TextChannel' || target.channel_type === 'VoiceChannel'));
 
     if (typeof iconURL === 'undefined') {
         if (isServerChannel) {
-            return (
-                <Hash size={size} />
-            )
+            if (target?.channel_type === 'VoiceChannel') {
+                return (
+                    <Volume2 size={size} />
+                )
+            } else {
+                return (
+                    <Hash size={size} />
+                )
+            }
         }
     }
 
diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx
index 362c087..28ee957 100644
--- a/src/components/common/messaging/Message.tsx
+++ b/src/components/common/messaging/Message.tsx
@@ -22,13 +22,14 @@ interface Props {
     head?: boolean
 }
 
-export default function Message({ attachContext, message, contrast, content: replacement, head, queued }: Props) {
+export default function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) {
     // TODO: Can improve re-renders here by providing a list
     // TODO: of dependencies. We only need to update on u/avatar.
     const user = useUser(message.author);
     const client = useContext(AppContext);
 
     const content = message.content as string;
+    const head = (message.replies && message.replies.length > 0) || preferHead;
     return (
         <MessageBase id={message._id}
             head={head}
diff --git a/src/components/navigation/left/common.ts b/src/components/navigation/left/common.ts
index 12d8f9b..2a681d1 100644
--- a/src/components/navigation/left/common.ts
+++ b/src/components/navigation/left/common.ts
@@ -16,7 +16,8 @@ export function useUnreads({ channel, unreads, dispatcher }: UnreadProps, contex
         function checkUnread(target?: Channel) {
             if (!target) return;
             if (target._id !== channel._id) return;
-            if (target?.channel_type === "SavedMessages") return;
+            if (target.channel_type === "SavedMessages" ||
+                target.channel_type === "VoiceChannel") return;
 
             const unread = unreads[channel._id]?.last_id;
             if (target.last_message) {
diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx
index 67b5f01..f64acce 100644
--- a/src/context/intermediate/Intermediate.tsx
+++ b/src/context/intermediate/Intermediate.tsx
@@ -26,11 +26,11 @@ export type Screen =
     { type: "delete_message", target: Channels.Message } |
     { type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
     { type: "kick_member", target: Servers.Server, user: string } |
-    { type: "ban_member", target: Servers.Server, user: string }
+    { type: "ban_member", target: Servers.Server, user: string } |
+    { type: "create_channel", target: Servers.Server }
 )) |
 ({ id: "special_input" } & (
-    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
-    { type: "create_channel", server: string }
+    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" }
 ))
 | {
       id: "_input";
diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx
index 3d28785..b589737 100644
--- a/src/context/intermediate/modals/Input.tsx
+++ b/src/context/intermediate/modals/Input.tsx
@@ -65,8 +65,7 @@ export function InputModal({
 }
 
 type SpecialProps = { onClose: () => void } & (
-    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
-    { type: "create_channel", server: string }
+    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" }
 )
 
 export function SpecialInputModal(props: SpecialProps) {
@@ -110,24 +109,6 @@ export function SpecialInputModal(props: SpecialProps) {
                 }}
             />;
         }
-        case "create_channel": {
-            return <InputModal
-                onClose={onClose}
-                question={<Text id="app.context_menu.create_channel" />}
-                field={<Text id="app.main.servers.channel_name" />}
-                callback={async name => {
-                    const channel = await client.servers.createChannel(
-                        props.server,
-                        {
-                            name,
-                            nonce: ulid()
-                        }
-                    );
-
-                    history.push(`/server/${props.server}/channel/${channel._id}`);
-                }}
-            />;
-        }
         case "set_custom_status": {
             return <InputModal
                 onClose={onClose}
diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx
index 1a48e62..78048d8 100644
--- a/src/context/intermediate/modals/Prompt.tsx
+++ b/src/context/intermediate/modals/Prompt.tsx
@@ -1,5 +1,8 @@
+import { ulid } from "ulid";
 import { Text } from "preact-i18n";
 import styles from './Prompt.module.scss';
+import { useHistory } from "react-router-dom";
+import Radio from "../../../components/ui/Radio";
 import { Children } from "../../../types/Preact";
 import { useIntermediate } from "../Intermediate";
 import InputBox from "../../../components/ui/InputBox";
@@ -44,7 +47,8 @@ type SpecialProps = { onClose: () => void } & (
     { type: "delete_message", target: Channels.Message } |
     { type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
     { type: "kick_member", target: Servers.Server, user: string } |
-    { type: "ban_member", target: Servers.Server, user: string }
+    { type: "ban_member", target: Servers.Server, user: string } |
+    { type: "create_channel", target: Servers.Server }
 )
 
 export function SpecialPromptModal(props: SpecialProps) {
@@ -263,6 +267,59 @@ export function SpecialPromptModal(props: SpecialProps) {
                 />
             )
         }
+        case 'create_channel': {
+            const [ name, setName ] = useState('');
+            const [ type, setType ] = useState<'Text' | 'Voice'>('Text');
+            const history = useHistory();
+
+            return (
+                <PromptModal
+                    onClose={onClose}
+                    question={<Text id="app.context_menu.create_channel" />}
+                    actions={[
+                        {
+                            confirmation: true,
+                            contrast: true,
+                            text: <Text id="app.special.modals.actions.create" />,
+                            onClick: async () => {
+                                setProcessing(true);
+
+                                try {
+                                    const channel = await client.servers.createChannel(
+                                        props.target._id,
+                                        {
+                                            type,
+                                            name,
+                                            nonce: ulid()
+                                        }
+                                    );
+                
+                                    history.push(`/server/${props.target._id}/channel/${channel._id}`);
+                                    onClose();
+                                } catch (err) {
+                                    setError(takeError(err));
+                                    setProcessing(false);
+                                }
+                            }
+                        },
+                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
+                    ]}
+                    content={<>
+                        <Overline block type="subtle"><Text id="app.main.servers.channel_type" /></Overline>
+                        <Radio checked={type === 'Text'} onSelect={() => setType('Text')}>
+                            <Text id="app.main.servers.text_channel" /></Radio>
+                        <Radio checked={type === 'Voice'} onSelect={() => setType('Voice')}>
+                            <Text id="app.main.servers.voice_channel" /></Radio>
+                        <Overline block type="subtle"><Text id="app.main.servers.channel_name" /></Overline>
+                        <InputBox
+                            value={name}
+                            onChange={e => setName(e.currentTarget.value)} />
+                    </>}
+                    disabled={processing}
+                    error={error}
+                />
+            )
+        }
         default: return null;
     }
 }
diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx
index 73e5403..d5c8467 100644
--- a/src/lib/ContextMenus.tsx
+++ b/src/lib/ContextMenus.tsx
@@ -60,7 +60,7 @@ type Action =
     | { action: "set_presence"; presence: Users.Presence }
     | { action: "set_status" }
     | { action: "clear_status" }
-    | { action: "create_channel"; server: string }
+    | { action: "create_channel"; target: Servers.Server }
     | { action: "create_invite"; target: Channels.GroupChannel | Channels.TextChannel }
     | { action: "leave_group"; target: Channels.GroupChannel }
     | { action: "delete_channel"; target: Channels.TextChannel }
@@ -92,7 +92,8 @@ function ContextMenus(props: WithDispatcher) {
                     break;
                 case "mark_as_read":
                     {
-                        if (data.channel.channel_type === 'SavedMessages') return;
+                        if (data.channel.channel_type === 'SavedMessages' ||
+                            data.channel.channel_type === 'VoiceChannel') return;
 
                         let message = data.channel.channel_type === 'TextChannel' ? data.channel.last_message : data.channel.last_message._id;
                         props.dispatcher({
@@ -280,14 +281,13 @@ function ContextMenus(props: WithDispatcher) {
                 case "delete_channel":
                 case "delete_server":
                 case "delete_message":
+                case "create_channel":
                 // @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;
@@ -341,9 +341,12 @@ function ContextMenus(props: WithDispatcher) {
                     }
 
                     if (server_list) {
+                        let server = useServer(server_list, forceUpdate);
                         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 });
+                        if (server) {
+                            if (permissions & ServerPermission.ManageChannels) generateAction({ action: 'create_channel', target: server });
+                            if (permissions & ServerPermission.ManageServer) generateAction({ action: 'open_server_settings', id: server_list });
+                        }
 
                         return elements;
                     }
diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx
index 8d32b3b..70a7d02 100644
--- a/src/pages/channels/Channel.tsx
+++ b/src/pages/channels/Channel.tsx
@@ -10,6 +10,7 @@ import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
 import MemberSidebar from "../../components/navigation/right/MemberSidebar";
 import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
 import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
+import { Channel } from "revolt.js";
 
 const ChannelMain = styled.div`
     flex-grow: 1;
@@ -31,22 +32,36 @@ export function Channel({ id }: { id: string }) {
     const channel = useChannel(id, ctx);
 
     if (!channel) return null;
+
+    if (channel.channel_type === 'VoiceChannel') {
+        return <VoiceChannel channel={channel} />;
+    } else {
+        return <TextChannel channel={channel} />;
+    }
+}
+
+function TextChannel({ channel }: { channel: Channel }) {
     const [ showMembers, setMembers ] = useState(true);
 
-    return (
-        <>
-            <ChannelHeader channel={channel} toggleSidebar={() => setMembers(!showMembers)} />
-            <ChannelMain>
-                <ChannelContent>
-                    <MessageArea id={id} />
-                    <TypingIndicator id={channel._id} />
-                    <JumpToBottom id={id} />
-                    <MessageBox channel={channel} />
-                </ChannelContent>
-                { !isTouchscreenDevice && showMembers && <MemberSidebar channel={channel} /> }
-            </ChannelMain>
-        </>
-    )
+    let id = channel._id;
+    return <>
+        <ChannelHeader channel={channel} toggleSidebar={() => setMembers(!showMembers)} />
+        <ChannelMain>
+            <ChannelContent>
+                <MessageArea id={id} />
+                <TypingIndicator id={id} />
+                <JumpToBottom id={id} />
+                <MessageBox channel={channel} />
+            </ChannelContent>
+            { !isTouchscreenDevice && showMembers && <MemberSidebar channel={channel} /> }
+        </ChannelMain>
+    </>;
+}
+
+function VoiceChannel({ channel }: { channel: Channel }) {
+    return <>
+        <ChannelHeader channel={channel} />
+    </>;
 }
 
 export default function() {
diff --git a/src/pages/settings/channel/Overview.tsx b/src/pages/settings/channel/Overview.tsx
index 30c36b9..60e8cce 100644
--- a/src/pages/settings/channel/Overview.tsx
+++ b/src/pages/settings/channel/Overview.tsx
@@ -9,7 +9,7 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { FileUploader } from "../../../context/revoltjs/FileUploads";
 
 interface Props {
-    channel: Channels.GroupChannel | Channels.TextChannel;
+    channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
 }
 
 export function Overview({ channel }: Props) {
diff --git a/yarn.lock b/yarn.lock
index 18f56c4..2ec6929 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3344,10 +3344,10 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
-revolt.js@4.3.1-alpha.0:
-  version "4.3.1-alpha.0"
-  resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.1-alpha.0.tgz#21abb0706852468a0b7991a80d81093f547d25f3"
-  integrity sha512-YwDdDgioVYeBYkgZtgtXM37//96WmT18XVPJ7cBJzDQ3GWUKKPrw4VFjmi9FSh0ksfgfkSIrA7/hqmztZWbnVw==
+revolt.js@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.2.tgz#2e613ff1d918d77266e9c777e226bfbddd5a9b87"
+  integrity sha512-JyD3fRaory3Rhy/sAWcvHjLb/CluJRZap2Di2ZFFf9uiRJBgLNlClS/3RkBLAcQqx4KVx7Ua3WbKq1/dU6x7dQ==
   dependencies:
     "@insertish/mutable" "1.1.0"
     axios "^0.19.2"
-- 
GitLab