From 1768264272e8c68953bc3a84b61f6e43d378da08 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 4 Jul 2021 15:53:06 +0100 Subject: [PATCH] Add collapsible section component. Can now collapse server categories. Client remembers collapse state, incl. advanced appearance settings. --- src/components/common/CollapsibleSection.tsx | 52 +++++++++++++++++++ .../navigation/left/HomeSidebar.tsx | 15 ++---- .../navigation/left/ServerSidebar.tsx | 21 +++++--- src/components/ui/Category.tsx | 2 +- src/components/ui/Details.tsx | 19 +++++-- src/components/ui/Overline.tsx | 6 ++- src/context/Locale.tsx | 6 --- src/pages/friends/Friend.module.scss | 24 --------- src/pages/friends/Friends.tsx | 31 +++++------ src/pages/settings/panes/Appearance.tsx | 9 ++-- src/redux/index.ts | 8 ++- src/redux/reducers/index.ts | 5 +- src/redux/reducers/section_toggle.ts | 37 +++++++++++++ 13 files changed, 157 insertions(+), 78 deletions(-) create mode 100644 src/components/common/CollapsibleSection.tsx create mode 100644 src/redux/reducers/section_toggle.ts diff --git a/src/components/common/CollapsibleSection.tsx b/src/components/common/CollapsibleSection.tsx new file mode 100644 index 00000000..1f31e21b --- /dev/null +++ b/src/components/common/CollapsibleSection.tsx @@ -0,0 +1,52 @@ +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 ( +
setState(e.currentTarget.open)} + {...detailsProps}> + + + { summary } + {/**/} + {/*
*/} + {/*
*/} + {/*
*/} +
+ { children } +
+ ) +} diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx index edde0aaa..16e586bb 100644 --- a/src/components/navigation/left/HomeSidebar.tsx +++ b/src/components/navigation/left/HomeSidebar.tsx @@ -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,16 +105,9 @@ function HomeSidebar(props: Props) { )} - - - ) as any - } - action={() => openScreen({ id: "special_input", type: "create_group" })} - /> - + } + action={() => openScreen({ id: "special_input", type: "create_group" })} /> {channelsArr.length === 0 && } {channelsArr.map(x => { let user; diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index 49e57959..3dd7aff9 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -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 ( - + ); - + let channels = []; for (let id of category.channels) { uncategorised.delete(id); - elements.push(addChannel(id)); + channels.push(addChannel(id)); } + + elements.push( + }> + { channels } + + ); } } - for (let id of uncategorised) { + for (let id of Array.from(uncategorised).reverse()) { elements.unshift(addChannel(id)); } diff --git a/src/components/ui/Category.tsx b/src/components/ui/Category.tsx index ccdd4e91..3f76d284 100644 --- a/src/components/ui/Category.tsx +++ b/src/components/ui/Category.tsx @@ -31,7 +31,7 @@ const CategoryBase = styled.div>` ` } `; -type Props = Omit, 'children' | 'as'> & { +type Props = Omit, 'children' | 'as' | 'action'> & { text: Children; action?: () => void; variant?: 'default' | 'uniform'; diff --git a/src/components/ui/Details.tsx b/src/components/ui/Details.tsx index 8bf47059..0a032022 100644 --- a/src/components/ui/Details.tsx +++ b/src/components/ui/Details.tsx @@ -1,16 +1,29 @@ -import styled from "styled-components"; +import styled, { css } from "styled-components"; -export default styled.details` +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; + ` } + outline: none; + display: flex; + cursor: pointer; list-style: none; + align-items: center; transition: .2s opacity; &::marker, &::-webkit-details-marker { display: none; } - svg { + > svg { flex-shrink: 0; transition: .2s ease transform; } diff --git a/src/components/ui/Overline.tsx b/src/components/ui/Overline.tsx index 5a9f9d04..17159077 100644 --- a/src/components/ui/Overline.tsx +++ b/src/components/ui/Overline.tsx @@ -5,6 +5,7 @@ import { Text } from 'preact-i18n'; type Props = Omit, 'children' | 'as'> & { error?: string; block?: boolean; + spaced?: boolean; children?: Children; type?: "default" | "subtle" | "error"; } @@ -12,7 +13,10 @@ type Props = Omit, 'children' | 'as'> & { const OverlineBase = styled.div>` display: inline; margin: 0.4em 0; - margin-top: 0.8em; + + ${ props => props.spaced && css` + margin-top: 0.8em; + ` } font-size: 14px; font-weight: 600; diff --git a/src/context/Locale.tsx b/src/context/Locale.tsx index 4afc9f50..873d57a6 100644 --- a/src/context/Locale.tsx +++ b/src/context/Locale.tsx @@ -150,12 +150,6 @@ function Locale({ children, locale }: Props) { return; } - if (lang.i18n === "hardcore") { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - setDefinition({} as any); - return; - } - import(`../../external/lang/${lang.i18n}.json`).then( async (lang_file) => { const defn = transformLanguage(lang_file.default); diff --git a/src/pages/friends/Friend.module.scss b/src/pages/friends/Friend.module.scss index d6338774..ee9c6f90 100644 --- a/src/pages/friends/Friend.module.scss +++ b/src/pages/friends/Friend.module.scss @@ -14,30 +14,6 @@ padding: 0 10px 10px 10px; user-select: none; overflow-y: scroll; - - summary { - position: sticky; - z-index: 10; - top: -1px; - } - - .overline { - display: flex; - align-items: center; - background: var(--primary-background); - padding: 5px 0; - cursor: pointer; - - .title { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - svg { - margin-inline-end: 4px; - } - } &[data-empty="true"] { img { diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index 284641e7..01d36085 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -15,6 +15,7 @@ import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid"; import { TextReact } from "../../lib/i18n"; import { Children } from "../../types/Preact"; import Details from "../../components/ui/Details"; +import CollapsibleSection from "../../components/common/CollapsibleSection"; export default function Friends() { const { openScreen } = useIntermediate(); @@ -30,17 +31,17 @@ export default function Friends() { ) ], [ 'app.special.friends.sent', users.filter(x => x.relationship === Users.Relationship.Outgoing - ) ], + ), 'outgoing' ], [ 'app.status.online', friends.filter(x => x.online && x.status?.presence !== Users.Presence.Invisible - ) ], + ), 'online' ], [ 'app.status.offline', friends.filter(x => !x.online || x.status?.presence === Users.Presence.Invisible - ) ], - [ 'app.special.friends.blocked', friends.filter(x => + ), 'offline' ], + [ 'app.special.friends.blocked', users.filter(x => x.relationship === Users.Relationship.Blocked - ) ] - ] as [ string, User[] ][]; + ), 'blocked' ] + ] as [ string, User[], string ][]; const incoming = lists[0][1]; const userlist: Children[] = incoming.map(x => { x.username }); @@ -108,22 +109,18 @@ export default function Friends() { } { - lists.map(([i18n, list], index) => { + lists.map(([i18n, list, section_id], index) => { if (index === 0) return; if (list.length === 0) return; return ( -
- - - -
- — { list.length } -
-
-
+ — { list.length }}> { list.map(x => ) } -
+ ) }) } diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx index b0ed1dae..6ddd2dc5 100644 --- a/src/pages/settings/panes/Appearance.tsx +++ b/src/pages/settings/panes/Appearance.tsx @@ -22,6 +22,7 @@ import mutantSVG from '../assets/mutant_emoji.svg'; import notoSVG from '../assets/noto_emoji.svg'; import openmojiSVG from '../assets/openmoji_emoji.svg'; import twemojiSVG from '../assets/twemoji_emoji.svg'; +import CollapsibleSection from "../../../components/common/CollapsibleSection"; interface Props { settings: Settings; @@ -171,11 +172,7 @@ export function Component(props: Props & WithDispatcher) { -
- - -
-
+ }>

@@ -272,7 +269,7 @@ export function Component(props: Props & WithDispatcher) { code value={css} onChange={ev => setCSS(ev.currentTarget.value)} /> -
+ ); } diff --git a/src/redux/index.ts b/src/redux/index.ts index dc90bec0..4bee0dcd 100644 --- a/src/redux/index.ts +++ b/src/redux/index.ts @@ -14,6 +14,7 @@ import { QueuedMessage } from "./reducers/queue"; import { ExperimentOptions } from "./reducers/experiments"; import { LastOpened } from "./reducers/last_opened"; import { Notifications } from "./reducers/notifications"; +import { SectionToggle } from "./reducers/section_toggle"; export type State = { config: Core.RevoltNodeConfiguration, @@ -28,6 +29,7 @@ export type State = { experiments: ExperimentOptions; lastOpened: LastOpened; notifications: Notifications; + sectionToggle: SectionToggle; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -56,7 +58,8 @@ store.subscribe(() => { sync, experiments, lastOpened, - notifications + notifications, + sectionToggle } = store.getState() as State; localForage.setItem("state", { @@ -70,6 +73,7 @@ store.subscribe(() => { sync, experiments, lastOpened, - notifications + notifications, + sectionToggle }); }); diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index fe47ccbd..baf217dc 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -13,6 +13,7 @@ import { sync, SyncAction } from "./sync"; import { experiments, ExperimentsAction } from "./experiments"; import { lastOpened, LastOpenedAction } from "./last_opened"; import { notifications, NotificationsAction } from "./notifications"; +import { sectionToggle, SectionToggleAction } from "./section_toggle"; export default combineReducers({ config, @@ -26,7 +27,8 @@ export default combineReducers({ sync, experiments, lastOpened, - notifications + notifications, + sectionToggle }); export type Action = @@ -42,6 +44,7 @@ export type Action = | ExperimentsAction | LastOpenedAction | NotificationsAction + | SectionToggleAction | { type: "__INIT"; state: State }; export type WithDispatcher = { dispatcher: (action: Action) => void }; diff --git a/src/redux/reducers/section_toggle.ts b/src/redux/reducers/section_toggle.ts new file mode 100644 index 00000000..26b23dca --- /dev/null +++ b/src/redux/reducers/section_toggle.ts @@ -0,0 +1,37 @@ +export interface SectionToggle { + [key: string]: boolean +} + +export type SectionToggleAction = + | { type: undefined } + | { + type: "SECTION_TOGGLE_SET"; + id: string; + state: boolean; + } + | { + type: "SECTION_TOGGLE_UNSET"; + id: string; + } + | { + type: "RESET"; + }; + +export function sectionToggle(state = {} as SectionToggle, action: SectionToggleAction): SectionToggle { + switch (action.type) { + case "SECTION_TOGGLE_SET": { + return { + ...state, + [action.id]: action.state + } + } + case "SECTION_TOGGLE_UNSET": { + const { [action.id]: _, ...newState } = state; + return newState; + } + case "RESET": + return {}; + default: + return state; + } +}