Run prettier on all files.

This commit is contained in:
Paul
2021-07-05 11:23:23 +01:00
parent 56058c1e75
commit 7bd33d8d34
181 changed files with 18084 additions and 13521 deletions

View File

@@ -1,15 +1,16 @@
import { Link, LinkProps } from "react-router-dom";
type Props = LinkProps & JSX.HTMLAttributes<HTMLAnchorElement> & {
active: boolean
};
type Props = LinkProps &
JSX.HTMLAttributes<HTMLAnchorElement> & {
active: boolean;
};
export default function ConditionalLink(props: Props) {
const { active, ...linkProps } = props;
const { active, ...linkProps } = props;
if (active) {
return <a>{ props.children }</a>;
} else {
return <Link {...linkProps} />;
}
if (active) {
return <a>{props.children}</a>;
} else {
return <Link {...linkProps} />;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,21 @@ import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {};
export default function PaintCounter({ small, always }: { small?: boolean, always?: boolean }) {
if (import.meta.env.PROD && !always) return null;
export default function PaintCounter({
small,
always,
}: {
small?: boolean;
always?: boolean;
}) {
if (import.meta.env.PROD && !always) return null;
const [uniqueId] = useState('' + Math.random());
const count = counts[uniqueId] ?? 0;
counts[uniqueId] = count + 1;
return (
<div style={{ textAlign: 'center', fontSize: '0.8em' }}>
{ small ? <>P: { count + 1 }</> : <>
Painted {count + 1} time(s).
</> }
</div>
)
const [uniqueId] = useState("" + Math.random());
const count = counts[uniqueId] ?? 0;
counts[uniqueId] = count + 1;
return (
<div style={{ textAlign: "center", fontSize: "0.8em" }}>
{small ? <>P: {count + 1}</> : <>Painted {count + 1} time(s).</>}
</div>
);
}

View File

@@ -1,80 +1,113 @@
import { useEffect, useRef } from "preact/hooks";
import TextArea, {
DEFAULT_LINE_HEIGHT,
DEFAULT_TEXT_AREA_PADDING,
TextAreaProps,
TEXT_AREA_BORDER_WIDTH,
} from "../components/ui/TextArea";
import { internalSubscribe } from "./eventEmitter";
import { isTouchscreenDevice } from "./isTouchscreenDevice";
import TextArea, { DEFAULT_LINE_HEIGHT, DEFAULT_TEXT_AREA_PADDING, TextAreaProps, TEXT_AREA_BORDER_WIDTH } from "../components/ui/TextArea";
type TextAreaAutoSizeProps = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'style' | 'value'> & TextAreaProps & {
forceFocus?: boolean
autoFocus?: boolean
minHeight?: number
maxRows?: number
value: string
type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value"
> &
TextAreaProps & {
forceFocus?: boolean;
autoFocus?: boolean;
minHeight?: number;
maxRows?: number;
value: string;
id?: string
};
id?: string;
};
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
const { autoFocus, minHeight, maxRows, value, padding, lineHeight, hideBorder, forceFocus, children, as, ...textAreaProps } = props;
const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
const {
autoFocus,
minHeight,
maxRows,
value,
padding,
lineHeight,
hideBorder,
forceFocus,
children,
as,
...textAreaProps
} = props;
const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
const heightPadding = ((padding ?? DEFAULT_TEXT_AREA_PADDING) + (hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) * 2;
const height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * line + heightPadding, minHeight ?? 0);
const heightPadding =
((padding ?? DEFAULT_TEXT_AREA_PADDING) +
(hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) *
2;
const height = Math.max(
Math.min(value.split("\n").length, maxRows ?? Infinity) * line +
heightPadding,
minHeight ?? 0,
);
const ref = useRef<HTMLTextAreaElement>();
const ref = useRef<HTMLTextAreaElement>();
useEffect(() => {
if (isTouchscreenDevice) return;
autoFocus && ref.current.focus();
}, [value]);
const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
useEffect(() => {
if (isTouchscreenDevice) return;
autoFocus && ref.current.focus();
}, [value]);
useEffect(() => {
if (forceFocus) {
ref.current.focus();
}
const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
if (isTouchscreenDevice) return;
if (autoFocus && !inputSelected()) {
ref.current.focus();
}
useEffect(() => {
if (forceFocus) {
ref.current.focus();
}
// ? if you are wondering what this is
// ? it is a quick and dirty hack to fix
// ? value not setting correctly
// ? I have no clue what's going on
ref.current.value = value;
if (isTouchscreenDevice) return;
if (autoFocus && !inputSelected()) {
ref.current.focus();
}
if (!autoFocus) return;
function keyDown(e: KeyboardEvent) {
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
if (e.key.length !== 1) return;
if (ref && !inputSelected()) {
ref.current.focus();
}
}
// ? if you are wondering what this is
// ? it is a quick and dirty hack to fix
// ? value not setting correctly
// ? I have no clue what's going on
ref.current.value = value;
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
if (!autoFocus) return;
function keyDown(e: KeyboardEvent) {
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
if (e.key.length !== 1) return;
if (ref && !inputSelected()) {
ref.current.focus();
}
}
useEffect(() => {
function focus(id: string) {
if (id === props.id) {
ref.current.focus();
}
}
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
return internalSubscribe("TextArea", "focus", focus);
}, [ref]);
useEffect(() => {
function focus(id: string) {
if (id === props.id) {
ref.current.focus();
}
}
return <TextArea
ref={ref}
value={value}
padding={padding}
style={{ height }}
hideBorder={hideBorder}
lineHeight={lineHeight}
{...textAreaProps} />;
return internalSubscribe("TextArea", "focus", focus);
}, [ref]);
return (
<TextArea
ref={ref}
value={value}
padding={padding}
style={{ height }}
hideBorder={hideBorder}
lineHeight={lineHeight}
{...textAreaProps}
/>
);
}

View File

@@ -1,9 +1,9 @@
export function urlBase64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}

View File

@@ -1,15 +1,15 @@
export function debounce(cb: Function, duration: number) {
// Store the timer variable.
let timer: NodeJS.Timeout;
// This function is given to React.
return (...args: any[]) => {
// Get rid of the old timer.
clearTimeout(timer);
// Set a new timer.
timer = setTimeout(() => {
// Instead calling the new function.
// (with the newer data)
cb(...args);
}, duration);
};
// Store the timer variable.
let timer: NodeJS.Timeout;
// This function is given to React.
return (...args: any[]) => {
// Get rid of the old timer.
clearTimeout(timer);
// Set a new timer.
timer = setTimeout(() => {
// Instead calling the new function.
// (with the newer data)
cb(...args);
}, duration);
};
}

View File

@@ -1,13 +1,18 @@
import EventEmitter from "eventemitter3";
export const InternalEvent = new EventEmitter();
export function internalSubscribe(ns: string, event: string, fn: (...args: any[]) => void) {
InternalEvent.addListener(ns + '/' + event, fn);
return () => InternalEvent.removeListener(ns + '/' + event, fn);
export function internalSubscribe(
ns: string,
event: string,
fn: (...args: any[]) => void,
) {
InternalEvent.addListener(ns + "/" + event, fn);
return () => InternalEvent.removeListener(ns + "/" + event, fn);
}
export function internalEmit(ns: string, event: string, ...args: any[]) {
InternalEvent.emit(ns + '/' + event, ...args);
InternalEvent.emit(ns + "/" + event, ...args);
}
// Event structure: namespace/event

View File

@@ -1,9 +1,9 @@
export function determineFileSize(size: number) {
if (size > 1e6) {
return `${(size / 1e6).toFixed(2)} MB`;
} else if (size > 1e3) {
return `${(size / 1e3).toFixed(2)} KB`;
}
if (size > 1e6) {
return `${(size / 1e6).toFixed(2)} MB`;
} else if (size > 1e3) {
return `${(size / 1e3).toFixed(2)} KB`;
}
return `${size} B`;
return `${size} B`;
}

View File

@@ -1,59 +1,62 @@
import { IntlContext, translate } from "preact-i18n";
import { useContext } from "preact/hooks";
import { Children } from "../types/Preact";
interface Fields {
[key: string]: Children
[key: string]: Children;
}
interface Props {
id: string;
fields: Fields
id: string;
fields: Fields;
}
export interface IntlType {
intl: {
dictionary: {
[key: string]: Object | string
}
}
intl: {
dictionary: {
[key: string]: Object | string;
};
};
}
// This will exhibit O(2^n) behaviour.
function recursiveReplaceFields(input: string, fields: Fields) {
const key = Object.keys(fields)[0];
if (key) {
const { [key]: field, ...restOfFields } = fields;
if (typeof field === 'undefined') return [ input ];
const key = Object.keys(fields)[0];
if (key) {
const { [key]: field, ...restOfFields } = fields;
if (typeof field === "undefined") return [input];
const values: (Children | string[])[] = input.split(`{{${key}}}`)
.map(v => recursiveReplaceFields(v, restOfFields));
const values: (Children | string[])[] = input
.split(`{{${key}}}`)
.map((v) => recursiveReplaceFields(v, restOfFields));
for (let i=values.length - 1;i>0;i-=2) {
values.splice(i, 0, field);
}
return values.flat();
} else {
// base case
return [ input ];
}
for (let i = values.length - 1; i > 0; i -= 2) {
values.splice(i, 0, field);
}
return values.flat();
} else {
// base case
return [input];
}
}
export function TextReact({ id, fields }: Props) {
const { intl } = useContext(IntlContext) as unknown as IntlType;
const { intl } = useContext(IntlContext) as unknown as IntlType;
const path = id.split('.');
let entry = intl.dictionary[path.shift()!];
for (let key of path) {
// @ts-expect-error
entry = entry[key];
}
const path = id.split(".");
let entry = intl.dictionary[path.shift()!];
for (let key of path) {
// @ts-expect-error
entry = entry[key];
}
return <>{ recursiveReplaceFields(entry as string, fields) }</>;
return <>{recursiveReplaceFields(entry as string, fields)}</>;
}
export function useTranslation() {
const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) => translate(id, "", intl.dictionary, fields, plural, fallback);
const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) =>
translate(id, "", intl.dictionary, fields, plural, fallback);
}

View File

@@ -1,7 +1,8 @@
import { isDesktop, isMobile, isTablet } from "react-device-detect";
export const isTouchscreenDevice =
isDesktop && !isTablet
? false
: (typeof window !== "undefined"
? navigator.maxTouchPoints > 0
: false) || isMobile;
isDesktop && !isTablet
? false
: (typeof window !== "undefined"
? navigator.maxTouchPoints > 0
: false) || isMobile;

View File

@@ -1,192 +1,206 @@
import { RendererRoutines, RenderState, ScrollState } from "./types";
import { SimpleRenderer } from "./simple/SimpleRenderer";
import { useEffect, useState } from "preact/hooks";
import EventEmitter3 from 'eventemitter3';
import EventEmitter3 from "eventemitter3";
import { Client, Message } from "revolt.js";
import { useEffect, useState } from "preact/hooks";
import { SimpleRenderer } from "./simple/SimpleRenderer";
import { RendererRoutines, RenderState, ScrollState } from "./types";
export const SMOOTH_SCROLL_ON_RECEIVE = false;
export class SingletonRenderer extends EventEmitter3 {
client?: Client;
channel?: string;
state: RenderState;
currentRenderer: RendererRoutines;
client?: Client;
channel?: string;
state: RenderState;
currentRenderer: RendererRoutines;
stale = false;
fetchingTop = false;
fetchingBottom = false;
stale = false;
fetchingTop = false;
fetchingBottom = false;
constructor() {
super();
constructor() {
super();
this.receive = this.receive.bind(this);
this.edit = this.edit.bind(this);
this.delete = this.delete.bind(this);
this.receive = this.receive.bind(this);
this.edit = this.edit.bind(this);
this.delete = this.delete.bind(this);
this.state = { type: 'LOADING' };
this.currentRenderer = SimpleRenderer;
}
this.state = { type: "LOADING" };
this.currentRenderer = SimpleRenderer;
}
private receive(message: Message) {
this.currentRenderer.receive(this, message);
}
private receive(message: Message) {
this.currentRenderer.receive(this, message);
}
private edit(id: string, patch: Partial<Message>) {
this.currentRenderer.edit(this, id, patch);
}
private edit(id: string, patch: Partial<Message>) {
this.currentRenderer.edit(this, id, patch);
}
private delete(id: string) {
this.currentRenderer.delete(this, id);
}
private delete(id: string) {
this.currentRenderer.delete(this, id);
}
subscribe(client: Client) {
if (this.client) {
this.client.removeListener('message', this.receive);
this.client.removeListener('message/update', this.edit);
this.client.removeListener('message/delete', this.delete);
}
subscribe(client: Client) {
if (this.client) {
this.client.removeListener("message", this.receive);
this.client.removeListener("message/update", this.edit);
this.client.removeListener("message/delete", this.delete);
}
this.client = client;
client.addListener('message', this.receive);
client.addListener('message/update', this.edit);
client.addListener('message/delete', this.delete);
}
this.client = client;
client.addListener("message", this.receive);
client.addListener("message/update", this.edit);
client.addListener("message/delete", this.delete);
}
private setStateUnguarded(state: RenderState, scroll?: ScrollState) {
this.state = state;
this.emit('state', state);
private setStateUnguarded(state: RenderState, scroll?: ScrollState) {
this.state = state;
this.emit("state", state);
if (scroll) {
this.emit('scroll', scroll);
}
}
if (scroll) {
this.emit("scroll", scroll);
}
}
setState(id: string, state: RenderState, scroll?: ScrollState) {
if (id !== this.channel) return;
this.setStateUnguarded(state, scroll);
}
setState(id: string, state: RenderState, scroll?: ScrollState) {
if (id !== this.channel) return;
this.setStateUnguarded(state, scroll);
}
markStale() {
this.stale = true;
}
markStale() {
this.stale = true;
}
async init(id: string) {
this.channel = id;
this.stale = false;
this.setStateUnguarded({ type: 'LOADING' });
await this.currentRenderer.init(this, id);
}
async init(id: string) {
this.channel = id;
this.stale = false;
this.setStateUnguarded({ type: "LOADING" });
await this.currentRenderer.init(this, id);
}
async reloadStale(id: string) {
if (this.stale) {
this.stale = false;
await this.init(id);
}
}
async reloadStale(id: string) {
if (this.stale) {
this.stale = false;
await this.init(id);
}
}
async loadTop(ref?: HTMLDivElement) {
if (this.fetchingTop) return;
this.fetchingTop = true;
async loadTop(ref?: HTMLDivElement) {
if (this.fetchingTop) return;
this.fetchingTop = true;
function generateScroll(end: string): ScrollState {
if (ref) {
let heightRemoved = 0;
let messageContainer = ref.children[0];
if (messageContainer) {
for (let child of Array.from(messageContainer.children)) {
// If this child has a ulid.
if (child.id?.length === 26) {
// Check whether it was removed.
if (child.id.localeCompare(end) === 1) {
heightRemoved += child.clientHeight +
// We also need to take into account the top margin of the container.
parseInt(window.getComputedStyle(child).marginTop.slice(0, -2));
}
}
}
}
function generateScroll(end: string): ScrollState {
if (ref) {
let heightRemoved = 0;
let messageContainer = ref.children[0];
if (messageContainer) {
for (let child of Array.from(messageContainer.children)) {
// If this child has a ulid.
if (child.id?.length === 26) {
// Check whether it was removed.
if (child.id.localeCompare(end) === 1) {
heightRemoved +=
child.clientHeight +
// We also need to take into account the top margin of the container.
parseInt(
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
);
}
}
}
}
return {
type: 'OffsetTop',
previousHeight: ref.scrollHeight - heightRemoved
}
} else {
return {
type: 'OffsetTop',
previousHeight: 0
}
}
}
return {
type: "OffsetTop",
previousHeight: ref.scrollHeight - heightRemoved,
};
} else {
return {
type: "OffsetTop",
previousHeight: 0,
};
}
}
await this.currentRenderer.loadTop(this, generateScroll);
await this.currentRenderer.loadTop(this, generateScroll);
// Allow state updates to propagate.
setTimeout(() => this.fetchingTop = false, 0);
}
// Allow state updates to propagate.
setTimeout(() => (this.fetchingTop = false), 0);
}
async loadBottom(ref?: HTMLDivElement) {
if (this.fetchingBottom) return;
this.fetchingBottom = true;
async loadBottom(ref?: HTMLDivElement) {
if (this.fetchingBottom) return;
this.fetchingBottom = true;
function generateScroll(start: string): ScrollState {
if (ref) {
let heightRemoved = 0;
let messageContainer = ref.children[0];
if (messageContainer) {
for (let child of Array.from(messageContainer.children)) {
// If this child has a ulid.
if (child.id?.length === 26) {
// Check whether it was removed.
if (child.id.localeCompare(start) === -1) {
heightRemoved += child.clientHeight +
// We also need to take into account the top margin of the container.
parseInt(window.getComputedStyle(child).marginTop.slice(0, -2));
}
}
}
}
function generateScroll(start: string): ScrollState {
if (ref) {
let heightRemoved = 0;
let messageContainer = ref.children[0];
if (messageContainer) {
for (let child of Array.from(messageContainer.children)) {
// If this child has a ulid.
if (child.id?.length === 26) {
// Check whether it was removed.
if (child.id.localeCompare(start) === -1) {
heightRemoved +=
child.clientHeight +
// We also need to take into account the top margin of the container.
parseInt(
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
);
}
}
}
}
return {
type: 'ScrollTop',
y: ref.scrollTop - heightRemoved
}
} else {
return {
type: 'ScrollToBottom'
}
}
}
return {
type: "ScrollTop",
y: ref.scrollTop - heightRemoved,
};
} else {
return {
type: "ScrollToBottom",
};
}
}
await this.currentRenderer.loadBottom(this, generateScroll);
await this.currentRenderer.loadBottom(this, generateScroll);
// Allow state updates to propagate.
setTimeout(() => this.fetchingBottom = false, 0);
}
// Allow state updates to propagate.
setTimeout(() => (this.fetchingBottom = false), 0);
}
async jumpToBottom(id: string, smooth: boolean) {
if (id !== this.channel) return;
if (this.state.type === 'RENDER' && this.state.atBottom) {
this.emit('scroll', { type: 'ScrollToBottom', smooth });
} else {
await this.currentRenderer.init(this, id, true);
}
}
async jumpToBottom(id: string, smooth: boolean) {
if (id !== this.channel) return;
if (this.state.type === "RENDER" && this.state.atBottom) {
this.emit("scroll", { type: "ScrollToBottom", smooth });
} else {
await this.currentRenderer.init(this, id, true);
}
}
}
export const SingletonMessageRenderer = new SingletonRenderer();
export function useRenderState(id: string) {
const [state, setState] = useState<Readonly<RenderState>>(SingletonMessageRenderer.state);
if (typeof id === "undefined") return;
const [state, setState] = useState<Readonly<RenderState>>(
SingletonMessageRenderer.state,
);
if (typeof id === "undefined") return;
function render(state: RenderState) {
setState(state);
}
function render(state: RenderState) {
setState(state);
}
useEffect(() => {
SingletonMessageRenderer.addListener("state", render);
return () => SingletonMessageRenderer.removeListener("state", render);
}, [id]);
useEffect(() => {
SingletonMessageRenderer.addListener("state", render);
return () => SingletonMessageRenderer.removeListener("state", render);
}, [id]);
return state;
return state;
}

View File

@@ -1,178 +1,183 @@
import { mapMessage } from "../../../context/revoltjs/util";
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
import { RendererRoutines } from "../types";
export const SimpleRenderer: RendererRoutines = {
init: async (renderer, id, smooth) => {
if (renderer.client!.websocket.connected) {
renderer.client!.channels
.fetchMessagesWithUsers(id, { }, true)
.then(({ messages: data }) => {
data.reverse();
let messages = data.map(x => mapMessage(x));
renderer.setState(
id,
{
type: 'RENDER',
messages,
atTop: data.length < 50,
atBottom: true
},
{ type: 'ScrollToBottom', smooth }
);
});
} else {
renderer.setState(id, { type: 'WAITING_FOR_NETWORK' });
}
},
receive: async (renderer, message) => {
if (message.channel !== renderer.channel) return;
if (renderer.state.type !== 'RENDER') return;
if (renderer.state.messages.find(x => x._id === message._id)) return;
if (!renderer.state.atBottom) return;
init: async (renderer, id, smooth) => {
if (renderer.client!.websocket.connected) {
renderer
.client!.channels.fetchMessagesWithUsers(id, {}, true)
.then(({ messages: data }) => {
data.reverse();
let messages = data.map((x) => mapMessage(x));
renderer.setState(
id,
{
type: "RENDER",
messages,
atTop: data.length < 50,
atBottom: true,
},
{ type: "ScrollToBottom", smooth },
);
});
} else {
renderer.setState(id, { type: "WAITING_FOR_NETWORK" });
}
},
receive: async (renderer, message) => {
if (message.channel !== renderer.channel) return;
if (renderer.state.type !== "RENDER") return;
if (renderer.state.messages.find((x) => x._id === message._id)) return;
if (!renderer.state.atBottom) return;
let messages = [ ...renderer.state.messages, mapMessage(message) ];
let atTop = renderer.state.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
atTop = false;
}
let messages = [...renderer.state.messages, mapMessage(message)];
let atTop = renderer.state.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
atTop = false;
}
renderer.setState(
message.channel,
{
...renderer.state,
messages,
atTop
},
{ type: 'StayAtBottom', smooth: SMOOTH_SCROLL_ON_RECEIVE }
);
},
edit: async (renderer, id, patch) => {
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== 'RENDER') return;
let messages = [ ...renderer.state.messages ];
let index = messages.findIndex(x => x._id === id);
renderer.setState(
message.channel,
{
...renderer.state,
messages,
atTop,
},
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
);
},
edit: async (renderer, id, patch) => {
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== "RENDER") return;
if (index > -1) {
let message = { ...messages[index], ...mapMessage(patch) };
messages.splice(index, 1, message);
let messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id);
renderer.setState(
channel,
{
...renderer.state,
messages
},
{ type: 'StayAtBottom' }
);
}
},
delete: async (renderer, id) => {
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== 'RENDER') return;
let messages = [ ...renderer.state.messages ];
let index = messages.findIndex(x => x._id === id);
if (index > -1) {
let message = { ...messages[index], ...mapMessage(patch) };
messages.splice(index, 1, message);
if (index > -1) {
messages.splice(index, 1);
renderer.setState(
channel,
{
...renderer.state,
messages,
},
{ type: "StayAtBottom" },
);
}
},
delete: async (renderer, id) => {
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== "RENDER") return;
renderer.setState(
channel,
{
...renderer.state,
messages
},
{ type: 'StayAtBottom' }
);
}
},
loadTop: async (renderer, generateScroll) => {
const channel = renderer.channel;
if (!channel) return;
let messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id);
const state = renderer.state;
if (state.type !== 'RENDER') return;
if (state.atTop) return;
if (index > -1) {
messages.splice(index, 1);
const { messages: data } = await renderer.client!.channels.fetchMessagesWithUsers(channel, {
before: state.messages[0]._id
}, true);
renderer.setState(
channel,
{
...renderer.state,
messages,
},
{ type: "StayAtBottom" },
);
}
},
loadTop: async (renderer, generateScroll) => {
const channel = renderer.channel;
if (!channel) return;
if (data.length === 0) {
return renderer.setState(
channel,
{
...state,
atTop: true
}
);
}
const state = renderer.state;
if (state.type !== "RENDER") return;
if (state.atTop) return;
data.reverse();
let messages = [ ...data.map(x => mapMessage(x)), ...state.messages ];
const { messages: data } =
await renderer.client!.channels.fetchMessagesWithUsers(
channel,
{
before: state.messages[0]._id,
},
true,
);
let atTop = false;
if (data.length < 50) {
atTop = true;
}
if (data.length === 0) {
return renderer.setState(channel, {
...state,
atTop: true,
});
}
let atBottom = state.atBottom;
if (messages.length > 150) {
messages = messages.slice(0, 150);
atBottom = false;
}
data.reverse();
let messages = [...data.map((x) => mapMessage(x)), ...state.messages];
renderer.setState(
channel,
{ ...state, atTop, atBottom, messages },
generateScroll(messages[messages.length - 1]._id)
);
},
loadBottom: async (renderer, generateScroll) => {
const channel = renderer.channel;
if (!channel) return;
let atTop = false;
if (data.length < 50) {
atTop = true;
}
const state = renderer.state;
if (state.type !== 'RENDER') return;
if (state.atBottom) return;
let atBottom = state.atBottom;
if (messages.length > 150) {
messages = messages.slice(0, 150);
atBottom = false;
}
const { messages: data } = await renderer.client!.channels.fetchMessagesWithUsers(channel, {
after: state.messages[state.messages.length - 1]._id,
sort: 'Oldest'
}, true);
renderer.setState(
channel,
{ ...state, atTop, atBottom, messages },
generateScroll(messages[messages.length - 1]._id),
);
},
loadBottom: async (renderer, generateScroll) => {
const channel = renderer.channel;
if (!channel) return;
if (data.length === 0) {
return renderer.setState(
channel,
{
...state,
atBottom: true
}
);
}
const state = renderer.state;
if (state.type !== "RENDER") return;
if (state.atBottom) return;
let messages = [ ...state.messages, ...data.map(x => mapMessage(x)) ];
const { messages: data } =
await renderer.client!.channels.fetchMessagesWithUsers(
channel,
{
after: state.messages[state.messages.length - 1]._id,
sort: "Oldest",
},
true,
);
let atBottom = false;
if (data.length < 50) {
atBottom = true;
}
if (data.length === 0) {
return renderer.setState(channel, {
...state,
atBottom: true,
});
}
let atTop = state.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
atTop = false;
}
let messages = [...state.messages, ...data.map((x) => mapMessage(x))];
renderer.setState(
channel,
{ ...state, atTop, atBottom, messages },
generateScroll(messages[0]._id)
);
}
let atBottom = false;
if (data.length < 50) {
atBottom = true;
}
let atTop = state.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
atTop = false;
}
renderer.setState(
channel,
{ ...state, atTop, atBottom, messages },
generateScroll(messages[0]._id),
);
},
};

View File

@@ -1,32 +1,48 @@
import { Message } from "revolt.js";
import { SingletonRenderer } from "./Singleton";
import { MessageObject } from "../../context/revoltjs/util";
import { SingletonRenderer } from "./Singleton";
export type ScrollState =
| { type: "Free" }
| { type: "Bottom", scrollingUntil?: number }
| { type: "ScrollToBottom" | "StayAtBottom", smooth?: boolean }
| { type: "OffsetTop"; previousHeight: number }
| { type: "ScrollTop"; y: number };
| { type: "Free" }
| { type: "Bottom"; scrollingUntil?: number }
| { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean }
| { type: "OffsetTop"; previousHeight: number }
| { type: "ScrollTop"; y: number };
export type RenderState =
| {
type: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY";
}
| {
type: "RENDER";
atTop: boolean;
atBottom: boolean;
messages: MessageObject[];
};
| {
type: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY";
}
| {
type: "RENDER";
atTop: boolean;
atBottom: boolean;
messages: MessageObject[];
};
export interface RendererRoutines {
init: (renderer: SingletonRenderer, id: string, smooth?: boolean) => Promise<void>
receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
edit: (renderer: SingletonRenderer, id: string, partial: Partial<Message>) => Promise<void>;
delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
init: (
renderer: SingletonRenderer,
id: string,
smooth?: boolean,
) => Promise<void>;
loadTop: (renderer: SingletonRenderer, generateScroll: (end: string) => ScrollState) => Promise<void>;
loadBottom: (renderer: SingletonRenderer, generateScroll: (start: string) => ScrollState) => Promise<void>;
receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
edit: (
renderer: SingletonRenderer,
id: string,
partial: Partial<Message>,
) => Promise<void>;
delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
loadTop: (
renderer: SingletonRenderer,
generateScroll: (end: string) => ScrollState,
) => Promise<void>;
loadBottom: (
renderer: SingletonRenderer,
generateScroll: (start: string) => ScrollState,
) => Promise<void>;
}

View File

@@ -1,5 +1,8 @@
export const stopPropagation = (ev: JSX.TargetedMouseEvent<HTMLDivElement>, _consume?: any) => {
ev.preventDefault();
ev.stopPropagation();
return true;
export const stopPropagation = (
ev: JSX.TargetedMouseEvent<HTMLDivElement>,
_consume?: any,
) => {
ev.preventDefault();
ev.stopPropagation();
return true;
};

View File

@@ -1,188 +1,189 @@
import EventEmitter from "eventemitter3";
import {
RtpCapabilities,
RtpParameters
RtpCapabilities,
RtpParameters,
} from "mediasoup-client/lib/RtpParameters";
import { DtlsParameters } from "mediasoup-client/lib/Transport";
import {
AuthenticationResult,
Room,
TransportInitDataTuple,
WSCommandType,
WSErrorCode,
ProduceType,
ConsumerData
AuthenticationResult,
Room,
TransportInitDataTuple,
WSCommandType,
WSErrorCode,
ProduceType,
ConsumerData,
} from "./Types";
interface SignalingEvents {
open: (event: Event) => void;
close: (event: CloseEvent) => void;
error: (event: Event) => void;
data: (data: any) => void;
open: (event: Event) => void;
close: (event: CloseEvent) => void;
error: (event: Event) => void;
data: (data: any) => void;
}
export default class Signaling extends EventEmitter<SignalingEvents> {
ws?: WebSocket;
index: number;
pending: Map<number, (data: unknown) => void>;
ws?: WebSocket;
index: number;
pending: Map<number, (data: unknown) => void>;
constructor() {
super();
this.index = 0;
this.pending = new Map();
}
constructor() {
super();
this.index = 0;
this.pending = new Map();
}
connected(): boolean {
return (
this.ws !== undefined &&
this.ws.readyState !== WebSocket.CLOSING &&
this.ws.readyState !== WebSocket.CLOSED
);
}
connected(): boolean {
return (
this.ws !== undefined &&
this.ws.readyState !== WebSocket.CLOSING &&
this.ws.readyState !== WebSocket.CLOSED
);
}
connect(address: string): Promise<void> {
this.disconnect();
this.ws = new WebSocket(address);
this.ws.onopen = e => this.emit("open", e);
this.ws.onclose = e => this.emit("close", e);
this.ws.onerror = e => this.emit("error", e);
this.ws.onmessage = e => this.parseData(e);
connect(address: string): Promise<void> {
this.disconnect();
this.ws = new WebSocket(address);
this.ws.onopen = (e) => this.emit("open", e);
this.ws.onclose = (e) => this.emit("close", e);
this.ws.onerror = (e) => this.emit("error", e);
this.ws.onmessage = (e) => this.parseData(e);
let finished = false;
return new Promise((resolve, reject) => {
this.once("open", () => {
if (finished) return;
finished = true;
resolve();
});
let finished = false;
return new Promise((resolve, reject) => {
this.once("open", () => {
if (finished) return;
finished = true;
resolve();
});
this.once("error", () => {
if (finished) return;
finished = true;
reject();
});
});
}
this.once("error", () => {
if (finished) return;
finished = true;
reject();
});
});
}
disconnect() {
if (
this.ws !== undefined &&
this.ws.readyState !== WebSocket.CLOSED &&
this.ws.readyState !== WebSocket.CLOSING
)
this.ws.close(1000);
}
disconnect() {
if (
this.ws !== undefined &&
this.ws.readyState !== WebSocket.CLOSED &&
this.ws.readyState !== WebSocket.CLOSING
)
this.ws.close(1000);
}
private parseData(event: MessageEvent) {
if (typeof event.data !== "string") return;
const json = JSON.parse(event.data);
const entry = this.pending.get(json.id);
if (entry === undefined) {
this.emit("data", json);
return;
}
private parseData(event: MessageEvent) {
if (typeof event.data !== "string") return;
const json = JSON.parse(event.data);
const entry = this.pending.get(json.id);
if (entry === undefined) {
this.emit("data", json);
return;
}
entry(json);
}
entry(json);
}
sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected });
sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected });
const ws = this.ws;
return new Promise((resolve, reject) => {
if (this.index >= 2 ** 32) this.index = 0;
while (this.pending.has(this.index)) this.index++;
const onClose = (e: CloseEvent) => {
reject({
error: e.code,
message: e.reason
});
};
const ws = this.ws;
return new Promise((resolve, reject) => {
if (this.index >= 2 ** 32) this.index = 0;
while (this.pending.has(this.index)) this.index++;
const onClose = (e: CloseEvent) => {
reject({
error: e.code,
message: e.reason,
});
};
const finishedFn = (data: any) => {
this.removeListener("close", onClose);
if (data.error)
reject({
error: data.error,
message: data.message,
data: data.data
});
resolve(data.data);
};
const finishedFn = (data: any) => {
this.removeListener("close", onClose);
if (data.error)
reject({
error: data.error,
message: data.message,
data: data.data,
});
resolve(data.data);
};
this.pending.set(this.index, finishedFn);
this.once("close", onClose);
const json = {
id: this.index,
type: type,
data
};
ws.send(JSON.stringify(json) + "\n");
this.index++;
});
}
this.pending.set(this.index, finishedFn);
this.once("close", onClose);
const json = {
id: this.index,
type: type,
data,
};
ws.send(JSON.stringify(json) + "\n");
this.index++;
});
}
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
}
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
}
async roomInfo(): Promise<Room> {
const room = await this.sendRequest(WSCommandType.RoomInfo);
return {
id: room.id,
videoAllowed: room.videoAllowed,
users: new Map(Object.entries(room.users))
};
}
async roomInfo(): Promise<Room> {
const room = await this.sendRequest(WSCommandType.RoomInfo);
return {
id: room.id,
videoAllowed: room.videoAllowed,
users: new Map(Object.entries(room.users)),
};
}
initializeTransports(
rtpCapabilities: RtpCapabilities
): Promise<TransportInitDataTuple> {
return this.sendRequest(WSCommandType.InitializeTransports, {
mode: "SplitWebRTC",
rtpCapabilities
});
}
initializeTransports(
rtpCapabilities: RtpCapabilities,
): Promise<TransportInitDataTuple> {
return this.sendRequest(WSCommandType.InitializeTransports, {
mode: "SplitWebRTC",
rtpCapabilities,
});
}
connectTransport(
id: string,
dtlsParameters: DtlsParameters
): Promise<void> {
return this.sendRequest(WSCommandType.ConnectTransport, {
id,
dtlsParameters
});
}
connectTransport(
id: string,
dtlsParameters: DtlsParameters,
): Promise<void> {
return this.sendRequest(WSCommandType.ConnectTransport, {
id,
dtlsParameters,
});
}
async startProduce(
type: ProduceType,
rtpParameters: RtpParameters
): Promise<string> {
let result = await this.sendRequest(WSCommandType.StartProduce, {
type,
rtpParameters
});
return result.producerId;
}
async startProduce(
type: ProduceType,
rtpParameters: RtpParameters,
): Promise<string> {
let result = await this.sendRequest(WSCommandType.StartProduce, {
type,
rtpParameters,
});
return result.producerId;
}
stopProduce(type: ProduceType): Promise<void> {
return this.sendRequest(WSCommandType.StopProduce, { type });
}
stopProduce(type: ProduceType): Promise<void> {
return this.sendRequest(WSCommandType.StopProduce, { type });
}
startConsume(userId: string, type: ProduceType): Promise<ConsumerData> {
return this.sendRequest(WSCommandType.StartConsume, { type, userId });
}
startConsume(userId: string, type: ProduceType): Promise<ConsumerData> {
return this.sendRequest(WSCommandType.StartConsume, { type, userId });
}
stopConsume(consumerId: string): Promise<void> {
return this.sendRequest(WSCommandType.StopConsume, { id: consumerId });
}
stopConsume(consumerId: string): Promise<void> {
return this.sendRequest(WSCommandType.StopConsume, { id: consumerId });
}
setConsumerPause(consumerId: string, paused: boolean): Promise<void> {
return this.sendRequest(WSCommandType.SetConsumerPause, {
id: consumerId,
paused
});
}
setConsumerPause(consumerId: string, paused: boolean): Promise<void> {
return this.sendRequest(WSCommandType.SetConsumerPause, {
id: consumerId,
paused,
});
}
}

View File

@@ -1,111 +1,111 @@
import { Consumer } from "mediasoup-client/lib/Consumer";
import {
MediaKind,
RtpCapabilities,
RtpParameters
MediaKind,
RtpCapabilities,
RtpParameters,
} from "mediasoup-client/lib/RtpParameters";
import { SctpParameters } from "mediasoup-client/lib/SctpParameters";
import {
DtlsParameters,
IceCandidate,
IceParameters
DtlsParameters,
IceCandidate,
IceParameters,
} from "mediasoup-client/lib/Transport";
export enum WSEventType {
UserJoined = "UserJoined",
UserLeft = "UserLeft",
UserJoined = "UserJoined",
UserLeft = "UserLeft",
UserStartProduce = "UserStartProduce",
UserStopProduce = "UserStopProduce"
UserStartProduce = "UserStartProduce",
UserStopProduce = "UserStopProduce",
}
export enum WSCommandType {
Authenticate = "Authenticate",
RoomInfo = "RoomInfo",
Authenticate = "Authenticate",
RoomInfo = "RoomInfo",
InitializeTransports = "InitializeTransports",
ConnectTransport = "ConnectTransport",
InitializeTransports = "InitializeTransports",
ConnectTransport = "ConnectTransport",
StartProduce = "StartProduce",
StopProduce = "StopProduce",
StartProduce = "StartProduce",
StopProduce = "StopProduce",
StartConsume = "StartConsume",
StopConsume = "StopConsume",
SetConsumerPause = "SetConsumerPause"
StartConsume = "StartConsume",
StopConsume = "StopConsume",
SetConsumerPause = "SetConsumerPause",
}
export enum WSErrorCode {
NotConnected = 0,
NotFound = 404,
NotConnected = 0,
NotFound = 404,
TransportConnectionFailure = 601,
TransportConnectionFailure = 601,
ProducerFailure = 611,
ProducerNotFound = 614,
ProducerFailure = 611,
ProducerNotFound = 614,
ConsumerFailure = 621,
ConsumerNotFound = 624
ConsumerFailure = 621,
ConsumerNotFound = 624,
}
export enum WSCloseCode {
// Sent when the received data is not a string, or is unparseable
InvalidData = 1003,
Unauthorized = 4001,
RoomClosed = 4004,
// Sent when a client tries to send an opcode in the wrong state
InvalidState = 1002,
ServerError = 1011
// Sent when the received data is not a string, or is unparseable
InvalidData = 1003,
Unauthorized = 4001,
RoomClosed = 4004,
// Sent when a client tries to send an opcode in the wrong state
InvalidState = 1002,
ServerError = 1011,
}
export interface VoiceError {
error: WSErrorCode | WSCloseCode;
message: string;
error: WSErrorCode | WSCloseCode;
message: string;
}
export type ProduceType = "audio"; //| "video" | "saudio" | "svideo";
export interface AuthenticationResult {
userId: string;
roomId: string;
rtpCapabilities: RtpCapabilities;
userId: string;
roomId: string;
rtpCapabilities: RtpCapabilities;
}
export interface Room {
id: string;
videoAllowed: boolean;
users: Map<string, VoiceUser>;
id: string;
videoAllowed: boolean;
users: Map<string, VoiceUser>;
}
export interface VoiceUser {
audio?: boolean;
//video?: boolean,
//saudio?: boolean,
//svideo?: boolean,
audio?: boolean;
//video?: boolean,
//saudio?: boolean,
//svideo?: boolean,
}
export interface ConsumerList {
audio?: Consumer;
//video?: Consumer,
//saudio?: Consumer,
//svideo?: Consumer,
audio?: Consumer;
//video?: Consumer,
//saudio?: Consumer,
//svideo?: Consumer,
}
export interface TransportInitData {
id: string;
iceParameters: IceParameters;
iceCandidates: IceCandidate[];
dtlsParameters: DtlsParameters;
sctpParameters: SctpParameters | undefined;
id: string;
iceParameters: IceParameters;
iceCandidates: IceCandidate[];
dtlsParameters: DtlsParameters;
sctpParameters: SctpParameters | undefined;
}
export interface TransportInitDataTuple {
sendTransport: TransportInitData;
recvTransport: TransportInitData;
sendTransport: TransportInitData;
recvTransport: TransportInitData;
}
export interface ConsumerData {
id: string;
producerId: string;
kind: MediaKind;
rtpParameters: RtpParameters;
id: string;
producerId: string;
kind: MediaKind;
rtpParameters: RtpParameters;
}

View File

@@ -1,331 +1,331 @@
import EventEmitter from "eventemitter3";
import * as mediasoupClient from "mediasoup-client";
import {
Device,
Producer,
Transport,
UnsupportedError
Device,
Producer,
Transport,
UnsupportedError,
} from "mediasoup-client/lib/types";
import {
ProduceType,
WSEventType,
VoiceError,
VoiceUser,
ConsumerList,
WSErrorCode
} from "./Types";
import Signaling from "./Signaling";
import {
ProduceType,
WSEventType,
VoiceError,
VoiceUser,
ConsumerList,
WSErrorCode,
} from "./Types";
interface VoiceEvents {
ready: () => void;
error: (error: Error) => void;
close: (error?: VoiceError) => void;
ready: () => void;
error: (error: Error) => void;
close: (error?: VoiceError) => void;
startProduce: (type: ProduceType) => void;
stopProduce: (type: ProduceType) => void;
startProduce: (type: ProduceType) => void;
stopProduce: (type: ProduceType) => void;
userJoined: (userId: string) => void;
userLeft: (userId: string) => void;
userJoined: (userId: string) => void;
userLeft: (userId: string) => void;
userStartProduce: (userId: string, type: ProduceType) => void;
userStopProduce: (userId: string, type: ProduceType) => void;
userStartProduce: (userId: string, type: ProduceType) => void;
userStopProduce: (userId: string, type: ProduceType) => void;
}
export default class VoiceClient extends EventEmitter<VoiceEvents> {
private _supported: boolean;
private _supported: boolean;
device?: Device;
signaling: Signaling;
device?: Device;
signaling: Signaling;
sendTransport?: Transport;
recvTransport?: Transport;
sendTransport?: Transport;
recvTransport?: Transport;
userId?: string;
roomId?: string;
participants: Map<string, VoiceUser>;
consumers: Map<string, ConsumerList>;
userId?: string;
roomId?: string;
participants: Map<string, VoiceUser>;
consumers: Map<string, ConsumerList>;
audioProducer?: Producer;
constructor() {
super();
this._supported = mediasoupClient.detectDevice() !== undefined;
this.signaling = new Signaling();
audioProducer?: Producer;
constructor() {
super();
this._supported = mediasoupClient.detectDevice() !== undefined;
this.signaling = new Signaling();
this.participants = new Map();
this.consumers = new Map();
this.participants = new Map();
this.consumers = new Map();
this.signaling.on(
"data",
json => {
const data = json.data;
switch (json.type) {
case WSEventType.UserJoined: {
this.participants.set(data.id, {});
this.emit("userJoined", data.id);
break;
}
case WSEventType.UserLeft: {
this.participants.delete(data.id);
this.emit("userLeft", data.id);
this.signaling.on(
"data",
(json) => {
const data = json.data;
switch (json.type) {
case WSEventType.UserJoined: {
this.participants.set(data.id, {});
this.emit("userJoined", data.id);
break;
}
case WSEventType.UserLeft: {
this.participants.delete(data.id);
this.emit("userLeft", data.id);
if (this.recvTransport) this.stopConsume(data.id);
break;
}
case WSEventType.UserStartProduce: {
const user = this.participants.get(data.id);
if (user === undefined) return;
switch (data.type) {
case "audio":
user.audio = true;
break;
default:
throw new Error(
`Invalid produce type ${data.type}`
);
}
if (this.recvTransport) this.stopConsume(data.id);
break;
}
case WSEventType.UserStartProduce: {
const user = this.participants.get(data.id);
if (user === undefined) return;
switch (data.type) {
case "audio":
user.audio = true;
break;
default:
throw new Error(
`Invalid produce type ${data.type}`,
);
}
if (this.recvTransport)
this.startConsume(data.id, data.type);
this.emit("userStartProduce", data.id, data.type);
break;
}
case WSEventType.UserStopProduce: {
const user = this.participants.get(data.id);
if (user === undefined) return;
switch (data.type) {
case "audio":
user.audio = false;
break;
default:
throw new Error(
`Invalid produce type ${data.type}`
);
}
if (this.recvTransport)
this.startConsume(data.id, data.type);
this.emit("userStartProduce", data.id, data.type);
break;
}
case WSEventType.UserStopProduce: {
const user = this.participants.get(data.id);
if (user === undefined) return;
switch (data.type) {
case "audio":
user.audio = false;
break;
default:
throw new Error(
`Invalid produce type ${data.type}`,
);
}
if (this.recvTransport)
this.stopConsume(data.id, data.type);
this.emit("userStopProduce", data.id, data.type);
break;
}
}
},
this
);
if (this.recvTransport)
this.stopConsume(data.id, data.type);
this.emit("userStopProduce", data.id, data.type);
break;
}
}
},
this,
);
this.signaling.on(
"error",
error => {
this.emit("error", new Error("Signaling error"));
},
this
);
this.signaling.on(
"error",
(error) => {
this.emit("error", new Error("Signaling error"));
},
this,
);
this.signaling.on(
"close",
error => {
this.disconnect(
{
error: error.code,
message: error.reason
},
true
);
},
this
);
}
this.signaling.on(
"close",
(error) => {
this.disconnect(
{
error: error.code,
message: error.reason,
},
true,
);
},
this,
);
}
supported() {
return this._supported;
}
throwIfUnsupported() {
if (!this._supported) throw new UnsupportedError("RTC not supported");
}
supported() {
return this._supported;
}
throwIfUnsupported() {
if (!this._supported) throw new UnsupportedError("RTC not supported");
}
connect(address: string, roomId: string) {
this.throwIfUnsupported();
this.device = new Device();
this.roomId = roomId;
return this.signaling.connect(address);
}
connect(address: string, roomId: string) {
this.throwIfUnsupported();
this.device = new Device();
this.roomId = roomId;
return this.signaling.connect(address);
}
disconnect(error?: VoiceError, ignoreDisconnected?: boolean) {
if (!this.signaling.connected() && !ignoreDisconnected) return;
this.signaling.disconnect();
this.participants = new Map();
this.consumers = new Map();
this.userId = undefined;
this.roomId = undefined;
disconnect(error?: VoiceError, ignoreDisconnected?: boolean) {
if (!this.signaling.connected() && !ignoreDisconnected) return;
this.signaling.disconnect();
this.participants = new Map();
this.consumers = new Map();
this.userId = undefined;
this.roomId = undefined;
this.audioProducer = undefined;
this.audioProducer = undefined;
if (this.sendTransport) this.sendTransport.close();
if (this.recvTransport) this.recvTransport.close();
this.sendTransport = undefined;
this.recvTransport = undefined;
if (this.sendTransport) this.sendTransport.close();
if (this.recvTransport) this.recvTransport.close();
this.sendTransport = undefined;
this.recvTransport = undefined;
this.emit("close", error);
}
this.emit("close", error);
}
async authenticate(token: string) {
this.throwIfUnsupported();
if (this.device === undefined || this.roomId === undefined)
throw new ReferenceError("Voice Client is in an invalid state");
const result = await this.signaling.authenticate(token, this.roomId);
let [room] = await Promise.all([
this.signaling.roomInfo(),
this.device.load({ routerRtpCapabilities: result.rtpCapabilities })
]);
async authenticate(token: string) {
this.throwIfUnsupported();
if (this.device === undefined || this.roomId === undefined)
throw new ReferenceError("Voice Client is in an invalid state");
const result = await this.signaling.authenticate(token, this.roomId);
let [room] = await Promise.all([
this.signaling.roomInfo(),
this.device.load({ routerRtpCapabilities: result.rtpCapabilities }),
]);
this.userId = result.userId;
this.participants = room.users;
}
this.userId = result.userId;
this.participants = room.users;
}
async initializeTransports() {
this.throwIfUnsupported();
if (this.device === undefined)
throw new ReferenceError("Voice Client is in an invalid state");
const initData = await this.signaling.initializeTransports(
this.device.rtpCapabilities
);
async initializeTransports() {
this.throwIfUnsupported();
if (this.device === undefined)
throw new ReferenceError("Voice Client is in an invalid state");
const initData = await this.signaling.initializeTransports(
this.device.rtpCapabilities,
);
this.sendTransport = this.device.createSendTransport(
initData.sendTransport
);
this.recvTransport = this.device.createRecvTransport(
initData.recvTransport
);
this.sendTransport = this.device.createSendTransport(
initData.sendTransport,
);
this.recvTransport = this.device.createRecvTransport(
initData.recvTransport,
);
const connectTransport = (transport: Transport) => {
transport.on("connect", ({ dtlsParameters }, callback, errback) => {
this.signaling
.connectTransport(transport.id, dtlsParameters)
.then(callback)
.catch(errback);
});
};
const connectTransport = (transport: Transport) => {
transport.on("connect", ({ dtlsParameters }, callback, errback) => {
this.signaling
.connectTransport(transport.id, dtlsParameters)
.then(callback)
.catch(errback);
});
};
connectTransport(this.sendTransport);
connectTransport(this.recvTransport);
connectTransport(this.sendTransport);
connectTransport(this.recvTransport);
this.sendTransport.on("produce", (parameters, callback, errback) => {
const type = parameters.appData.type;
if (
parameters.kind === "audio" &&
type !== "audio" &&
type !== "saudio"
)
return errback();
if (
parameters.kind === "video" &&
type !== "video" &&
type !== "svideo"
)
return errback();
this.signaling
.startProduce(type, parameters.rtpParameters)
.then(id => callback({ id }))
.catch(errback);
});
this.sendTransport.on("produce", (parameters, callback, errback) => {
const type = parameters.appData.type;
if (
parameters.kind === "audio" &&
type !== "audio" &&
type !== "saudio"
)
return errback();
if (
parameters.kind === "video" &&
type !== "video" &&
type !== "svideo"
)
return errback();
this.signaling
.startProduce(type, parameters.rtpParameters)
.then((id) => callback({ id }))
.catch(errback);
});
this.emit("ready");
for (let user of this.participants) {
if (user[1].audio && user[0] !== this.userId)
this.startConsume(user[0], "audio");
}
}
this.emit("ready");
for (let user of this.participants) {
if (user[1].audio && user[0] !== this.userId)
this.startConsume(user[0], "audio");
}
}
private async startConsume(userId: string, type: ProduceType) {
if (this.recvTransport === undefined)
throw new Error("Receive transport undefined");
const consumers = this.consumers.get(userId) || {};
const consumerParams = await this.signaling.startConsume(userId, type);
const consumer = await this.recvTransport.consume(consumerParams);
switch (type) {
case "audio":
consumers.audio = consumer;
}
private async startConsume(userId: string, type: ProduceType) {
if (this.recvTransport === undefined)
throw new Error("Receive transport undefined");
const consumers = this.consumers.get(userId) || {};
const consumerParams = await this.signaling.startConsume(userId, type);
const consumer = await this.recvTransport.consume(consumerParams);
switch (type) {
case "audio":
consumers.audio = consumer;
}
const mediaStream = new MediaStream([consumer.track]);
const audio = new Audio();
audio.srcObject = mediaStream;
await this.signaling.setConsumerPause(consumer.id, false);
audio.play();
this.consumers.set(userId, consumers);
}
const mediaStream = new MediaStream([consumer.track]);
const audio = new Audio();
audio.srcObject = mediaStream;
await this.signaling.setConsumerPause(consumer.id, false);
audio.play();
this.consumers.set(userId, consumers);
}
private async stopConsume(userId: string, type?: ProduceType) {
const consumers = this.consumers.get(userId);
if (consumers === undefined) return;
if (type === undefined) {
if (consumers.audio !== undefined) consumers.audio.close();
this.consumers.delete(userId);
} else {
switch (type) {
case "audio": {
if (consumers.audio !== undefined) {
consumers.audio.close();
this.signaling.stopConsume(consumers.audio.id);
}
consumers.audio = undefined;
break;
}
}
private async stopConsume(userId: string, type?: ProduceType) {
const consumers = this.consumers.get(userId);
if (consumers === undefined) return;
if (type === undefined) {
if (consumers.audio !== undefined) consumers.audio.close();
this.consumers.delete(userId);
} else {
switch (type) {
case "audio": {
if (consumers.audio !== undefined) {
consumers.audio.close();
this.signaling.stopConsume(consumers.audio.id);
}
consumers.audio = undefined;
break;
}
}
this.consumers.set(userId, consumers);
}
}
this.consumers.set(userId, consumers);
}
}
async startProduce(track: MediaStreamTrack, type: ProduceType) {
if (this.sendTransport === undefined)
throw new Error("Send transport undefined");
const producer = await this.sendTransport.produce({
track,
appData: { type }
});
async startProduce(track: MediaStreamTrack, type: ProduceType) {
if (this.sendTransport === undefined)
throw new Error("Send transport undefined");
const producer = await this.sendTransport.produce({
track,
appData: { type },
});
switch (type) {
case "audio":
this.audioProducer = producer;
break;
}
switch (type) {
case "audio":
this.audioProducer = producer;
break;
}
const participant = this.participants.get(this.userId || "");
if (participant !== undefined) {
participant[type] = true;
this.participants.set(this.userId || "", participant);
}
const participant = this.participants.get(this.userId || "");
if (participant !== undefined) {
participant[type] = true;
this.participants.set(this.userId || "", participant);
}
this.emit("startProduce", type);
}
this.emit("startProduce", type);
}
async stopProduce(type: ProduceType) {
let producer;
switch (type) {
case "audio":
producer = this.audioProducer;
this.audioProducer = undefined;
break;
}
async stopProduce(type: ProduceType) {
let producer;
switch (type) {
case "audio":
producer = this.audioProducer;
this.audioProducer = undefined;
break;
}
if (producer !== undefined) {
producer.close();
this.emit("stopProduce", type);
}
if (producer !== undefined) {
producer.close();
this.emit("stopProduce", type);
}
const participant = this.participants.get(this.userId || "");
if (participant !== undefined) {
participant[type] = false;
this.participants.set(this.userId || "", participant);
}
const participant = this.participants.get(this.userId || "");
if (participant !== undefined) {
participant[type] = false;
this.participants.set(this.userId || "", participant);
}
try {
await this.signaling.stopProduce(type);
} catch (error) {
if (error.error === WSErrorCode.ProducerNotFound) return;
else throw error;
}
}
try {
await this.signaling.stopProduce(type);
} catch (error) {
if (error.error === WSErrorCode.ProducerNotFound) return;
else throw error;
}
}
}

View File

@@ -1,24 +1,24 @@
import { useEffect, useState } from "preact/hooks";
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener("resize", handleResize);
handleResize();
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
return windowSize;
}