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) => (