Run prettier on all files.

This commit is contained in:
Paul
2021-07-05 11:23:23 +01:00
parent 50cd6fc1ee
commit a9ce64c9fe
181 changed files with 18084 additions and 13521 deletions

View File

@@ -1,122 +1,153 @@
import styled from "styled-components";
import { useEffect, useState } from "preact/hooks";
import ChannelHeader from "./ChannelHeader";
import { useParams, useHistory } from "react-router-dom";
import { MessageArea } from "./messaging/MessageArea";
import Checkbox from "../../components/ui/Checkbox";
import Button from "../../components/ui/Button";
// import { useRenderState } from "../../lib/renderer/Singleton";
import { Channels } from "revolt.js/dist/api/objects";
import styled from "styled-components";
import { useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import MessageBox from "../../components/common/messaging/MessageBox";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
import MemberSidebar from "../../components/navigation/right/MemberSidebar";
import MessageBox from "../../components/common/messaging/MessageBox";
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
import { Channel } from "revolt.js";
import Button from "../../components/ui/Button";
import Checkbox from "../../components/ui/Checkbox";
import MemberSidebar from "../../components/navigation/right/MemberSidebar";
import ChannelHeader from "./ChannelHeader";
import { MessageArea } from "./messaging/MessageArea";
import VoiceHeader from "./voice/VoiceHeader";
const ChannelMain = styled.div`
flex-grow: 1;
display: flex;
min-height: 0;
overflow: hidden;
flex-direction: row;
flex-grow: 1;
display: flex;
min-height: 0;
overflow: hidden;
flex-direction: row;
`;
const ChannelContent = styled.div`
flex-grow: 1;
display: flex;
overflow: hidden;
flex-direction: column;
flex-grow: 1;
display: flex;
overflow: hidden;
flex-direction: column;
`;
const AgeGate = styled.div`
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
padding: 12px;
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
padding: 12px;
img {
height: 150px;
}
img {
height: 150px;
}
.subtext {
color: var(--secondary-foreground);
margin-bottom: 12px;
font-size: 14px;
}
.subtext {
color: var(--secondary-foreground);
margin-bottom: 12px;
font-size: 14px;
}
.actions {
margin-top: 20px;
display: flex;
gap: 12px;
}
.actions {
margin-top: 20px;
display: flex;
gap: 12px;
}
`;
export function Channel({ id }: { id: string }) {
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
if (!channel) return null;
if (channel.channel_type === 'VoiceChannel') {
return <VoiceChannel channel={channel} />;
} else {
return <TextChannel channel={channel} />;
}
if (channel.channel_type === "VoiceChannel") {
return <VoiceChannel channel={channel} />;
} else {
return <TextChannel channel={channel} />;
}
}
function TextChannel({ channel }: { channel: Channel }) {
const [ showMembers, setMembers ] = useState(true);
function TextChannel({ channel }: { channel: Channels.Channel }) {
const [showMembers, setMembers] = useState(true);
if ((channel.channel_type === 'TextChannel' || channel.channel_type === 'Group') && channel.name.includes('nsfw')) {
const goBack = useHistory();
const [ consent, setConsent ] = useState(false);
const [ ageGate, setAgeGate ] = useState(false);
if (!ageGate) {
return (
<AgeGate>
<img src={"https://static.revolt.chat/emoji/mutant/26a0.svg"} draggable={false}/>
<h2>{channel.name}</h2>
<span className="subtext">This channel is marked as NSFW. <a href="#">Learn more</a></span>
if (
(channel.channel_type === "TextChannel" ||
channel.channel_type === "Group") &&
channel.name.includes("nsfw")
) {
const goBack = useHistory();
const [consent, setConsent] = useState(false);
const [ageGate, setAgeGate] = useState(false);
if (!ageGate) {
return (
<AgeGate>
<img
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
draggable={false}
/>
<h2>{channel.name}</h2>
<span className="subtext">
This channel is marked as NSFW.{" "}
<a href="#">Learn more</a>
</span>
<Checkbox checked={consent} onChange={v => setConsent(v)}>I confirm that I am at least 18 years old.</Checkbox>
<div className="actions">
<Button contrast onClick={() => goBack}>Go back</Button>
<Button contrast onClick={() => consent && setAgeGate(true)}>Enter Channel</Button>
</div>
</AgeGate>
)
}
}
<Checkbox checked={consent} onChange={(v) => setConsent(v)}>
I confirm that I am at least 18 years old.
</Checkbox>
<div className="actions">
<Button contrast onClick={() => goBack}>
Go back
</Button>
<Button
contrast
onClick={() => consent && setAgeGate(true)}>
Enter Channel
</Button>
</div>
</AgeGate>
);
}
}
let id = channel._id;
return <>
<ChannelHeader channel={channel} toggleSidebar={() => setMembers(!showMembers)} />
<ChannelMain>
<ChannelContent>
<VoiceHeader id={id} />
<MessageArea id={id} />
<TypingIndicator id={id} />
<JumpToBottom id={id} />
<MessageBox channel={channel} />
</ChannelContent>
{ !isTouchscreenDevice && showMembers && <MemberSidebar channel={channel} /> }
</ChannelMain>
</>;
let id = channel._id;
return (
<>
<ChannelHeader
channel={channel}
toggleSidebar={() => setMembers(!showMembers)}
/>
<ChannelMain>
<ChannelContent>
<VoiceHeader id={id} />
<MessageArea id={id} />
<TypingIndicator id={id} />
<JumpToBottom id={id} />
<MessageBox channel={channel} />
</ChannelContent>
{!isTouchscreenDevice && showMembers && (
<MemberSidebar channel={channel} />
)}
</ChannelMain>
</>
);
}
function VoiceChannel({ channel }: { channel: Channel }) {
return <>
<ChannelHeader channel={channel} />
<VoiceHeader id={channel._id} />
</>;
function VoiceChannel({ channel }: { channel: Channels.Channel }) {
return (
<>
<ChannelHeader channel={channel} />
<VoiceHeader id={channel._id} />
</>
);
}
export default function() {
const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />;
export default function () {
const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />;
}

View File

@@ -1,116 +1,139 @@
import styled from "styled-components";
import { Channel, User } from "revolt.js";
import { useContext } from "preact/hooks";
import Header from "../../components/ui/Header";
import HeaderActions from "./actions/HeaderActions";
import Markdown from "../../components/markdown/Markdown";
import { getChannelName } from "../../context/revoltjs/util";
import UserStatus from "../../components/common/user/UserStatus";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { At, Hash } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid";
import { useStatusColour } from "../../components/common/user/UserIcon";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { Channel, User } from "revolt.js";
import styled from "styled-components";
import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util";
import { useStatusColour } from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus";
import Header from "../../components/ui/Header";
import Markdown from "../../components/markdown/Markdown";
import HeaderActions from "./actions/HeaderActions";
export interface ChannelHeaderProps {
channel: Channel,
toggleSidebar?: () => void
channel: Channel;
toggleSidebar?: () => void;
}
const Info = styled.div`
flex-grow: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
flex-grow: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
display: flex;
gap: 8px;
align-items: center;
display: flex;
gap: 8px;
align-items: center;
* {
display: inline-block;
}
* {
display: inline-block;
}
.divider {
height: 20px;
margin: 0 5px;
padding-left: 1px;
background-color: var(--tertiary-background);
}
.divider {
height: 20px;
margin: 0 5px;
padding-left: 1px;
background-color: var(--tertiary-background);
}
.status {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-inline-end: 6px;
}
.status {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-inline-end: 6px;
}
.desc {
cursor: pointer;
margin-top: 2px;
font-size: 0.8em;
font-weight: 400;
color: var(--secondary-foreground);
}
.desc {
cursor: pointer;
margin-top: 2px;
font-size: 0.8em;
font-weight: 400;
color: var(--secondary-foreground);
}
`;
export default function ChannelHeader({ channel, toggleSidebar }: ChannelHeaderProps) {
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
export default function ChannelHeader({
channel,
toggleSidebar,
}: ChannelHeaderProps) {
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const name = getChannelName(client, channel);
let icon, recipient;
switch (channel.channel_type) {
case "SavedMessages":
icon = <Notepad size={24} />;
break;
case "DirectMessage":
icon = <At size={24} />;
const uid = client.channels.getRecipient(channel._id);
recipient = client.users.get(uid);
break;
case "Group":
icon = <Group size={24} />;
break;
case "TextChannel":
icon = <Hash size={24} />;
break;
}
const name = getChannelName(client, channel);
let icon, recipient;
switch (channel.channel_type) {
case "SavedMessages":
icon = <Notepad size={24} />;
break;
case "DirectMessage":
icon = <At size={24} />;
const uid = client.channels.getRecipient(channel._id);
recipient = client.users.get(uid);
break;
case "Group":
icon = <Group size={24} />;
break;
case "TextChannel":
icon = <Hash size={24} />;
break;
}
return (
<Header placement="primary">
{ icon }
<Info>
<span className="name">{ name }</span>
{isTouchscreenDevice && channel.channel_type === "DirectMessage" && (
<>
<div className="divider" />
<span className="desc">
<div className="status" style={{ backgroundColor: useStatusColour(recipient as User) }} />
<UserStatus user={recipient as User} />
</span>
</>
)}
{!isTouchscreenDevice && (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && channel.description && (
<>
<div className="divider" />
<span
className="desc"
onClick={() =>
openScreen({
id: "channel_info",
channel_id: channel._id
})
}>
<Markdown content={channel.description.split("\n")[0] ?? ""} disallowBigEmoji />
</span>
</>
)}
</Info>
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
</Header>
)
return (
<Header placement="primary">
{icon}
<Info>
<span className="name">{name}</span>
{isTouchscreenDevice &&
channel.channel_type === "DirectMessage" && (
<>
<div className="divider" />
<span className="desc">
<div
className="status"
style={{
backgroundColor: useStatusColour(
recipient as User,
),
}}
/>
<UserStatus user={recipient as User} />
</span>
</>
)}
{!isTouchscreenDevice &&
(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") &&
channel.description && (
<>
<div className="divider" />
<span
className="desc"
onClick={() =>
openScreen({
id: "channel_info",
channel_id: channel._id,
})
}>
<Markdown
content={
channel.description.split("\n")[0] ?? ""
}
disallowBigEmoji
/>
</span>
</>
)}
</Info>
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
</Header>
);
}

View File

@@ -1,81 +1,112 @@
import { useContext } from "preact/hooks";
import { useHistory } from "react-router-dom";
import { ChannelHeaderProps } from "../ChannelHeader";
import IconButton from "../../../components/ui/IconButton";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import UpdateIndicator from "../../../components/common/UpdateIndicator";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice";
import { UserPlus, Cog, PhoneCall, PhoneOutgoing } from "@styled-icons/boxicons-solid";
import { Sidebar as SidebarIcon } from "@styled-icons/boxicons-regular";
import {
UserPlus,
Cog,
PhoneCall,
PhoneOutgoing,
} from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom";
export default function HeaderActions({ channel, toggleSidebar }: ChannelHeaderProps) {
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const history = useHistory();
import { useContext } from "preact/hooks";
return (
<>
<UpdateIndicator />
{ channel.channel_type === "Group" && (
<>
<IconButton onClick={() =>
openScreen({
id: "user_picker",
omit: channel.recipients,
callback: async users => {
for (const user of users) {
await client.channels.addMember(channel._id, user);
}
}
})}>
<UserPlus size={27} />
</IconButton>
<IconButton onClick={() => history.push(`/channel/${channel._id}/settings`)}>
<Cog size={24} />
</IconButton>
</>
) }
<VoiceActions channel={channel} />
{ (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && !isTouchscreenDevice && (
<IconButton onClick={toggleSidebar}>
<SidebarIcon size={22} />
</IconButton>
) }
</>
)
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import {
VoiceContext,
VoiceOperationsContext,
VoiceStatus,
} from "../../../context/Voice";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import UpdateIndicator from "../../../components/common/UpdateIndicator";
import IconButton from "../../../components/ui/IconButton";
import { ChannelHeaderProps } from "../ChannelHeader";
export default function HeaderActions({
channel,
toggleSidebar,
}: ChannelHeaderProps) {
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const history = useHistory();
return (
<>
<UpdateIndicator />
{channel.channel_type === "Group" && (
<>
<IconButton
onClick={() =>
openScreen({
id: "user_picker",
omit: channel.recipients,
callback: async (users) => {
for (const user of users) {
await client.channels.addMember(
channel._id,
user,
);
}
},
})
}>
<UserPlus size={27} />
</IconButton>
<IconButton
onClick={() =>
history.push(`/channel/${channel._id}/settings`)
}>
<Cog size={24} />
</IconButton>
</>
)}
<VoiceActions channel={channel} />
{(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") &&
!isTouchscreenDevice && (
<IconButton onClick={toggleSidebar}>
<SidebarIcon size={22} />
</IconButton>
)}
</>
);
}
function VoiceActions({ channel }: Pick<ChannelHeaderProps, 'channel'>) {
if (channel.channel_type === 'SavedMessages' ||
channel.channel_type === 'TextChannel') return null;
function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
if (
channel.channel_type === "SavedMessages" ||
channel.channel_type === "TextChannel"
)
return null;
const voice = useContext(VoiceContext);
const { connect, disconnect } = useContext(VoiceOperationsContext);
const voice = useContext(VoiceContext);
const { connect, disconnect } = useContext(VoiceOperationsContext);
if (voice.status >= VoiceStatus.READY) {
if (voice.roomId === channel._id) {
return (
<IconButton onClick={disconnect}>
<PhoneOutgoing size={22} />
</IconButton>
)
} else {
return (
<IconButton onClick={() => {
disconnect();
connect(channel._id);
}}>
<PhoneCall size={24} />
</IconButton>
)
}
} else {
return (
<IconButton>
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
</IconButton>
)
}
if (voice.status >= VoiceStatus.READY) {
if (voice.roomId === channel._id) {
return (
<IconButton onClick={disconnect}>
<PhoneOutgoing size={22} />
</IconButton>
);
} else {
return (
<IconButton
onClick={() => {
disconnect();
connect(channel._id);
}}>
<PhoneCall size={24} />
</IconButton>
);
}
} else {
return (
<IconButton>
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
</IconButton>
);
}
}

View File

@@ -1,38 +1,40 @@
import { Text } from "preact-i18n";
import styled from "styled-components";
import { getChannelName } from "../../../context/revoltjs/util";
import { Text } from "preact-i18n";
import { useChannel, useForceUpdate } from "../../../context/revoltjs/hooks";
import { getChannelName } from "../../../context/revoltjs/util";
const StartBase = styled.div`
margin: 18px 16px 10px 16px;
margin: 18px 16px 10px 16px;
h1 {
font-size: 23px;
margin: 0 0 8px 0;
}
h1 {
font-size: 23px;
margin: 0 0 8px 0;
}
h4 {
font-weight: 400;
margin: 0;
font-size: 14px;
}
h4 {
font-weight: 400;
margin: 0;
font-size: 14px;
}
`;
interface Props {
id: string;
id: string;
}
export default function ConversationStart({ id }: Props) {
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
return (
<StartBase>
<h1>{ getChannelName(ctx.client, channel, true) }</h1>
<h4>
<Text id="app.main.channel.start.group" />
</h4>
</StartBase>
);
return (
<StartBase>
<h1>{getChannelName(ctx.client, channel, true)}</h1>
<h4>
<Text id="app.main.channel.start.group" />
</h4>
</StartBase>
);
}

View File

@@ -1,232 +1,259 @@
import styled from "styled-components";
import { createContext } from "preact";
import { animateScroll } from "react-scroll";
import MessageRenderer from "./MessageRenderer";
import ConversationStart from './ConversationStart';
import styled from "styled-components";
import useResizeObserver from "use-resize-observer";
import Preloader from "../../../components/ui/Preloader";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { RenderState, ScrollState } from "../../../lib/renderer/types";
import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { createContext } from "preact";
import {
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "preact/hooks";
import { defer } from "../../../lib/defer";
import { internalEmit } from "../../../lib/eventEmitter";
import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
import { RenderState, ScrollState } from "../../../lib/renderer/types";
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import {
ClientStatus,
StatusContext,
} from "../../../context/revoltjs/RevoltClient";
import Preloader from "../../../components/ui/Preloader";
import ConversationStart from "./ConversationStart";
import MessageRenderer from "./MessageRenderer";
const Area = styled.div`
height: 100%;
flex-grow: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
word-break: break-word;
height: 100%;
flex-grow: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
word-break: break-word;
> div {
display: flex;
min-height: 100%;
padding-bottom: 20px;
flex-direction: column;
justify-content: flex-end;
}
> div {
display: flex;
min-height: 100%;
padding-bottom: 20px;
flex-direction: column;
justify-content: flex-end;
}
`;
interface Props {
id: string;
id: string;
}
export const MessageAreaWidthContext = createContext(0);
export const MESSAGE_AREA_PADDING = 82;
export function MessageArea({ id }: Props) {
const status = useContext(StatusContext);
const { focusTaken } = useContext(IntermediateContext);
const status = useContext(StatusContext);
const { focusTaken } = useContext(IntermediateContext);
// ? This is the scroll container.
const ref = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
// ? This is the scroll container.
const ref = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
// ? Current channel state.
const [state, setState] = useState<RenderState>({ type: "LOADING" });
// ? Current channel state.
const [state, setState] = useState<RenderState>({ type: "LOADING" });
// ? useRef to avoid re-renders
const scrollState = useRef<ScrollState>({ type: "Free" });
// ? useRef to avoid re-renders
const scrollState = useRef<ScrollState>({ type: "Free" });
const setScrollState = (v: ScrollState) => {
if (v.type === 'StayAtBottom') {
if (scrollState.current.type === 'Bottom' || atBottom()) {
scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth };
} else {
scrollState.current = { type: 'Free' };
}
} else {
scrollState.current = v;
}
const setScrollState = (v: ScrollState) => {
if (v.type === "StayAtBottom") {
if (scrollState.current.type === "Bottom" || atBottom()) {
scrollState.current = {
type: "ScrollToBottom",
smooth: v.smooth,
};
} else {
scrollState.current = { type: "Free" };
}
} else {
scrollState.current = v;
}
defer(() => {
if (scrollState.current.type === "ScrollToBottom") {
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
animateScroll.scrollToBottom({
container: ref.current,
duration: scrollState.current.smooth ? 150 : 0
});
} else if (scrollState.current.type === "OffsetTop") {
animateScroll.scrollTo(
Math.max(
101,
ref.current.scrollTop +
(ref.current.scrollHeight - scrollState.current.previousHeight)
),
{
container: ref.current,
duration: 0
}
);
defer(() => {
if (scrollState.current.type === "ScrollToBottom") {
setScrollState({
type: "Bottom",
scrollingUntil: +new Date() + 150,
});
setScrollState({ type: "Free" });
} else if (scrollState.current.type === "ScrollTop") {
animateScroll.scrollTo(scrollState.current.y, {
container: ref.current,
duration: 0
});
animateScroll.scrollToBottom({
container: ref.current,
duration: scrollState.current.smooth ? 150 : 0,
});
} else if (scrollState.current.type === "OffsetTop") {
animateScroll.scrollTo(
Math.max(
101,
ref.current.scrollTop +
(ref.current.scrollHeight -
scrollState.current.previousHeight),
),
{
container: ref.current,
duration: 0,
},
);
setScrollState({ type: "Free" });
}
});
}
setScrollState({ type: "Free" });
} else if (scrollState.current.type === "ScrollTop") {
animateScroll.scrollTo(scrollState.current.y, {
container: ref.current,
duration: 0,
});
// ? Determine if we are at the bottom of the scroll container.
// -> https://stackoverflow.com/a/44893438
// By default, we assume we are at the bottom, i.e. when we first load.
const atBottom = (offset = 0) =>
ref.current
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
offset <=
ref.current.clientHeight
: true;
const atTop = (offset = 0) => ref.current.scrollTop <= offset;
setScrollState({ type: "Free" });
}
});
};
// ? Handle events from renderer.
useEffect(() => {
SingletonMessageRenderer.addListener('state', setState);
return () => SingletonMessageRenderer.removeListener('state', setState);
}, [ ]);
// ? Determine if we are at the bottom of the scroll container.
// -> https://stackoverflow.com/a/44893438
// By default, we assume we are at the bottom, i.e. when we first load.
const atBottom = (offset = 0) =>
ref.current
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
offset <=
ref.current.clientHeight
: true;
useEffect(() => {
SingletonMessageRenderer.addListener('scroll', setScrollState);
return () => SingletonMessageRenderer.removeListener('scroll', setScrollState);
}, [ scrollState ]);
const atTop = (offset = 0) => ref.current.scrollTop <= offset;
// ? Load channel initially.
useEffect(() => {
SingletonMessageRenderer.init(id);
}, [ id ]);
// ? Handle events from renderer.
useEffect(() => {
SingletonMessageRenderer.addListener("state", setState);
return () => SingletonMessageRenderer.removeListener("state", setState);
}, []);
// ? If we are waiting for network, try again.
useEffect(() => {
switch (status) {
case ClientStatus.ONLINE:
if (state.type === 'WAITING_FOR_NETWORK') {
SingletonMessageRenderer.init(id);
} else {
SingletonMessageRenderer.reloadStale(id);
}
useEffect(() => {
SingletonMessageRenderer.addListener("scroll", setScrollState);
return () =>
SingletonMessageRenderer.removeListener("scroll", setScrollState);
}, [scrollState]);
break;
case ClientStatus.OFFLINE:
case ClientStatus.DISCONNECTED:
case ClientStatus.CONNECTING:
SingletonMessageRenderer.markStale();
break;
}
}, [ status, state ]);
// ? Load channel initially.
useEffect(() => {
SingletonMessageRenderer.init(id);
}, [id]);
// ? When the container is scrolled.
// ? Also handle StayAtBottom
useEffect(() => {
async function onScroll() {
if (scrollState.current.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" });
} else if (scrollState.current.type === "Bottom" && !atBottom()) {
if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return;
setScrollState({ type: "Free" });
}
}
// ? If we are waiting for network, try again.
useEffect(() => {
switch (status) {
case ClientStatus.ONLINE:
if (state.type === "WAITING_FOR_NETWORK") {
SingletonMessageRenderer.init(id);
} else {
SingletonMessageRenderer.reloadStale(id);
}
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref, scrollState]);
break;
case ClientStatus.OFFLINE:
case ClientStatus.DISCONNECTED:
case ClientStatus.CONNECTING:
SingletonMessageRenderer.markStale();
break;
}
}, [status, state]);
// ? Top and bottom loaders.
useEffect(() => {
async function onScroll() {
if (atTop(100)) {
SingletonMessageRenderer.loadTop(ref.current);
}
// ? When the container is scrolled.
// ? Also handle StayAtBottom
useEffect(() => {
async function onScroll() {
if (scrollState.current.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" });
} else if (scrollState.current.type === "Bottom" && !atBottom()) {
if (
scrollState.current.scrollingUntil &&
scrollState.current.scrollingUntil > +new Date()
)
return;
setScrollState({ type: "Free" });
}
}
if (atBottom(100)) {
SingletonMessageRenderer.loadBottom(ref.current);
}
}
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref, scrollState]);
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref]);
// ? Top and bottom loaders.
useEffect(() => {
async function onScroll() {
if (atTop(100)) {
SingletonMessageRenderer.loadTop(ref.current);
}
// ? Scroll down whenever the message area resizes.
function stbOnResize() {
if (!atBottom() && scrollState.current.type === "Bottom") {
animateScroll.scrollToBottom({
container: ref.current,
duration: 0
});
if (atBottom(100)) {
SingletonMessageRenderer.loadBottom(ref.current);
}
}
setScrollState({ type: "Bottom" });
}
}
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref]);
// ? Scroll down when container resized.
useLayoutEffect(() => {
stbOnResize();
}, [height]);
// ? Scroll down whenever the message area resizes.
function stbOnResize() {
if (!atBottom() && scrollState.current.type === "Bottom") {
animateScroll.scrollToBottom({
container: ref.current,
duration: 0,
});
// ? Scroll down whenever the window resizes.
useLayoutEffect(() => {
document.addEventListener("resize", stbOnResize);
return () => document.removeEventListener("resize", stbOnResize);
}, [ref, scrollState]);
setScrollState({ type: "Bottom" });
}
}
// ? Scroll to bottom when pressing 'Escape'.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
SingletonMessageRenderer.jumpToBottom(id, true);
internalEmit("TextArea", "focus", "message");
}
}
// ? Scroll down when container resized.
useLayoutEffect(() => {
stbOnResize();
}, [height]);
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [ref, focusTaken]);
// ? Scroll down whenever the window resizes.
useLayoutEffect(() => {
document.addEventListener("resize", stbOnResize);
return () => document.removeEventListener("resize", stbOnResize);
}, [ref, scrollState]);
return (
<MessageAreaWidthContext.Provider value={(width ?? 0) - MESSAGE_AREA_PADDING}>
<Area ref={ref}>
<div>
{state.type === "LOADING" && <Preloader type="ring" />}
{state.type === "WAITING_FOR_NETWORK" && (
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>
)}
{state.type === "RENDER" && (
<MessageRenderer id={id} state={state} />
)}
{state.type === "EMPTY" && <ConversationStart id={id} />}
</div>
</Area>
</MessageAreaWidthContext.Provider>
);
// ? Scroll to bottom when pressing 'Escape'.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
SingletonMessageRenderer.jumpToBottom(id, true);
internalEmit("TextArea", "focus", "message");
}
}
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [ref, focusTaken]);
return (
<MessageAreaWidthContext.Provider
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
<Area ref={ref}>
<div>
{state.type === "LOADING" && <Preloader type="ring" />}
{state.type === "WAITING_FOR_NETWORK" && (
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>
)}
{state.type === "RENDER" && (
<MessageRenderer id={id} state={state} />
)}
{state.type === "EMPTY" && <ConversationStart id={id} />}
</div>
</Area>
</MessageAreaWidthContext.Provider>
);
}

View File

@@ -1,115 +1,133 @@
import styled from "styled-components";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { MessageObject } from "../../../context/revoltjs/util";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { IntermediateContext, useIntermediate } from "../../../context/intermediate/Intermediate";
import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
import {
IntermediateContext,
useIntermediate,
} from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import AutoComplete, {
useAutoComplete,
} from "../../../components/common/AutoComplete";
const EditorBase = styled.div`
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
textarea {
resize: none;
padding: 12px;
font-size: .875rem;
border-radius: 3px;
white-space: pre-wrap;
background: var(--secondary-header);
}
textarea {
resize: none;
padding: 12px;
font-size: 0.875rem;
border-radius: 3px;
white-space: pre-wrap;
background: var(--secondary-header);
}
.caption {
padding: 2px;
font-size: 11px;
color: var(--tertiary-foreground);
.caption {
padding: 2px;
font-size: 11px;
color: var(--tertiary-foreground);
a {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
a {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
`;
interface Props {
message: MessageObject
finish: () => void
message: MessageObject;
finish: () => void;
}
export default function MessageEditor({ message, finish }: Props) {
const [ content, setContent ] = useState(message.content as string ?? '');
const { focusTaken } = useContext(IntermediateContext);
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const [content, setContent] = useState((message.content as string) ?? "");
const { focusTaken } = useContext(IntermediateContext);
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
async function save() {
finish();
async function save() {
finish();
if (content.length === 0) {
// @ts-expect-error
openScreen({ id: 'special_prompt', type: 'delete_message', target: message });
} else if (content !== message.content) {
await client.channels.editMessage(
message.channel,
message._id,
{ content }
);
}
}
if (content.length === 0) {
openScreen({
id: "special_prompt",
// @ts-expect-error
type: "delete_message",
// @ts-expect-error
target: message,
});
} else if (content !== message.content) {
await client.channels.editMessage(message.channel, message._id, {
content,
});
}
}
// ? Stop editing when pressing ESC.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
finish();
}
}
// ? Stop editing when pressing ESC.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
finish();
}
}
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken]);
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken]);
const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } =
useAutoComplete(v => setContent(v ?? ''), {
users: { type: 'all' }
});
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete((v) => setContent(v ?? ""), {
users: { type: "all" },
});
return (
<EditorBase>
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
forceFocus
maxRows={3}
padding={12}
value={content}
maxLength={2000}
onChange={ev => {
onChange(ev);
setContent(ev.currentTarget.value)
}}
onKeyDown={e => {
if (onKeyDown(e)) return;
return (
<EditorBase>
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
forceFocus
maxRows={3}
padding={12}
value={content}
maxLength={2000}
onChange={(ev) => {
onChange(ev);
setContent(ev.currentTarget.value);
}}
onKeyDown={(e) => {
if (onKeyDown(e)) return;
if (
!e.shiftKey &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
e.preventDefault();
save();
}
}}
onKeyUp={onKeyUp}
onFocus={onFocus}
onBlur={onBlur}
/>
<span className="caption">
escape to <a onClick={finish}>cancel</a> &middot;
enter to <a onClick={save}>save</a>
</span>
</EditorBase>
)
if (
!e.shiftKey &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
e.preventDefault();
save();
}
}}
onKeyUp={onKeyUp}
onFocus={onFocus}
onBlur={onBlur}
/>
<span className="caption">
escape to <a onClick={finish}>cancel</a> &middot; enter to{" "}
<a onClick={save}>save</a>
</span>
</EditorBase>
);
}

View File

@@ -1,198 +1,215 @@
import { decodeTime } from "ulid";
import { memo } from "preact/compat";
import styled from "styled-components";
import MessageEditor from "./MessageEditor";
import { Children } from "../../../types/Preact";
import { Users } from "revolt.js/dist/api/objects";
import { X } from "@styled-icons/boxicons-regular";
import ConversationStart from "./ConversationStart";
import { connectState } from "../../../redux/connector";
import Preloader from "../../../components/ui/Preloader";
import { RenderState } from "../../../lib/renderer/types";
import DateDivider from "../../../components/ui/DateDivider";
import { QueuedMessage } from "../../../redux/reducers/queue";
import { Users } from "revolt.js/dist/api/objects";
import styled from "styled-components";
import { decodeTime } from "ulid";
import { memo } from "preact/compat";
import { useContext, useEffect, useState } from "preact/hooks";
import { MessageObject } from "../../../context/revoltjs/util";
import Message from "../../../components/common/messaging/Message";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
import { RenderState } from "../../../lib/renderer/types";
import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import Message from "../../../components/common/messaging/Message";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
import DateDivider from "../../../components/ui/DateDivider";
import Preloader from "../../../components/ui/Preloader";
import { Children } from "../../../types/Preact";
import ConversationStart from "./ConversationStart";
import MessageEditor from "./MessageEditor";
interface Props {
id: string;
state: RenderState;
queue: QueuedMessage[];
id: string;
state: RenderState;
queue: QueuedMessage[];
}
const BlockedMessage = styled.div`
font-size: 0.8em;
margin-top: 6px;
padding: 4px 64px;
color: var(--tertiary-foreground);
font-size: 0.8em;
margin-top: 6px;
padding: 4px 64px;
color: var(--tertiary-foreground);
&:hover {
background: var(--hover);
}
&:hover {
background: var(--hover);
}
`;
function MessageRenderer({ id, state, queue }: Props) {
if (state.type !== 'RENDER') return null;
if (state.type !== "RENDER") return null;
const client = useContext(AppContext);
const userId = client.user!._id;
const client = useContext(AppContext);
const userId = client.user!._id;
const [editing, setEditing] = useState<string | undefined>(undefined);
const stopEditing = () => {
setEditing(undefined);
internalEmit("TextArea", "focus", "message");
};
const [editing, setEditing] = useState<string | undefined>(undefined);
const stopEditing = () => {
setEditing(undefined);
internalEmit("TextArea", "focus", "message");
};
useEffect(() => {
function editLast() {
if (state.type !== 'RENDER') return;
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author === userId) {
setEditing(state.messages[i]._id);
return;
}
}
}
useEffect(() => {
function editLast() {
if (state.type !== "RENDER") return;
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author === userId) {
setEditing(state.messages[i]._id);
return;
}
}
}
const subs = [
internalSubscribe("MessageRenderer", "edit_last", editLast),
internalSubscribe("MessageRenderer", "edit_message", setEditing)
]
const subs = [
internalSubscribe("MessageRenderer", "edit_last", editLast),
internalSubscribe("MessageRenderer", "edit_message", setEditing),
];
return () => subs.forEach(unsub => unsub());
}, [state.messages]);
return () => subs.forEach((unsub) => unsub());
}, [state.messages]);
let render: Children[] = [],
previous: MessageObject | undefined;
let render: Children[] = [],
previous: MessageObject | undefined;
if (state.atTop) {
render.push(<ConversationStart id={id} />);
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>
);
}
if (state.atTop) {
render.push(<ConversationStart id={id} />);
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>,
);
}
let head = true;
function compare(
current: string,
curAuthor: string,
previous: string,
prevAuthor: string
) {
const atime = decodeTime(current),
adate = new Date(atime),
btime = decodeTime(previous),
bdate = new Date(btime);
let head = true;
function compare(
current: string,
curAuthor: string,
previous: string,
prevAuthor: string,
) {
const atime = decodeTime(current),
adate = new Date(atime),
btime = decodeTime(previous),
bdate = new Date(btime);
if (
adate.getFullYear() !== bdate.getFullYear() ||
adate.getMonth() !== bdate.getMonth() ||
adate.getDate() !== bdate.getDate()
) {
render.push(<DateDivider date={adate} />);
head = true;
}
if (
adate.getFullYear() !== bdate.getFullYear() ||
adate.getMonth() !== bdate.getMonth() ||
adate.getDate() !== bdate.getDate()
) {
render.push(<DateDivider date={adate} />);
head = true;
}
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
}
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
}
let blocked = 0;
function pushBlocked() {
render.push(<BlockedMessage><X size={16} /> { blocked } blocked messages</BlockedMessage>);
blocked = 0;
}
let blocked = 0;
function pushBlocked() {
render.push(
<BlockedMessage>
<X size={16} /> {blocked} blocked messages
</BlockedMessage>,
);
blocked = 0;
}
for (const message of state.messages) {
if (previous) {
compare(
message._id,
message.author,
previous._id,
previous.author
);
}
for (const message of state.messages) {
if (previous) {
compare(message._id, message.author, previous._id, previous.author);
}
if (message.author === "00000000000000000000000000") {
render.push(<SystemMessage key={message._id} message={message} attachContext />);
} else {
// ! FIXME: temp solution
if (client.users.get(message.author)?.relationship === Users.Relationship.Blocked) {
blocked++;
} else {
if (blocked > 0) pushBlocked();
if (message.author === "00000000000000000000000000") {
render.push(
<SystemMessage
key={message._id}
message={message}
attachContext
/>,
);
} else {
// ! FIXME: temp solution
if (
client.users.get(message.author)?.relationship ===
Users.Relationship.Blocked
) {
blocked++;
} else {
if (blocked > 0) pushBlocked();
render.push(
<Message message={message}
key={message._id}
head={head}
content={
editing === message._id ?
<MessageEditor message={message} finish={stopEditing} />
: undefined
}
attachContext />
);
}
}
render.push(
<Message
message={message}
key={message._id}
head={head}
content={
editing === message._id ? (
<MessageEditor
message={message}
finish={stopEditing}
/>
) : undefined
}
attachContext
/>,
);
}
}
previous = message;
}
if (blocked > 0) pushBlocked();
previous = message;
}
const nonces = state.messages.map(x => x.nonce);
if (state.atBottom) {
for (const msg of queue) {
if (msg.channel !== id) continue;
if (nonces.includes(msg.id)) continue;
if (blocked > 0) pushBlocked();
if (previous) {
compare(
msg.id,
userId!,
previous._id,
previous.author
);
previous = {
_id: msg.id,
data: { author: userId! }
} as any;
}
const nonces = state.messages.map((x) => x.nonce);
if (state.atBottom) {
for (const msg of queue) {
if (msg.channel !== id) continue;
if (nonces.includes(msg.id)) continue;
render.push(
<Message
message={{
...msg.data,
replies: msg.data.replies.map(x => x.id)
}}
key={msg.id}
queued={msg}
head={head}
attachContext />
);
}
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>
);
}
if (previous) {
compare(msg.id, userId!, previous._id, previous.author);
return <>{ render }</>;
previous = {
_id: msg.id,
data: { author: userId! },
} as any;
}
render.push(
<Message
message={{
...msg.data,
replies: msg.data.replies.map((x) => x.id),
}}
key={msg.id}
queued={msg}
head={head}
attachContext
/>,
);
}
} else {
render.push(
<RequiresOnline>
<Preloader type="ring" />
</RequiresOnline>,
);
}
return <>{render}</>;
}
export default memo(connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
return {
queue: state.queue
};
}));
export default memo(
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
return {
queue: state.queue,
};
}),
);

View File

@@ -1,117 +1,138 @@
import { Text } from "preact-i18n";
import styled from "styled-components";
import { useContext } from "preact/hooks";
import { BarChart } from "@styled-icons/boxicons-regular";
import Button from "../../../components/ui/Button";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import {
VoiceContext,
VoiceOperationsContext,
VoiceStatus,
} from "../../../context/Voice";
import {
useForceUpdate,
useSelf,
useUsers,
} from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import { useForceUpdate, useSelf, useUsers } from "../../../context/revoltjs/hooks";
import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice";
import Button from "../../../components/ui/Button";
interface Props {
id: string
id: string;
}
const VoiceBase = styled.div`
padding: 20px;
background: var(--secondary-background);
padding: 20px;
background: var(--secondary-background);
.status {
position: absolute;
color: var(--success);
background: var(--primary-background);
display: flex;
align-items: center;
padding: 10px;
font-size: 14px;
font-weight: 600;
border-radius: 7px;
flex: 1 0;
user-select: none;
.status {
position: absolute;
color: var(--success);
background: var(--primary-background);
display: flex;
align-items: center;
padding: 10px;
font-size: 14px;
font-weight: 600;
border-radius: 7px;
flex: 1 0;
user-select: none;
svg {
margin-inline-end: 4px;
cursor: help;
}
}
svg {
margin-inline-end: 4px;
cursor: help;
}
}
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
.participants {
margin: 20px 0;
justify-content: center;
pointer-events: none;
user-select: none;
display: flex;
gap: 16px;
.participants {
margin: 20px 0;
justify-content: center;
pointer-events: none;
user-select: none;
display: flex;
gap: 16px;
.disconnected {
opacity: 0.5;
}
}
.disconnected {
opacity: 0.5;
}
}
.actions {
display: flex;
justify-content: center;
gap: 10px;
}
.actions {
display: flex;
justify-content: center;
gap: 10px;
}
`;
export default function VoiceHeader({ id }: Props) {
const { status, participants, roomId } = useContext(VoiceContext);
if (roomId !== id) return null;
const { status, participants, roomId } = useContext(VoiceContext);
if (roomId !== id) return null;
const { isProducing, startProducing, stopProducing, disconnect } = useContext(VoiceOperationsContext);
const { isProducing, startProducing, stopProducing, disconnect } =
useContext(VoiceOperationsContext);
const ctx = useForceUpdate();
const self = useSelf(ctx);
const keys = participants ? Array.from(participants.keys()) : undefined;
const users = keys ? useUsers(keys, ctx) : undefined;
return (
<VoiceBase>
<div className="participants">
{ users && users.length !== 0 ? users.map((user, index) => {
const id = keys![index];
return (
<div key={id}>
<UserIcon
size={80}
target={user}
status={false}
voice={ participants!.get(id)?.audio ? undefined : "muted" }
/>
</div>
);
}) : self !== undefined && (
<div key={self._id} className="disconnected">
<UserIcon
size={80}
target={self}
status={false} />
</div>
)}
</div>
<div className="status">
<BarChart size={20} />
{ status === VoiceStatus.CONNECTED && <Text id="app.main.channel.voice.connected" /> }
</div>
<div className="actions">
<Button error onClick={disconnect}>
<Text id="app.main.channel.voice.leave" />
</Button>
{ isProducing("audio") ? (
<Button onClick={() => stopProducing("audio")}>
<Text id="app.main.channel.voice.mute" />
</Button>
) : (
<Button onClick={() => startProducing("audio")}>
<Text id="app.main.channel.voice.unmute" />
</Button>
)}
</div>
</VoiceBase>
)
const ctx = useForceUpdate();
const self = useSelf(ctx);
const keys = participants ? Array.from(participants.keys()) : undefined;
const users = keys ? useUsers(keys, ctx) : undefined;
return (
<VoiceBase>
<div className="participants">
{users && users.length !== 0
? users.map((user, index) => {
const id = keys![index];
return (
<div key={id}>
<UserIcon
size={80}
target={user}
status={false}
voice={
participants!.get(id)?.audio
? undefined
: "muted"
}
/>
</div>
);
})
: self !== undefined && (
<div key={self._id} className="disconnected">
<UserIcon
size={80}
target={self}
status={false}
/>
</div>
)}
</div>
<div className="status">
<BarChart size={20} />
{status === VoiceStatus.CONNECTED && (
<Text id="app.main.channel.voice.connected" />
)}
</div>
<div className="actions">
<Button error onClick={disconnect}>
<Text id="app.main.channel.voice.leave" />
</Button>
{isProducing("audio") ? (
<Button onClick={() => stopProducing("audio")}>
<Text id="app.main.channel.voice.mute" />
</Button>
) : (
<Button onClick={() => startProducing("audio")}>
<Text id="app.main.channel.voice.unmute" />
</Button>
)}
</div>
</VoiceBase>
);
}
/**{voice.roomId === id && (