diff --git a/src/components/common/messaging/embed/Embed.tsx b/src/components/common/messaging/embed/Embed.tsx
index 7875b364..c53ebdfb 100644
--- a/src/components/common/messaging/embed/Embed.tsx
+++ b/src/components/common/messaging/embed/Embed.tsx
@@ -83,7 +83,7 @@ export default function Embed({ embed }: Props) {
className={classNames(styles.embed, styles.website)}
style={{
borderInlineStartColor:
- embed.color ?? "var(--tertiary-background)",
+ embed.colour ?? "var(--tertiary-background)",
width: width + CONTAINER_PADDING,
}}>
diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx
index d79c2bbe..81fd852a 100644
--- a/src/components/common/user/UserIcon.tsx
+++ b/src/components/common/user/UserIcon.tsx
@@ -12,8 +12,9 @@ import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient";
import IconBase, { IconBaseProps } from "../IconBase";
import fallback from "../assets/user.png";
+import {VolumeMute} from "@styled-icons/boxicons-solid";
-type VoiceStatus = "muted";
+type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps
{
mask?: string;
status?: boolean;
@@ -47,7 +48,7 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
}
${(props) =>
- props.status === "muted" &&
+ (props.status === "muted" || props.status === "deaf") &&
css`
background: var(--error);
`}
@@ -125,7 +126,9 @@ export default observer(
{props.voice && (
- {props.voice === "muted" && (
+ {props.voice === "deaf" && (
+
+ ) ||props.voice === "muted" && (
)}
diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx
index fe73b11a..9fe876b4 100644
--- a/src/components/markdown/Renderer.tsx
+++ b/src/components/markdown/Renderer.tsx
@@ -24,6 +24,7 @@ import { generateEmoji } from "../common/Emoji";
import { emojiDictionary } from "../../assets/emojis";
import { MarkdownProps } from "./Markdown";
+import {useIntermediate} from "../../context/intermediate/Intermediate";
// TODO: global.d.ts file for defining globals
declare global {
@@ -32,6 +33,13 @@ declare global {
}
}
+const ALLOWED_ORIGINS = [
+ location.hostname,
+ 'app.revolt.chat',
+ 'nightly.revolt.chat',
+ 'local.revolt.chat',
+];
+
// Handler for code block copy.
if (typeof window !== "undefined") {
window.copycode = function (element: HTMLDivElement) {
@@ -90,6 +98,8 @@ const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const client = useContext(AppContext);
+ const { openScreen } = useIntermediate();
+
if (typeof content === "undefined") return null;
if (content.length === 0) return null;
@@ -172,7 +182,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
try {
const url = new URL(href, location.href);
- if (url.hostname === location.hostname) {
+ if (ALLOWED_ORIGINS.includes(url.hostname)) {
internal = true;
element.addEventListener(
"click",
@@ -191,6 +201,13 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
if (!internal) {
element.setAttribute("target", "_blank");
+ element.onclick = (ev) => {
+ ev.preventDefault();
+ openScreen({
+ id: "external_link_prompt",
+ link: href
+ })
+ }
}
},
);
diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx
index 61784aae..a4fef41d 100644
--- a/src/components/navigation/left/ServerListSidebar.tsx
+++ b/src/components/navigation/left/ServerListSidebar.tsx
@@ -240,15 +240,16 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0;
for (const x of channels) {
- if (
- (x.channel?.channel_type === "DirectMessage"
- ? x.channel?.active
- : x.channel?.channel_type === "Group") &&
- x.unread
- ) {
+ if (x.channel?.channel_type === "Group" && x.unread) {
homeUnread = "unread";
alertCount += x.alertCount ?? 0;
}
+ if (
+ x.channel?.channel_type === "DirectMessage" &&
+ x.unread &&
+ x.unread.length > 0
+ )
+ alertCount++;
}
alertCount += [...client.users.values()].filter(
diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx
index a3d30489..59a3cc1c 100644
--- a/src/context/intermediate/Intermediate.tsx
+++ b/src/context/intermediate/Intermediate.tsx
@@ -24,6 +24,7 @@ export type Screen =
| { id: "signed_out" }
| { id: "error"; error: string }
| { id: "clipboard"; text: string }
+ | { id: "external_link_prompt"; link: string }
| {
id: "_prompt";
question: Children;
diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx
index fdd3dc6e..8815a16d 100644
--- a/src/context/intermediate/Modals.tsx
+++ b/src/context/intermediate/Modals.tsx
@@ -9,6 +9,7 @@ import { InputModal } from "./modals/Input";
import { OnboardingModal } from "./modals/Onboarding";
import { PromptModal } from "./modals/Prompt";
import { SignedOutModal } from "./modals/SignedOut";
+import {ExternalLinkModal} from "./modals/ExternalLinkPrompt";
export interface Props {
screen: Screen;
@@ -34,6 +35,8 @@ export default function Modals({ screen, openScreen }: Props) {
return ;
case "onboarding":
return ;
+ case "external_link_prompt":
+ return ;
}
return null;
diff --git a/src/context/intermediate/modals/ExternalLinkPrompt.tsx b/src/context/intermediate/modals/ExternalLinkPrompt.tsx
new file mode 100644
index 00000000..04fecfcf
--- /dev/null
+++ b/src/context/intermediate/modals/ExternalLinkPrompt.tsx
@@ -0,0 +1,34 @@
+import { Text } from "preact-i18n";
+
+import Modal from "../../../components/ui/Modal";
+
+interface Props {
+ onClose: () => void;
+ link: string;
+}
+
+export function ExternalLinkModal({ onClose, link }: Props) {
+ return (
+ }
+ actions={[
+ {
+ onClick: ()=>{window.open(link, "_blank");},
+ confirmation: true,
+ contrast: true,
+ accent: true,
+ children: "Continue",
+ },
+ {
+ onClick: onClose,
+ confirmation: false,
+ children: "Cancel",
+ },
+ ]}>
+
+ {link}
+
+ );
+}
diff --git a/src/lib/vortex/VoiceClient.ts b/src/lib/vortex/VoiceClient.ts
index 8a8bab28..507f1f1e 100644
--- a/src/lib/vortex/VoiceClient.ts
+++ b/src/lib/vortex/VoiceClient.ts
@@ -40,6 +40,8 @@ export default class VoiceClient extends EventEmitter {
sendTransport?: Transport;
recvTransport?: Transport;
+ isDeaf?: boolean;
+
userId?: string;
roomId?: string;
participants: Map;
@@ -54,6 +56,8 @@ export default class VoiceClient extends EventEmitter {
this.participants = new Map();
this.consumers = new Map();
+ this.isDeaf = false;
+
this.signaling.on(
"data",
(json) => {
diff --git a/src/lib/vortex/VoiceState.ts b/src/lib/vortex/VoiceState.ts
index 0c32fc16..9ce9efdd 100644
--- a/src/lib/vortex/VoiceState.ts
+++ b/src/lib/vortex/VoiceState.ts
@@ -143,6 +143,33 @@ class VoiceStateReference {
}
}
+ isDeaf() {
+ if(!this.client)
+ return false;
+
+ return this.client.isDeaf;
+ }
+
+ async startDeafen() {
+ if(!this.client)
+ return console.log("No client object"); // ! TODO: let the user know
+
+ this.client.isDeaf = true;
+
+ this.client?.consumers.forEach(consumer => {
+ consumer.audio?.pause();
+ })
+ }
+ async stopDeafen() {
+ if(!this.client)
+ return console.log("No client object"); // ! TODO: let the user know
+
+ this.client.isDeaf = false;
+ this.client?.consumers.forEach(consumer => {
+ consumer.audio?.resume();
+ })
+ }
+
async startProducing(type: ProduceType) {
switch (type) {
case "audio": {
@@ -152,8 +179,10 @@ class VoiceStateReference {
if (navigator.mediaDevices === undefined)
return console.log("No media devices."); // ! TODO: let the user know
+ const mediaDevice = window.localStorage.getItem("audioInputDevice");
+
const mediaStream = await navigator.mediaDevices.getUserMedia({
- audio: true,
+ audio: mediaDevice?{deviceId: mediaDevice}:true
});
await this.client?.startProduce(
diff --git a/src/pages/channels/messaging/MessageEditor.tsx b/src/pages/channels/messaging/MessageEditor.tsx
index 7b4af96e..1a036d87 100644
--- a/src/pages/channels/messaging/MessageEditor.tsx
+++ b/src/pages/channels/messaging/MessageEditor.tsx
@@ -96,7 +96,7 @@ export default function MessageEditor({ message, finish }: Props) {
{
target={user}
status={false}
voice={
- voiceState.participants!.get(id)
+ client.user?._id === id && voiceState.isDeaf()?"deaf"
+ : voiceState.participants!.get(id)
?.audio
? undefined
: "muted"
@@ -115,18 +128,38 @@ export default observer(({ id }: Props) => {
)}
-
+
+
+
{voiceState.isProducing("audio") ? (
-
+
+
+
) : (
-
+
+
+
)}
+ {voiceState.isDeaf() ? (
+
+
+
+ ): (
+
+
+
+ )
+ }
);
diff --git a/src/pages/login/forms/MailProvider.tsx b/src/pages/login/forms/MailProvider.tsx
index f4deca50..eb255665 100644
--- a/src/pages/login/forms/MailProvider.tsx
+++ b/src/pages/login/forms/MailProvider.tsx
@@ -16,10 +16,12 @@ function mapMailProvider(email?: string): [string, string] | undefined {
const domain = match[1];
switch (domain) {
case "gmail.com":
+ case "googlemail.com":
return ["Gmail", "https://gmail.com"];
case "tuta.io":
return ["Tutanota", "https://mail.tutanota.com"];
case "outlook.com":
+ case "hotmail.com":
return ["Outlook", "https://outlook.live.com"];
case "yahoo.com":
return ["Yahoo", "https://mail.yahoo.com"];
@@ -27,11 +29,24 @@ function mapMailProvider(email?: string): [string, string] | undefined {
return ["WP Poczta", "https://poczta.wp.pl"];
case "protonmail.com":
case "protonmail.ch":
+ case "pm.me":
return ["ProtonMail", "https://mail.protonmail.com"];
case "seznam.cz":
case "email.cz":
case "post.cz":
return ["Seznam", "https://email.seznam.cz"];
+ case "zoho.com":
+ return ["Zoho Mail", "https://mail.zoho.com/zm/"];
+ case "aol.com":
+ case "aim.com":
+ return ["AOL Mail", "https://mail.aol.com/"];
+ case "icloud.com":
+ return ["iCloud Mail", "https://mail.aol.com/"];
+ case "mail.com":
+ case "email.com":
+ return ["mail.com", "https://www.mail.com/mail/"];
+ case "yandex.com":
+ return ["Yandex Mail", "https://mail.yandex.com/"];
default:
return [domain, `https://${domain}`];
}
diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index 315a782d..c2754ed5 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -15,6 +15,7 @@ import {
Flask,
User,
Megaphone,
+ Speaker,
} from "@styled-icons/boxicons-solid";
import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js";
@@ -37,6 +38,7 @@ import { APP_VERSION } from "../../version";
import { GenericSettings } from "./GenericSettings";
import { Account } from "./panes/Account";
import { Appearance } from "./panes/Appearance";
+import { Audio } from "./panes/Audio";
import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages";
@@ -85,6 +87,12 @@ export default function Settings() {
category: (
),
+ id: "audio",
+ icon: ,
+ title: ,
+ },
+ {
+
id: "appearance",
icon: ,
title: ,
@@ -141,6 +149,9 @@ export default function Settings() {
+
+
+
diff --git a/src/pages/settings/panes/Audio.tsx b/src/pages/settings/panes/Audio.tsx
new file mode 100644
index 00000000..73a460d9
--- /dev/null
+++ b/src/pages/settings/panes/Audio.tsx
@@ -0,0 +1,61 @@
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
+
+import { connectState } from "../../../redux/connector";
+
+import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState";
+
+import ComboBox from "../../../components/ui/ComboBox";
+import {useEffect, useState} from "preact/hooks";
+
+export function Component() {
+ const [mediaDevices, setMediaDevices] = useState(undefined);
+
+ useEffect(() => {
+ navigator
+ .mediaDevices
+ .enumerateDevices()
+ .then( devices => {
+ setMediaDevices(devices)
+ })
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ changeAudioDevice(e.currentTarget.value, "input")}>
+ {
+ mediaDevices?.filter(device => device.kind === "audioinput").map(device => {
+ return (
+
+ )
+ })
+ }
+
+
+ >
+ );
+}
+
+function changeAudioDevice(deviceId: string, deviceType: string) {
+ if(deviceType === "input") {
+ window.localStorage.setItem("audioInputDevice", deviceId)
+ if(voiceState.isProducing("audio")) {
+ voiceState.stopProducing("audio");
+ voiceState.startProducing("audio");
+ }
+ }else if(deviceType === "output") {
+ window.localStorage.setItem("audioOutputDevice", deviceId)
+ }
+}
+
+export const Audio = connectState(Component, () => {
+ return;
+});
diff --git a/src/pages/settings/server/Overview.tsx b/src/pages/settings/server/Overview.tsx
index b72eb60f..c4b962a2 100644
--- a/src/pages/settings/server/Overview.tsx
+++ b/src/pages/settings/server/Overview.tsx
@@ -152,7 +152,7 @@ export const Overview = observer(({ server }: Props) => {
{server.channels
- .filter((x) => typeof x !== "undefined")
+ .filter((x) => (typeof x !== "undefined" && x.channel_type === "TextChannel"))
.map((channel) => (