mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
Run prettier on all files.
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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> ·
|
||||
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> · enter to{" "}
|
||||
<a onClick={save}>save</a>
|
||||
</span>
|
||||
</EditorBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user