From 363789c825b443e3c43fb08921cc803fdbb4cfa1 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 24 Jun 2021 16:22:45 +0100 Subject: [PATCH] Fix: Icons collapsing in flex. Feature: Remember what channel was opened last. Channels: ESC to focus message box / cancel editing. --- src/components/common/IconBase.tsx | 3 ++ .../navigation/left/HomeSidebar.tsx | 29 ++++++++---- .../navigation/left/ServerListSidebar.tsx | 45 +++++++++++-------- .../navigation/left/ServerSidebar.tsx | 21 +++++++-- src/context/Settings.tsx | 2 - src/lib/ConditionalLink.tsx | 15 +++++++ src/pages/channels/actions/HeaderActions.tsx | 2 +- src/pages/channels/messaging/MessageArea.tsx | 2 + .../channels/messaging/MessageEditor.tsx | 16 ++++++- src/redux/index.ts | 4 ++ src/redux/reducers/index.ts | 3 ++ src/redux/reducers/last_opened.ts | 29 ++++++++++++ 12 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 src/lib/ConditionalLink.tsx create mode 100644 src/redux/reducers/last_opened.ts diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx index bebc0a8f..acb5387e 100644 --- a/src/components/common/IconBase.tsx +++ b/src/components/common/IconBase.tsx @@ -14,6 +14,8 @@ interface IconModifiers { } export default styled.svg` + flex-shrink: 0; + img { width: 100%; height: 100%; @@ -26,6 +28,7 @@ export default styled.svg` `; export const ImageIconBase = styled.img` + flex-shrink: 0; object-fit: cover; ${ props => !props.square && css` diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx index ac05418e..c0e657e0 100644 --- a/src/components/navigation/left/HomeSidebar.tsx +++ b/src/components/navigation/left/HomeSidebar.tsx @@ -1,5 +1,5 @@ import { Localizer, Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; +import { useContext, useEffect } from "preact/hooks"; import { Home, Users, Tool, Save } from "@styled-icons/feather"; import Category from '../../ui/Category'; @@ -10,6 +10,7 @@ import { connectState } from "../../../redux/connector"; import ConnectionStatus from '../items/ConnectionStatus'; import { WithDispatcher } from "../../../redux/reducers"; import { Unreads } from "../../../redux/reducers/unreads"; +import ConditionalLink from "../../../lib/ConditionalLink"; import { mapChannelWithUnread, useUnreads } from "./common"; import { Users as UsersNS } from 'revolt.js/dist/api/objects'; import ButtonItem, { ChannelButton } from '../items/ButtonItem'; @@ -37,6 +38,16 @@ function HomeSidebar(props: Props) { if (channel && !obj) return ; if (obj) useUnreads({ ...props, channel: obj }); + useEffect(() => { + if (!channel) return; + + props.dispatcher({ + type: 'LAST_OPENED_SET', + parent: 'home', + child: channel + }); + }, [ channel ]); + const channelsArr = channels .filter(x => x.channel_type !== 'SavedMessages') .map(x => mapChannelWithUnread(x, props.unreads)); @@ -55,13 +66,13 @@ function HomeSidebar(props: Props) { {!isTouchscreenDevice && ( <> - + - - + + - + )} - + - + {import.meta.env.DEV && ( @@ -115,7 +126,7 @@ function HomeSidebar(props: Props) { } return ( - + - + ); })} diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx index c7e2fc91..4f25f7dd 100644 --- a/src/components/navigation/left/ServerListSidebar.tsx +++ b/src/components/navigation/left/ServerListSidebar.tsx @@ -8,9 +8,11 @@ import { PlusCircle } from "@styled-icons/feather"; import PaintCounter from "../../../lib/PaintCounter"; import { attachContextMenu } from 'preact-context-menu'; import { connectState } from "../../../redux/connector"; +import { useLocation, useParams } from "react-router-dom"; import { Unreads } from "../../../redux/reducers/unreads"; +import ConditionalLink from "../../../lib/ConditionalLink"; import { Channel, Servers } from "revolt.js/dist/api/objects"; -import { Link, useLocation, useParams } from "react-router-dom"; +import { LastOpened } from "../../../redux/reducers/last_opened"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useChannels, useForceUpdate, useServers } from "../../../context/revoltjs/hooks"; @@ -104,9 +106,10 @@ const ServerEntry = styled.div<{ active: boolean, invert?: boolean }>` interface Props { unreads: Unreads; + lastOpened: LastOpened; } -export function ServerListSidebar({ unreads }: Props) { +export function ServerListSidebar({ unreads, lastOpened }: Props) { const ctx = useForceUpdate(); const activeServers = useServers(undefined, ctx) as Servers.Server[]; const channels = (useChannels(undefined, ctx) as Channel[]) @@ -148,31 +151,36 @@ export function ServerListSidebar({ unreads }: Props) { } if (alertCount > 0) homeUnread = 'mention'; + const homeActive = typeof server === 'undefined' && !path.startsWith('/invite'); return ( - - + + - + { - servers.map(entry => - - - - - - - - ) + servers.map(entry => { + const active = entry!._id === server?._id; + const id = lastOpened[entry!._id]; + + return ( + + + + + + + + ) + }) } openScreen({ id: 'special_input', type: 'create_server' })}> @@ -187,7 +195,8 @@ export default connectState( ServerListSidebar, state => { return { - unreads: state.unreads + unreads: state.unreads, + lastOpened: state.lastOpened }; } ); diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index 03f175c3..d987af1e 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -15,6 +15,8 @@ import PaintCounter from "../../../lib/PaintCounter"; import styled from "styled-components"; import { attachContextMenu } from 'preact-context-menu'; import ServerHeader from "../../common/ServerHeader"; +import { useEffect } from "preact/hooks"; +import ConditionalLink from "../../../lib/ConditionalLink"; interface Props { unreads: Unreads; @@ -51,24 +53,37 @@ function ServerSidebar(props: Props & WithDispatcher) { .map(x => mapChannelWithUnread(x, props.unreads)); const channel = channels.find(x => x?._id === channel_id); + if (channel_id && !channel) return ; if (channel) useUnreads({ ...props, channel }, ctx); + useEffect(() => { + if (!channel_id) return; + + props.dispatcher({ + type: 'LAST_OPENED_SET', + parent: server_id!, + child: channel_id! + }); + }, [ channel_id ]); + return ( {channels.map(entry => { + const active = channel?._id === entry._id; + return ( - + - + ); })} diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx index 92737e41..58e05deb 100644 --- a/src/context/Settings.tsx +++ b/src/context/Settings.tsx @@ -24,11 +24,9 @@ interface Props { } function Settings({ settings, children }: Props) { - console.info(settings.notification); const play = useMemo(() => { const enabled: SoundOptions = defaultsDeep(settings.notification ?? {}, DEFAULT_SOUNDS); return (sound: Sounds) => { - console.info('check if we can play sound', enabled[sound]); if (enabled[sound]) { playSound(sound); } diff --git a/src/lib/ConditionalLink.tsx b/src/lib/ConditionalLink.tsx new file mode 100644 index 00000000..b739219d --- /dev/null +++ b/src/lib/ConditionalLink.tsx @@ -0,0 +1,15 @@ +import { Link, LinkProps } from "react-router-dom"; + +type Props = LinkProps & JSX.HTMLAttributes & { + active: boolean +}; + +export default function ConditionalLink(props: Props) { + const { active, ...linkProps } = props; + + if (active) { + return { props.children }; + } else { + return ; + } +} diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index 6916361b..368f603f 100644 --- a/src/pages/channels/actions/HeaderActions.tsx +++ b/src/pages/channels/actions/HeaderActions.tsx @@ -35,7 +35,7 @@ export default function HeaderActions({ channel, toggleSidebar }: ChannelHeaderP ) } - { channel.channel_type === "Group" && !isTouchscreenDevice && ( + { (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && !isTouchscreenDevice && ( diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index 11cc849b..9753abcd 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -12,6 +12,7 @@ import { IntermediateContext } from "../../../context/intermediate/Intermediate" import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient"; import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import { defer } from "../../../lib/defer"; +import { internalEmit } from "../../../lib/eventEmitter"; const Area = styled.div` height: 100%; @@ -246,6 +247,7 @@ export function MessageArea({ id }: Props) { function keyUp(e: KeyboardEvent) { if (e.key === "Escape" && !focusTaken) { SingletonMessageRenderer.jumpToBottom(id, true); + internalEmit("TextArea", "focus", "message"); } } diff --git a/src/pages/channels/messaging/MessageEditor.tsx b/src/pages/channels/messaging/MessageEditor.tsx index 3d93a753..b46a10ab 100644 --- a/src/pages/channels/messaging/MessageEditor.tsx +++ b/src/pages/channels/messaging/MessageEditor.tsx @@ -1,9 +1,10 @@ import styled from "styled-components"; -import { useContext, useState } from "preact/hooks"; +import { useContext, useEffect, useState } from "preact/hooks"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import { MessageObject } from "../../../context/revoltjs/util"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; +import { IntermediateContext } from "../../../context/intermediate/Intermediate"; const EditorBase = styled.div` display: flex; @@ -38,6 +39,7 @@ interface Props { export default function MessageEditor({ message, finish }: Props) { const [ content, setContent ] = useState(message.content as string ?? ''); + const { focusTaken } = useContext(IntermediateContext); const client = useContext(AppContext); async function save() { @@ -55,6 +57,18 @@ export default function MessageEditor({ message, finish }: Props) { } } + // ? 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]); + return ( { drafts, sync, experiments, + lastOpened } = store.getState() as State; localForage.setItem("state", { @@ -63,5 +66,6 @@ store.subscribe(() => { drafts, sync, experiments, + lastOpened }); }); diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index a5e81c65..6c84f87f 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -11,6 +11,7 @@ import { typing, TypingAction } from "./typing"; import { drafts, DraftAction } from "./drafts"; import { sync, SyncAction } from "./sync"; import { experiments, ExperimentsAction } from "./experiments"; +import { lastOpened, LastOpenedAction } from "./last_opened"; export default combineReducers({ config, @@ -23,6 +24,7 @@ export default combineReducers({ drafts, sync, experiments, + lastOpened }); export type Action = @@ -36,6 +38,7 @@ export type Action = | DraftAction | SyncAction | ExperimentsAction + | LastOpenedAction | { type: "__INIT"; state: State }; export type WithDispatcher = { dispatcher: (action: Action) => void }; diff --git a/src/redux/reducers/last_opened.ts b/src/redux/reducers/last_opened.ts new file mode 100644 index 00000000..0b29a156 --- /dev/null +++ b/src/redux/reducers/last_opened.ts @@ -0,0 +1,29 @@ +export interface LastOpened { + [key: string]: string +} + +export type LastOpenedAction = + | { type: undefined } + | { + type: "LAST_OPENED_SET"; + parent: string; + child: string; + } + | { + type: "RESET"; + }; + +export function lastOpened(state = {} as LastOpened, action: LastOpenedAction): LastOpened { + switch (action.type) { + case "LAST_OPENED_SET": { + return { + ...state, + [action.parent]: action.child + } + } + case "RESET": + return {}; + default: + return state; + } +}