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)}
style={{
borderInlineStartColor:
embed.color ?? "var(--tertiary-background)",
embed.colour ?? "var(--tertiary-background)",
width: width + CONTAINER_PADDING,
}}>
<div>

View file

@ -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<User> {
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 && (
<foreignObject x="22" y="22" width="10" height="10">
<VoiceIndicator status={props.voice}>
{props.voice === "muted" && (
{props.voice === "deaf" && (
<VolumeMute size={6} />
) ||props.voice === "muted" && (
<MicrophoneOff size={6} />
)}
</VoiceIndicator>

View file

@ -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
})
}
}
},
);

View file

@ -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(

View file

@ -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;

View file

@ -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 <ClipboardModal onClose={onClose} {...screen} />;
case "onboarding":
return <OnboardingModal onClose={onClose} {...screen} />;
case "external_link_prompt":
return <ExternalLinkModal onClose={onClose} {...screen} />;
}
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;
recvTransport?: Transport;
isDeaf?: boolean;
userId?: string;
roomId?: string;
participants: Map<string, VoiceUser>;
@ -54,6 +56,8 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.participants = new Map();
this.consumers = new Map();
this.isDeaf = false;
this.signaling.on(
"data",
(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) {
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(

View file

@ -96,7 +96,7 @@ export default function MessageEditor({ message, finish }: Props) {
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
forceFocus
maxRows={3}
maxRows={10}
value={content}
maxLength={2000}
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 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 {
id: string;
@ -89,7 +101,8 @@ export default observer(({ id }: 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) => {
)}
</div>
<div className="actions">
<Button error onClick={voiceState.disconnect}>
<Text id="app.main.channel.voice.leave" />
</Button>
<Tooltip content={"Leave call"} placement={"bottom"}>
<Button error onClick={voiceState.disconnect}>
<PhoneOff width={25} />
</Button>
</Tooltip>
{voiceState.isProducing("audio") ? (
<Button onClick={() => voiceState.stopProducing("audio")}>
<Text id="app.main.channel.voice.mute" />
</Button>
<Tooltip content={"Mute microphone"} placement={"bottom"}>
<Button onClick={() => voiceState.stopProducing("audio")}>
<Microphone width={25} />
</Button>
</Tooltip>
) : (
<Button onClick={() => voiceState.startProducing("audio")}>
<Text id="app.main.channel.voice.unmute" />
</Button>
<Tooltip content={"Unmute microphone"} placement={"bottom"}>
<Button onClick={() => voiceState.startProducing("audio")}>
<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>
</VoiceBase>
);

View file

@ -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}`];
}

View file

@ -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: (
<Text id="app.settings.categories.client_settings" />
),
id: "audio",
icon: <Speaker size={20} />,
title: <Text id="app.settings.pages.audio.title" />,
},
{
id: "appearance",
icon: <Palette size={20} />,
title: <Text id="app.settings.pages.appearance.title" />,
@ -141,6 +149,9 @@ export default function Settings() {
<Route path="/settings/appearance">
<Appearance />
</Route>
<Route path="/settings/audio">
<Audio />
</Route>
<Route path="/settings/notifications">
<Notifications />
</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" />
</option>
{server.channels
.filter((x) => typeof x !== "undefined")
.filter((x) => (typeof x !== "undefined" && x.channel_type === "TextChannel"))
.map((channel) => (
<option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)}

View file

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

View file

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