x.username).join(", ")
+ }}
+ />
+ );
+ } else {
+ text = (
+
+ );
+ }
+
+ return (
+
+
+
+ {users.map(user => (
+

+ ))}
+
+
{text}
+
+
+ );
+ }
+
+ return null;
+}
+
+export default connectState<{ id: string }>(TypingIndicator, (state, props) => {
+ return {
+ typing: state.typing && state.typing[props.id]
+ };
+});
diff --git a/src/components/navigation/RightSidebar.tsx b/src/components/navigation/RightSidebar.tsx
index 775f5834..45371a8f 100644
--- a/src/components/navigation/RightSidebar.tsx
+++ b/src/components/navigation/RightSidebar.tsx
@@ -1,19 +1,18 @@
import { Route, Switch } from "react-router";
import SidebarBase from "./SidebarBase";
-// import { MemberSidebar } from "./right/MemberSidebar";
+import MemberSidebar from "./right/MemberSidebar";
export default function RightSidebar() {
return (
- {/*
- */ }
+
);
diff --git a/src/components/navigation/SidebarBase.tsx b/src/components/navigation/SidebarBase.tsx
index cefe6b7d..b9060c06 100644
--- a/src/components/navigation/SidebarBase.tsx
+++ b/src/components/navigation/SidebarBase.tsx
@@ -12,3 +12,22 @@ export default styled.div`
padding-bottom: 50px;
` }
`;
+
+export const GenericSidebarBase = styled.div`
+ height: 100%;
+ width: 240px;
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ background: var(--secondary-background);
+`;
+
+export const GenericSidebarList = styled.div`
+ padding: 6px;
+ flex-grow: 1;
+ overflow-y: scroll;
+
+ > svg {
+ width: 100%;
+ }
+`;
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
index 9c6cf7a4..4437ccc7 100644
--- a/src/components/navigation/left/HomeSidebar.tsx
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -2,7 +2,6 @@ import { Localizer, Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import { Home, Users, Tool, Save } from "@styled-icons/feather";
-import styled from "styled-components";
import Category from '../../ui/Category';
import PaintCounter from "../../../lib/PaintCounter";
import UserHeader from "../../common/user/UserHeader";
@@ -16,6 +15,7 @@ import { Users as UsersNS } from 'revolt.js/dist/api/objects';
import ButtonItem, { ChannelButton } from '../items/ButtonItem';
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useDMs, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
@@ -24,25 +24,6 @@ type Props = WithDispatcher & {
unreads: Unreads;
}
-const HomeBase = styled.div`
- height: 100%;
- width: 240px;
- display: flex;
- flex-shrink: 0;
- flex-direction: column;
- background: var(--secondary-background);
-`;
-
-const HomeList = styled.div`
- padding: 6px;
- flex-grow: 1;
- overflow-y: scroll;
-
- > svg {
- width: 100%;
- }
-`;
-
function HomeSidebar(props: Props) {
const { pathname } = useLocation();
const client = useContext(AppContext);
@@ -68,10 +49,10 @@ function HomeSidebar(props: Props) {
channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
return (
-
+
-
+
{!isTouchscreenDevice && (
<>
@@ -146,8 +127,8 @@ function HomeSidebar(props: Props) {
);
})}
-
-
+
+
);
};
diff --git a/src/components/navigation/right/ChannelDebugInfo.tsx b/src/components/navigation/right/ChannelDebugInfo.tsx
new file mode 100644
index 00000000..85842881
--- /dev/null
+++ b/src/components/navigation/right/ChannelDebugInfo.tsx
@@ -0,0 +1,37 @@
+import { useRenderState } from "../../../lib/renderer/Singleton";
+
+interface Props {
+ id: string;
+}
+
+export function ChannelDebugInfo({ id }: Props) {
+ if (process.env.NODE_ENV !== "development") return null;
+ let view = useRenderState(id);
+ if (!view) return null;
+
+ return (
+
+
+ Channel Info
+
+
+ State: { view.type }
+ { view.type === 'RENDER' && view.messages.length > 0 &&
+ <>
+ Start: {view.messages[0]._id}
+ End: {view.messages[view.messages.length - 1]._id}
+ At Top: {view.atTop ? "Yes" : "No"}
+ At Bottom: {view.atBottom ? "Yes" : "No"}
+ >
+ }
+
+
+ );
+}
diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx
new file mode 100644
index 00000000..97383549
--- /dev/null
+++ b/src/components/navigation/right/MemberSidebar.tsx
@@ -0,0 +1,206 @@
+import { Text } from "preact-i18n";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+import { User } from "revolt.js";
+import Category from "../../ui/Category";
+import { useParams } from "react-router";
+import { UserButton } from "../items/ButtonItem";
+import { ChannelDebugInfo } from "./ChannelDebugInfo";
+import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
+import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
+import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
+import { HookContext, useChannel, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
+
+import placeholderSVG from "../items/placeholder.svg";
+import { AppContext, ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
+
+interface Props {
+ ctx: HookContext
+}
+
+export default function MemberSidebar(props: { channel?: Channels.Channel }) {
+ const ctx = useForceUpdate();
+ const { channel: cid } = useParams<{ channel: string }>();
+ const channel = props.channel ?? useChannel(cid, ctx);
+
+ switch (channel?.channel_type) {
+ case 'Group': return ;
+ case 'TextChannel': return ;
+ default: return null;
+ }
+}
+
+export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels.GroupChannel }) {
+ const users = useUsers(undefined, ctx);
+ let members = channel.recipients
+ .map(x => users.find(y => y?._id === x))
+ .filter(x => typeof x !== "undefined") as User[];
+
+ /*const voice = useContext(VoiceContext);
+ const voiceActive = voice.roomId === channel._id;
+
+ let voiceParticipants: User[] = [];
+ if (voiceActive) {
+ const idArray = Array.from(voice.participants.keys());
+ voiceParticipants = idArray
+ .map(x => users.find(y => y?._id === x))
+ .filter(x => typeof x !== "undefined") as User[];
+
+ members = members.filter(member => idArray.indexOf(member._id) === -1);
+
+ voiceParticipants.sort((a, b) => a.username.localeCompare(b.username));
+ }*/
+
+ members.sort((a, b) => {
+ // ! FIXME: should probably rewrite all this code
+ let l = ((a.online &&
+ a.status?.presence !== Users.Presence.Invisible) ??
+ false) as any | 0;
+ let r = ((b.online &&
+ b.status?.presence !== Users.Presence.Invisible) ??
+ false) as any | 0;
+
+ let n = r - l;
+ if (n !== 0) {
+ return n;
+ }
+
+ return a.username.localeCompare(b.username);
+ });
+
+ return (
+
+
+
+ {/*voiceActive && voiceParticipants.length !== 0 && (
+
+
+ {" "}
+ — {voiceParticipants.length}
+
+ }
+ />
+ {voiceParticipants.map(
+ user =>
+ user && (
+
+
+
+ )
+ )}
+
+ )*/}
+ {!(members.length === 0 /*&& voiceActive*/) && (
+
+ —{" "}
+ {channel.recipients.length}
+
+ }
+ />
+ )}
+ {members.length === 0 && /*!voiceActive &&*/
}
+ {members.map(
+ user =>
+ user && (
+ //
+
+ //
+ )
+ )}
+
+
+ );
+}
+
+export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channels.TextChannel }) {
+ const [members, setMembers] = useState(undefined);
+ const users = useUsers(members?.map(x => x._id.user) ?? []).filter(x => typeof x !== 'undefined', ctx) as Users.User[];
+ const status = useContext(StatusContext);
+ const client = useContext(AppContext);
+
+ useEffect(() => {
+ if (status === ClientStatus.ONLINE && typeof members === 'undefined') {
+ client.servers.members.fetchMembers(channel.server)
+ .then(members => setMembers(members))
+ }
+ }, [ status ]);
+
+ // ! FIXME: temporary code
+ useEffect(() => {
+ function onPacket(packet: ClientboundNotification) {
+ if (!members) return;
+ if (packet.type === 'ServerMemberJoin') {
+ if (packet.id !== channel.server) return;
+ setMembers([ ...members, { _id: { server: packet.id, user: packet.user } } ]);
+ } else if (packet.type === 'ServerMemberLeave') {
+ if (packet.id !== channel.server) return;
+ setMembers(members.filter(x => !(x._id.user === packet.user && x._id.server === packet.id)));
+ }
+ }
+
+ client.addListener('packet', onPacket);
+ return () => client.removeListener('packet', onPacket);
+ }, [ members ]);
+
+ // copy paste from above
+ users.sort((a, b) => {
+ // ! FIXME: should probably rewrite all this code
+ let l = ((a.online &&
+ a.status?.presence !== Users.Presence.Invisible) ??
+ false) as any | 0;
+ let r = ((b.online &&
+ b.status?.presence !== Users.Presence.Invisible) ??
+ false) as any | 0;
+
+ let n = r - l;
+ if (n !== 0) {
+ return n;
+ }
+
+ return a.username.localeCompare(b.username);
+ });
+
+ return (
+
+
+
+
+ —{" "}
+ {users.length}
+
+ }
+ />
+ {users.length === 0 &&
}
+ {users.map(
+ user =>
+ user && (
+ //
+
+ //
+ )
+ )}
+
+
+ );
+}
diff --git a/src/components/ui/ComboBox.tsx b/src/components/ui/ComboBox.tsx
index de2796c5..ef9ba9f9 100644
--- a/src/components/ui/ComboBox.tsx
+++ b/src/components/ui/ComboBox.tsx
@@ -5,4 +5,12 @@ export default styled.select`
border-radius: 2px;
color: var(--secondary-foreground);
background: var(--secondary-background);
+
+ border: none;
+ outline: 2px solid transparent;
+ transition: outline-color 0.2s ease-in-out;
+
+ &:focus {
+ outline-color: var(--accent);
+ }
`;
diff --git a/src/components/ui/InputBox.tsx b/src/components/ui/InputBox.tsx
index b27e6c57..ac374646 100644
--- a/src/components/ui/InputBox.tsx
+++ b/src/components/ui/InputBox.tsx
@@ -8,18 +8,21 @@ export default styled.input`
z-index: 1;
padding: 8px 16px;
border-radius: 6px;
+
color: var(--foreground);
- border: 2px solid transparent;
background: var(--primary-background);
transition: 0.2s ease background-color;
- transition: border-color 0.2s ease-in-out;
+
+ border: none;
+ outline: 2px solid transparent;
+ transition: outline-color 0.2s ease-in-out;
&:hover {
background: var(--secondary-background);
}
&:focus {
- border: 2px solid var(--accent);
+ outline: 2px solid var(--accent);
}
${(props) =>
diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx
index cb4bc81a..7cdde086 100644
--- a/src/components/ui/TextArea.tsx
+++ b/src/components/ui/TextArea.tsx
@@ -7,23 +7,39 @@ import styled, { css } from "styled-components";
export interface TextAreaProps {
code?: boolean;
padding?: number;
+ lineHeight?: number;
+ hideBorder?: boolean;
}
+export const TEXT_AREA_BORDER_WIDTH = 2;
+export const DEFAULT_TEXT_AREA_PADDING = 16;
+export const DEFAULT_LINE_HEIGHT = 20;
+
export default styled.textarea`
width: 100%;
resize: none;
display: block;
- border-radius: 4px;
- padding: ${ props => props.padding ?? 16 }px;
-
color: var(--foreground);
- border: 2px solid transparent;
background: var(--secondary-background);
- transition: border-color .2s ease-in-out;
+ padding: ${ props => props.padding ?? DEFAULT_TEXT_AREA_PADDING }px;
+ line-height: ${ props => props.lineHeight ?? DEFAULT_LINE_HEIGHT }px;
+
+ ${ props => props.hideBorder && css`
+ border: none;
+ ` }
+
+ ${ props => !props.hideBorder && css`
+ border-radius: 4px;
+ transition: border-color .2s ease-in-out;
+ border: ${TEXT_AREA_BORDER_WIDTH}px solid transparent;
+ ` }
&:focus {
outline: none;
- border: 2px solid var(--accent);
+
+ ${ props => !props.hideBorder && css`
+ border: ${TEXT_AREA_BORDER_WIDTH}px solid var(--accent);
+ ` }
}
${ props => props.code ? css`
diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx
index 61df4630..d4852503 100644
--- a/src/context/revoltjs/FileUploads.tsx
+++ b/src/context/revoltjs/FileUploads.tsx
@@ -1,18 +1,15 @@
-// ! FIXME: also TEMP CODE
-// ! RE-WRITE WITH STYLED-COMPONENTS
-
import { Text } from "preact-i18n";
import { takeError } from "./util";
import classNames from "classnames";
+import { AppContext } from "./RevoltClient";
import styles from './FileUploads.module.scss';
import Axios, { AxiosRequestConfig } from "axios";
import { useContext, useState } from "preact/hooks";
-import { Edit, Plus, X } from "@styled-icons/feather";
import Preloader from "../../components/ui/Preloader";
import { determineFileSize } from "../../lib/fileSize";
import IconButton from '../../components/ui/IconButton';
+import { Edit, Plus, X, XCircle } from "@styled-icons/feather";
import { useIntermediate } from "../intermediate/Intermediate";
-import { AppContext } from "./RevoltClient";
type Props = {
maxFileSize: number
@@ -20,6 +17,7 @@ type Props = {
fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners'
} & (
{ behaviour: 'ask', onChange: (file: File) => void } |
+ { behaviour: 'multi', onChange: (files: File[]) => void } |
{ behaviour: 'upload', onUpload: (id: string) => Promise }
) & (
{ style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } |
@@ -40,6 +38,26 @@ export async function uploadFile(autumnURL: string, tag: string, file: File, con
return res.data.id;
}
+export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooLarge: () => void, multiple?: boolean) {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.multiple = multiple ?? false;
+
+ input.onchange = async e => {
+ const files = (e.target as any)?.files;
+ if (!files) return;
+ for (let file of files) {
+ if (file.size > maxFileSize) {
+ return tooLarge();
+ }
+ }
+
+ cb(Array.from(files));
+ };
+
+ input.click();
+}
+
export function FileUploader(props: Props) {
const { fileType, maxFileSize, remove } = props;
const { openScreen } = useIntermediate();
@@ -50,35 +68,25 @@ export function FileUploader(props: Props) {
function onClick() {
if (uploading) return;
- const input = document.createElement("input");
- input.type = "file";
-
- input.onchange = async e => {
+ grabFiles(maxFileSize, async files => {
setUploading(true);
try {
- const files = (e.target as any)?.files;
- if (files && files[0]) {
- let file = files[0];
-
- if (file.size > maxFileSize) {
- return openScreen({ id: "error", error: "FileTooLarge" });
- }
-
- if (props.behaviour === 'ask') {
- await props.onChange(file);
- } else {
- await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, file));
- }
+ if (props.behaviour === 'multi') {
+ props.onChange(files);
+ } else if (props.behaviour === 'ask') {
+ props.onChange(files[0]);
+ } else {
+ await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, files[0]));
}
} catch (err) {
return openScreen({ id: "error", error: takeError(err) });
} finally {
setUploading(false);
}
- };
-
- input.click();
+ }, () =>
+ openScreen({ id: "error", error: "FileTooLarge" }),
+ props.behaviour === 'multi');
}
function removeOrUpload() {
@@ -139,7 +147,7 @@ export function FileUploader(props: Props) {
if (attached) return remove();
onClick();
}}>
- { attached ? : }
+ { uploading ? : attached ? : }
)
}
diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx
index 09b8eaf1..0845a62a 100644
--- a/src/context/revoltjs/Notifications.tsx
+++ b/src/context/revoltjs/Notifications.tsx
@@ -1,8 +1,8 @@
import { decodeTime } from "ulid";
import { AppContext } from "./RevoltClient";
+import { useTranslation } from "../../lib/i18n";
import { Users } from "revolt.js/dist/api/objects";
import { useContext, useEffect } from "preact/hooks";
-import { IntlContext, translate } from "preact-i18n";
import { connectState } from "../../redux/connector";
import { playSound } from "../../assets/sounds/Audio";
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
@@ -25,7 +25,7 @@ async function createNotification(title: string, options: globalThis.Notificatio
}
function Notifier(props: Props) {
- const { intl } = useContext(IntlContext) as any;
+ const translate = useTranslation();
const showNotification = props.options?.desktopEnabled ?? false;
// const playIncoming = props.options?.soundEnabled ?? true;
// const playOutgoing = props.options?.outgoingSoundEnabled ?? true;
@@ -88,45 +88,37 @@ function Notifier(props: Props) {
} else {
let users = client.users;
switch (msg.content.type) {
- // ! FIXME: update to support new replacements
case "user_added":
- body = `${users.get(msg.content.id)?.username} ${translate(
- "app.main.channel.system.user_joined",
- "",
- intl.dictionary
- )} (${translate(
- "app.main.channel.system.added_by",
- "",
- intl.dictionary
- )} ${users.get(msg.content.by)?.username})`;
- icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
- break;
case "user_remove":
- body = `${users.get(msg.content.id)?.username} ${translate(
- "app.main.channel.system.user_left",
- "",
- intl.dictionary
- )} (${translate(
- "app.main.channel.system.added_by",
- "",
- intl.dictionary
- )} ${users.get(msg.content.by)?.username})`;
+ body = translate(
+ `app.main.channel.system.${msg.content.type === 'user_added' ? 'added_by' : 'removed_by'}`,
+ { user: users.get(msg.content.id)?.username, other_user: users.get(msg.content.by)?.username }
+ );
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
break;
+ case "user_joined":
case "user_left":
- body = `${users.get(msg.content.id)?.username} ${translate(
- "app.main.channel.system.user_left",
- "",
- intl.dictionary
- )}`;
+ case "user_kicked":
+ case "user_banned":
+ body = translate(
+ `app.main.channel.system.${msg.content.type}`,
+ { user: users.get(msg.content.id)?.username }
+ );
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
break;
case "channel_renamed":
- body = `${users.get(msg.content.by)?.username} ${translate(
- "app.main.channel.system.channel_renamed",
- "",
- intl.dictionary
- )} ${msg.content.name}`;
+ body = translate(
+ `app.main.channel.system.channel_renamed`,
+ { user: users.get(msg.content.by)?.username, name: msg.content.name }
+ );
+ icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
+ break;
+ case "channel_description_changed":
+ case "channel_icon_changed":
+ body = translate(
+ `app.main.channel.system.${msg.content.type}`,
+ { user: users.get(msg.content.by)?.username }
+ );
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
break;
}
@@ -173,20 +165,10 @@ function Notifier(props: Props) {
let event;
switch (user.relationship) {
case Users.Relationship.Incoming:
- event = translate(
- "notifications.sent_request",
- "",
- intl.dictionary,
- { person: user.username }
- );
+ event = translate("notifications.sent_request", { person: user.username });
break;
case Users.Relationship.Friend:
- event = translate(
- "notifications.now_friends",
- "",
- intl.dictionary,
- { person: user.username }
- );
+ event = translate("notifications.now_friends", { person: user.username });
break;
default:
return;
diff --git a/src/lib/TextAreaAutoSize.tsx b/src/lib/TextAreaAutoSize.tsx
index 2b1fd73f..deca4350 100644
--- a/src/lib/TextAreaAutoSize.tsx
+++ b/src/lib/TextAreaAutoSize.tsx
@@ -1,6 +1,5 @@
-import styled from "styled-components";
-import TextArea, { TextAreaProps } from "../components/ui/TextArea";
-import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
+import TextArea, { DEFAULT_LINE_HEIGHT, DEFAULT_TEXT_AREA_PADDING, TextAreaProps, TEXT_AREA_BORDER_WIDTH } from "../components/ui/TextArea";
+import { useEffect, useRef } from "preact/hooks";
type TextAreaAutoSizeProps = Omit, 'style' | 'value'> & TextAreaProps & {
autoFocus?: boolean,
@@ -9,51 +8,15 @@ type TextAreaAutoSizeProps = Omit, 'styl
value: string
};
-const lineHeight = 20;
-
-const Ghost = styled.div`
- width: 100%;
- overflow: hidden;
- position: relative;
-
- > div {
- width: 100%;
- white-space: pre-wrap;
-
- top: 0;
- position: absolute;
- visibility: hidden;
- }
-`;
-
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
- const { autoFocus, minHeight, maxRows, value, padding, children, as, ...textAreaProps } = props;
+ const { autoFocus, minHeight, maxRows, value, padding, lineHeight, hideBorder, children, as, ...textAreaProps } = props;
+ const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
- const heightPadding = (padding ?? 0) * 2;
- const minimumHeight = (minHeight ?? lineHeight) + heightPadding;
-
- var height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * lineHeight + heightPadding, minimumHeight);
+ const heightPadding = ((padding ?? DEFAULT_TEXT_AREA_PADDING) + (hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) * 2;
+ const height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * line + heightPadding, minHeight ?? 0);
+ console.log(value.split('\n').length, line, heightPadding, height);
const ref = useRef();
- /*function setHeight(h: number = lineHeight) {
- let newHeight = Math.min(
- Math.max(
- lineHeight,
- maxRows ? Math.min(h, maxRows * lineHeight) : h
- ),
- minHeight ?? Infinity
- );
-
- if (heightPadding) newHeight += heightPadding;
- if (height !== newHeight) {
- setHeightState(newHeight);
- }
- }*/
-
- {/*useLayoutEffect(() => {
- setHeight(ghost.current.clientHeight);
- }, [ghost, value]);*/}
-
useEffect(() => {
autoFocus && ref.current.focus();
}, [value]);
@@ -89,17 +52,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
- return <>
-
- {/*
- { props.value.split('\n')
- .map(x => `\u0020${x}`)
- .join('\n') }
-
*/}
- >;
+ return ;
}
diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx
index 5f270b2e..a58dff14 100644
--- a/src/lib/i18n.tsx
+++ b/src/lib/i18n.tsx
@@ -1,4 +1,4 @@
-import { IntlContext } from "preact-i18n";
+import { IntlContext, translate } from "preact-i18n";
import { useContext } from "preact/hooks";
import { Children } from "../types/Preact";
@@ -52,3 +52,8 @@ export function TextReact({ id, fields }: Props) {
return <>{ recursiveReplaceFields(entry as string, fields) }>;
}
+
+export function useTranslation() {
+ const { intl } = useContext(IntlContext) as unknown as IntlType;
+ return (id: string, fields?: Object, plural?: number, fallback?: string) => translate(id, "", intl.dictionary, fields, plural, fallback);
+}
diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx
index 5e4ec842..8d32b3b3 100644
--- a/src/pages/channels/Channel.tsx
+++ b/src/pages/channels/Channel.tsx
@@ -1,10 +1,15 @@
import styled from "styled-components";
+import { useState } from "preact/hooks";
+import ChannelHeader from "./ChannelHeader";
import { useParams } from "react-router-dom";
-import Header from "../../components/ui/Header";
-import { useRenderState } from "../../lib/renderer/Singleton";
-import { useChannel, useForceUpdate, useUsers } from "../../context/revoltjs/hooks";
import { MessageArea } from "./messaging/MessageArea";
+// import { useRenderState } from "../../lib/renderer/Singleton";
+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 JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
+import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
const ChannelMain = styled.div`
flex-grow: 1;
@@ -21,26 +26,30 @@ const ChannelContent = styled.div`
flex-direction: column;
`;
-export default function Channel() {
- const { channel: id } = useParams<{ channel: string }>();
-
+export function Channel({ id }: { id: string }) {
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
- // const view = useRenderState(id);
+ const [ showMembers, setMembers ] = useState(true);
return (
<>
-
+ setMembers(!showMembers)} />
+
+
+ { !isTouchscreenDevice && showMembers && }
>
)
}
+
+export default function() {
+ const { channel } = useParams<{ channel: string }>();
+ return ;
+}
diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx
new file mode 100644
index 00000000..929ac9fb
--- /dev/null
+++ b/src/pages/channels/ChannelHeader.tsx
@@ -0,0 +1,136 @@
+import styled from "styled-components";
+import { Channel, User } from "revolt.js";
+import { useContext } from "preact/hooks";
+import { useHistory } from "react-router-dom";
+import Header from "../../components/ui/Header";
+import IconButton from "../../components/ui/IconButton";
+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 { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+import { useStatusColour } from "../../components/common/user/UserIcon";
+import { useIntermediate } from "../../context/intermediate/Intermediate";
+import { Save, AtSign, Users, Hash, UserPlus, Settings, Sidebar as SidebarIcon } from "@styled-icons/feather";
+
+interface Props {
+ channel: Channel,
+ toggleSidebar?: () => void
+}
+
+const Info = styled.div`
+ flex-grow: 1;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+
+ * {
+ display: inline-block;
+ }
+
+ .divider {
+ height: 14px;
+ 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;
+ }
+
+ .desc {
+ cursor: pointer;
+ font-size: 0.8em;
+ font-weight: 400;
+ color: var(--secondary-foreground);
+ }
+`;
+
+export default function ChannelHeader({ channel, toggleSidebar }: Props) {
+ const { openScreen } = useIntermediate();
+ const client = useContext(AppContext);
+ const history = useHistory();
+
+ const name = getChannelName(client, channel);
+ let icon, recipient;
+ switch (channel.channel_type) {
+ case "SavedMessages":
+ icon = ;
+ break;
+ case "DirectMessage":
+ icon = ;
+ const uid = client.channels.getRecipient(channel._id);
+ recipient = client.users.get(uid);
+ break;
+ case "Group":
+ icon = ;
+ break;
+ case "TextChannel":
+ icon = ;
+ break;
+ }
+
+ return (
+
+ { icon }
+
+ { name }
+ {channel.channel_type === "DirectMessage" && (
+ <>
+
+
+
+
+
+ >
+ )}
+ {(channel.channel_type === "Group" || channel.channel_type === "TextChannel") && channel.description && (
+ <>
+
+
+ openScreen({
+ id: "channel_info",
+ channel_id: channel._id
+ })
+ }>
+
+
+ >
+ )}
+
+ <>
+ { channel.channel_type === "Group" && (
+ <>
+
+ openScreen({
+ id: "user_picker",
+ omit: channel.recipients,
+ callback: async users => {
+ for (const user of users) {
+ await client.channels.addMember(channel._id, user);
+ }
+ }
+ })}>
+
+
+ history.push(`/channel/${channel._id}/settings`)}>
+
+
+ >
+ ) }
+ { channel.channel_type === "Group" && !isTouchscreenDevice && (
+
+
+
+ ) }
+ >
+
+ )
+}
diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx
index b0412291..6e609662 100644
--- a/src/pages/channels/messaging/MessageArea.tsx
+++ b/src/pages/channels/messaging/MessageArea.tsx
@@ -23,6 +23,7 @@ const Area = styled.div`
> div {
display: flex;
min-height: 100%;
+ padding-bottom: 20px;
flex-direction: column;
justify-content: flex-end;
}
diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx
index 9e690e79..b1b54936 100644
--- a/src/pages/channels/messaging/MessageRenderer.tsx
+++ b/src/pages/channels/messaging/MessageRenderer.tsx
@@ -160,7 +160,7 @@ function MessageRenderer({ id, state, queue }: Props) {
);
}
- render.push(end
);
+ // render.push(end
);
} else {
render.push(
diff --git a/src/pages/settings/channel/Overview.tsx b/src/pages/settings/channel/Overview.tsx
index a73a3fde..30c36b9f 100644
--- a/src/pages/settings/channel/Overview.tsx
+++ b/src/pages/settings/channel/Overview.tsx
@@ -3,7 +3,7 @@ import styles from "./Panes.module.scss";
import Button from "../../../components/ui/Button";
import { Channels } from "revolt.js/dist/api/objects";
import InputBox from "../../../components/ui/InputBox";
-import TextArea from "../../../components/ui/TextArea";
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
@@ -70,9 +70,9 @@ export function Overview({ channel }: Props) {
:
}
-