From f9d01dab0a006e36f7b40f467163a6f253cd0cd7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 13:38:08 -0500 Subject: [PATCH 01/14] Make the ThemeToggle look decent, unhide it --- app/soapbox/features/ui/components/navbar.tsx | 4 +--- app/soapbox/features/ui/components/theme_toggle.tsx | 4 ++-- app/styles/components/theme-toggle.scss | 9 ++++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index d01b2fd27..6ba320195 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -68,9 +68,7 @@ const Navbar = () => {
- {settings.get('isDeveloper') && ( - - )} + {account ? (
diff --git a/app/soapbox/features/ui/components/theme_toggle.tsx b/app/soapbox/features/ui/components/theme_toggle.tsx index 5f04be848..e8c63c60f 100644 --- a/app/soapbox/features/ui/components/theme_toggle.tsx +++ b/app/soapbox/features/ui/components/theme_toggle.tsx @@ -37,8 +37,8 @@ function ThemeToggle({ showLabel }: IThemeToggle) { id={id} checked={themeMode === 'light'} icons={{ - checked: , - unchecked: , + checked: , + unchecked: , }} onChange={onToggle} /> diff --git a/app/styles/components/theme-toggle.scss b/app/styles/components/theme-toggle.scss index 5b59d80b1..112c14972 100644 --- a/app/styles/components/theme-toggle.scss +++ b/app/styles/components/theme-toggle.scss @@ -27,8 +27,11 @@ } } - .svg-icon { - width: 18px; - height: 18px; + .react-toggle-track { + @apply dark:bg-slate-600; + } + + .react-toggle-thumb { + @apply dark:bg-slate-900 dark:border-slate-800; } } From f316dac83e91e934f85b78bdab7ca05617d855f9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 19:59:53 -0500 Subject: [PATCH 02/14] eslint: scream if I try putting a JS comment in a JSX text node --- .eslintrc.js | 1 + app/soapbox/components/ui/icon/svg-icon.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9a92e50a8..d885cbeea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -141,6 +141,7 @@ module.exports = { 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], 'react/jsx-indent': ['error', 2], // 'react/jsx-no-bind': ['error'], + 'react/jsx-no-comment-textnodes': 'error', 'react/jsx-no-duplicate-props': 'error', 'react/jsx-no-undef': 'error', 'react/jsx-tag-spacing': 'error', diff --git a/app/soapbox/components/ui/icon/svg-icon.tsx b/app/soapbox/components/ui/icon/svg-icon.tsx index 5cb2dd192..84604150d 100644 --- a/app/soapbox/components/ui/icon/svg-icon.tsx +++ b/app/soapbox/components/ui/icon/svg-icon.tsx @@ -30,7 +30,7 @@ const SvgIcon: React.FC = ({ src, alt, size = 24, className }): JSX.El loader={loader} data-testid='svg-icon' > - /* If the fetch fails, fall back to displaying the loader */ + {/* If the fetch fails, fall back to displaying the loader */} {loader} ); From 0912700d153ae7efebd4cf313d79053571872791 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 20:31:24 -0500 Subject: [PATCH 03/14] Create preliminary EmojiButtonWrapper component --- app/soapbox/actions/{modals.js => modals.ts} | 6 +- .../components/emoji-button-wrapper.tsx | 98 +++++++++++++++++++ app/soapbox/components/hoverable.tsx | 63 ------------ app/soapbox/components/status_action_bar.tsx | 15 +-- .../ui/emoji-selector/emoji-selector.tsx | 4 +- .../features/status/components/action-bar.tsx | 45 ++++++--- 6 files changed, 139 insertions(+), 92 deletions(-) rename app/soapbox/actions/{modals.js => modals.ts} (59%) create mode 100644 app/soapbox/components/emoji-button-wrapper.tsx delete mode 100644 app/soapbox/components/hoverable.tsx diff --git a/app/soapbox/actions/modals.js b/app/soapbox/actions/modals.ts similarity index 59% rename from app/soapbox/actions/modals.js rename to app/soapbox/actions/modals.ts index 72604ecc6..9d6e85139 100644 --- a/app/soapbox/actions/modals.js +++ b/app/soapbox/actions/modals.ts @@ -1,7 +1,8 @@ export const MODAL_OPEN = 'MODAL_OPEN'; export const MODAL_CLOSE = 'MODAL_CLOSE'; -export function openModal(type, props) { +/** Open a modal of the given type */ +export function openModal(type: string, props?: any) { return { type: MODAL_OPEN, modalType: type, @@ -9,7 +10,8 @@ export function openModal(type, props) { }; } -export function closeModal(type) { +/** Close the modal */ +export function closeModal(type: string) { return { type: MODAL_CLOSE, modalType: type, diff --git a/app/soapbox/components/emoji-button-wrapper.tsx b/app/soapbox/components/emoji-button-wrapper.tsx new file mode 100644 index 000000000..eb2ec2f3b --- /dev/null +++ b/app/soapbox/components/emoji-button-wrapper.tsx @@ -0,0 +1,98 @@ +import classNames from 'classnames'; +import React, { useState, useRef } from 'react'; +import { usePopper } from 'react-popper'; +import { useDispatch } from 'react-redux'; + +import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; +import { openModal } from 'soapbox/actions/modals'; +import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector'; +import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks'; + +interface IEmojiButtonWrapper { + statusId: string, + children: JSX.Element, +} + +/** Provides emoji reaction functionality to the underlying button component */ +const EmojiButtonWrapper: React.FC = ({ statusId, children }): JSX.Element | null => { + const dispatch = useDispatch(); + const ownAccount = useOwnAccount(); + const status = useAppSelector(state => state.statuses.get(statusId)); + const soapboxConfig = useSoapboxConfig(); + + const [visible, setVisible] = useState(false); + // const [focused, setFocused] = useState(false); + + const ref = useRef(null); + const popperRef = useRef(null); + + const { styles, attributes } = usePopper(ref.current, popperRef.current, { + placement: 'top-start', + strategy: 'fixed', + modifiers: [ + { + name: 'offset', + options: { + offset: [-10, 0], + }, + }, + ], + }); + + if (!status) return null; + + const handleMouseEnter = () => { + setVisible(true); + }; + + const handleMouseLeave = () => { + setVisible(false); + }; + + const handleReact = (emoji: string): void => { + if (ownAccount) { + dispatch(simpleEmojiReact(status, emoji)); + } else { + dispatch(openModal('UNAUTHORIZED', { + action: 'FAVOURITE', + ap_id: status.url, + })); + } + + setVisible(false); + }; + + // const handleUnfocus: React.EventHandler = () => { + // setFocused(false); + // }; + + const selector = ( +
+ +
+ ); + + return ( +
+ {React.cloneElement(children, { + ref, + })} + + {selector} +
+ ); +}; + +export default EmojiButtonWrapper; diff --git a/app/soapbox/components/hoverable.tsx b/app/soapbox/components/hoverable.tsx deleted file mode 100644 index 751c413c1..000000000 --- a/app/soapbox/components/hoverable.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; -import { usePopper } from 'react-popper'; - -interface IHoverable { - component: JSX.Element, -} - -/** Wrapper to render a given component when hovered */ -const Hoverable: React.FC = ({ - component, - children, -}): JSX.Element => { - - const [portalActive, setPortalActive] = useState(false); - - const ref = useRef(null); - const popperRef = useRef(null); - - const handleMouseEnter = () => { - setPortalActive(true); - }; - - const handleMouseLeave = () => { - setPortalActive(false); - }; - - const { styles, attributes } = usePopper(ref.current, popperRef.current, { - placement: 'top-start', - strategy: 'fixed', - modifiers: [ - { - name: 'offset', - options: { - offset: [-10, 0], - }, - }, - ], - }); - - return ( -
- {children} - -
- {component} -
-
- ); -}; - -export default Hoverable; diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index 9011bec71..dbafd64fd 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -6,8 +6,7 @@ import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; -import EmojiSelector from 'soapbox/components/emoji_selector'; -import Hoverable from 'soapbox/components/hoverable'; +import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; import StatusActionButton from 'soapbox/components/status-action-button'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import { isUserTouching } from 'soapbox/is_mobile'; @@ -641,15 +640,7 @@ class StatusActionBar extends ImmutablePureComponent - )} - > + - + ): ( = ({ emoji, className, onClick, tabInd }; interface IEmojiSelector { - emojis: string[], + emojis: Iterable, onReact: (emoji: string) => void, visible?: boolean, focused?: boolean, @@ -40,7 +40,7 @@ const EmojiSelector: React.FC = ({ emojis, onReact, visible = fa space={2} className={classNames('bg-white dark:bg-slate-900 p-3 rounded-full shadow-md z-[999] w-max')} > - {emojis.map((emoji, i) => ( + {Array.from(emojis).map((emoji, i) => ( { {reblogButton} - + {features.emojiReacts ? ( + + + + ) : ( + + )} {canShare && ( Date: Sun, 10 Apr 2022 20:41:00 -0500 Subject: [PATCH 04/14] EmojiButtonWrapper: handle click --- .../components/emoji-button-wrapper.tsx | 19 +++++++++++++++++++ app/soapbox/components/status_action_bar.tsx | 1 - .../features/status/components/action-bar.tsx | 1 - 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/emoji-button-wrapper.tsx b/app/soapbox/components/emoji-button-wrapper.tsx index eb2ec2f3b..32159b329 100644 --- a/app/soapbox/components/emoji-button-wrapper.tsx +++ b/app/soapbox/components/emoji-button-wrapper.tsx @@ -7,6 +7,8 @@ import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; import { openModal } from 'soapbox/actions/modals'; import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector'; import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks'; +import { isUserTouching } from 'soapbox/is_mobile'; +import { getReactForStatus } from 'soapbox/utils/emoji_reacts'; interface IEmojiButtonWrapper { statusId: string, @@ -62,6 +64,22 @@ const EmojiButtonWrapper: React.FC = ({ statusId, children setVisible(false); }; + const handleClick: React.EventHandler = e => { + const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍'; + + if (isUserTouching()) { + if (visible) { + handleReact(meEmojiReact); + } else { + setVisible(true); + } + } else { + handleReact(meEmojiReact); + } + + e.stopPropagation(); + }; + // const handleUnfocus: React.EventHandler = () => { // setFocused(false); // }; @@ -87,6 +105,7 @@ const EmojiButtonWrapper: React.FC = ({ statusId, children return (
{React.cloneElement(children, { + onClick: handleClick, ref, })} diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index dbafd64fd..fa99ffa4e 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -645,7 +645,6 @@ class StatusActionBar extends ImmutablePureComponent diff --git a/app/soapbox/features/status/components/action-bar.tsx b/app/soapbox/features/status/components/action-bar.tsx index 7efb40aff..9ac9b007f 100644 --- a/app/soapbox/features/status/components/action-bar.tsx +++ b/app/soapbox/features/status/components/action-bar.tsx @@ -588,7 +588,6 @@ class ActionBar extends React.PureComponent { 'fill-accent-300': Boolean(meEmojiReact), })} text={meEmojiTitle} - onClick={this.handleLikeButtonClick} /> ) : ( From c5c1f83f36a51168414fdf9dd113c143b5dd19c5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 20:49:36 -0500 Subject: [PATCH 05/14] Fix lint --- app/soapbox/components/status_action_bar.tsx | 2 +- app/soapbox/features/ui/index.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index fa99ffa4e..41c873c7c 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -553,7 +553,7 @@ class StatusActionBar extends ImmutablePureComponent - // NOTE: we cannot nest routes in a fragment - // https://stackoverflow.com/a/68637108 + {/* + NOTE: we cannot nest routes in a fragment + https://stackoverflow.com/a/68637108 + */} {features.federating && } {features.federating && } {features.federating && } From 8cef636093874e279bcb9e2e4aa4761641abc0f0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 11 Apr 2022 14:27:32 -0500 Subject: [PATCH 06/14] Upgrade to Node.js 16.x --- .gitlab-ci.yml | 2 +- .tool-versions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 546e3810e..0d140029f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: node:14 +image: node:16 variables: NODE_ENV: test diff --git a/.tool-versions b/.tool-versions index 2d8169e51..009455657 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 14.17.6 +nodejs 16.14.2 From 9b7f8b38165db863363b9b9212900a02072ce3aa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 12 Apr 2022 12:13:51 -0500 Subject: [PATCH 07/14] Fix API mock in verification test --- app/soapbox/__mocks__/api.ts | 2 +- app/soapbox/features/verification/__tests__/index.test.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/__mocks__/api.ts b/app/soapbox/__mocks__/api.ts index 2f3a0d7b8..99797009e 100644 --- a/app/soapbox/__mocks__/api.ts +++ b/app/soapbox/__mocks__/api.ts @@ -9,7 +9,7 @@ export const __stub = (func: Function) => mocks.push(func); export const __clear = (): Function[] => mocks = []; const setupMock = (axios: AxiosInstance) => { - const mock = new MockAdapter(axios); + const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); mocks.map(func => func(mock)); }; diff --git a/app/soapbox/features/verification/__tests__/index.test.tsx b/app/soapbox/features/verification/__tests__/index.test.tsx index b4c28509e..27a88957e 100644 --- a/app/soapbox/features/verification/__tests__/index.test.tsx +++ b/app/soapbox/features/verification/__tests__/index.test.tsx @@ -2,7 +2,8 @@ import { Map as ImmutableMap } from 'immutable'; import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { __stub } from '../../../__mocks__/api'; +import { __stub } from 'soapbox/api'; + import { render, screen } from '../../../jest/test-helpers'; import Verification from '../index'; From 7394452ad913df03a36699342f1e9f19c4f3d516 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 12 Apr 2022 12:38:19 -0500 Subject: [PATCH 08/14] ForkTsCheckerWebpackPlugin: increase typescript memory limit to 8GB, fixes #865 --- webpack/shared.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/shared.js b/webpack/shared.js index 2daba677a..1fcdef898 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -75,7 +75,7 @@ module.exports = { new webpack.ProvidePlugin({ process: 'process/browser', }), - new ForkTsCheckerWebpackPlugin(), + new ForkTsCheckerWebpackPlugin({ typescript: { memoryLimit: 8192 } }), new MiniCssExtractPlugin({ filename: 'packs/css/[name]-[contenthash:8].css', chunkFilename: 'packs/css/[name]-[contenthash:8].chunk.css', From 93a6945b7fcb1e861f3b21853cd580b2456d51f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 12 Apr 2022 15:23:18 -0500 Subject: [PATCH 09/14] Let a custom auth app be embedded in the build --- app/soapbox/actions/auth.js | 22 ++++++++++++++++++---- app/soapbox/{custom.js => custom.ts} | 7 ++++--- docs/development/build-config.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) rename app/soapbox/{custom.js => custom.ts} (57%) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 1a700b9ec..5c0fc467a 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -14,6 +14,7 @@ import { createApp } from 'soapbox/actions/apps'; import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me'; import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; import snackbar from 'soapbox/actions/snackbar'; +import { custom } from 'soapbox/custom'; import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; @@ -39,12 +40,14 @@ export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST'; export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS'; export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL'; +const customApp = custom('app'); + export const messages = defineMessages({ loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' }, invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, }); -const noOp = () => () => new Promise(f => f()); +const noOp = () => new Promise(f => f()); const getScopes = state => { const instance = state.get('instance'); @@ -54,12 +57,23 @@ const getScopes = state => { function createAppAndToken() { return (dispatch, getState) => { - return dispatch(createAuthApp()).then(() => { + return dispatch(getAuthApp()).then(() => { return dispatch(createAppToken()); }); }; } +/** Create an auth app, or use it from build config */ +function getAuthApp() { + return (dispatch, getState) => { + if (customApp?.client_secret) { + return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); + } else { + return dispatch(createAuthApp()); + } + }; +} + function createAuthApp() { return (dispatch, getState) => { const params = { @@ -117,7 +131,7 @@ export function refreshUserToken() { const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']); const app = getState().getIn(['auth', 'app']); - if (!refreshToken) return dispatch(noOp()); + if (!refreshToken) return dispatch(noOp); const params = { client_id: app.get('client_id'), @@ -200,7 +214,7 @@ export function loadCredentials(token, accountUrl) { export function logIn(intl, username, password) { return (dispatch, getState) => { - return dispatch(createAuthApp()).then(() => { + return dispatch(getAuthApp()).then(() => { return dispatch(createUserToken(username, password)); }).catch(error => { if (error.response.data.error === 'mfa_required') { diff --git a/app/soapbox/custom.js b/app/soapbox/custom.ts similarity index 57% rename from app/soapbox/custom.js rename to app/soapbox/custom.ts index 623cb22b3..4bccb386d 100644 --- a/app/soapbox/custom.js +++ b/app/soapbox/custom.ts @@ -1,12 +1,13 @@ /** * Functions for dealing with custom build configuration. */ -import { NODE_ENV } from 'soapbox/build_config'; +import * as BuildConfig from 'soapbox/build_config'; /** Require a custom JSON file if it exists */ -export const custom = (filename, fallback = {}) => { - if (NODE_ENV === 'test') return fallback; +export const custom = (filename: string, fallback: any = {}): any => { + if (BuildConfig.NODE_ENV === 'test') return fallback; + // @ts-ignore: yes it does const context = require.context('custom', false, /\.json$/); const path = `./${filename}.json`; diff --git a/docs/development/build-config.md b/docs/development/build-config.md index 0d7f44b99..4467dbfe0 100644 --- a/docs/development/build-config.md +++ b/docs/development/build-config.md @@ -38,6 +38,34 @@ For example: See `app/soapbox/utils/features.js` for the full list of features. +### Embedded app (`custom/app.json`) + +By default, Soapbox will create a new OAuth app every time a user tries to register or log in. +This is usually the desired behavior, as it works "out of the box" without any additional configuration, and it is resistant to tampering and subtle client bugs. +However, some larger servers may wish to skip this step for performance reasons. + +If an app is supplied in `custom/app.json`, it will be used for authorization. +The full app entity must be provided, for example: + +```json +{ + "client_id": "cf5yI6ffXH1UcDkEApEIrtHpwCi5Tv9xmju8IKdMAkE", + "client_secret": "vHmSDpm6BJGUvR4_qWzmqWjfHcSYlZumxpFfohRwNNQ", + "id": "7132", + "name": "Soapbox FE", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "website": "https://soapbox.pub/", + "vapid_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4" +} +``` + +It is crucial that the app has the expected scopes. +You can obtain one with the following curl command (replace `MY_DOMAIN`): + +```sh +curl -X POST -H "Content-Type: application/json" -d '{"client_name": "Soapbox FE", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "read write follow push admin", "website": "https://soapbox.pub/"}' "https://MY_DOMAIN.com/api/v1/apps" +``` + ### Custom files (`custom/instance/*`) You can place arbitrary files of any type in the `custom/instance/` directory. From 8377e3c86b9e840f554383e74d9e75ce1b7f5315 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 12 Apr 2022 19:08:31 -0500 Subject: [PATCH 10/14] Make darkMode a feature (for now) --- app/soapbox/features/ui/components/navbar.tsx | 8 ++++++-- app/soapbox/utils/features.ts | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index 6ba320195..8b7f9286b 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; import { Avatar, Button, Icon } from 'soapbox/components/ui'; import Search from 'soapbox/features/compose/components/search'; import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; -import { useOwnAccount, useSoapboxConfig, useSettings } from 'soapbox/hooks'; +import { useOwnAccount, useSoapboxConfig, useSettings, useFeatures } from 'soapbox/hooks'; import { openSidebar } from '../../../actions/sidebar'; @@ -19,6 +19,7 @@ const Navbar = () => { const account = useOwnAccount(); const settings = useSettings(); + const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); const singleUserMode = soapboxConfig.get('singleUserMode'); @@ -68,7 +69,10 @@ const Navbar = () => {
- + {/* TODO: make this available for everyone when it's ready (possibly in a different place) */} + {(features.darkMode || settings.get('isDeveloper')) && ( + + )} {account ? (
diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index e1c5d12e0..468015696 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -134,6 +134,10 @@ const getInstanceFeatures = (instance: Instance) => { trendingTruths: v.software === TRUTHSOCIAL, trendingStatuses: v.software === MASTODON && gte(v.compatVersion, '3.5.0'), pepe: v.software === TRUTHSOCIAL, + + // FIXME: long-term this shouldn't be a feature, + // but for now we want it to be overrideable in the build + darkMode: true, }; }; From 9c79ae386a32c01c257013ff9c10f05a1a34bb6c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 12 Apr 2022 19:52:20 -0500 Subject: [PATCH 11/14] SidebarMenu: convert to tsx --- .../{sidebar_menu.js => sidebar_menu.tsx} | 78 +++++++++---------- .../reducers/{sidebar.js => sidebar.ts} | 12 ++- 2 files changed, 50 insertions(+), 40 deletions(-) rename app/soapbox/components/{sidebar_menu.js => sidebar_menu.tsx} (82%) rename app/soapbox/reducers/{sidebar.js => sidebar.ts} (50%) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.tsx similarity index 82% rename from app/soapbox/components/sidebar_menu.js rename to app/soapbox/components/sidebar_menu.tsx index bba7d06ce..61a38a0ba 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.tsx @@ -1,23 +1,24 @@ import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { Link, NavLink } from 'react-router-dom'; import { logOut, switchAccount } from 'soapbox/actions/auth'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import Account from 'soapbox/components/account'; import { Stack } from 'soapbox/components/ui'; import ProfileStats from 'soapbox/features/ui/components/profile_stats'; -import { getFeatures } from 'soapbox/utils/features'; +import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks'; import { closeSidebar } from '../actions/sidebar'; import { makeGetAccount, makeGetOtherAccounts } from '../selectors'; import { HStack, Icon, IconButton, Text } from './ui'; +import type { List as ImmutableList } from 'immutable'; +import type { Account as AccountEntity } from 'soapbox/types/entities'; + const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, follows: { id: 'account.follows', defaultMessage: 'Follows' }, @@ -33,7 +34,14 @@ const messages = defineMessages({ logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, }); -const SidebarLink = ({ to, icon, text, onClick }) => ( +interface ISidebarLink { + to: string, + icon: string, + text: string, + onClick: React.EventHandler, +} + +const SidebarLink: React.FC = ({ to, icon, text, onClick }) => (
@@ -45,25 +53,19 @@ const SidebarLink = ({ to, icon, text, onClick }) => ( ); -SidebarLink.propTypes = { - to: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -}; +const getOtherAccounts = makeGetOtherAccounts(); -const SidebarMenu = () => { +const SidebarMenu: React.FC = (): JSX.Element | null => { const intl = useIntl(); const dispatch = useDispatch(); - const logo = useSelector((state) => getSoapboxConfig(state).get('logo')); - const features = useSelector((state) => getFeatures(state.get('instance'))); + const { logo } = useSoapboxConfig(); + const features = useFeatures(); const getAccount = makeGetAccount(); - const getOtherAccounts = makeGetOtherAccounts(); - const me = useSelector((state) => state.get('me')); - const account = useSelector((state) => getAccount(state, me)); - const otherAccounts = useSelector((state) => getOtherAccounts(state)); - const sidebarOpen = useSelector((state) => state.get('sidebar').sidebarOpen); + const me = useAppSelector((state) => state.me); + const account = useAppSelector((state) => me ? getAccount(state, me) : null); + const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); + const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen); const closeButtonRef = React.useRef(null); @@ -76,25 +78,27 @@ const SidebarMenu = () => { onClose(); }; - const handleSwitchAccount = (event, account) => { - event.preventDefault(); - switchAccount(account); - dispatch(switchAccount(account.get('id'))); + const handleSwitchAccount = (account: AccountEntity): React.EventHandler => { + return (e) => { + e.preventDefault(); + switchAccount(account); + dispatch(switchAccount(account.id)); + }; }; - const onClickLogOut = (event) => { - event.preventDefault(); + const onClickLogOut: React.EventHandler = (e) => { + e.preventDefault(); dispatch(logOut(intl)); }; - const handleSwitcherClick = (e) => { + const handleSwitcherClick: React.EventHandler = (e) => { e.preventDefault(); setSwitcher((prevState) => (!prevState)); }; - const renderAccount = (account) => ( - handleSwitchAccount(event, account)} key={account.get('id')}> + const renderAccount = (account: AccountEntity) => ( + ); @@ -103,17 +107,13 @@ const SidebarMenu = () => { dispatch(fetchOwnAccounts()); }, []); - if (!account) { - return null; - } - - const acct = account.get('acct'); - const classes = classNames('sidebar-menu__root', { - 'sidebar-menu__root--visible': sidebarOpen, - }); + if (!account) return null; return ( -
+
{ - + @@ -184,7 +184,7 @@ const SidebarMenu = () => {
Date: Tue, 12 Apr 2022 20:10:47 -0500 Subject: [PATCH 12/14] Add timeline links to navigation --- app/soapbox/components/sidebar-navigation.tsx | 40 +++++++++---------- app/soapbox/components/sidebar_menu.tsx | 30 ++++++++++++-- app/soapbox/utils/features.ts | 8 ++++ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 0898279f9..b15b2d72a 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -27,7 +27,7 @@ const SidebarNavigation = () => { } + text={} /> {account && ( @@ -42,7 +42,7 @@ const SidebarNavigation = () => { to='/notifications' icon={require('icons/alert.svg')} count={notificationCount} - text={} + text={} /> { /> )} - {/* {features.federating ? ( - - - {instance.title} - - ) : ( - - - - + {(features.localTimeline || features.publicTimeline) && ( +
)} - {features.federating && ( - - - - - )} */} + {features.localTimeline && ( + } + /> + )} + + {(features.publicTimeline && features.federating) && ( + } + /> + )}
{account && ( diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index 61a38a0ba..c31ec29db 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link, NavLink } from 'react-router-dom'; @@ -37,7 +37,7 @@ const messages = defineMessages({ interface ISidebarLink { to: string, icon: string, - text: string, + text: string | JSX.Element, onClick: React.EventHandler, } @@ -62,6 +62,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const { logo } = useSoapboxConfig(); const features = useFeatures(); const getAccount = makeGetAccount(); + const instance = useAppSelector((state) => state.instance); const me = useAppSelector((state) => state.me); const account = useAppSelector((state) => me ? getAccount(state, me) : null); const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); @@ -130,7 +131,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { {logo ? ( - Logo + Logo ): ( { + {/* TODO: make this available to everyone */} {account.staff && (