This commit is contained in:
Paul 2021-08-30 12:35:14 +01:00
commit ea2e6ada82
17 changed files with 244 additions and 27 deletions

View file

@ -83,7 +83,7 @@ export default function Embed({ embed }: Props) {
className={classNames(styles.embed, styles.website)} className={classNames(styles.embed, styles.website)}
style={{ style={{
borderInlineStartColor: borderInlineStartColor:
embed.color ?? "var(--tertiary-background)", embed.colour ?? "var(--tertiary-background)",
width: width + CONTAINER_PADDING, width: width + CONTAINER_PADDING,
}}> }}>
<div> <div>

View file

@ -12,8 +12,9 @@ import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient";
import IconBase, { IconBaseProps } from "../IconBase"; import IconBase, { IconBaseProps } from "../IconBase";
import fallback from "../assets/user.png"; import fallback from "../assets/user.png";
import {VolumeMute} from "@styled-icons/boxicons-solid";
type VoiceStatus = "muted"; type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps<User> { interface Props extends IconBaseProps<User> {
mask?: string; mask?: string;
status?: boolean; status?: boolean;
@ -47,7 +48,7 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
} }
${(props) => ${(props) =>
props.status === "muted" && (props.status === "muted" || props.status === "deaf") &&
css` css`
background: var(--error); background: var(--error);
`} `}
@ -125,7 +126,9 @@ export default observer(
{props.voice && ( {props.voice && (
<foreignObject x="22" y="22" width="10" height="10"> <foreignObject x="22" y="22" width="10" height="10">
<VoiceIndicator status={props.voice}> <VoiceIndicator status={props.voice}>
{props.voice === "muted" && ( {props.voice === "deaf" && (
<VolumeMute size={6} />
) ||props.voice === "muted" && (
<MicrophoneOff size={6} /> <MicrophoneOff size={6} />
)} )}
</VoiceIndicator> </VoiceIndicator>

View file

@ -24,6 +24,7 @@ import { generateEmoji } from "../common/Emoji";
import { emojiDictionary } from "../../assets/emojis"; import { emojiDictionary } from "../../assets/emojis";
import { MarkdownProps } from "./Markdown"; import { MarkdownProps } from "./Markdown";
import {useIntermediate} from "../../context/intermediate/Intermediate";
// TODO: global.d.ts file for defining globals // TODO: global.d.ts file for defining globals
declare global { 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. // Handler for code block copy.
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.copycode = function (element: HTMLDivElement) { window.copycode = function (element: HTMLDivElement) {
@ -90,6 +98,8 @@ const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { openScreen } = useIntermediate();
if (typeof content === "undefined") return null; if (typeof content === "undefined") return null;
if (content.length === 0) return null; if (content.length === 0) return null;
@ -172,7 +182,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
try { try {
const url = new URL(href, location.href); const url = new URL(href, location.href);
if (url.hostname === location.hostname) { if (ALLOWED_ORIGINS.includes(url.hostname)) {
internal = true; internal = true;
element.addEventListener( element.addEventListener(
"click", "click",
@ -191,6 +201,13 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
if (!internal) { if (!internal) {
element.setAttribute("target", "_blank"); element.setAttribute("target", "_blank");
element.onclick = (ev) => {
ev.preventDefault();
openScreen({
id: "external_link_prompt",
link: href
})
}
} }
}, },
); );

View file

@ -240,15 +240,16 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
let homeUnread: "mention" | "unread" | undefined; let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0; let alertCount = 0;
for (const x of channels) { for (const x of channels) {
if ( if (x.channel?.channel_type === "Group" && x.unread) {
(x.channel?.channel_type === "DirectMessage"
? x.channel?.active
: x.channel?.channel_type === "Group") &&
x.unread
) {
homeUnread = "unread"; homeUnread = "unread";
alertCount += x.alertCount ?? 0; alertCount += x.alertCount ?? 0;
} }
if (
x.channel?.channel_type === "DirectMessage" &&
x.unread &&
x.unread.length > 0
)
alertCount++;
} }
alertCount += [...client.users.values()].filter( alertCount += [...client.users.values()].filter(

View file

@ -24,6 +24,7 @@ export type Screen =
| { id: "signed_out" } | { id: "signed_out" }
| { id: "error"; error: string } | { id: "error"; error: string }
| { id: "clipboard"; text: string } | { id: "clipboard"; text: string }
| { id: "external_link_prompt"; link: string }
| { | {
id: "_prompt"; id: "_prompt";
question: Children; question: Children;

View file

@ -9,6 +9,7 @@ import { InputModal } from "./modals/Input";
import { OnboardingModal } from "./modals/Onboarding"; import { OnboardingModal } from "./modals/Onboarding";
import { PromptModal } from "./modals/Prompt"; import { PromptModal } from "./modals/Prompt";
import { SignedOutModal } from "./modals/SignedOut"; import { SignedOutModal } from "./modals/SignedOut";
import {ExternalLinkModal} from "./modals/ExternalLinkPrompt";
export interface Props { export interface Props {
screen: Screen; screen: Screen;
@ -34,6 +35,8 @@ export default function Modals({ screen, openScreen }: Props) {
return <ClipboardModal onClose={onClose} {...screen} />; return <ClipboardModal onClose={onClose} {...screen} />;
case "onboarding": case "onboarding":
return <OnboardingModal onClose={onClose} {...screen} />; return <OnboardingModal onClose={onClose} {...screen} />;
case "external_link_prompt":
return <ExternalLinkModal onClose={onClose} {...screen} />;
} }
return null; return null;

View file

@ -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 (
<Modal
visible={true}
onClose={onClose}
title={<Text id={"app.special.modals.external_links.title"} />}
actions={[
{
onClick: ()=>{window.open(link, "_blank");},
confirmation: true,
contrast: true,
accent: true,
children: "Continue",
},
{
onClick: onClose,
confirmation: false,
children: "Cancel",
},
]}>
<Text id={"app.special.modals.external_links.short"} /> <br />
<a>{link}</a>
</Modal>
);
}

View file

@ -40,6 +40,8 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
sendTransport?: Transport; sendTransport?: Transport;
recvTransport?: Transport; recvTransport?: Transport;
isDeaf?: boolean;
userId?: string; userId?: string;
roomId?: string; roomId?: string;
participants: Map<string, VoiceUser>; participants: Map<string, VoiceUser>;
@ -54,6 +56,8 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.participants = new Map(); this.participants = new Map();
this.consumers = new Map(); this.consumers = new Map();
this.isDeaf = false;
this.signaling.on( this.signaling.on(
"data", "data",
(json) => { (json) => {

View file

@ -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) { async startProducing(type: ProduceType) {
switch (type) { switch (type) {
case "audio": { case "audio": {
@ -152,8 +179,10 @@ class VoiceStateReference {
if (navigator.mediaDevices === undefined) if (navigator.mediaDevices === undefined)
return console.log("No media devices."); // ! TODO: let the user know return console.log("No media devices."); // ! TODO: let the user know
const mediaDevice = window.localStorage.getItem("audioInputDevice");
const mediaStream = await navigator.mediaDevices.getUserMedia({ const mediaStream = await navigator.mediaDevices.getUserMedia({
audio: true, audio: mediaDevice?{deviceId: mediaDevice}:true
}); });
await this.client?.startProduce( await this.client?.startProduce(

View file

@ -96,7 +96,7 @@ export default function MessageEditor({ message, finish }: Props) {
<AutoComplete detached {...autoCompleteProps} /> <AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize <TextAreaAutoSize
forceFocus forceFocus
maxRows={3} maxRows={10}
value={content} value={content}
maxLength={2000} maxLength={2000}
padding="var(--message-box-padding)" padding="var(--message-box-padding)"

View file

@ -11,6 +11,18 @@ import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import {
Megaphone,
Microphone,
MicrophoneOff,
PhoneOff,
Speaker,
VolumeFull,
VolumeMute
} from "@styled-icons/boxicons-solid";
import Tooltip from "../../../components/common/Tooltip";
import {Hashnode, Speakerdeck, Teamspeak} from "@styled-icons/simple-icons";
import VoiceClient from "../../../lib/vortex/VoiceClient";
interface Props { interface Props {
id: string; id: string;
@ -89,7 +101,8 @@ export default observer(({ id }: Props) => {
target={user} target={user}
status={false} status={false}
voice={ voice={
voiceState.participants!.get(id) client.user?._id === id && voiceState.isDeaf()?"deaf"
: voiceState.participants!.get(id)
?.audio ?.audio
? undefined ? undefined
: "muted" : "muted"
@ -115,18 +128,38 @@ export default observer(({ id }: Props) => {
)} )}
</div> </div>
<div className="actions"> <div className="actions">
<Button error onClick={voiceState.disconnect}> <Tooltip content={"Leave call"} placement={"bottom"}>
<Text id="app.main.channel.voice.leave" /> <Button error onClick={voiceState.disconnect}>
</Button> <PhoneOff width={25} />
</Button>
</Tooltip>
{voiceState.isProducing("audio") ? ( {voiceState.isProducing("audio") ? (
<Button onClick={() => voiceState.stopProducing("audio")}> <Tooltip content={"Mute microphone"} placement={"bottom"}>
<Text id="app.main.channel.voice.mute" /> <Button onClick={() => voiceState.stopProducing("audio")}>
</Button> <Microphone width={25} />
</Button>
</Tooltip>
) : ( ) : (
<Button onClick={() => voiceState.startProducing("audio")}> <Tooltip content={"Unmute microphone"} placement={"bottom"}>
<Text id="app.main.channel.voice.unmute" /> <Button onClick={() => voiceState.startProducing("audio")}>
</Button> <MicrophoneOff width={25} />
</Button>
</Tooltip>
)} )}
{voiceState.isDeaf() ? (
<Tooltip content={"Deafen"} placement={"bottom"}>
<Button onClick={() => voiceState.stopDeafen()}>
<VolumeMute width={25} />
</Button>
</Tooltip>
): (
<Tooltip content={"Deafen"} placement={"bottom"}>
<Button onClick={() => voiceState.startDeafen()}>
<VolumeFull width={25} />
</Button>
</Tooltip>
)
}
</div> </div>
</VoiceBase> </VoiceBase>
); );

View file

@ -16,10 +16,12 @@ function mapMailProvider(email?: string): [string, string] | undefined {
const domain = match[1]; const domain = match[1];
switch (domain) { switch (domain) {
case "gmail.com": case "gmail.com":
case "googlemail.com":
return ["Gmail", "https://gmail.com"]; return ["Gmail", "https://gmail.com"];
case "tuta.io": case "tuta.io":
return ["Tutanota", "https://mail.tutanota.com"]; return ["Tutanota", "https://mail.tutanota.com"];
case "outlook.com": case "outlook.com":
case "hotmail.com":
return ["Outlook", "https://outlook.live.com"]; return ["Outlook", "https://outlook.live.com"];
case "yahoo.com": case "yahoo.com":
return ["Yahoo", "https://mail.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"]; return ["WP Poczta", "https://poczta.wp.pl"];
case "protonmail.com": case "protonmail.com":
case "protonmail.ch": case "protonmail.ch":
case "pm.me":
return ["ProtonMail", "https://mail.protonmail.com"]; return ["ProtonMail", "https://mail.protonmail.com"];
case "seznam.cz": case "seznam.cz":
case "email.cz": case "email.cz":
case "post.cz": case "post.cz":
return ["Seznam", "https://email.seznam.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: default:
return [domain, `https://${domain}`]; return [domain, `https://${domain}`];
} }

View file

@ -15,6 +15,7 @@ import {
Flask, Flask,
User, User,
Megaphone, Megaphone,
Speaker,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { Route, Switch, useHistory } from "react-router-dom"; import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js"; import { LIBRARY_VERSION } from "revolt.js";
@ -37,6 +38,7 @@ import { APP_VERSION } from "../../version";
import { GenericSettings } from "./GenericSettings"; import { GenericSettings } from "./GenericSettings";
import { Account } from "./panes/Account"; import { Account } from "./panes/Account";
import { Appearance } from "./panes/Appearance"; import { Appearance } from "./panes/Appearance";
import { Audio } from "./panes/Audio";
import { ExperimentsPage } from "./panes/Experiments"; import { ExperimentsPage } from "./panes/Experiments";
import { Feedback } from "./panes/Feedback"; import { Feedback } from "./panes/Feedback";
import { Languages } from "./panes/Languages"; import { Languages } from "./panes/Languages";
@ -85,6 +87,12 @@ export default function Settings() {
category: ( category: (
<Text id="app.settings.categories.client_settings" /> <Text id="app.settings.categories.client_settings" />
), ),
id: "audio",
icon: <Speaker size={20} />,
title: <Text id="app.settings.pages.audio.title" />,
},
{
id: "appearance", id: "appearance",
icon: <Palette size={20} />, icon: <Palette size={20} />,
title: <Text id="app.settings.pages.appearance.title" />, title: <Text id="app.settings.pages.appearance.title" />,
@ -141,6 +149,9 @@ export default function Settings() {
<Route path="/settings/appearance"> <Route path="/settings/appearance">
<Appearance /> <Appearance />
</Route> </Route>
<Route path="/settings/audio">
<Audio />
</Route>
<Route path="/settings/notifications"> <Route path="/settings/notifications">
<Notifications /> <Notifications />
</Route> </Route>

View file

@ -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<MediaDeviceInfo[] | undefined>(undefined);
useEffect(() => {
navigator
.mediaDevices
.enumerateDevices()
.then( devices => {
setMediaDevices(devices)
})
}, []);
return (
<>
<div>
<h3>
<Text id="app.settings.pages.audio.input_device" />
</h3>
<ComboBox
value={window.localStorage.getItem("audioInputDevice") ?? 0}
onChange={(e) => changeAudioDevice(e.currentTarget.value, "input")}>
{
mediaDevices?.filter(device => device.kind === "audioinput").map(device => {
return (
<option value={device.deviceId} key={device.deviceId}>
{device.label}
</option>
)
})
}
</ComboBox>
</div>
</>
);
}
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;
});

View file

@ -152,7 +152,7 @@ export const Overview = observer(({ server }: Props) => {
<Text id="general.disabled" /> <Text id="general.disabled" />
</option> </option>
{server.channels {server.channels
.filter((x) => typeof x !== "undefined") .filter((x) => (typeof x !== "undefined" && x.channel_type === "TextChannel"))
.map((channel) => ( .map((channel) => (
<option key={channel!._id} value={channel!._id}> <option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)} {getChannelName(channel!, true)}

View file

@ -6,7 +6,7 @@ import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; import {useCallback, useContext, useEffect, useMemo, useState} from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
@ -17,6 +17,7 @@ import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline"; import Overline from "../../../components/ui/Overline";
import ButtonItem from "../../../components/navigation/items/ButtonItem"; import ButtonItem from "../../../components/navigation/items/ButtonItem";
import {AppContext} from "../../../context/revoltjs/RevoltClient";
interface Props { interface Props {
server: Server; server: Server;
@ -26,6 +27,7 @@ const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
// ! FIXME: bad code :) // ! FIXME: bad code :)
export const Roles = observer(({ server }: Props) => { export const Roles = observer(({ server }: Props) => {
const client = useContext(AppContext);
const [role, setRole] = useState("default"); const [role, setRole] = useState("default");
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const roles = useMemo(() => server.roles ?? {}, [server]); const roles = useMemo(() => server.roles ?? {}, [server]);
@ -35,6 +37,8 @@ export const Roles = observer(({ server }: Props) => {
return null; return null;
} }
const clientPermissions = client.servers.get(server._id)!.permission;
const { const {
name: roleName, name: roleName,
colour: roleColour, colour: roleColour,
@ -207,6 +211,7 @@ export const Roles = observer(({ server }: Props) => {
onChange={() => onChange={() =>
setPerm([perm[0] ^ value, perm[1]]) setPerm([perm[0] ^ value, perm[1]])
} }
disabled={!(clientPermissions & value)}
description={ description={
<Text id={`permissions.server.${key}.d`} /> <Text id={`permissions.server.${key}.d`} />
}> }>
@ -233,7 +238,7 @@ export const Roles = observer(({ server }: Props) => {
onChange={() => onChange={() =>
setPerm([perm[0], perm[1] ^ value]) setPerm([perm[0], perm[1] ^ value])
} }
disabled={key === "View"} disabled={key === "View" || (!(clientPermissions & value))}
description={ description={
<Text id={`permissions.channel.${key}.d`} /> <Text id={`permissions.channel.${key}.d`} />
}> }>

View file

@ -1,5 +1,5 @@
export type Experiments = "search"; export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["search"]; export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
export const EXPERIMENTS: { export const EXPERIMENTS: {
[key in Experiments]: { title: string; description: string }; [key in Experiments]: { title: string; description: string };
} = { } = {