mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
Merge branch 'master' into cleanup
This commit is contained in:
50
src/components/common/CollapsibleSection.tsx
Normal file
50
src/components/common/CollapsibleSection.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Details from "../ui/Details";
|
||||
import { State, store } from "../../redux";
|
||||
import { Action } from "../../redux/reducers";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
defaultValue: boolean;
|
||||
|
||||
sticky?: boolean;
|
||||
large?: boolean;
|
||||
|
||||
summary: Children;
|
||||
children: Children;
|
||||
}
|
||||
|
||||
export default function CollapsibleSection({ id, defaultValue, summary, children, ...detailsProps }: Props) {
|
||||
const state: State = store.getState();
|
||||
|
||||
function setState(state: boolean) {
|
||||
if (state === defaultValue) {
|
||||
store.dispatch({
|
||||
type: 'SECTION_TOGGLE_UNSET',
|
||||
id
|
||||
} as Action);
|
||||
} else {
|
||||
store.dispatch({
|
||||
type: 'SECTION_TOGGLE_SET',
|
||||
id,
|
||||
state
|
||||
} as Action);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Details
|
||||
open={state.sectionToggle[id] ?? defaultValue}
|
||||
onToggle={e => setState(e.currentTarget.open)}
|
||||
{...detailsProps}>
|
||||
<summary>
|
||||
<div class="padding">
|
||||
<ChevronDown size={20} />
|
||||
{ summary }
|
||||
</div>
|
||||
</summary>
|
||||
{ children }
|
||||
</Details>
|
||||
)
|
||||
}
|
||||
@@ -24,14 +24,16 @@ const PermissionTooltipBase = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--secondary-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Fira Mono';
|
||||
font-family: var(--monoscape-font);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
border-radius: 6px;
|
||||
margin: .125rem 0 .125rem;
|
||||
|
||||
height: auto;
|
||||
|
||||
max-height: 640px;
|
||||
max-width: min(480px, 100%);
|
||||
|
||||
object-fit: contain;
|
||||
|
||||
&[data-spoiler="true"] {
|
||||
filter: blur(30px);
|
||||
pointer-events: none;
|
||||
@@ -71,7 +78,7 @@
|
||||
}
|
||||
|
||||
pre code {
|
||||
font-family: "Fira Mono", sans-serif;
|
||||
font-family: var(--monoscape-font), sans-serif;
|
||||
}
|
||||
|
||||
&[data-loading="true"] {
|
||||
|
||||
@@ -15,60 +15,33 @@ interface Props {
|
||||
}
|
||||
|
||||
const MAX_ATTACHMENT_WIDTH = 480;
|
||||
const MAX_ATTACHMENT_HEIGHT = 640;
|
||||
|
||||
export default function Attachment({ attachment, hasContent }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const { filename, metadata } = attachment;
|
||||
const [ spoiler, setSpoiler ] = useState(filename.startsWith("SPOILER_"));
|
||||
const maxWidth = Math.min(useContext(MessageAreaWidthContext), MAX_ATTACHMENT_WIDTH);
|
||||
|
||||
const url = client.generateFileURL(attachment, { width: MAX_ATTACHMENT_WIDTH * 1.5 }, true);
|
||||
let width = 0,
|
||||
height = 0;
|
||||
|
||||
if (metadata.type === 'Image' || metadata.type === 'Video') {
|
||||
let limitingWidth = Math.min(
|
||||
maxWidth,
|
||||
metadata.width
|
||||
);
|
||||
|
||||
let limitingHeight = Math.min(
|
||||
MAX_ATTACHMENT_HEIGHT,
|
||||
metadata.height
|
||||
);
|
||||
|
||||
// Calculate smallest possible WxH.
|
||||
width = Math.min(
|
||||
limitingWidth,
|
||||
limitingHeight * (metadata.width / metadata.height)
|
||||
);
|
||||
|
||||
height = Math.min(
|
||||
limitingHeight,
|
||||
limitingWidth * (metadata.height / metadata.width)
|
||||
);
|
||||
}
|
||||
|
||||
switch (metadata.type) {
|
||||
case "Image": {
|
||||
return (
|
||||
<div
|
||||
style={{ width }}
|
||||
className={styles.container}
|
||||
onClick={() => spoiler && setSpoiler(false)}
|
||||
>
|
||||
{spoiler && (
|
||||
<div className={styles.overflow}>
|
||||
<div style={{ width, height }}>
|
||||
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
|
||||
</div>
|
||||
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={url}
|
||||
alt={filename}
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(styles.attachment, styles.image)}
|
||||
@@ -79,7 +52,6 @@ export default function Attachment({ attachment, hasContent }: Props) {
|
||||
ev.button === 1 &&
|
||||
window.open(url, "_blank")
|
||||
}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -102,13 +74,10 @@ export default function Attachment({ attachment, hasContent }: Props) {
|
||||
onClick={() => spoiler && setSpoiler(false)}>
|
||||
{spoiler && (
|
||||
<div className={styles.overflow}>
|
||||
<div style={{ width, height }}>
|
||||
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
|
||||
</div>
|
||||
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{ width }}
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(styles.attachment, styles.video)}
|
||||
@@ -117,7 +86,6 @@ export default function Attachment({ attachment, hasContent }: Props) {
|
||||
<video
|
||||
src={url}
|
||||
controls
|
||||
style={{ width, height }}
|
||||
onMouseDown={ev =>
|
||||
ev.button === 1 &&
|
||||
window.open(url, "_blank")
|
||||
|
||||
@@ -101,6 +101,7 @@ const PreviewBox = styled.div`
|
||||
|
||||
.icon {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "@fontsource/fira-mono/400.css";
|
||||
|
||||
.markdown {
|
||||
:global(.emoji) {
|
||||
height: 1.25em;
|
||||
@@ -89,7 +87,7 @@
|
||||
font-size: 90%;
|
||||
border-radius: 4px;
|
||||
background: var(--block);
|
||||
font-family: "Fira Mono", monospace;
|
||||
font-family: var(--monoscape-font), monospace;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@@ -136,7 +134,7 @@
|
||||
}
|
||||
|
||||
:global(.code) {
|
||||
font-family: "Fira Mono", monospace;
|
||||
font-family: var(--monoscape-font), monospace;
|
||||
|
||||
:global(.lang) {
|
||||
// height: 8px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { Home, UserDetail, Wrench, Notepad } from "@styled-icons/boxicons-solid";
|
||||
|
||||
@@ -105,13 +105,9 @@ function HomeSidebar(props: Props) {
|
||||
</ButtonItem>
|
||||
</Link>
|
||||
)}
|
||||
<Localizer>
|
||||
<Category
|
||||
text={<Text id="app.main.categories.conversations" />}
|
||||
/** @ts-ignore : ignored due to conflicting naming between the Category property name and the existing JSX attribute */
|
||||
action={() => openScreen({ id: "special_input", type: "create_group" })}
|
||||
/>
|
||||
</Localizer>
|
||||
<Category
|
||||
text={<Text id="app.main.categories.conversations" />}
|
||||
action={() => openScreen({ id: "special_input", type: "create_group" })} />
|
||||
{channelsArr.length === 0 && <img src={placeholderSVG} />}
|
||||
{channelsArr.map(x => {
|
||||
let user;
|
||||
|
||||
@@ -14,6 +14,7 @@ import ServerHeader from "../../common/ServerHeader";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import Category from "../../ui/Category";
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||
|
||||
interface Props {
|
||||
unreads: Unreads;
|
||||
@@ -69,6 +70,7 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
|
||||
let uncategorised = new Set(server.channels);
|
||||
let elements = [];
|
||||
|
||||
function addChannel(id: string) {
|
||||
const entry = channels.find(x => x._id === id);
|
||||
if (!entry) return;
|
||||
@@ -76,9 +78,8 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
const active = channel?._id === entry._id;
|
||||
|
||||
return (
|
||||
<ConditionalLink active={active} to={`/server/${server!._id}/channel/${entry._id}`}>
|
||||
<ConditionalLink key={entry._id} active={active} to={`/server/${server!._id}/channel/${entry._id}`}>
|
||||
<ChannelButton
|
||||
key={entry._id}
|
||||
channel={entry}
|
||||
active={active}
|
||||
alert={entry.unread}
|
||||
@@ -90,16 +91,24 @@ function ServerSidebar(props: Props & WithDispatcher) {
|
||||
|
||||
if (server.categories) {
|
||||
for (let category of server.categories) {
|
||||
elements.push(<Category text={category.title} />);
|
||||
|
||||
let channels = [];
|
||||
for (let id of category.channels) {
|
||||
uncategorised.delete(id);
|
||||
elements.push(addChannel(id));
|
||||
channels.push(addChannel(id));
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category text={category.title} />}>
|
||||
{ channels }
|
||||
</CollapsibleSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let id of uncategorised) {
|
||||
for (let id of Array.from(uncategorised).reverse()) {
|
||||
elements.unshift(addChannel(id));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { User } from "revolt.js";
|
||||
import Details from "../../../components/ui/Details";
|
||||
import Category from "../../ui/Category";
|
||||
import { useParams } from "react-router";
|
||||
import { UserButton } from "../items/ButtonItem";
|
||||
|
||||
@@ -10,7 +10,7 @@ export default styled.button<Props>`
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-family: inherit;
|
||||
|
||||
transition: 0.2s ease opacity;
|
||||
transition: 0.2s ease background-color;
|
||||
|
||||
@@ -31,7 +31,7 @@ const CategoryBase = styled.div<Pick<Props, 'variant'>>`
|
||||
` }
|
||||
`;
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as' | 'action'> & {
|
||||
text: Children;
|
||||
// TODO: rename from action to prevent type conflicts with the dom
|
||||
action?: () => void;
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Children } from "../../types/Preact";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
const CheckboxBase = styled.label`
|
||||
margin-top: 20px;
|
||||
gap: 4px;
|
||||
z-index: 1;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
@@ -16,25 +16,19 @@ const CheckboxBase = styled.label`
|
||||
|
||||
transition: 0.2s ease all;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary-background);
|
||||
|
||||
.check {
|
||||
background: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: unset;
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: unset;
|
||||
@@ -43,15 +37,15 @@ const CheckboxBase = styled.label`
|
||||
`;
|
||||
|
||||
const CheckboxContent = styled.span`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const CheckboxDescription = styled.span`
|
||||
font-size: 0.8em;
|
||||
font-size: .75rem;
|
||||
font-weight: 400;
|
||||
color: var(--secondary-foreground);
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef } from "preact/hooks";
|
||||
import { Check, Pencil } from "@styled-icons/boxicons-regular";
|
||||
import { Check } from "@styled-icons/boxicons-regular";
|
||||
import { Palette } from "@styled-icons/boxicons-solid";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
interface Props {
|
||||
@@ -98,7 +99,7 @@ export default function ColourSwatches({ value, onChange }: Props) {
|
||||
type="large"
|
||||
onClick={() => ref.current.click()}
|
||||
>
|
||||
<Pencil size={32} />
|
||||
<Palette size={32} />
|
||||
</Swatch>
|
||||
<input
|
||||
type="color"
|
||||
|
||||
@@ -2,15 +2,19 @@ import styled from "styled-components";
|
||||
|
||||
export default styled.select`
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
color: var(--secondary-foreground);
|
||||
background: var(--secondary-background);
|
||||
|
||||
font-size: .875rem;
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
transition: outline-color 0.2s ease-in-out;
|
||||
transition: box-shadow .3s;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline-color: var(--accent);
|
||||
box-shadow: 0 0 0 2pt var(--accent);
|
||||
}
|
||||
`;
|
||||
|
||||
68
src/components/ui/Details.tsx
Normal file
68
src/components/ui/Details.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
export default styled.details<{ sticky?: boolean, large?: boolean }>`
|
||||
summary {
|
||||
${ props => props.sticky && css`
|
||||
top: -1px;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
` }
|
||||
|
||||
${ props => props.large && css`
|
||||
/*padding: 5px 0;*/
|
||||
background: var(--primary-background);
|
||||
color: var(--secondary-foreground);
|
||||
|
||||
.padding { /*TOFIX: make this applicable only for the friends list menu, DO NOT REMOVE.*/
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
margin: 0.8em 0px 0.4em;
|
||||
cursor: pointer;
|
||||
}
|
||||
` }
|
||||
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
transition: .2s opacity;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::marker, &::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
margin-top: 1px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.padding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> svg {
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: 4px;
|
||||
transition: .2s ease transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not([open]) {
|
||||
summary {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
summary svg {
|
||||
transform: rotateZ(-90deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -46,6 +46,6 @@ export default styled.div<Props>`
|
||||
` }
|
||||
|
||||
${ props => props.borders && css`
|
||||
border-end-start-radius: 8px;
|
||||
border-start-start-radius: 8px;
|
||||
` }
|
||||
`;
|
||||
|
||||
@@ -9,6 +9,7 @@ export default styled.input<Props>`
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
|
||||
font-family: inherit;
|
||||
color: var(--foreground);
|
||||
background: var(--primary-background);
|
||||
transition: 0.2s ease background-color;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Text } from 'preact-i18n';
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
|
||||
error?: string;
|
||||
block?: boolean;
|
||||
spaced?: boolean;
|
||||
children?: Children;
|
||||
type?: "default" | "subtle" | "error";
|
||||
}
|
||||
@@ -12,7 +13,10 @@ type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
|
||||
const OverlineBase = styled.div<Omit<Props, "children" | "error">>`
|
||||
display: inline;
|
||||
margin: 0.4em 0;
|
||||
margin-top: 0.8em;
|
||||
|
||||
${ props => props.spaced && css`
|
||||
margin-top: 0.8em;
|
||||
` }
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -39,8 +39,10 @@ export default styled.textarea<TextAreaProps>`
|
||||
}
|
||||
|
||||
${ props => props.code ? css`
|
||||
font-family: 'Fira Mono', 'Courier New', Courier, monospace;
|
||||
font-family: var(--monoscape-font-font), monospace;
|
||||
` : css`
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-family: inherit;
|
||||
` }
|
||||
|
||||
font-variant-ligatures: var(--ligatures);
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,13 @@ interface Props {
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export const Separator = styled.div<Props>`
|
||||
height: 1px;
|
||||
width: calc(100% - 10px);
|
||||
background: var(--secondary-header);
|
||||
margin: 18px auto;
|
||||
`;
|
||||
|
||||
export const TipBase = styled.div<Props>`
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
@@ -46,9 +53,13 @@ export const TipBase = styled.div<Props>`
|
||||
export default function Tip(props: Props & { children: Children }) {
|
||||
const { children, ...tipProps } = props;
|
||||
return (
|
||||
<TipBase {...tipProps}>
|
||||
<InfoCircle size={20} />
|
||||
<span>{props.children}</span>
|
||||
</TipBase>
|
||||
<>
|
||||
<Separator />
|
||||
<TipBase {...tipProps}>
|
||||
<InfoCircle size={20} />
|
||||
<span>{props.children}</span>
|
||||
</TipBase>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user