diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..4157440bf --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 01c6314c9..cdf22dd2e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,7 @@ deps: cache: <<: *cache policy: push + interruptible: true danger: stage: test @@ -33,6 +34,7 @@ danger: - export CI_MERGE_REQUEST_IID=${CI_OPEN_MERGE_REQUESTS#*!} - npx danger ci allow_failure: true + interruptible: true lint-js: stage: test @@ -45,6 +47,7 @@ lint-js: - "**/*.tsx" - ".eslintignore" - ".eslintrc.js" + interruptible: true lint-sass: stage: test @@ -54,6 +57,7 @@ lint-sass: - "**/*.scss" - "**/*.css" - ".stylelintrc.json" + interruptible: true jest: stage: test @@ -76,6 +80,7 @@ jest: coverage_report: coverage_format: cobertura path: .coverage/cobertura-coverage.xml + interruptible: true nginx-test: stage: test @@ -85,6 +90,7 @@ nginx-test: only: changes: - "installation/mastodon.conf" + interruptible: true build-production: stage: test @@ -94,6 +100,7 @@ build-production: artifacts: paths: - static + interruptible: true docs-deploy: stage: deploy @@ -107,6 +114,7 @@ docs-deploy: - develop changes: - "docs/**/*" + interruptible: true # Supposed to fail when translations are outdated, instead always passes # @@ -127,6 +135,7 @@ review: script: - npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub allow_failure: true + interruptible: true pages: stage: deploy @@ -142,6 +151,7 @@ pages: only: refs: - develop + interruptible: true docker: stage: deploy @@ -157,4 +167,5 @@ docker: - docker push $CI_REGISTRY_IMAGE only: refs: - - develop \ No newline at end of file + - develop + interruptible: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3850599..90a3507f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,87 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Editing: ability to edit posts and view edit history (on Rebased, Pleroma, and Mastodon). +- Events: ability to create, view, and comment on Events (on Rebased). +- Onboarding: display an introduction wizard to newly registered accounts. +- Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin). +- Posts: ability to view quotes of a post (on Rebased). +- Posts: hover the "replying to" line to see a preview card of the parent post. +- Chats: ability to leave a chat (on Rebased, Truth Social). +- Chats: ability to disable chats for yourself. +- Layout: added right-to-left support for Arabic, Hebrew, Persian, and Central Kurdish languages. +- Composer: support custom emoji categories. +- Search: ability to search posts from a specific account (on Pleroma, Rebased). +- Theme: auto-detect system theme by default. +- Profile: remove a specific user from your followers (on Rebased, Mastodon). +- Suggestions: ability to view all suggested profiles. +- 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. +- i18n: updated translations for Italian, Polish, Arabic, Hebrew, and German. + +### Changed +- UI: the whole UI has been overhauled both inside and out. 97% of the codebase has been rewritten to TypeScript, and a new component library has been introduced with Tailwind CSS. +- Chats: redesigned chats. Includes an improved desktop UI, unified chat widget, expanding textarea, and autosuggestions. +- Lists: ability to edit and delete a list. +- Settings: unified settings under one path with separate sections. +- Posts: changed the thumbs-up icon to a heart. +- Posts: move instance favicon beside username instead of post timestamp. +- Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design. +- Posts: redesigned interaction counters to use text instead of icons. +- Profile: overhauled user profiles to be consistent with the rest of the UI. +- Composer: move emoji button alongside other composer buttons, add numerical counter. +- Birthdays: move today's birthdays out of notifications into right sidebar. +- Performance: improve scrolling/navigation between feeds by using a virtual window library. +- Admin: reorganize UI into 3-column layout. +- Admin: include external link to frontend repo for the running commit. + +### Removed +- Theme: Halloween theme. +- Settings: advanced notification settings. +- Settings: dyslexic mode. +- Settings: demetricator. +- Profile: ability to set and view private notes on an account. +- Feeds: per-feed filters for replies, media, etc. +- Backup and export functionality (for now). +- Posts: hide non-emoji images embedded in post content. + +### Security +- Glitch Social: fixed XSS vulnerability on Glitch Social where custom emojis could be exploited to embed a script tag. + +## [2.0.0] - 2022-05-01 +### Added +- Quote Posting: repost with comment on Fedibird and Rebased. +- Profile: ability to feature other users on your profile (on Rebased, Mastodon). +- Profile: ability to add location to the user's profile (on Rebased, Truth Social). +- Birthdays: ability to add a birthday to your profile (on Rebased, Pleroma). +- Birthdays: support for age-gated registration if configured by the admin (on Rebased, Pleroma). +- Birthdays: display today's birthdays in notifications. +- Notifications: added unread badge to favicon when user has notifications. +- Notifications: display full attachments in notifications instead of links. +- Search: added a dedicated search page with prefilled suggestions. +- Compatibility: improved support for Mastodon, added support for Mitra. +- Ethereum: Metamask sign-in with Mitra. +- i18n: added Shavian alphabet (`en-Shaw`) transliteration. +- i18n: added Icelandic translation. + +### Changed +- Feeds: added gaps between posts in feeds. +- Feeds: automatically load new posts when scrolled to the top of the feed. +- Layout: improved design of top navigation bar. +- Layout: add left sidebar navigation. +- Icons: replaced Fork Awesome icons with Tabler icons. +- Posts: moved mentions out of the post content into an area above the post for replies (on Pleroma and Rebased - Mastodon falls back to the old behavior). +- Composer: use graphical ring counter for character count. + +### Fixed +- Multi-Account: fix switching between profiles on different servers with the same local username. + ## [1.3.0] - 2021-07-02 ### Changed - Layout: show right sidebar on all pages. diff --git a/README.md b/README.md index 754e68d4c..677995a86 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,80 @@ -# Soapbox - ![Soapbox Screenshot](soapbox-screenshot.png) -**Soapbox** is a frontend for Mastodon and Pleroma with a focus on custom branding and ease of use. +**Soapbox** is customizable open-source software that puts the power of social media in the hands of the people. Feature-rich and hyper-focused on providing a user experience to rival Big Tech, Soapbox is already home to some of the biggest alternative social platforms. -## Try it out +# On The Fediverse -Visit https://fe.soapbox.pub/ and point it to your favorite instance. +You may have heard of **Mastodon**. Soapbox builds upon what Mastodon made great to make something even better. -## :rocket: Deploy on Pleroma +You can run **Mastodon+Soapbox**, **Rebased+Soapbox**, and more. -Installing Soapbox on an existing Pleroma server is extremely easy. -Just ssh into the server and download a .zip of the latest build: +Soapbox is the **frontend** (what users see) while Mastodon is the **backend** (data, APIs). You can mix-and-match in the Fediverse ecosystem. -```sh -curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox.zip -``` +> 💡 If you're starting a new server, we highly recommend **Rebased+Soapbox**. Rebased is our custom-built backend just for Soapbox, providing important new features such as **quote posting** and **chats**. +> +> See: [Installing Rebased+Soapbox](https://soapbox.pub/install/) -Then unpack it into Pleroma's `instance` directory: +# Try It Out -```sh -busybox unzip soapbox.zip -o -d /opt/pleroma/instance -``` +Want to give Soapbox a shot? Here are some suggested servers: -**That's it!** :tada: -**Soapbox is installed.** -The change will take effect immediately, just refresh your browser tab. -It's not necessary to restart the Pleroma service. +- [gleasonator.com](https://gleasonator.com/) - operated by the lead developer of Soapbox +- [social.teci.world](https://social.teci.world/) - free speech server run by a Soapbox contributor +- [spinster.xyz](https://spinster.xyz/) - one of the largest feminist communities on the internet +- [poa.st](https://poa.st/) - the largest Soapbox server on the network -***For OTP releases,*** *unpack to /var/lib/pleroma instead.* +Want to use Soapbox against **any existing Mastodon/Pleroma server?** Try: -To remove Soapbox and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files). +- [fe.soapbox.pub](https://fe.soapbox.pub) - enter your server's domain name to use Soapbox on any server! -## :elephant: Deploy on Mastodon +# 🚀 Starting Your Own Server -See [Installing Soapbox over Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/). +Starting your own server is one of the best ways to have freedom online! We recommend installing **Rebased+Soapbox**. -## How does it work? +See here for a detailed setup guide: [Installing Rebased+Soapbox](https://soapbox.pub/install/) -Soapbox is a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) that runs entirely in the browser with JavaScript. +# Adding Soapbox to an Existing Server -It has a single HTML file, `index.html`, responsible only for loading the required JavaScript and CSS. -It interacts with the backend through [XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). +Already have a server? No problem — it is still possible to use Soapbox. -Here is a simplified example with Nginx: +- [Deploying on Pleroma](https://docs.soapbox.pub/frontend/installing/#install-soapbox) +- [Deploying on Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/) -```nginx -location /api { - proxy_pass http://backend; -} +> 💡 If using Pleroma, it's recommended to [upgrade it to Rebased](https://gitlab.com/-/snippets/2411739). This comes with better support and many new features, helping you get the most out of Soapbox. -location / { - root /opt/soapbox; - try_files $uri index.html; -} -``` +# Developing Soapbox -(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) for a full example.) +tl;dr — `git clone`, `yarn`, and `yarn dev`. -Soapbox incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/), [Pleroma API](https://api.pleroma.social/), and more. -It detects features supported by the backend to provide the right experience for the backend. +For detailed guides, see these pages: -# Running locally +1. [Soapbox local development](https://docs.soapbox.pub/frontend/development/running-locally/) +2. [yarn commands](https://docs.soapbox.pub/frontend/development/yarn-commands/) +3. [How it works](https://docs.soapbox.pub/frontend/development/how-it-works/) +4. [Environment variables](https://docs.soapbox.pub/frontend/development/local-config/) +5. [Developing a backend](https://docs.soapbox.pub/frontend/development/developing-backend/) -To get it running, just clone the repo: - -```sh -git clone https://gitlab.com/soapbox-pub/soapbox.git -cd soapbox -``` - -Ensure that Node.js and Yarn are installed, then install dependencies: - -```sh -yarn -``` - -Finally, run the dev server: - -```sh -yarn dev -``` - -**That's it!** :tada: - -It will serve at `http://localhost:3036` by default. - -You should see an input box - just enter the domain name of your instance to log in. - -Tip: you can even enter a local instance like `http://localhost:3000`! - -### Troubleshooting: `ERROR: NODE_ENV must be set` - -Create a `.env` file if you haven't already. - -```sh -cp .env.example .env -``` - -And ensure that it contains `NODE_ENV=development`. -Try again. - -### Troubleshooting: it's not working! - -Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.tool-versions). -If they don't match, try installing [asdf](https://asdf-vm.com/). - -## Local Dev Configuration - -The following configuration variables are supported supported in local development. -Edit `.env` to set them. - -All configuration is optional, except `NODE_ENV`. - -#### `NODE_ENV` - -The Node environment. -Soapbox checks for the following options: - -- `development` - What you should use while developing Soapbox. -- `production` - Use when compiling to deploy to a live server. -- `test` - Use when running automated tests. - -#### `BACKEND_URL` - -URL to the backend server. -Can be http or https, and can include a port. -For https, be sure to also set `PROXY_HTTPS_INSECURE=true`. - -**Default:** `http://localhost:4000` - -#### `PROXY_HTTPS_INSECURE` - -Allows using an HTTPS backend if set to `true`. - -This is needed if `BACKEND_URL` is set to an `https://` value. -[More info](https://stackoverflow.com/a/48624590/8811886). - -**Default:** `false` - -# Yarn Commands - -The following commands are supported. -You must set `NODE_ENV` to use these commands. -To do so, you can add the following line to your `.env` file: - -```sh -NODE_ENV=development -``` - -#### Local dev server -- `yarn dev` - Run the local dev server. - -#### Building -- `yarn build` - Compile without a dev server, into `/static` directory. - -#### Translations -- `yarn manage:translations` - Normalizes translation files. Should always be run after editing i18n strings. - -#### Tests -- `yarn test:all` - Runs all tests and linters. - -- `yarn test` - Runs Jest for frontend unit tests. - -- `yarn lint` - Runs all linters. - -- `yarn lint:js` - Runs only JavaScript linter. - -- `yarn lint:sass` - Runs only SASS linter. - -# Contributing +## Contributing We welcome contributions to this project. To contribute, see [Contributing to Soapbox](docs/contributing.md). -# Customization +Translators can help by providing [translations through Weblate](https://hosted.weblate.org/projects/soapbox-pub/soapbox/). +Native speakers from all around the world are welcome! -Soapbox supports customization of the user interface, to allow per-instance branding and other features. -Some examples include: +# Project Philosophy -- Instance name -- Site logo -- Favicon -- About page -- Terms of Service page -- Privacy Policy page -- Copyright Policy (DMCA) page -- Promo panel list items, e.g. blog site link -- Soapbox extensions, e.g. Patron module -- Default settings, e.g. default theme +Soapbox was born out of the need to build independent platforms with **a unique identity and brand**. -More details can be found in [Customizing Soapbox](docs/customization.md). +This is in contrast to Mastodon's idea, where all servers are called "Mastodon" and use the Mastodon colors and logo. Users won't see the word "Soapbox" throughout the UI, they'll see the name of **your website** and your logo. To facilitate this, Soapbox has a robust customization UI and integrated moderation tools. Large servers are a priority. + +One disadvantage of this approach is that it does not help the software spread. Some of the biggest servers on the network and running Soapbox and people don't even know it! # License & Credits -Soapbox is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend. - -- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0. +© Alex Gleason & other Soapbox contributors +© Eugen Rochko & other Mastodon contributors +© Gab AI, Inc. Soapbox is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -205,8 +83,8 @@ the Free Software Foundation, either version 3 of the License, or Soapbox is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License -along with Soapbox. If not, see . +along with Soapbox. If not, see . diff --git a/app/assets/sounds/LICENSE.md b/app/assets/sounds/LICENSE.md new file mode 100644 index 000000000..42d569b40 --- /dev/null +++ b/app/assets/sounds/LICENSE.md @@ -0,0 +1,6 @@ +# Sound licenses + +- `chat.mp3` +- `chat.oga` + +© [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by-sa/4.0/). 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/components/radio.tsx b/app/soapbox/components/radio.tsx new file mode 100644 index 000000000..ef204b06f --- /dev/null +++ b/app/soapbox/components/radio.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import List, { ListItem } from './list'; + +interface IRadioGroup { + onChange: React.ChangeEventHandler + children: React.ReactElement<{ onChange: React.ChangeEventHandler }>[] +} + +const RadioGroup = ({ onChange, children }: IRadioGroup) => { + const childrenWithProps = React.Children.map(children, child => + React.cloneElement(child, { onChange }), + ); + + return {childrenWithProps}; +}; + +interface IRadioItem { + label: React.ReactNode, + hint?: React.ReactNode, + value: string, + checked: boolean, + onChange?: React.ChangeEventHandler, +} + +const RadioItem: React.FC = ({ label, hint, checked = false, onChange, value }) => { + return ( + + + + ); +}; + +export { + RadioGroup, + RadioItem, +}; \ No newline at end of file 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..009e7db5e --- /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 diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index a47eaa6d1..2beaa516c 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -41,6 +41,7 @@ import { useInstance, } from 'soapbox/hooks'; import MESSAGES from 'soapbox/locales/messages'; +import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import { queryClient } from 'soapbox/queries/client'; import { useCachedLocationHandler } from 'soapbox/utils/redirect'; import { generateThemeCss } from 'soapbox/utils/theme'; @@ -267,8 +268,9 @@ const SoapboxHead: React.FC = ({ children }) => { const settings = useSettings(); const soapboxConfig = useSoapboxConfig(); + const demo = !!settings.get('demo'); const darkMode = useTheme() === 'dark'; - const themeCss = generateThemeCss(soapboxConfig); + const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig); const bodyClass = classNames('bg-white dark:bg-gray-800 text-base h-full', { 'no-reduce-motion': !settings.get('reduceMotion'), diff --git a/app/soapbox/features/account/components/header.tsx b/app/soapbox/features/account/components/header.tsx index ee4304e4e..e269b1668 100644 --- a/app/soapbox/features/account/components/header.tsx +++ b/app/soapbox/features/account/components/header.tsx @@ -526,11 +526,11 @@ const Header: React.FC = ({ account }) => { }; const renderMessageButton = () => { - if (features.chatsWithFollowers) { // Truth Social - if (!ownAccount || !account || account.id === ownAccount?.id) { - return null; - } + if (!ownAccount || !account || account.id === ownAccount?.id) { + return null; + } + if (features.chatsWithFollowers) { // Truth Social const canChat = account.relationship?.followed_by; if (!canChat) { return null; diff --git a/app/soapbox/features/admin/components/dashcounter.tsx b/app/soapbox/features/admin/components/dashcounter.tsx new file mode 100644 index 000000000..a42986535 --- /dev/null +++ b/app/soapbox/features/admin/components/dashcounter.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { FormattedNumber } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { Text } from 'soapbox/components/ui'; +import { isNumber } from 'soapbox/utils/numbers'; + +interface IDashCounter { + count: number | undefined + label: React.ReactNode + to?: string + percent?: boolean +} + +/** Displays a (potentially clickable) dashboard statistic. */ +const DashCounter: React.FC = ({ count, label, to = '#', percent = false }) => { + + if (!isNumber(count)) { + return null; + } + + return ( + + + + + + {label} + + + ); +}; + +interface IDashCounters { + children: React.ReactNode +} + +/** Wrapper container for dash counters. */ +const DashCounters: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export { + DashCounter, + DashCounters, +}; \ No newline at end of file diff --git a/app/soapbox/features/admin/components/registration-mode-picker.tsx b/app/soapbox/features/admin/components/registration-mode-picker.tsx index 2c830153a..eab7f2f94 100644 --- a/app/soapbox/features/admin/components/registration-mode-picker.tsx +++ b/app/soapbox/features/admin/components/registration-mode-picker.tsx @@ -3,12 +3,7 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { updateConfig } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; -import { - SimpleForm, - FieldsGroup, - RadioGroup, - RadioItem, -} from 'soapbox/features/forms'; +import { RadioGroup, RadioItem } from 'soapbox/components/radio'; import { useAppDispatch, useInstance } from 'soapbox/hooks'; import type { Instance } from 'soapbox/types/entities'; @@ -54,33 +49,26 @@ const RegistrationModePicker: React.FC = () => { }; return ( - - - } - onChange={onChange} - > - } - hint={} - checked={mode === 'open'} - value='open' - /> - } - hint={} - checked={mode === 'approval'} - value='approval' - /> - } - hint={} - checked={mode === 'closed'} - value='closed' - /> - - - + + } + hint={} + checked={mode === 'open'} + value='open' + /> + } + hint={} + checked={mode === 'approval'} + value='approval' + /> + } + hint={} + checked={mode === 'closed'} + value='closed' + /> + ); }; diff --git a/app/soapbox/features/admin/components/unapproved-account.tsx b/app/soapbox/features/admin/components/unapproved-account.tsx index c1dae46fb..514fcb370 100644 --- a/app/soapbox/features/admin/components/unapproved-account.tsx +++ b/app/soapbox/features/admin/components/unapproved-account.tsx @@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { approveUsers } from 'soapbox/actions/admin'; import { rejectUserModal } from 'soapbox/actions/moderation'; import snackbar from 'soapbox/actions/snackbar'; -import IconButton from 'soapbox/components/icon-button'; +import { Stack, HStack, Text, IconButton } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; @@ -45,16 +45,31 @@ const UnapprovedAccount: React.FC = ({ accountId }) => { }; return ( -
-
-
@{account.get('acct')}
-
{adminAccount?.invite_request || ''}
-
-
- - -
-
+ + + + @{account.get('acct')} + + + {adminAccount?.invite_request || ''} + + + + + + + + ); }; diff --git a/app/soapbox/features/admin/moderation-log.tsx b/app/soapbox/features/admin/moderation-log.tsx index baf002eb4..324f1f202 100644 --- a/app/soapbox/features/admin/moderation-log.tsx +++ b/app/soapbox/features/admin/moderation-log.tsx @@ -3,8 +3,9 @@ import { defineMessages, FormattedDate, useIntl } from 'react-intl'; import { fetchModerationLog } from 'soapbox/actions/admin'; import ScrollableList from 'soapbox/components/scrollable-list'; -import { Column } from 'soapbox/components/ui'; +import { Column, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { AdminLog } from 'soapbox/types/entities'; const messages = defineMessages({ heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' }, @@ -18,6 +19,7 @@ const ModerationLog = () => { const items = useAppSelector((state) => { return state.admin_log.index.map((i) => state.admin_log.items.get(String(i))); }); + const hasMore = useAppSelector((state) => state.admin_log.total - state.admin_log.index.count() > 0); const [isLoading, setIsLoading] = useState(true); @@ -54,26 +56,38 @@ const ModerationLog = () => { emptyMessage={intl.formatMessage(messages.emptyMessage)} hasMore={hasMore} onLoadMore={handleLoadMore} + className='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > - {items.map((item) => item && ( -
-
{item.message}
-
- -
-
+ {items.map(item => item && ( + ))} ); }; +interface ILogItem { + log: AdminLog +} + +const LogItem: React.FC = ({ log }) => { + return ( + + {log.message} + + + + + + ); +}; + export default ModerationLog; diff --git a/app/soapbox/features/admin/tabs/awaiting-approval.tsx b/app/soapbox/features/admin/tabs/awaiting-approval.tsx index 0a412f400..36a86b61a 100644 --- a/app/soapbox/features/admin/tabs/awaiting-approval.tsx +++ b/app/soapbox/features/admin/tabs/awaiting-approval.tsx @@ -33,9 +33,12 @@ const AwaitingApproval: React.FC = () => { showLoading={showLoading} scrollKey='awaiting-approval' emptyMessage={intl.formatMessage(messages.emptyMessage)} + className='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > {accountIds.map(id => ( - +
+ +
))} ); diff --git a/app/soapbox/features/admin/tabs/dashboard.tsx b/app/soapbox/features/admin/tabs/dashboard.tsx index a43bd8d83..aa127f5a1 100644 --- a/app/soapbox/features/admin/tabs/dashboard.tsx +++ b/app/soapbox/features/admin/tabs/dashboard.tsx @@ -1,44 +1,49 @@ import React from 'react'; -import { FormattedMessage, FormattedNumber } from 'react-intl'; -import { Link } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list'; -import { Text } from 'soapbox/components/ui'; +import List, { ListItem } from 'soapbox/components/list'; +import { CardTitle, Icon, IconButton, Stack } from 'soapbox/components/ui'; import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks'; import sourceCode from 'soapbox/utils/code'; import { download } from 'soapbox/utils/download'; import { parseVersion } from 'soapbox/utils/features'; -import { isNumber } from 'soapbox/utils/numbers'; +import { DashCounter, DashCounters } from '../components/dashcounter'; import RegistrationModePicker from '../components/registration-mode-picker'; const Dashboard: React.FC = () => { const dispatch = useAppDispatch(); + const history = useHistory(); const instance = useInstance(); const features = useFeatures(); 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(); }; + const navigateToSoapboxConfig = () => history.push('/soapbox/config'); + const navigateToModerationLog = () => history.push('/soapbox/admin/log'); + const v = parseVersion(instance.version); const userCount = instance.stats.get('user_count'); @@ -46,87 +51,121 @@ const Dashboard: React.FC = () => { const domainCount = instance.stats.get('domain_count'); const mau = instance.pleroma.getIn(['stats', 'mau']) as number | undefined; - const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : null; + const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined; if (!account) return null; return ( - <> -
- {isNumber(mau) && ( -
- - - - - - -
- )} - {isNumber(userCount) && ( - - - - - - - - - )} - {isNumber(retention) && ( -
- - {retention}% - - - - -
- )} - {isNumber(statusCount) && ( - - - - - - - - - )} - {isNumber(domainCount) && ( -
- - - - - - -
- )} -
+ + + } + /> + } + /> + } + percent + /> + } + /> + } + /> + - {account.admin && } - -
-
-

-
    -
  • {sourceCode.displayName} {sourceCode.version}
  • -
  • {v.software + (v.build ? `+${v.build}` : '')} {v.version}
  • -
-
- {features.emailList && account.admin && ( - + + {account.admin && ( + } + /> )} -
- + + } + /> + + + {account.admin && ( + <> + } + /> + + + + )} + + } + /> + + + }> + + {sourceCode.displayName} {sourceCode.version} + + + + + + }> + {v.software + (v.build ? `+${v.build}` : '')} {v.version} + + + + {(features.emailList && account.admin) && ( + <> + } + /> + + + + + + + + + + + + + + + + )} +
); }; diff --git a/app/soapbox/features/chats/components/chat-page/components/welcome.tsx b/app/soapbox/features/chats/components/chat-page/components/welcome.tsx index 6de9f821f..376d20627 100644 --- a/app/soapbox/features/chats/components/chat-page/components/welcome.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/welcome.tsx @@ -36,7 +36,7 @@ const Welcome = () => { }; return ( - +
{intl.formatMessage(messages.title, { br:
})} diff --git a/app/soapbox/features/developers/settings-store.tsx b/app/soapbox/features/developers/settings-store.tsx index 521ed0dab..6342ef1ae 100644 --- a/app/soapbox/features/developers/settings-store.tsx +++ b/app/soapbox/features/developers/settings-store.tsx @@ -103,6 +103,13 @@ const SettingsStore: React.FC = () => { + } + hint={} + > + + + }> 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/features/forms/index.tsx b/app/soapbox/features/forms/index.tsx index b4b13a503..5d853e581 100644 --- a/app/soapbox/features/forms/index.tsx +++ b/app/soapbox/features/forms/index.tsx @@ -1,8 +1,8 @@ import classNames from 'clsx'; -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { Text, Select } from '../../components/ui'; +import { Select } from '../../components/ui'; interface IInputContainer { label?: React.ReactNode, @@ -175,52 +175,6 @@ export const Checkbox: React.FC = (props) => ( ); -interface IRadioGroup { - label?: React.ReactNode, - onChange?: React.ChangeEventHandler, -} - -export const RadioGroup: React.FC = (props) => { - const { label, children, onChange } = props; - - const childrenWithProps = React.Children.map(children, child => - // @ts-ignore - React.cloneElement(child, { onChange }), - ); - - return ( -
-
- -
    {childrenWithProps}
-
-
- ); -}; - -interface IRadioItem { - label?: React.ReactNode, - hint?: React.ReactNode, - value: string, - checked: boolean, - onChange?: React.ChangeEventHandler, -} - -export const RadioItem: React.FC = (props) => { - const { current: id } = useRef(uuidv4()); - const { label, hint, checked = false, ...rest } = props; - - return ( -
  • - -
  • - ); -}; - interface ISelectDropdown { label?: React.ReactNode, hint?: React.ReactNode, diff --git a/app/soapbox/features/settings/components/messages-settings.tsx b/app/soapbox/features/settings/components/messages-settings.tsx index 4ff005742..78d46e751 100644 --- a/app/soapbox/features/settings/components/messages-settings.tsx +++ b/app/soapbox/features/settings/components/messages-settings.tsx @@ -7,7 +7,7 @@ import { useOwnAccount } from 'soapbox/hooks'; import { useUpdateCredentials } from 'soapbox/queries/accounts'; const messages = defineMessages({ - label: { id: 'settings.messages.label', defaultMessage: 'Allow users you follow to start a new chat with you' }, + label: { id: 'settings.messages.label', defaultMessage: 'Allow users to start a new chat with you' }, }); const MessagesSettings = () => { diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index 68558e40a..c0a7a3056 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -106,7 +106,7 @@ const Settings = () => { {features.chats ? ( <> - + } /> 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 c1c7dd3cd..be166cb9f 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 ( -
    +
    }> + + + ); +}; + +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 diff --git a/app/soapbox/features/ui/components/bundle.tsx b/app/soapbox/features/ui/components/bundle.tsx index 79851b978..f2fe4413d 100644 --- a/app/soapbox/features/ui/components/bundle.tsx +++ b/app/soapbox/features/ui/components/bundle.tsx @@ -45,7 +45,7 @@ class Bundle extends React.PureComponent { this.load(this.props); } - componentWillReceiveProps(nextProps: BundleProps) { + UNSAFE_componentWillReceiveProps(nextProps: BundleProps) { if (nextProps.fetchComponent !== this.props.fetchComponent) { this.load(nextProps); } diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index c2c4b54ad..84f5fb454 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -108,6 +108,7 @@ import { TestTimeline, LogoutPage, AuthTokenList, + ThemeEditor, Quotes, ServiceWorkerInfo, EventInformation, @@ -312,6 +313,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 7489a7bc6..fe2cf8d1e 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'); } diff --git a/app/soapbox/locales/locale-data/README.md b/app/soapbox/locales/locale-data/README.md deleted file mode 100644 index 83368fae7..000000000 --- a/app/soapbox/locales/locale-data/README.md +++ /dev/null @@ -1,221 +0,0 @@ -# Custom Locale Data - -This folder is used to store custom locale data. These custom locale data are -not yet provided by [Unicode Common Locale Data Repository](http://cldr.unicode.org/development/new-cldr-developers) -and hence not provided in [react-intl/locale-data/*](https://github.com/yahoo/react-intl). - -The locale data should support [Locale Data APIs](https://github.com/yahoo/react-intl/wiki/API#locale-data-apis) -of the react-intl library. - -It is recommended to start your custom locale data from this sample English -locale data ([*](#plural-rules)): - -```javascript -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ - -export default [ - { - locale: "en", - pluralRuleFunction: function(e, a) { - var n = String(e).split("."), - l = !n[1], - o = Number(n[0]) == e, - t = o && n[0].slice(-1), - r = o && n[0].slice(-2); - return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" - }, - fields: { - year: { - displayName: "year", - relative: { - 0: "this year", - 1: "next year", - "-1": "last year" - }, - relativeTime: { - future: { - one: "in {0} year", - other: "in {0} years" - }, - past: { - one: "{0} year ago", - other: "{0} years ago" - } - } - }, - month: { - displayName: "month", - relative: { - 0: "this month", - 1: "next month", - "-1": "last month" - }, - relativeTime: { - future: { - one: "in {0} month", - other: "in {0} months" - }, - past: { - one: "{0} month ago", - other: "{0} months ago" - } - } - }, - day: { - displayName: "day", - relative: { - 0: "today", - 1: "tomorrow", - "-1": "yesterday" - }, - relativeTime: { - future: { - one: "in {0} day", - other: "in {0} days" - }, - past: { - one: "{0} day ago", - other: "{0} days ago" - } - } - }, - hour: { - displayName: "hour", - relativeTime: { - future: { - one: "in {0} hour", - other: "in {0} hours" - }, - past: { - one: "{0} hour ago", - other: "{0} hours ago" - } - } - }, - minute: { - displayName: "minute", - relativeTime: { - future: { - one: "in {0} minute", - other: "in {0} minutes" - }, - past: { - one: "{0} minute ago", - other: "{0} minutes ago" - } - } - }, - second: { - displayName: "second", - relative: { - 0: "now" - }, - relativeTime: { - future: { - one: "in {0} second", - other: "in {0} seconds" - }, - past: { - one: "{0} second ago", - other: "{0} seconds ago" - } - } - } - } - } -] - -``` - -## Notes - -### Plural Rules - -The function `pluralRuleFunction()` should return the key to proper string of -a plural form(s). The purpose of the function is to provide key of translate -strings of correct plural form according. The different forms are described in -[CLDR's Plural Rules][cldr-plural-rules], - -[cldr-plural-rules]: http://cldr.unicode.org/index/cldr-spec/plural-rules - -#### Quick Overview on CLDR Rules - -Let's take English as an example. - -When you describe a number, you can be either describe it as: -* Cardinals: 1st, 2nd, 3rd ... 11th, 12th ... 21st, 22nd, 23nd .... -* Ordinals: 1, 2, 3 ... - -In any of these cases, the nouns will reflect the number with singular or plural -form. For example: -* in 0 days -* in 1 day -* in 2 days - -The `pluralRuleFunction` receives 2 parameters: -* `e`: a string representation of the number. Such as, "`1`", "`2`", "`2.1`". -* `a`: `true` if this is "cardinal" type of description. `false` for ordinal and other case. - -#### How you should write `pluralRuleFunction` - -The first rule to write pluralRuleFunction is never translate the output string -into your language. [Plural Rules][cldr-plural-rules] specified you should use -these as the return values: - - * "`zero`" - * "`one`" (singular) - * "`two`" (dual) - * "`few`" (paucal) - * "`many`" (also used for fractions if they have a separate class) - * "`other`" (required—general plural form—also used if the language only has a single form) - -Again, we'll use English as the example here. - -Let's read the `return` statement in the pluralRuleFunction above: -```javascript - return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" -``` - -This nested ternary is hard to read. It basically means: -```javascript -// e: the number variable to examine -// a: "true" if cardinals -// l: "true" if the variable e has nothin after decimal mark (e.g. "1.0" would be false) -// o: "true" if the variable e is an integer -// t: the "ones" of the number. e.g. "3" for number "9123" -// r: the "ones" and "tens" of the number. e.g. "23" for number "9123" -if (a == true) { - if (t == 1 && r != 11) { - return "one"; // i.e. 1st, 21st, 101st, 121st ... - } else if (t == 2 && r != 12) { - return "two"; // i.e. 2nd, 22nd, 102nd, 122nd ... - } else if (t == 3 && r != 13) { - return "few"; // i.e. 3rd, 23rd, 103rd, 123rd ... - } else { - return "other"; // i.e. 4th, 11th, 12th, 24th ... - } -} else { - if (e == 1 && l) { - return "one"; // i.e. 1 day - } else { - return "other"; // i.e. 0 days, 2 days, 3 days - } -} -``` - -If your language, like French, do not have complicated cardinal rules, you may -use the French's version of it: -```javascript -function (e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; -} -``` - -If your language, like Chinese, do not have any pluralization rule at all you -may use the Chinese's version of it: -```javascript -function (e, a) { - return "other"; -} -``` diff --git a/app/soapbox/locales/locale-data/co.js b/app/soapbox/locales/locale-data/co.js deleted file mode 100644 index dc94f1044..000000000 --- a/app/soapbox/locales/locale-data/co.js +++ /dev/null @@ -1,108 +0,0 @@ -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ -/*eslint quotes: "off"*/ - -export default [{ - locale: "co", - pluralRuleFunction: function(e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; - }, - fields: { - year: { - displayName: "annu", - relative: { - 0: "quist'annu", - 1: "l'annu chì vene", - "-1": "l'annu passatu", - }, - relativeTime: { - future: { - one: "in {0} annu", - other: "in {0} anni", - }, - past: { - one: "{0} annu fà", - other: "{0} anni fà", - }, - }, - }, - month: { - displayName: "mese", - relative: { - 0: "Questu mese", - 1: "u mese chì vene", - "-1": "u mese passatu", - }, - relativeTime: { - future: { - one: "in {0} mese", - other: "in {0} mesi", - }, - past: { - one: "{0} mese fà", - other: "{0} mesi fà", - }, - }, - }, - day: { - displayName: "ghjornu", - relative: { - 0: "oghje", - 1: "dumane", - "-1": "eri", - }, - relativeTime: { - future: { - one: "in {0} ghjornu", - other: "in {0} ghjornu", - }, - past: { - one: "{0} ghjornu fà", - other: "{0} ghjorni fà", - }, - }, - }, - hour: { - displayName: "ora", - relativeTime: { - future: { - one: "in {0} ora", - other: "in {0} ore", - }, - past: { - one: "{0} ora fà", - other: "{0} ore fà", - }, - }, - }, - minute: { - displayName: "minuta", - relativeTime: { - future: { - one: "in {0} minuta", - other: "in {0} minute", - }, - past: { - one: "{0} minuta fà", - other: "{0} minute fà", - }, - }, - }, - second: { - displayName: "siconda", - relative: { - 0: "avà", - }, - relativeTime: { - future: { - one: "in {0} siconda", - other: "in {0} siconde", - }, - past: { - one: "{0} siconda fà", - other: "{0} siconde fà", - }, - }, - }, - }, -}]; diff --git a/app/soapbox/locales/locale-data/oc.js b/app/soapbox/locales/locale-data/oc.js deleted file mode 100644 index 7a0f34094..000000000 --- a/app/soapbox/locales/locale-data/oc.js +++ /dev/null @@ -1,108 +0,0 @@ -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ -/*eslint quotes: "off"*/ - -export default [{ - locale: "oc", - pluralRuleFunction: function(e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; - }, - fields: { - year: { - displayName: "an", - relative: { - 0: "ongan", - 1: "l'an que ven", - "-1": "l'an passat", - }, - relativeTime: { - future: { - one: "d’aquí {0} an", - other: "d’aquí {0} ans", - }, - past: { - one: "fa {0} an", - other: "fa {0} ans", - }, - }, - }, - month: { - displayName: "mes", - relative: { - 0: "aqueste mes", - 1: "lo mes que ven", - "-1": "lo mes passat", - }, - relativeTime: { - future: { - one: "d’aquí {0} mes", - other: "d’aquí {0} meses", - }, - past: { - one: "fa {0} mes", - other: "fa {0} meses", - }, - }, - }, - day: { - displayName: "jorn", - relative: { - 0: "uèi", - 1: "deman", - "-1": "ièr", - }, - relativeTime: { - future: { - one: "d’aquí {0} jorn", - other: "d’aquí {0} jorns", - }, - past: { - one: "fa {0} jorn", - other: "fa {0} jorns", - }, - }, - }, - hour: { - displayName: "ora", - relativeTime: { - future: { - one: "d’aquí {0} ora", - other: "d’aquí {0} oras", - }, - past: { - one: "fa {0} ora", - other: "fa {0} oras", - }, - }, - }, - minute: { - displayName: "minuta", - relativeTime: { - future: { - one: "d’aquí {0} minuta", - other: "d’aquí {0} minutas", - }, - past: { - one: "fa {0} minuta", - other: "fa {0} minutas", - }, - }, - }, - second: { - displayName: "segonda", - relative: { - 0: "ara", - }, - relativeTime: { - future: { - one: "d’aquí {0} segonda", - other: "d’aquí {0} segondas", - }, - past: { - one: "fa {0} segonda", - other: "fa {0} segondas", - }, - }, - }, - }, -}]; 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/soapbox/reducers/admin-log.ts b/app/soapbox/reducers/admin-log.ts index d8500e343..1ffe4438f 100644 --- a/app/soapbox/reducers/admin-log.ts +++ b/app/soapbox/reducers/admin-log.ts @@ -9,7 +9,7 @@ import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin'; import type { AnyAction } from 'redux'; import type { APIEntity } from 'soapbox/types/entities'; -const LogEntryRecord = ImmutableRecord({ +export const LogEntryRecord = ImmutableRecord({ data: ImmutableMap(), id: 0, message: '', diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index 239df6a6d..e432a0b81 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -26,10 +26,12 @@ import { StatusRecord, TagRecord, } from 'soapbox/normalizers'; +import { LogEntryRecord } from 'soapbox/reducers/admin-log'; import type { Record as ImmutableRecord } from 'immutable'; type AdminAccount = ReturnType; +type AdminLog = ReturnType; type AdminReport = ReturnType; type Announcement = ReturnType; type AnnouncementReaction = ReturnType; @@ -72,6 +74,7 @@ type EmbeddedEntity = null | string | ReturnType new URL(url).pathname.substring(1); const trimHash = hash => hash.substring(0, 7); @@ -10,14 +12,12 @@ const tryGit = cmd => { try { return String(execSync(cmd)); } catch (e) { - return null; + return undefined; } }; const version = pkg => { // Try to discern from GitLab CI first - const { CI_COMMIT_TAG, CI_COMMIT_REF_NAME, CI_COMMIT_SHA } = process.env; - if (CI_COMMIT_TAG === `v${pkg.version}` || CI_COMMIT_REF_NAME === 'stable') { return pkg.version; } @@ -43,4 +43,5 @@ module.exports = { repository: shortRepoName(pkg.repository.url), version: version(pkg), homepage: pkg.homepage, + ref: CI_COMMIT_TAG || CI_COMMIT_SHA || tryGit('git rev-parse HEAD'), }; 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); 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 diff --git a/app/styles/application.scss b/app/styles/application.scss index 9d4532f14..6769e1559 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -48,7 +48,6 @@ @import 'components/audio-player'; @import 'components/filters'; @import 'components/snackbar'; -@import 'components/admin'; @import 'components/backups'; @import 'components/crypto-donate'; @import 'components/aliases'; diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss deleted file mode 100644 index bba02ffe8..000000000 --- a/app/styles/components/admin.scss +++ /dev/null @@ -1,67 +0,0 @@ -.dashcounters { - @apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mb-4; -} - -.dashcounter { - @apply bg-gray-200 dark:bg-gray-800 p-4 rounded flex flex-col items-center space-y-2 hover:-translate-y-1 transition-transform cursor-pointer; -} - -.dashwidgets { - display: flex; - flex-wrap: wrap; - margin: 0 -5px; - padding: 0 20px 20px 20px; -} - -.dashwidget { - flex: 1; - margin-bottom: 20px; - padding: 0 5px; - - h4 { - text-transform: uppercase; - font-size: 13px; - font-weight: 700; - color: hsla(var(--primary-text-color_hsl), 0.6); - padding-bottom: 8px; - margin-bottom: 8px; - border-bottom: 1px solid var(--accent-color--med); - } - - a { - color: var(--brand-color); - } -} - -.unapproved-account { - padding: 15px 20px; - font-size: 14px; - display: flex; - - &__nickname { - font-weight: bold; - } - - &__actions { - margin-left: auto; - display: flex; - flex-wrap: nowrap; - column-gap: 10px; - padding-left: 20px; - - .svg-icon { - height: 24px; - width: 24px; - } - } -} - -.logentry { - padding: 15px; - - &__timestamp { - color: var(--primary-text-color--faint); - font-size: 13px; - text-align: right; - } -} 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/dangerfile.ts b/dangerfile.ts index aa18c0ad9..6ed716fbc 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -1,5 +1,8 @@ import { danger, warn, message } from 'danger'; +// App changes +const app = danger.git.fileMatch('app/soapbox/**'); + // Docs changes const docs = danger.git.fileMatch('docs/**/*.md'); @@ -7,6 +10,13 @@ if (docs.edited) { message('Thanks - We :heart: our [documentarians](http://www.writethedocs.org/)!'); } +// Enforce CHANGELOG.md additions +const changelog = danger.git.fileMatch('CHANGELOG.md'); + +if (app.edited && !changelog.edited) { + warn('You have not updated `CHANGELOG.md`. If this change directly impacts admins or users, please update the changelog. Otherwise you can ignore this message. See: https://keepachangelog.com'); +} + // UI components const uiCode = danger.git.fileMatch('app/soapbox/components/ui/**'); const uiTests = danger.git.fileMatch('app/soapbox/components/ui/**/__tests__/**'); diff --git a/jest.config.js b/jest.config.js index 396486a39..74e38cfdd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,7 +21,6 @@ module.exports = { 'app/soapbox/**/*.ts', 'app/soapbox/**/*.tsx', '!app/soapbox/features/emoji/emoji-compressed.js', - '!app/soapbox/locales/locale-data/*.js', '!app/soapbox/service-worker/entry.ts', '!app/soapbox/jest/test-setup.ts', '!app/soapbox/jest/test-helpers.ts', diff --git a/soapbox-screenshot.png b/soapbox-screenshot.png index 61c57bb47..c5e01e382 100644 Binary files a/soapbox-screenshot.png and b/soapbox-screenshot.png differ 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)'); }); });