mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Port settings.
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import styles from "./Item.module.scss";
|
||||
import Tooltip from '../../common/Tooltip';
|
||||
import IconButton from '../../ui/IconButton';
|
||||
import UserIcon from '../../common/UserIcon';
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { X, Zap } from "@styled-icons/feather";
|
||||
import UserStatus from '../../common/UserStatus';
|
||||
import { Children } from "../../../types/Preact";
|
||||
import ChannelIcon from '../../common/ChannelIcon';
|
||||
import { attachContextMenu } from 'preact-context-menu';
|
||||
import { Channels, Users } from "revolt.js/dist/api/objects";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { useIntermediate } from '../../../context/intermediate/Intermediate';
|
||||
|
||||
interface CommonProps {
|
||||
active?: boolean
|
||||
@@ -22,7 +26,7 @@ type UserProps = CommonProps & {
|
||||
}
|
||||
|
||||
export function UserButton({ active, alert, alertCount, user, context, channel }: UserProps) {
|
||||
// const { openScreen } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -30,13 +34,12 @@ export function UserButton({ active, alert, alertCount, user, context, channel }
|
||||
data-active={active}
|
||||
data-alert={typeof alert === 'string'}
|
||||
data-online={typeof channel !== 'undefined' || (user.online && user.status?.presence !== Users.Presence.Invisible)}
|
||||
/*onContextMenu={attachContextMenu('Menu', {
|
||||
onContextMenu={attachContextMenu('Menu', {
|
||||
user: user._id,
|
||||
channel: channel?._id,
|
||||
unread: alert,
|
||||
contextualChannel: context?._id
|
||||
})}*/
|
||||
>
|
||||
})}>
|
||||
<div className={styles.avatar}>
|
||||
<UserIcon target={user} size={32} status />
|
||||
</div>
|
||||
@@ -56,24 +59,22 @@ export function UserButton({ active, alert, alertCount, user, context, channel }
|
||||
{ context?.channel_type === "Group" &&
|
||||
context.owner === user._id && (
|
||||
<Localizer>
|
||||
{/*<Tooltip
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.main.groups.owner" />
|
||||
}
|
||||
>*/}
|
||||
>
|
||||
<Zap size={20} />
|
||||
{/*</Tooltip>*/}
|
||||
</Tooltip>
|
||||
</Localizer>
|
||||
)}
|
||||
{alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
|
||||
{ !isTouchscreenDevice && channel &&
|
||||
/*<IconButton
|
||||
<IconButton
|
||||
className={styles.icon}
|
||||
style="default"
|
||||
onClick={() => openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}
|
||||
>*/
|
||||
onClick={() => openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}>
|
||||
<X size={24} />
|
||||
/*</IconButton>*/
|
||||
</IconButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,15 +94,14 @@ export function ChannelButton({ active, alert, alertCount, channel, user, compac
|
||||
return <UserButton {...{ active, alert, channel, user }} />
|
||||
}
|
||||
|
||||
//const { openScreen } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-active={active}
|
||||
data-alert={typeof alert === 'string'}
|
||||
className={classNames(styles.item, { [styles.compact]: compact })}
|
||||
//onContextMenu={attachContextMenu('Menu', { channel: channel._id })}>
|
||||
>
|
||||
onContextMenu={attachContextMenu('Menu', { channel: channel._id })}>
|
||||
<div className={styles.avatar}>
|
||||
<ChannelIcon target={channel} size={compact ? 24 : 32} />
|
||||
</div>
|
||||
@@ -124,13 +124,11 @@ export function ChannelButton({ active, alert, alertCount, channel, user, compac
|
||||
<div className={styles.button}>
|
||||
{alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
|
||||
{!isTouchscreenDevice && channel.channel_type === "Group" && (
|
||||
/*<IconButton
|
||||
<IconButton
|
||||
className={styles.icon}
|
||||
style="default"
|
||||
onClick={() => openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}
|
||||
>*/
|
||||
onClick={() => openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}>
|
||||
<X size={24} />
|
||||
/*</IconButton>*/
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { useContext, useLayoutEffect } from "preact/hooks";
|
||||
import { Home, Users, Tool, Settings, Save } from "@styled-icons/feather";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { Home, Users, Tool, Save } from "@styled-icons/feather";
|
||||
|
||||
import { Link, Redirect, useHistory, useLocation, useParams } from "react-router-dom";
|
||||
import { Link, Redirect, useLocation, useParams } from "react-router-dom";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
||||
import { User } from "revolt.js";
|
||||
import { Users as UsersNS } from 'revolt.js/dist/api/objects';
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
import { Channels } from "revolt.js/dist/api/objects";
|
||||
import UserIcon from '../../common/UserIcon';
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import ConnectionStatus from '../items/ConnectionStatus';
|
||||
import UserStatus from '../../common/UserStatus';
|
||||
import ButtonItem, { ChannelButton } from '../items/ButtonItem';
|
||||
import styled from "styled-components";
|
||||
import Header from '../../ui/Header';
|
||||
import UserHeader from "../../common/UserHeader";
|
||||
import Category from '../../ui/Category';
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import { PlusCircle } from "@styled-icons/feather";
|
||||
import { Channel, Servers } from "revolt.js/dist/api/objects";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import { useChannels, useForceUpdate, useServers } from "../../../context/revoltjs/hooks";
|
||||
@@ -11,6 +9,7 @@ import { Children } from "../../../types/Preact";
|
||||
import LineDivider from "../../ui/LineDivider";
|
||||
import ServerIcon from "../../common/ServerIcon";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { attachContextMenu } from 'preact-context-menu';
|
||||
|
||||
function Icon({ children, unread, size }: { children: Children, unread?: 'mention' | 'unread', size: number }) {
|
||||
return (
|
||||
@@ -157,8 +156,7 @@ export function ServerListSidebar({ unreads }: Props) {
|
||||
<Link to={`/server/${entry!._id}`}>
|
||||
<ServerEntry
|
||||
active={entry!._id === server?._id}
|
||||
//onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
|
||||
>
|
||||
onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
|
||||
<Icon size={36} unread={entry.unread}>
|
||||
<ServerIcon size={32} target={entry} />
|
||||
</Icon>
|
||||
@@ -169,6 +167,7 @@ export function ServerListSidebar({ unreads }: Props) {
|
||||
<PaintCounter small />
|
||||
</ServerList>
|
||||
</ServersBase>
|
||||
// ! FIXME: add overlay back
|
||||
/*<div className={styles.servers}>
|
||||
<div className={styles.list}>
|
||||
<Link to={`/`}>
|
||||
|
||||
@@ -12,11 +12,32 @@ import Header from '../../ui/Header';
|
||||
import ConnectionStatus from '../items/ConnectionStatus';
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import styled from "styled-components";
|
||||
import { attachContextMenu } from 'preact-context-menu';
|
||||
|
||||
interface Props {
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
||||
const ServerBase = styled.div`
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
background: var(--secondary-background);
|
||||
`;
|
||||
|
||||
const ServerList = styled.div`
|
||||
padding: 6px;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
function ServerSidebar(props: Props & WithDispatcher) {
|
||||
const { server: server_id, channel: channel_id } = useParams<{ server?: string, channel?: string }>();
|
||||
const ctx = useForceUpdate();
|
||||
@@ -33,7 +54,7 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
if (channel) useUnreads({ ...props, channel }, ctx);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ServerBase>
|
||||
<Header placement="secondary" background style={{ background: `url('${ctx.client.servers.getBannerURL(server._id, { width: 480 }, true)}')` }}>
|
||||
<div>
|
||||
{ server.name }
|
||||
@@ -45,9 +66,7 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
</div> }
|
||||
</Header>
|
||||
<ConnectionStatus />
|
||||
<div
|
||||
//onContextMenu={attachContextMenu('Menu', { server_list: server._id })}>
|
||||
>
|
||||
<ServerList onContextMenu={attachContextMenu('Menu', { server_list: server._id })}>
|
||||
{channels.map(entry => {
|
||||
return (
|
||||
<Link to={`/server/${server._id}/channel/${entry._id}`}>
|
||||
@@ -61,9 +80,9 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ServerList>
|
||||
<PaintCounter small />
|
||||
</div>
|
||||
</ServerBase>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@ const CheckboxBase = styled.label`
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary-background);
|
||||
|
||||
.check {
|
||||
background: var(--background);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CheckboxContent = styled.span`
|
||||
@@ -46,6 +54,7 @@ const Checkmark = styled.div<{ checked: boolean }>`
|
||||
display: grid;
|
||||
border-radius: 4px;
|
||||
place-items: center;
|
||||
transition: 0.2s ease all;
|
||||
background: var(--secondary-background);
|
||||
|
||||
svg {
|
||||
@@ -56,7 +65,7 @@ const Checkmark = styled.div<{ checked: boolean }>`
|
||||
${(props) =>
|
||||
props.checked &&
|
||||
css`
|
||||
background: var(--accent);
|
||||
background: var(--accent) !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -71,7 +80,7 @@ export interface CheckboxProps {
|
||||
|
||||
export default function Checkbox(props: CheckboxProps) {
|
||||
return (
|
||||
<CheckboxBase disabled={props.disabled}>
|
||||
<CheckboxBase disabled={props.disabled} className={props.className}>
|
||||
<CheckboxContent>
|
||||
<span>{props.children}</span>
|
||||
{props.description && (
|
||||
@@ -87,7 +96,7 @@ export default function Checkbox(props: CheckboxProps) {
|
||||
!props.disabled && props.onChange(!props.checked)
|
||||
}
|
||||
/>
|
||||
<Checkmark checked={props.checked}>
|
||||
<Checkmark checked={props.checked} className="check">
|
||||
<Check size={20} />
|
||||
</Checkmark>
|
||||
</CheckboxBase>
|
||||
|
||||
@@ -47,7 +47,7 @@ const ModalContainer = styled.div`
|
||||
animation-timing-function: cubic-bezier(.3,.3,.18,1.1);
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>`
|
||||
const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border' | 'padding']?: boolean }>`
|
||||
border-radius: 8px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -56,10 +56,13 @@ const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'borde
|
||||
}
|
||||
|
||||
${ props => !props.noBackground && css`
|
||||
padding: 1.5em;
|
||||
background: var(--secondary-header);
|
||||
` }
|
||||
|
||||
${ props => props.padding && css`
|
||||
padding: 1.5em;
|
||||
` }
|
||||
|
||||
${ props => props.attachment && css`
|
||||
border-radius: 8px 8px 0 0;
|
||||
` }
|
||||
@@ -110,7 +113,8 @@ export default function Modal(props: Props) {
|
||||
<ModalContent
|
||||
attachment={!!props.actions}
|
||||
noBackground={props.noBackground}
|
||||
border={props.border}>
|
||||
border={props.border}
|
||||
padding={!props.dontModal}>
|
||||
{props.title && <h3>{props.title}</h3>}
|
||||
{props.children}
|
||||
</ModalContent>
|
||||
|
||||
31
src/components/ui/TextArea.module.scss
Normal file
31
src/components/ui/TextArea.module.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.container {
|
||||
font-size: 0.875rem;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
|
||||
textarea::placeholder {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
|
||||
top: 0;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
146
src/components/ui/TextArea.tsx
Normal file
146
src/components/ui/TextArea.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
// ! FIXME: temporarily here until re-written
|
||||
// ! DO NOT IMRPOVE, JUST RE-WRITE
|
||||
|
||||
import classNames from "classnames";
|
||||
import { memo } from "preact/compat";
|
||||
import styles from "./TextArea.module.scss";
|
||||
import { useState, useEffect, useRef, useLayoutEffect } from "preact/hooks";
|
||||
|
||||
export interface TextAreaProps {
|
||||
id?: string;
|
||||
value: string;
|
||||
maxRows?: number;
|
||||
padding?: number;
|
||||
minHeight?: number;
|
||||
disabled?: boolean;
|
||||
maxLength?: number;
|
||||
className?: string;
|
||||
autoFocus?: boolean;
|
||||
forceFocus?: boolean;
|
||||
placeholder?: string;
|
||||
onKeyDown?: (ev: KeyboardEvent) => void;
|
||||
onKeyUp?: (ev: KeyboardEvent) => void;
|
||||
onChange: (
|
||||
value: string,
|
||||
ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>
|
||||
) => void;
|
||||
onFocus?: (current: HTMLTextAreaElement) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
const lineHeight = 20;
|
||||
|
||||
export const TextArea = memo((props: TextAreaProps) => {
|
||||
const padding = props.padding ? props.padding * 2 : 0;
|
||||
|
||||
const [height, setHeightState] = useState(
|
||||
props.minHeight ?? lineHeight + padding
|
||||
);
|
||||
const ghost = useRef<HTMLDivElement>();
|
||||
const ref = useRef<HTMLTextAreaElement>();
|
||||
|
||||
function setHeight(h: number = lineHeight) {
|
||||
let newHeight = Math.min(
|
||||
Math.max(
|
||||
lineHeight,
|
||||
props.maxRows ? Math.min(h, props.maxRows * lineHeight) : h
|
||||
),
|
||||
props.minHeight ?? Infinity
|
||||
);
|
||||
|
||||
if (props.padding) newHeight += padding;
|
||||
if (height !== newHeight) {
|
||||
setHeightState(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
|
||||
props.onChange(ev.currentTarget.value, ev);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHeight(ghost.current.clientHeight);
|
||||
}, [ghost, props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.autoFocus) ref.current.focus();
|
||||
}, [props.value]);
|
||||
|
||||
const inputSelected = () =>
|
||||
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (props.forceFocus) {
|
||||
ref.current.focus();
|
||||
}
|
||||
|
||||
if (props.autoFocus && !inputSelected()) {
|
||||
ref.current.focus();
|
||||
}
|
||||
|
||||
// ? if you are wondering what this is
|
||||
// ? it is a quick and dirty hack to fix
|
||||
// ? value not setting correctly
|
||||
// ? I have no clue what's going on
|
||||
ref.current.value = props.value;
|
||||
|
||||
if (!props.autoFocus) return;
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
|
||||
if (e.key.length !== 1) return;
|
||||
if (ref && !inputSelected()) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
function focus(textarea_id: string) {
|
||||
if (props.id === textarea_id) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// InternalEventEmitter.addListener("focus_textarea", focus);
|
||||
// return () =>
|
||||
// InternalEventEmitter.removeListener("focus_textarea", focus);
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, props.className)}>
|
||||
<textarea
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
style={{ height }}
|
||||
value={props.value}
|
||||
onChange={onChange}
|
||||
disabled={props.disabled}
|
||||
maxLength={props.maxLength}
|
||||
className={styles.textarea}
|
||||
onKeyDown={props.onKeyDown}
|
||||
placeholder={props.placeholder}
|
||||
onContextMenu={e => e.stopPropagation()}
|
||||
onKeyUp={ev => {
|
||||
setHeight(ghost.current.clientHeight);
|
||||
props.onKeyUp && props.onKeyUp(ev);
|
||||
}}
|
||||
ref={ref}
|
||||
onFocus={() => props.onFocus && props.onFocus(ref.current)}
|
||||
onBlur={props.onBlur}
|
||||
/>
|
||||
<div className={styles.hide}>
|
||||
<div className={styles.ghost} ref={ghost}>
|
||||
{props.value
|
||||
? props.value
|
||||
.split("\n")
|
||||
.map(x => `${x}`)
|
||||
.join("\n")
|
||||
: undefined ?? "\n"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user