diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx index bebc0a8f8b23c57fa6d829af27c29c2358122207..acb5387ea9216d2af1346c8a955ab90fe2641662 100644 --- a/src/components/common/IconBase.tsx +++ b/src/components/common/IconBase.tsx @@ -14,6 +14,8 @@ interface IconModifiers { } export default styled.svg<IconModifiers>` + flex-shrink: 0; + img { width: 100%; height: 100%; @@ -26,6 +28,7 @@ export default styled.svg<IconModifiers>` `; export const ImageIconBase = styled.img<IconModifiers>` + 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 ac05418ee2dce6112568b7edf6ed0c5ee893b303..c0e657e0bbc1cd8601eb030254ee7260153c6716 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 <Redirect to="/" />; 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) { <GenericSidebarList> {!isTouchscreenDevice && ( <> - <Link to="/"> + <ConditionalLink active={pathname === "/"} to="/"> <ButtonItem active={pathname === "/"}> <Home size={20} /> <span><Text id="app.navigation.tabs.home" /></span> </ButtonItem> - </Link> - <Link to="/friends"> + </ConditionalLink> + <ConditionalLink active={pathname === "/friends"} to="/friends"> <ButtonItem active={pathname === "/friends"} alert={ @@ -75,15 +86,15 @@ function HomeSidebar(props: Props) { <Users size={20} /> <span><Text id="app.navigation.tabs.friends" /></span> </ButtonItem> - </Link> + </ConditionalLink> </> )} - <Link to="/open/saved"> + <ConditionalLink active={obj?.channel_type === "SavedMessages"} to="/open/saved"> <ButtonItem active={obj?.channel_type === "SavedMessages"}> <Save size={20} /> <span><Text id="app.navigation.tabs.saved" /></span> </ButtonItem> - </Link> + </ConditionalLink> {import.meta.env.DEV && ( <Link to="/dev"> <ButtonItem active={pathname === "/dev"}> @@ -115,7 +126,7 @@ function HomeSidebar(props: Props) { } return ( - <Link to={`/channel/${x._id}`}> + <ConditionalLink active={x._id === channel} to={`/channel/${x._id}`}> <ChannelButton user={user} channel={x} @@ -123,7 +134,7 @@ function HomeSidebar(props: Props) { alertCount={x.alertCount} active={x._id === channel} /> - </Link> + </ConditionalLink> ); })} <PaintCounter /> diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx index c7e2fc91836bb47bee83228de961d6907acfe7f3..4f25f7ddd37c6918440b2a3603f224d08a35be4a 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 ( <ServersBase> <ServerList> - <Link to={`/`}> - <ServerEntry invert - active={typeof server === 'undefined' && !path.startsWith('/invite')}> + <ConditionalLink active={homeActive} to={lastOpened.home ? `/channel/${lastOpened.home}` : '/'}> + <ServerEntry invert active={homeActive}> <Icon size={36} unread={homeUnread}> <img src={logoSVG} /> </Icon> </ServerEntry> - </Link> + </ConditionalLink> <LineDivider /> { - servers.map(entry => - <Link to={`/server/${entry!._id}`}> - <ServerEntry - active={entry!._id === server?._id} - onContextMenu={attachContextMenu('Menu', { server: entry!._id })}> - <Icon size={36} unread={entry.unread}> - <ServerIcon size={32} target={entry} /> - </Icon> - </ServerEntry> - </Link> - ) + servers.map(entry => { + const active = entry!._id === server?._id; + const id = lastOpened[entry!._id]; + + return ( + <ConditionalLink active={active} to={`/server/${entry!._id}` + (id ? `/channel/${id}` : '')}> + <ServerEntry + active={active} + onContextMenu={attachContextMenu('Menu', { server: entry!._id })}> + <Icon size={36} unread={entry.unread}> + <ServerIcon size={32} target={entry} /> + </Icon> + </ServerEntry> + </ConditionalLink> + ) + }) } <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_server' })}> <PlusCircle size={36} /> @@ -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 03f175c351ff9cf2dacc173a9505433e0607d512..d987af1eef083561a8dc40347e2636a6ab098d47 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 <Redirect to={`/server/${server_id}`} />; 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 ( <ServerBase> <ServerHeader server={server} ctx={ctx} /> <ConnectionStatus /> <ServerList onContextMenu={attachContextMenu('Menu', { server_list: server._id })}> {channels.map(entry => { + const active = channel?._id === entry._id; + return ( - <Link to={`/server/${server._id}/channel/${entry._id}`}> + <ConditionalLink active={active} to={`/server/${server._id}/channel/${entry._id}`}> <ChannelButton key={entry._id} channel={entry} - active={channel?._id === entry._id} + active={active} alert={entry.unread} compact /> - </Link> + </ConditionalLink> ); })} </ServerList> diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx index 92737e41f5187eed1dec8e4a6911c7b5f1838ca3..58e05deb220e21309d9690083867e5c436d2ef63 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 0000000000000000000000000000000000000000..b739219dcd380a3e7a47e1862841bcef5b7e6d9e --- /dev/null +++ b/src/lib/ConditionalLink.tsx @@ -0,0 +1,15 @@ +import { Link, LinkProps } from "react-router-dom"; + +type Props = LinkProps & JSX.HTMLAttributes<HTMLAnchorElement> & { + active: boolean +}; + +export default function ConditionalLink(props: Props) { + const { active, ...linkProps } = props; + + if (active) { + return <a>{ props.children }</a>; + } else { + return <Link {...linkProps} />; + } +} diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index 6916361bc4d1868b172211ca727c7c7213e7b4b0..368f603fc4c949c764973d7ce0976f1ffe0b18e2 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 </> ) } <VoiceActions channel={channel} /> - { channel.channel_type === "Group" && !isTouchscreenDevice && ( + { (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && !isTouchscreenDevice && ( <IconButton onClick={toggleSidebar}> <SidebarIcon size={22} /> </IconButton> diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index 11cc849bc80747f54d7b94fa7e4f4ae72776b0ac..9753abcdc6821a0d1d4241a5ebcdf030b8aa0634 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 3d93a7531a95f95bc6deb802c7906cdb064555b5..b46a10ab2c9899c830474dfd59a818d7613c36fa 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 ( <EditorBase> <TextAreaAutoSize diff --git a/src/redux/index.ts b/src/redux/index.ts index 6099d9e77480d71f2cad869d4e33f9055b43f250..33e6bbdca61960d7406ecfa865594b954db4cdb3 100644 --- a/src/redux/index.ts +++ b/src/redux/index.ts @@ -12,6 +12,7 @@ import { SyncOptions } from "./reducers/sync"; import { Settings } from "./reducers/settings"; import { QueuedMessage } from "./reducers/queue"; import { ExperimentOptions } from "./reducers/experiments"; +import { LastOpened } from "./reducers/last_opened"; export type State = { config: Core.RevoltNodeConfiguration, @@ -24,6 +25,7 @@ export type State = { drafts: Drafts; sync: SyncOptions; experiments: ExperimentOptions; + lastOpened: LastOpened; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -51,6 +53,7 @@ store.subscribe(() => { 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 a5e81c65267ec5a0b36ce48b08d673f1ae25fb46..6c84f87fa76e405ad008e322275bc2e2bbecb620 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 0000000000000000000000000000000000000000..0b29a156acba4cefe70d7aae7f653464184db98d --- /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; + } +}