From 3b1d8972b0d4dde08f27ae544647a6e42657e144 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Oct 2022 16:56:25 -0500 Subject: [PATCH 01/16] Scaffold theme editor --- .../components/color-with-picker.tsx | 9 +-- app/soapbox/features/soapbox_config/index.tsx | 4 +- .../theme-editor/components/palette.tsx | 41 ++++++++++ app/soapbox/features/theme-editor/index.tsx | 74 +++++++++++++++++++ app/soapbox/features/ui/index.tsx | 2 + .../features/ui/util/async-components.ts | 4 + 6 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 app/soapbox/features/theme-editor/components/palette.tsx create mode 100644 app/soapbox/features/theme-editor/index.tsx diff --git a/app/soapbox/features/soapbox_config/components/color-with-picker.tsx b/app/soapbox/features/soapbox_config/components/color-with-picker.tsx index c82e1bf11..70ccb624b 100644 --- a/app/soapbox/features/soapbox_config/components/color-with-picker.tsx +++ b/app/soapbox/features/soapbox_config/components/color-with-picker.tsx @@ -9,12 +9,12 @@ import ColorPicker from './color-picker'; import type { ColorChangeHandler } from 'react-color'; interface IColorWithPicker { - buttonId: string, value: string, onChange: ColorChangeHandler, + className?: string, } -const ColorWithPicker: React.FC = ({ buttonId, value, onChange }) => { +const ColorWithPicker: React.FC = ({ value, onChange, className }) => { const node = useRef(null); const [active, setActive] = useState(false); const [placement, setPlacement] = useState(null); @@ -39,11 +39,10 @@ const ColorWithPicker: React.FC = ({ buttonId, value, onChange }; return ( -
+
}> + + + ); +}; + +export default ThemeEditor; \ No newline at end of file diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 08d96e920..c5a9bb422 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -112,6 +112,7 @@ import { TestTimeline, LogoutPage, AuthTokenList, + ThemeEditor, } from './util/async-components'; import { WrappedRoute } from './util/react_router_helpers'; @@ -306,6 +307,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { + diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index f416c859a..84053f24a 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -310,6 +310,10 @@ export function ModerationLog() { return import(/* webpackChunkName: "features/admin/moderation_log" */'../../admin/moderation_log'); } +export function ThemeEditor() { + return import(/* webpackChunkName: "features/theme-editor" */'../../theme-editor'); +} + export function UserPanel() { return import(/* webpackChunkName: "features/ui" */'../components/user_panel'); } From 41a608481f90a1523d456f488ac10078b2541f30 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 25 Oct 2022 17:47:24 -0500 Subject: [PATCH 02/16] ThemeEditor: allow editing colors (sort of) --- .../theme-editor/components/color.tsx | 28 +++++++++++++ .../theme-editor/components/palette.tsx | 29 +++++++------- app/soapbox/features/theme-editor/index.tsx | 40 ++++++++++++++----- 3 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 app/soapbox/features/theme-editor/components/color.tsx diff --git a/app/soapbox/features/theme-editor/components/color.tsx b/app/soapbox/features/theme-editor/components/color.tsx new file mode 100644 index 000000000..1e2b6f3c1 --- /dev/null +++ b/app/soapbox/features/theme-editor/components/color.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import ColorWithPicker from 'soapbox/features/soapbox_config/components/color-with-picker'; + +import type { ColorChangeHandler } from 'react-color'; + +interface IColor { + color: string, + onChange: (color: string) => void, +} + +/** Color input. */ +const Color: React.FC = ({ color, onChange }) => { + + const handleChange: ColorChangeHandler = (result) => { + onChange(result.hex); + }; + + return ( + + ); +}; + +export default Color; \ No newline at end of file diff --git a/app/soapbox/features/theme-editor/components/palette.tsx b/app/soapbox/features/theme-editor/components/palette.tsx index 4f89fa3ae..a8011790f 100644 --- a/app/soapbox/features/theme-editor/components/palette.tsx +++ b/app/soapbox/features/theme-editor/components/palette.tsx @@ -2,7 +2,8 @@ import React from 'react'; import compareId from 'soapbox/compare_id'; import { HStack } from 'soapbox/components/ui'; -import ColorWithPicker from 'soapbox/features/soapbox_config/components/color-with-picker'; + +import Color from './color'; interface ColorGroup { [tint: string]: string, @@ -10,27 +11,27 @@ interface ColorGroup { interface IPalette { palette: ColorGroup, + onChange: (palette: ColorGroup) => void, } /** Editable color palette. */ -const Palette: React.FC = ({ palette }) => { +const Palette: React.FC = ({ palette, onChange }) => { const tints = Object.keys(palette).sort(compareId); - const result = tints.map(tint => { - const hex = palette[tint]; - - return ( - {}} - /> - ); - }); + const handleChange = (tint: string) => { + return (color: string) => { + onChange({ + ...palette, + [tint]: color, + }); + }; + }; return ( - {result} + {tints.map(tint => ( + + ))} ); }; diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index 06a0a6496..ae83a975d 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import List, { ListItem } from 'soapbox/components/list'; @@ -18,39 +18,58 @@ interface IThemeEditor { const ThemeEditor: React.FC = () => { const intl = useIntl(); const soapbox = useSoapboxConfig(); - const colors = soapbox.colors.toJS(); + + const [colors, setColors] = useState(soapbox.colors.toJS() as any); + + const updateColors = (key: string) => { + return (newColors: any) => { + setColors({ + ...colors, + [key]: { + ...colors[key], + ...newColors, + }, + }); + }; + }; return ( @@ -60,13 +79,14 @@ const ThemeEditor: React.FC = () => { interface IPaletteListItem { label: React.ReactNode, palette: ColorGroup, + onChange: (palette: ColorGroup) => void, } /** Palette editor inside a ListItem. */ -const PaletteListItem: React.FC = ({ label, palette }) => { +const PaletteListItem: React.FC = ({ label, palette, onChange }) => { return ( {label}
}> - + ); }; From 62accd555971462dc0ff7eb2a8c516863ed6696b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 16 Dec 2022 19:22:59 -0600 Subject: [PATCH 03/16] Fix import paths --- app/soapbox/features/theme-editor/components/color.tsx | 2 +- app/soapbox/features/theme-editor/components/palette.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/theme-editor/components/color.tsx b/app/soapbox/features/theme-editor/components/color.tsx index 1e2b6f3c1..79d1c9eb1 100644 --- a/app/soapbox/features/theme-editor/components/color.tsx +++ b/app/soapbox/features/theme-editor/components/color.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import ColorWithPicker from 'soapbox/features/soapbox_config/components/color-with-picker'; +import ColorWithPicker from 'soapbox/features/soapbox-config/components/color-with-picker'; import type { ColorChangeHandler } from 'react-color'; diff --git a/app/soapbox/features/theme-editor/components/palette.tsx b/app/soapbox/features/theme-editor/components/palette.tsx index a8011790f..cbadf019a 100644 --- a/app/soapbox/features/theme-editor/components/palette.tsx +++ b/app/soapbox/features/theme-editor/components/palette.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import compareId from 'soapbox/compare_id'; import { HStack } from 'soapbox/components/ui'; +import { compareId } from 'soapbox/utils/comparators'; import Color from './color'; From ed12246ae475faed3bf08a93a8e16c51de5b24be Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 11:47:00 -0600 Subject: [PATCH 04/16] Remove unused sea-blue color --- app/soapbox/normalizers/soapbox/soapbox-config.ts | 1 - app/styles/themes.scss | 1 - tailwind.config.js | 1 - tailwind/__tests__/colors-test.js | 2 -- 4 files changed, 5 deletions(-) diff --git a/app/soapbox/normalizers/soapbox/soapbox-config.ts b/app/soapbox/normalizers/soapbox/soapbox-config.ts index c4246d8a0..b221afabe 100644 --- a/app/soapbox/normalizers/soapbox/soapbox-config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox-config.ts @@ -43,7 +43,6 @@ const DEFAULT_COLORS = ImmutableMap({ 800: '#991b1b', 900: '#7f1d1d', }), - 'sea-blue': '#2feecc', 'greentext': '#789922', }); diff --git a/app/styles/themes.scss b/app/styles/themes.scss index 97a9b5f95..5efd0fc97 100644 --- a/app/styles/themes.scss +++ b/app/styles/themes.scss @@ -70,7 +70,6 @@ body, --dark-blue: #1d1953; --electric-blue: #5448ee; --electric-blue-contrast: #e8e7fd; - --sea-blue: #2feecc; // Sizes --border-radius-base: 4px; diff --git a/tailwind.config.js b/tailwind.config.js index aa09fb24c..c57acc16e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -58,7 +58,6 @@ module.exports = { 'accent-blue': true, 'gradient-start': true, 'gradient-end': true, - 'sea-blue': true, 'greentext': true, }), animation: { diff --git a/tailwind/__tests__/colors-test.js b/tailwind/__tests__/colors-test.js index 89836edb7..36bc2526c 100644 --- a/tailwind/__tests__/colors-test.js +++ b/tailwind/__tests__/colors-test.js @@ -42,12 +42,10 @@ describe('parseColorMatrix()', () => { accent: [300, 500], 'gradient-start': true, 'gradient-end': true, - 'sea-blue': true, }; const result = parseColorMatrix(colorMatrix); - expect(result['sea-blue']({})).toEqual('rgb(var(--color-sea-blue))'); expect(result['gradient-start']({ opacityValue: .7 })).toEqual('rgb(var(--color-gradient-start) / 0.7)'); }); }); From f906558dbab0191f065f8771fa352d1680539f3e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 13:05:50 -0600 Subject: [PATCH 05/16] Theme editor: allow saving theme --- app/soapbox/actions/admin.ts | 14 +++ app/soapbox/features/soapbox-config/index.tsx | 14 +-- app/soapbox/features/theme-editor/index.tsx | 118 ++++++++++++------ 3 files changed, 93 insertions(+), 53 deletions(-) diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index 02af5e87a..3cd5a25ba 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -103,6 +103,19 @@ const updateConfig = (configs: Record[]) => }); }; +const updateSoapboxConfig = (data: Record) => + (dispatch: AppDispatch, _getState: () => RootState) => { + const params = [{ + group: ':pleroma', + key: ':frontend_configurations', + value: [{ + tuple: [':soapbox_fe', data], + }], + }]; + + return dispatch(updateConfig(params)); + }; + const fetchMastodonReports = (params: Record) => (dispatch: AppDispatch, getState: () => RootState) => api(getState) @@ -585,6 +598,7 @@ export { ADMIN_USERS_UNSUGGEST_FAIL, fetchConfig, updateConfig, + updateSoapboxConfig, fetchReports, closeReports, fetchUsers, diff --git a/app/soapbox/features/soapbox-config/index.tsx b/app/soapbox/features/soapbox-config/index.tsx index 2c70d6d55..39c859ef1 100644 --- a/app/soapbox/features/soapbox-config/index.tsx +++ b/app/soapbox/features/soapbox-config/index.tsx @@ -2,7 +2,7 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import React, { useState, useEffect, useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { updateConfig } from 'soapbox/actions/admin'; +import { updateSoapboxConfig } from 'soapbox/actions/admin'; import { uploadMedia } from 'soapbox/actions/media'; import snackbar from 'soapbox/actions/snackbar'; import List, { ListItem } from 'soapbox/components/list'; @@ -99,18 +99,8 @@ const SoapboxConfig: React.FC = () => { setJsonValid(true); }; - const getParams = () => { - return [{ - group: ':pleroma', - key: ':frontend_configurations', - value: [{ - tuple: [':soapbox_fe', data.toJS()], - }], - }]; - }; - const handleSubmit: React.FormEventHandler = (e) => { - dispatch(updateConfig(getParams())).then(() => { + dispatch(updateSoapboxConfig(data.toJS())).then(() => { setLoading(false); dispatch(snackbar.success(intl.formatMessage(messages.saved))); }).catch(() => { diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index ae83a975d..c1b8c97a1 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -1,14 +1,19 @@ import React, { useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { updateSoapboxConfig } from 'soapbox/actions/admin'; +import { getHost } from 'soapbox/actions/instance'; +import snackbar from 'soapbox/actions/snackbar'; +import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; import List, { ListItem } from 'soapbox/components/list'; -import { Column } from 'soapbox/components/ui'; -import { useSoapboxConfig } from 'soapbox/hooks'; +import { Button, Column, Form, FormActions } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; import Palette, { ColorGroup } from './components/palette'; const messages = defineMessages({ title: { id: 'admin.theme.title', defaultMessage: 'Theme' }, + saved: { id: 'theme_editor.saved', defaultMessage: 'Theme updated!' }, }); interface IThemeEditor { @@ -17,12 +22,17 @@ interface IThemeEditor { /** UI for editing Tailwind theme colors. */ const ThemeEditor: React.FC = () => { const intl = useIntl(); + const dispatch = useAppDispatch(); + const soapbox = useSoapboxConfig(); + const host = useAppSelector(state => getHost(state)); + const rawConfig = useAppSelector(state => state.soapbox); const [colors, setColors] = useState(soapbox.colors.toJS() as any); + const [submitting, setSubmitting] = useState(false); const updateColors = (key: string) => { - return (newColors: any) => { + return (newColors: ColorGroup) => { setColors({ ...colors, [key]: { @@ -33,45 +43,71 @@ const ThemeEditor: React.FC = () => { }; }; + const updateTheme = async () => { + const params = rawConfig.set('colors', colors).toJS(); + await dispatch(updateSoapboxConfig(params)); + }; + + const handleSubmit = async() => { + setSubmitting(true); + + try { + await dispatch(fetchSoapboxConfig(host)); + await updateTheme(); + dispatch(snackbar.success(intl.formatMessage(messages.saved))); + setSubmitting(false); + } catch (e) { + setSubmitting(false); + } + }; + return ( - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + +
); }; From 819e03073b4189f19c063f5f6118a03cb9f19be5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 13:10:59 -0600 Subject: [PATCH 06/16] SoapboxConfig: link to theme editor --- app/soapbox/features/soapbox-config/index.tsx | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/app/soapbox/features/soapbox-config/index.tsx b/app/soapbox/features/soapbox-config/index.tsx index 39c859ef1..315e78adf 100644 --- a/app/soapbox/features/soapbox-config/index.tsx +++ b/app/soapbox/features/soapbox-config/index.tsx @@ -1,6 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import React, { useState, useEffect, useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import { updateSoapboxConfig } from 'soapbox/actions/admin'; import { uploadMedia } from 'soapbox/actions/media'; @@ -25,14 +26,11 @@ import ThemeSelector from 'soapbox/features/ui/components/theme-selector'; import { useAppSelector, useAppDispatch, useFeatures } from 'soapbox/hooks'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; -import ColorWithPicker from './components/color-with-picker'; import CryptoAddressInput from './components/crypto-address-input'; import FooterLinkInput from './components/footer-link-input'; import PromoPanelInput from './components/promo-panel-input'; import SitePreview from './components/site-preview'; -import type { ColorChangeHandler, ColorResult } from 'react-color'; - const messages = defineMessages({ heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, saved: { id: 'soapbox_config.saved', defaultMessage: 'Soapbox config saved!' }, @@ -59,7 +57,6 @@ const messages = defineMessages({ }); type ValueGetter = (e: React.ChangeEvent) => any; -type ColorValueGetter = (color: ColorResult, event: React.ChangeEvent) => any; type Template = ImmutableMap; type ConfigPath = Array; type ThemeChangeHandler = (theme: string) => void; @@ -72,6 +69,7 @@ const templates: Record = { const SoapboxConfig: React.FC = () => { const intl = useIntl(); + const history = useHistory(); const dispatch = useAppDispatch(); const features = useFeatures(); @@ -84,6 +82,8 @@ const SoapboxConfig: React.FC = () => { const [rawJSON, setRawJSON] = useState(JSON.stringify(initialData, null, 2)); const [jsonValid, setJsonValid] = useState(true); + const navigateToThemeEditor = () => history.push('/soapbox/admin/theme'); + const soapbox = useMemo(() => { return normalizeSoapboxConfig(data); }, [data]); @@ -122,12 +122,6 @@ const SoapboxConfig: React.FC = () => { }; }; - const handleColorChange = (path: ConfigPath, getValue: ColorValueGetter): ColorChangeHandler => { - return (color, event) => { - setConfig(path, getValue(color, event)); - }; - }; - const handleFileChange = (path: ConfigPath): React.ChangeEventHandler => { return e => { const data = new FormData(); @@ -214,21 +208,10 @@ const SoapboxConfig: React.FC = () => { /> - }> - color.hex)} - /> - - - }> - color.hex)} - /> - + } + onClick={navigateToThemeEditor} + /> From faf513c0495a39a71ab4aea11cf9fa7ebd5e8bb9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 18:31:56 -0600 Subject: [PATCH 07/16] Add Slider component (based on video volume slider) --- app/soapbox/components/ui/index.ts | 1 + app/soapbox/components/ui/slider/slider.tsx | 124 ++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 app/soapbox/components/ui/slider/slider.tsx diff --git a/app/soapbox/components/ui/index.ts b/app/soapbox/components/ui/index.ts index 0dd6fff52..c3f148833 100644 --- a/app/soapbox/components/ui/index.ts +++ b/app/soapbox/components/ui/index.ts @@ -41,6 +41,7 @@ export { default as PhoneInput } from './phone-input/phone-input'; export { default as ProgressBar } from './progress-bar/progress-bar'; export { default as RadioButton } from './radio-button/radio-button'; export { default as Select } from './select/select'; +export { default as Slider } from './slider/slider'; export { default as Spinner } from './spinner/spinner'; export { default as Stack } from './stack/stack'; export { default as Streamfield } from './streamfield/streamfield'; diff --git a/app/soapbox/components/ui/slider/slider.tsx b/app/soapbox/components/ui/slider/slider.tsx new file mode 100644 index 000000000..65cf94a9f --- /dev/null +++ b/app/soapbox/components/ui/slider/slider.tsx @@ -0,0 +1,124 @@ +import throttle from 'lodash/throttle'; +import React, { useRef } from 'react'; + +type Point = { x: number, y: number }; + +interface ISlider { + /** Value between 0 and 1. */ + value: number + /** Callback when the value changes. */ + onChange(value: number): void +} + +/** Draggable slider component. */ +const Slider: React.FC = ({ value, onChange }) => { + const node = useRef(null); + + const handleMouseDown: React.MouseEventHandler = e => { + document.addEventListener('mousemove', handleMouseSlide, true); + document.addEventListener('mouseup', handleMouseUp, true); + document.addEventListener('touchmove', handleMouseSlide, true); + document.addEventListener('touchend', handleMouseUp, true); + + handleMouseSlide(e); + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseSlide, true); + document.removeEventListener('mouseup', handleMouseUp, true); + document.removeEventListener('touchmove', handleMouseSlide, true); + document.removeEventListener('touchend', handleMouseUp, true); + }; + + const handleMouseSlide = throttle(e => { + if (node.current) { + const { x } = getPointerPosition(node.current, e); + + if (!isNaN(x)) { + let slideamt = x; + + if (x > 1) { + slideamt = 1; + } else if (x < 0) { + slideamt = 0; + } + + onChange(slideamt); + } + } + }, 60); + + return ( +
+
+
+ +
+ ); +}; + +const findElementPosition = (el: HTMLElement) => { + let box; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0, + }; + } + + const docEl = document.documentElement; + const body = document.body; + + const clientLeft = docEl.clientLeft || body.clientLeft || 0; + const scrollLeft = window.pageXOffset || body.scrollLeft; + const left = (box.left + scrollLeft) - clientLeft; + + const clientTop = docEl.clientTop || body.clientTop || 0; + const scrollTop = window.pageYOffset || body.scrollTop; + const top = (box.top + scrollTop) - clientTop; + + return { + left: Math.round(left), + top: Math.round(top), + }; +}; + + +const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => { + const box = findElementPosition(el); + const boxW = el.offsetWidth; + const boxH = el.offsetHeight; + const boxY = box.top; + const boxX = box.left; + + let pageY = event.pageY; + let pageX = event.pageX; + + if (event.changedTouches) { + pageX = event.changedTouches[0].pageX; + pageY = event.changedTouches[0].pageY; + } + + return { + y: Math.max(0, Math.min(1, (pageY - boxY) / boxH)), + x: Math.max(0, Math.min(1, (pageX - boxX) / boxW)), + }; +}; + +export default Slider; \ No newline at end of file From 2505f622e2c65212a09b4eca15b3951897e5d745 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 19:02:02 -0600 Subject: [PATCH 08/16] ThemeEditor: add hue sliders --- .../theme-editor/components/palette.tsx | 34 +++++++++++++++---- app/soapbox/utils/theme.ts | 15 ++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/theme-editor/components/palette.tsx b/app/soapbox/features/theme-editor/components/palette.tsx index cbadf019a..1d8060a5c 100644 --- a/app/soapbox/features/theme-editor/components/palette.tsx +++ b/app/soapbox/features/theme-editor/components/palette.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; -import { HStack } from 'soapbox/components/ui'; +import { HStack, Stack, Slider } from 'soapbox/components/ui'; +import { usePrevious } from 'soapbox/hooks'; import { compareId } from 'soapbox/utils/comparators'; +import { hueShift } from 'soapbox/utils/theme'; import Color from './color'; @@ -18,6 +20,9 @@ interface IPalette { const Palette: React.FC = ({ palette, onChange }) => { const tints = Object.keys(palette).sort(compareId); + const [hue, setHue] = useState(0); + const lastHue = usePrevious(hue); + const handleChange = (tint: string) => { return (color: string) => { onChange({ @@ -27,12 +32,27 @@ const Palette: React.FC = ({ palette, onChange }) => { }; }; + useEffect(() => { + const delta = hue - (lastHue || 0); + + const adjusted = Object.entries(palette).reduce((result, [tint, hex]) => { + result[tint] = hueShift(hex, delta * 360); + return result; + }, {}); + + onChange(adjusted); + }, [hue]); + return ( - - {tints.map(tint => ( - - ))} - + + + {tints.map(tint => ( + + ))} + + + + ); }; diff --git a/app/soapbox/utils/theme.ts b/app/soapbox/utils/theme.ts index 4f6126c17..86b2c4a01 100644 --- a/app/soapbox/utils/theme.ts +++ b/app/soapbox/utils/theme.ts @@ -116,3 +116,18 @@ export const colorsToCss = (colors: TailwindColorPalette): string => { export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => { return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette); }; + +const hexToHsl = (hex: string): Hsl | null => { + const rgb = hexToRgb(hex); + return rgb ? rgbToHsl(rgb) : null; +}; + +export const hueShift = (hex: string, delta: number): string => { + const { h, s, l } = hexToHsl(hex)!; + + return hslToHex({ + h: (h + delta) % 360, + s, + l, + }); +}; \ No newline at end of file From 69a9748b3d49a532f4a56b591271614e1043a0aa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 19:39:54 -0600 Subject: [PATCH 09/16] ThemeEditor: allow resetting the theme (that was harder than I expected) --- .../theme-editor/components/palette.tsx | 7 +++++- app/soapbox/features/theme-editor/index.tsx | 22 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/theme-editor/components/palette.tsx b/app/soapbox/features/theme-editor/components/palette.tsx index 1d8060a5c..95eb20e50 100644 --- a/app/soapbox/features/theme-editor/components/palette.tsx +++ b/app/soapbox/features/theme-editor/components/palette.tsx @@ -14,10 +14,11 @@ interface ColorGroup { interface IPalette { palette: ColorGroup, onChange: (palette: ColorGroup) => void, + resetKey?: string, } /** Editable color palette. */ -const Palette: React.FC = ({ palette, onChange }) => { +const Palette: React.FC = ({ palette, onChange, resetKey }) => { const tints = Object.keys(palette).sort(compareId); const [hue, setHue] = useState(0); @@ -43,6 +44,10 @@ const Palette: React.FC = ({ palette, onChange }) => { onChange(adjusted); }, [hue]); + useEffect(() => { + setHue(0); + }, [resetKey]); + return ( diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index c1b8c97a1..efedfce3d 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { v4 as uuidv4 } from 'uuid'; import { updateSoapboxConfig } from 'soapbox/actions/admin'; import { getHost } from 'soapbox/actions/instance'; @@ -30,6 +31,7 @@ const ThemeEditor: React.FC = () => { const [colors, setColors] = useState(soapbox.colors.toJS() as any); const [submitting, setSubmitting] = useState(false); + const [resetKey, setResetKey] = useState(uuidv4()); const updateColors = (key: string) => { return (newColors: ColorGroup) => { @@ -43,6 +45,11 @@ const ThemeEditor: React.FC = () => { }; }; + const resetTheme = () => { + setResetKey(uuidv4()); + setTimeout(() => setColors(soapbox.colors.toJS() as any)); + }; + const updateTheme = async () => { const params = rawConfig.set('colors', colors).toJS(); await dispatch(updateSoapboxConfig(params)); @@ -69,40 +76,50 @@ const ThemeEditor: React.FC = () => { label='Primary' palette={colors.primary} onChange={updateColors('primary')} + resetKey={resetKey} /> + + @@ -116,13 +133,14 @@ interface IPaletteListItem { label: React.ReactNode, palette: ColorGroup, onChange: (palette: ColorGroup) => void, + resetKey?: string, } /** Palette editor inside a ListItem. */ -const PaletteListItem: React.FC = ({ label, palette, onChange }) => { +const PaletteListItem: React.FC = ({ label, palette, onChange, resetKey }) => { return ( {label}
}> - + ); }; From b15871aaa814ba604ddc0e960f49cdce206e6d68 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 20:15:03 -0600 Subject: [PATCH 10/16] utils/download: take a string instead of AxiosResponse --- app/soapbox/features/admin/tabs/dashboard.tsx | 12 ++++++------ .../features/event/components/event-header.tsx | 4 ++-- app/soapbox/utils/download.ts | 6 ++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/admin/tabs/dashboard.tsx b/app/soapbox/features/admin/tabs/dashboard.tsx index dd0f9f1bd..aa127f5a1 100644 --- a/app/soapbox/features/admin/tabs/dashboard.tsx +++ b/app/soapbox/features/admin/tabs/dashboard.tsx @@ -21,22 +21,22 @@ const Dashboard: React.FC = () => { const account = useOwnAccount(); const handleSubscribersClick: React.MouseEventHandler = e => { - dispatch(getSubscribersCsv()).then((response) => { - download(response, 'subscribers.csv'); + dispatch(getSubscribersCsv()).then(({ data }) => { + download(data, 'subscribers.csv'); }).catch(() => {}); e.preventDefault(); }; const handleUnsubscribersClick: React.MouseEventHandler = e => { - dispatch(getUnsubscribersCsv()).then((response) => { - download(response, 'unsubscribers.csv'); + dispatch(getUnsubscribersCsv()).then(({ data }) => { + download(data, 'unsubscribers.csv'); }).catch(() => {}); e.preventDefault(); }; const handleCombinedClick: React.MouseEventHandler = e => { - dispatch(getCombinedCsv()).then((response) => { - download(response, 'combined.csv'); + dispatch(getCombinedCsv()).then(({ data }) => { + download(data, 'combined.csv'); }).catch(() => {}); e.preventDefault(); }; diff --git a/app/soapbox/features/event/components/event-header.tsx b/app/soapbox/features/event/components/event-header.tsx index adfb2dc32..5e0226e2c 100644 --- a/app/soapbox/features/event/components/event-header.tsx +++ b/app/soapbox/features/event/components/event-header.tsx @@ -102,8 +102,8 @@ const EventHeader: React.FC = ({ status }) => { }; const handleExportClick = () => { - dispatch(fetchEventIcs(status.id)).then((response) => { - download(response, 'calendar.ics'); + dispatch(fetchEventIcs(status.id)).then(({ data }) => { + download(data, 'calendar.ics'); }).catch(() => {}); }; diff --git a/app/soapbox/utils/download.ts b/app/soapbox/utils/download.ts index c877bee25..f756490c9 100644 --- a/app/soapbox/utils/download.ts +++ b/app/soapbox/utils/download.ts @@ -1,9 +1,7 @@ -import type { AxiosResponse } from 'axios'; - /** Download the file from the response instead of opening it in a tab. */ // https://stackoverflow.com/a/53230807 -export const download = (response: AxiosResponse, filename: string) => { - const url = URL.createObjectURL(new Blob([response.data])); +export const download = (data: string, filename: string): void => { + const url = URL.createObjectURL(new Blob([data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', filename); From c624fbcba3a64bbc8a25f34d37380c7ff2460bfc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 20:16:05 -0600 Subject: [PATCH 11/16] ThemeEditor: allow exporting a theme --- app/soapbox/features/theme-editor/index.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index efedfce3d..c466bec6a 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -8,13 +8,16 @@ import snackbar from 'soapbox/actions/snackbar'; import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; import List, { ListItem } from 'soapbox/components/list'; import { Button, Column, Form, FormActions } from 'soapbox/components/ui'; +import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; +import { download } from 'soapbox/utils/download'; import Palette, { ColorGroup } from './components/palette'; const messages = defineMessages({ title: { id: 'admin.theme.title', defaultMessage: 'Theme' }, saved: { id: 'theme_editor.saved', defaultMessage: 'Theme updated!' }, + export: { id: 'theme_editor.export', defaultMessage: 'Export theme' }, }); interface IThemeEditor { @@ -55,6 +58,11 @@ const ThemeEditor: React.FC = () => { await dispatch(updateSoapboxConfig(params)); }; + const exportTheme = () => { + const data = JSON.stringify(colors, null, 2); + download(data, 'theme.json'); + }; + const handleSubmit = async() => { setSubmitting(true); @@ -116,6 +124,12 @@ const ThemeEditor: React.FC = () => { + From 716cc311c721ab36e0954cc7df7055c9af6ab9a9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 20:29:25 -0600 Subject: [PATCH 12/16] ThemeEditor: allow importing a theme --- app/soapbox/features/theme-editor/index.tsx | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index c466bec6a..afe0ef51e 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { v4 as uuidv4 } from 'uuid'; @@ -10,6 +10,7 @@ import List, { ListItem } from 'soapbox/components/list'; import { Button, Column, Form, FormActions } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; +import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import { download } from 'soapbox/utils/download'; import Palette, { ColorGroup } from './components/palette'; @@ -18,6 +19,8 @@ const messages = defineMessages({ title: { id: 'admin.theme.title', defaultMessage: 'Theme' }, saved: { id: 'theme_editor.saved', defaultMessage: 'Theme updated!' }, export: { id: 'theme_editor.export', defaultMessage: 'Export theme' }, + import: { id: 'theme_editor.import', defaultMessage: 'Import theme' }, + importSuccess: { id: 'theme_editor.import_success', defaultMessage: 'Theme was successfully imported!' }, }); interface IThemeEditor { @@ -36,6 +39,8 @@ const ThemeEditor: React.FC = () => { const [submitting, setSubmitting] = useState(false); const [resetKey, setResetKey] = useState(uuidv4()); + const fileInput = useRef(null); + const updateColors = (key: string) => { return (newColors: ColorGroup) => { setColors({ @@ -63,6 +68,23 @@ const ThemeEditor: React.FC = () => { download(data, 'theme.json'); }; + const importTheme = () => { + fileInput.current?.click(); + }; + + const handleSelectFile: React.ChangeEventHandler = async (e) => { + const file = e.target.files?.item(0); + + if (file) { + const text = await file.text(); + const json = JSON.parse(text); + const colors = normalizeSoapboxConfig({ colors: json }).colors.toJS(); + + setColors(colors); + dispatch(snackbar.success(intl.formatMessage(messages.importSuccess))); + } + }; + const handleSubmit = async() => { setSubmitting(true); @@ -126,8 +148,13 @@ const ThemeEditor: React.FC = () => { + + ); }; From ab70af31cd0a9e96b9220c76b3a8a4b19b86d5ea Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 20:48:02 -0600 Subject: [PATCH 13/16] ThemeEditor: restore default theme --- app/soapbox/features/theme-editor/index.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index afe0ef51e..2cf081ea4 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -18,6 +18,7 @@ import Palette, { ColorGroup } from './components/palette'; const messages = defineMessages({ title: { id: 'admin.theme.title', defaultMessage: 'Theme' }, saved: { id: 'theme_editor.saved', defaultMessage: 'Theme updated!' }, + restore: { id: 'theme_editor.restore', defaultMessage: 'Restore default theme' }, export: { id: 'theme_editor.export', defaultMessage: 'Export theme' }, import: { id: 'theme_editor.import', defaultMessage: 'Import theme' }, importSuccess: { id: 'theme_editor.import_success', defaultMessage: 'Theme was successfully imported!' }, @@ -53,9 +54,13 @@ const ThemeEditor: React.FC = () => { }; }; - const resetTheme = () => { + const setTheme = (theme: any) => { setResetKey(uuidv4()); - setTimeout(() => setColors(soapbox.colors.toJS() as any)); + setTimeout(() => setColors(theme)); + }; + + const resetTheme = () => { + setTheme(soapbox.colors.toJS() as any); }; const updateTheme = async () => { @@ -63,6 +68,11 @@ const ThemeEditor: React.FC = () => { await dispatch(updateSoapboxConfig(params)); }; + const restoreDefaultTheme = () => { + const colors = normalizeSoapboxConfig({ brandColor: '#0482d8' }).colors.toJS(); + setTheme(colors); + }; + const exportTheme = () => { const data = JSON.stringify(colors, null, 2); download(data, 'theme.json'); @@ -80,7 +90,7 @@ const ThemeEditor: React.FC = () => { const json = JSON.parse(text); const colors = normalizeSoapboxConfig({ colors: json }).colors.toJS(); - setColors(colors); + setTheme(colors); dispatch(snackbar.success(intl.formatMessage(messages.importSuccess))); } }; @@ -148,6 +158,10 @@ const ThemeEditor: React.FC = () => { Date: Sat, 17 Dec 2022 21:13:30 -0600 Subject: [PATCH 14/16] ThemeEditor: configure single-color items --- app/soapbox/features/theme-editor/index.tsx | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/soapbox/features/theme-editor/index.tsx b/app/soapbox/features/theme-editor/index.tsx index 2cf081ea4..0bd4741ff 100644 --- a/app/soapbox/features/theme-editor/index.tsx +++ b/app/soapbox/features/theme-editor/index.tsx @@ -9,12 +9,15 @@ import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; import List, { ListItem } from 'soapbox/components/list'; import { Button, Column, Form, FormActions } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; +import ColorWithPicker from 'soapbox/features/soapbox-config/components/color-with-picker'; import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import { download } from 'soapbox/utils/download'; import Palette, { ColorGroup } from './components/palette'; +import type { ColorChangeHandler } from 'react-color'; + const messages = defineMessages({ title: { id: 'admin.theme.title', defaultMessage: 'Theme' }, saved: { id: 'theme_editor.saved', defaultMessage: 'Theme updated!' }, @@ -54,6 +57,15 @@ const ThemeEditor: React.FC = () => { }; }; + const updateColor = (key: string) => { + return (hex: string) => { + setColors({ + ...colors, + [key]: hex, + }); + }; + }; + const setTheme = (theme: any) => { setResetKey(uuidv4()); setTimeout(() => setColors(theme)); @@ -153,6 +165,30 @@ const ThemeEditor: React.FC = () => { onChange={updateColors('danger')} resetKey={resetKey} /> + + + + + + + + @@ -209,4 +245,27 @@ const PaletteListItem: React.FC = ({ label, palette, onChange, ); }; +interface IColorListItem { + label: React.ReactNode, + value: string, + onChange: (hex: string) => void, +} + +/** Single-color picker. */ +const ColorListItem: React.FC = ({ label, value, onChange }) => { + const handleChange: ColorChangeHandler = (color, _e) => { + onChange(color.hex); + }; + + return ( + + + + ); +}; + export default ThemeEditor; \ No newline at end of file From 34ebe9bd57523395eeae9fe62915d9a3c7faa71c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 17 Dec 2022 21:25:20 -0600 Subject: [PATCH 15/16] ThemeEditor: improve layout, improve color of slider --- app/soapbox/components/ui/slider/slider.tsx | 2 +- app/soapbox/features/theme-editor/index.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/ui/slider/slider.tsx b/app/soapbox/components/ui/slider/slider.tsx index 65cf94a9f..009e7db5e 100644 --- a/app/soapbox/components/ui/slider/slider.tsx +++ b/app/soapbox/components/ui/slider/slider.tsx @@ -57,7 +57,7 @@ const Slider: React.FC = ({ value, onChange }) => { onMouseDown={handleMouseDown} ref={node} > -
+
= () => { onChange={updateColors('danger')} resetKey={resetKey} /> + + Date: Sat, 17 Dec 2022 23:18:19 -0600 Subject: [PATCH 16/16] changelog: theme editor --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b17e16bf9..cc516b8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Compatibility: added compatibility with Truth Social, Fedibird, Pixelfed, Akkoma, and Glitch. - Developers: added Test feed, Service Worker debugger, and Network Error preview. - Reports: display server rules in reports. Let users select rule violations when submitting a report. +- Admin: added Theme Editor, a GUI for customizing the color scheme. - Admin: custom badges. Admins can add non-federating badges to any user's profile (on Rebased, Pleroma). - Admin: consolidated user dropdown actions (verify/suggest/etc) into a unified "Moderate User" modal.