diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 01941a9d30..b98f6a21e0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,6 +15,12 @@ "webben.browserslist" ], + "features": { + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. // This can be used to network with other containers or the host. "forwardPorts": [3000, 4000], diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index bf50afe8c7..51ee8a071a 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: hadolint/hadolint-action@v3.0.0 + - uses: hadolint/hadolint-action@v3.1.0 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - uses: docker/login-action@v2 diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml new file mode 100644 index 0000000000..5bf4349b3d --- /dev/null +++ b/.github/workflows/lint-json.yml @@ -0,0 +1,38 @@ +name: JSON Linting +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.prettier*' + - '**/*.json' + - '.github/workflows/lint-json.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.prettier*' + - '**/*.json' + - '.github/workflows/lint-json.yml' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn prettier --check "**/*.json" diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml new file mode 100644 index 0000000000..b939ec8ce2 --- /dev/null +++ b/.github/workflows/lint-yml.yml @@ -0,0 +1,40 @@ +name: YML Linting +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.prettier*' + - '**/*.yaml' + - '**/*.yml' + - '.github/workflows/lint-yml.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.prettier*' + - '**/*.yaml' + - '**/*.yml' + - '.github/workflows/lint-yml.yml' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn prettier --check "**/*.{yml,yaml}" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 319152e93d..b6438d6659 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -57,8 +57,6 @@ jobs: cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile - - name: Check prettier formatting - run: yarn format-check - name: Set-up RuboCop Problem Mathcher uses: r7kamura/rubocop-problem-matchers-action@v1 - name: Set-up Stylelint Problem Matcher diff --git a/.prettierignore b/.prettierignore index f72354a42f..dc8c536933 100644 --- a/.prettierignore +++ b/.prettierignore @@ -70,3 +70,10 @@ docker-compose.override.yml # Ignore locale files /app/javascript/mastodon/locales /config/locales + +# Ignore glitch-soc locale files +/app/javascript/flavours/glitch/locales +/config/locales-glitch + +# Ignore glitch-soc emoji map file +/app/javascript/flavours/glitch/features/emoji/emoji_map.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ad9e5fd9..9a1ad4ffcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,191 @@ Changelog All notable changes to this project will be documented in this file. +## [4.1.0] - UNRELEASED + +### Added + +- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470)) +- Add listing of followed hashtags ([connorshea](https://github.com/mastodon/mastodon/pull/21773)) +- Add support for editing media description and focus point of already-sent posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878)) +- Add follow request banner on account header ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785)) +- Add confirmation screen when handling reports ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156), [tribela](https://github.com/mastodon/mastodon/pull/23178)) +- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808)) +- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205)) +- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810)) +- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833)) +- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938)) +- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131)) +- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895)) +- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149)) +- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328)) +- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330)) +- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397)) +- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063)) +- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589)) +- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048)) +- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946)) +- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423)) +- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904)) +- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022)) +- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602)) +- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013)) +- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262)) +- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667)) +- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629)) +- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025)) +- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282)) +- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081)) +- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420)) +- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464)) +- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706)) +- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790)) +- Add `policy` attribute to web push subscription objects in `/api/v1/push/subscriptions` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23210)) +- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617)) +- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929)) +- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795)) +- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549)) +- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513)) +- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568)) +- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576)) +- Add `lang` attribute to compose form inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23240)) + +### Changed + +- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315)) +- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956)) +- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897)) +- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272)) +- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850)) +- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692)) +- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541)) +- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013)) +- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491)) +- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388)) +- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411)) +- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923)) +- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490)) +- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553)) +- Change email address input to be read-only for logged-in users when requesting a new confirmation e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23247)) +- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465)) +- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037)) +- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608)) +- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864)) +- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774)) +- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285)) +- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692)) +- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028)) +- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637)) +- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385)) +- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999)) +- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759)) +- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790)) +- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479)) +- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327)) +- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316)) +- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324)) +- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555)) +- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960)) +- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517)) +- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840)) +- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796)) +- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575)) +- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533)) +- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064)) +- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879)) +- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342)) + +### Removed + +- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477)) +- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693)) +- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078)) + +### Fixed + +- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135)) +- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487)) +- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363)) +- Fix being stuck in edit mode when deleting the edited status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126)) +- Fix filters not being applied to some notification types ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23211)) +- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907)) +- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091)) +- Fix moderation audit log items for warnings having incorrect links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23242)) +- Fix account activation being sometimes triggered before email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23245)) +- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979)) +- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700)) +- Fix attachments of edited statuses not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565)) +- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988)) +- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134)) +- Fix expanded statuses not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797)) +- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763)) +- Fix audio player volume control on Safari ([minacle](https://github.com/mastodon/mastodon/pull/23187)) +- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982)) +- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157)) +- Fix drag & drop upload area display in single-column mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23217)) +- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846)) +- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861)) +- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062)) +- Fix email with empty domain name labels passing validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23246)) +- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127)) +- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270)) +- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772)) +- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741)) +- Fix error in `VerifyLinkService` when processing links with invalid URLs ([untitaker](https://github.com/mastodon/mastodon/pull/23204)) +- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191)) +- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724)) +- Fix “Share @user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490)) +- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004)) +- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422)) +- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324)) +- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655)) +- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303)) +- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006)) +- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088)) +- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060)) +- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783)) +- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462)) +- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462)) +- Fix potential duplicate statuses in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121)) +- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120)) +- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302)) +- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326)) +- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117)) +- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006)) +- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994)) +- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202)) +- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231)) +- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141)) +- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275)) +- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829)) +- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932)) +- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943)) +- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926)) +- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440)) +- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814)) +- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606)) +- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463)) +- Fix minor status cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879)) +- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888)) +- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849)) +- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497)) +- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479)) +- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485)) +- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827)) +- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778)) +- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072)) +- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558)) +- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896)) +- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483)) +- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943)) +- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307)) + +### Security + +- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962)) +- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025)) +- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325)) + ## [4.0.2] - 2022-11-15 ### Fixed diff --git a/Gemfile b/Gemfile index 59d6e9d053..dd3d59d2db 100644 --- a/Gemfile +++ b/Gemfile @@ -10,12 +10,12 @@ gem 'puma', '~> 5.6' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.4' +gem 'rack', '~> 2.2.6' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.4' gem 'makara', '~> 0.5' -gem 'pghero', '~> 2.8' +gem 'pghero' gem 'dotenv-rails', '~> 2.8' gem 'aws-sdk-s3', '~> 1.117', require: false @@ -51,7 +51,7 @@ gem 'ed25519', '~> 1.3' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' -gem 'redis-namespace', '~> 1.9' +gem 'redis-namespace', '~> 1.10' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.1' gem 'http_accept_language', '~> 2.1' @@ -60,7 +60,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.13' +gem 'nokogiri', '~> 1.14' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.13' gem 'ox', '~> 2.14' @@ -120,14 +120,13 @@ end group :test do gem 'capybara', '~> 3.38' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 3.0' + gem 'faker', '~> 3.1' gem 'json-schema', '~> 3.0' - gem 'microformats', '~> 4.4' gem 'rack-test', '~> 2.0' gem 'rails-controller-testing', '~> 1.0' gem 'rspec_junit_formatter', '~> 0.6' gem 'rspec-sidekiq', '~> 3.1' - gem 'simplecov', '~> 0.21', require: false + gem 'simplecov', '~> 0.22', require: false gem 'webmock', '~> 3.18' end diff --git a/Gemfile.lock b/Gemfile.lock index dfab7cfb3d..2160c15791 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,40 +10,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7) - actionpack (= 6.1.7) - activesupport (= 6.1.7) + actioncable (6.1.7.1) + actionpack (= 6.1.7.1) + activesupport (= 6.1.7.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7) - actionpack (= 6.1.7) - activejob (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + actionmailbox (6.1.7.1) + actionpack (= 6.1.7.1) + activejob (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) mail (>= 2.7.1) - actionmailer (6.1.7) - actionpack (= 6.1.7) - actionview (= 6.1.7) - activejob (= 6.1.7) - activesupport (= 6.1.7) + actionmailer (6.1.7.1) + actionpack (= 6.1.7.1) + actionview (= 6.1.7.1) + activejob (= 6.1.7.1) + activesupport (= 6.1.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7) - actionview (= 6.1.7) - activesupport (= 6.1.7) + actionpack (6.1.7.1) + actionview (= 6.1.7.1) + activesupport (= 6.1.7.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7) - actionpack (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + actiontext (6.1.7.1) + actionpack (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) nokogiri (>= 1.8.5) - actionview (6.1.7) - activesupport (= 6.1.7) + actionview (6.1.7.1) + activesupport (= 6.1.7.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -54,22 +54,22 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (6.1.7) - activesupport (= 6.1.7) + activejob (6.1.7.1) + activesupport (= 6.1.7.1) globalid (>= 0.3.6) - activemodel (6.1.7) - activesupport (= 6.1.7) - activerecord (6.1.7) - activemodel (= 6.1.7) - activesupport (= 6.1.7) - activestorage (6.1.7) - actionpack (= 6.1.7) - activejob (= 6.1.7) - activerecord (= 6.1.7) - activesupport (= 6.1.7) + activemodel (6.1.7.1) + activesupport (= 6.1.7.1) + activerecord (6.1.7.1) + activemodel (= 6.1.7.1) + activesupport (= 6.1.7.1) + activestorage (6.1.7.1) + actionpack (= 6.1.7.1) + activejob (= 6.1.7.1) + activerecord (= 6.1.7.1) + activesupport (= 6.1.7.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7) + activesupport (6.1.7.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -130,7 +130,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.2.4) - bullet (7.0.4) + bullet (7.0.7) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundler-audit (0.9.1) @@ -174,7 +174,7 @@ GEM cocoon (1.2.15) coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) connection_pool (2.3.0) cose (1.2.1) cbor (~> 0.5.9) @@ -184,6 +184,7 @@ GEM crass (1.0.6) css_parser (1.12.0) addressable + date (3.3.3) debug_inspector (1.0.0) devise (4.8.1) bcrypt (~> 3.0) @@ -203,7 +204,7 @@ GEM diff-lcs (1.5.0) discard (1.2.1) activerecord (>= 4.2, < 8) - docile (1.3.4) + docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) doorkeeper (5.6.2) @@ -223,12 +224,12 @@ GEM faraday (~> 1) multi_json encryptor (3.0.0) - erubi (1.11.0) + erubi (1.12.0) et-orbi (1.2.7) tzinfo excon (0.95.0) fabrication (2.30.0) - faker (3.0.0) + faker (3.1.0) i18n (>= 1.8.11, < 2) faraday (1.9.3) faraday-em_http (~> 1.0) @@ -282,7 +283,7 @@ GEM addressable (~> 2.7) omniauth (>= 1.9, < 3) openid_connect (~> 1.2) - globalid (1.0.0) + globalid (1.0.1) activesupport (>= 5.0) hamlit (2.13.0) temple (>= 0.8.2) @@ -330,9 +331,9 @@ GEM idn-ruby (0.1.5) ipaddress (0.8.3) jmespath (1.6.2) - json (2.6.2) + json (2.6.3) json-canonicalization (0.3.0) - json-jwt (1.13.0) + json-jwt (1.14.0) activesupport (>= 4.2) aes_key_wrap bindata @@ -349,7 +350,7 @@ GEM json-schema (3.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.4.1) + jwt (2.5.0) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -389,8 +390,11 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.0.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp makara (0.5.1) activerecord (>= 5.2.0) marcel (1.0.2) @@ -399,19 +403,21 @@ GEM matrix (0.4.2) memory_profiler (1.0.1) method_source (1.0.0) - microformats (4.4.1) - json (~> 2.2) - nokogiri (~> 1.10) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.3) + mini_portile2 (2.8.1) + minitest (5.17.0) msgpack (1.6.0) multi_json (1.15.0) multipart-post (2.1.1) + net-imap (0.3.4) + date + net-protocol net-ldap (0.17.1) + net-pop (0.1.2) + net-protocol net-protocol (0.1.3) timeout net-scp (4.0.0.rc1) @@ -420,7 +426,7 @@ GEM net-protocol net-ssh (7.0.1) nio4r (2.5.8) - nokogiri (1.13.10) + nokogiri (1.14.0) mini_portile2 (~> 2.8.0) racc (~> 1.4) nsa (0.2.8) @@ -456,16 +462,16 @@ GEM openssl-signature_algorithm (1.2.1) openssl (> 2.0, < 3.1) orm_adapter (0.5.0) - ox (2.14.11) + ox (2.14.13) parallel (1.22.1) - parser (3.1.2.1) + parser (3.2.0.0) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.4.5) - pghero (2.8.3) - activerecord (>= 5) + pghero (3.1.0) + activerecord (>= 6) pkg-config (1.5.1) posix-spawn (0.3.15) premailer (1.18.0) @@ -491,8 +497,8 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.1) - rack (2.2.4) + racc (1.6.2) + rack (2.2.6.2) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -507,20 +513,20 @@ GEM rack rack-test (2.0.2) rack (>= 1.3) - rails (6.1.7) - actioncable (= 6.1.7) - actionmailbox (= 6.1.7) - actionmailer (= 6.1.7) - actionpack (= 6.1.7) - actiontext (= 6.1.7) - actionview (= 6.1.7) - activejob (= 6.1.7) - activemodel (= 6.1.7) - activerecord (= 6.1.7) - activestorage (= 6.1.7) - activesupport (= 6.1.7) + rails (6.1.7.1) + actioncable (= 6.1.7.1) + actionmailbox (= 6.1.7.1) + actionmailer (= 6.1.7.1) + actionpack (= 6.1.7.1) + actiontext (= 6.1.7.1) + actionview (= 6.1.7.1) + activejob (= 6.1.7.1) + activemodel (= 6.1.7.1) + activerecord (= 6.1.7.1) + activestorage (= 6.1.7.1) + activesupport (= 6.1.7.1) bundler (>= 1.15.0) - railties (= 6.1.7) + railties (= 6.1.7.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -536,9 +542,9 @@ GEM railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (6.1.7) - actionpack (= 6.1.7) - activesupport (= 6.1.7) + railties (6.1.7.1) + actionpack (= 6.1.7.1) + activesupport (= 6.1.7.1) method_source rake (>= 12.2) thor (~> 1.0) @@ -550,11 +556,11 @@ GEM rdf (~> 3.2) redcarpet (3.5.1) redis (4.5.1) - redis-namespace (1.9.0) + redis-namespace (1.10.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.6.0) + regexp_parser (2.6.2) request_store (1.5.1) rack (>= 1.4) responders (3.0.1) @@ -589,27 +595,30 @@ GEM rspec-support (3.11.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.39.0) + rubocop (1.44.0) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.23.0, < 2.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.23.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.24.1) parser (>= 3.1.1.0) - rubocop-performance (1.15.1) + rubocop-capybara (2.17.0) + rubocop (~> 1.41) + rubocop-performance (1.15.2) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.17.2) + rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.15.0) + rubocop-rspec (2.18.1) rubocop (~> 1.33) + rubocop-capybara (~> 2.17) ruby-progressbar (1.11.0) ruby-saml (1.13.0) nokogiri (>= 1.10.5) @@ -648,12 +657,12 @@ GEM simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) - simplecov_json_formatter (0.1.2) + simplecov_json_formatter (0.1.4) smart_properties (1.17.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -707,7 +716,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) uniform_notifier (1.16.0) validate_email (0.1.6) activemodel (>= 3.0) @@ -784,7 +793,7 @@ DEPENDENCIES dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 3.0) + faker (~> 3.1) fast_blank (~> 1.0) fastimage fog-core (<= 2.4.0) @@ -812,10 +821,9 @@ DEPENDENCIES makara (~> 0.5) mario-redis-lock (~> 1.2) memory_profiler - microformats (~> 4.4) mime-types (~> 3.4.1) net-ldap (~> 0.17) - nokogiri (~> 1.13) + nokogiri (~> 1.14) nsa (~> 0.2) oj (~> 3.13) omniauth (~> 1.9) @@ -825,7 +833,7 @@ DEPENDENCIES ox (~> 2.14) parslet pg (~> 1.4) - pghero (~> 2.8) + pghero pkg-config (~> 1.5) posix-spawn premailer-rails @@ -835,7 +843,7 @@ DEPENDENCIES public_suffix (~> 5.0) puma (~> 5.6) pundit (~> 2.3) - rack (~> 2.2.4) + rack (~> 2.2.6) rack-attack (~> 6.6) rack-cors (~> 1.1) rack-test (~> 2.0) @@ -846,7 +854,7 @@ DEPENDENCIES rdf-normalize (~> 0.5) redcarpet (~> 3.5) redis (~> 4.5) - redis-namespace (~> 1.9) + redis-namespace (~> 1.10) rexml (~> 3.2) rqrcode (~> 2.1) rspec-rails (~> 5.1) @@ -865,7 +873,7 @@ DEPENDENCIES sidekiq-unique-jobs (~> 7.1) simple-navigation (~> 4.4) simple_form (~> 5.1) - simplecov (~> 0.21) + simplecov (~> 0.22) sprockets (~> 3.7.2) sprockets-rails (~> 3.4) stackprof @@ -880,9 +888,3 @@ DEPENDENCIES webpacker (~> 5.4) webpush! xorcist (~> 1.1) - -RUBY VERSION - ruby 3.0.4p208 - -BUNDLED WITH - 2.2.33 diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index 3f2e28b6ae..e89404b609 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -21,7 +21,7 @@ module Admin account_action.save! if account_action.with_report? - redirect_to admin_reports_path + redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id]) else redirect_to admin_account_path(@account.id) end diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb index 57fb12c620..adfc39da21 100644 --- a/app/controllers/admin/export_domain_allows_controller.rb +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -23,9 +23,7 @@ module Admin @import = Admin::Import.new(import_params) return render :new unless @import.validate - parse_import_data!(export_headers) - - @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).each do |row| + @import.csv_rows.each do |row| domain = row['#domain'].strip next if DomainAllow.allowed?(domain) diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index fb0cd05d29..816422d4ff 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -23,24 +23,30 @@ module Admin @import = Admin::Import.new(import_params) return render :new unless @import.validate - parse_import_data!(export_headers) - @global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc)) @form = Form::DomainBlockBatch.new - @domain_blocks = @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).filter_map do |row| + @domain_blocks = @import.csv_rows.filter_map do |row| domain = row['#domain'].strip next if DomainBlock.rule_for(domain).present? domain_block = DomainBlock.new(domain: domain, - severity: row['#severity'].strip, - reject_media: row['#reject_media'].strip, - reject_reports: row['#reject_reports'].strip, + severity: row.fetch('#severity', :suspend), + reject_media: row.fetch('#reject_media', false), + reject_reports: row.fetch('#reject_reports', false), private_comment: @global_private_comment, - public_comment: row['#public_comment']&.strip, - obfuscate: row['#obfuscate'].strip) + public_comment: row['#public_comment'], + obfuscate: row.fetch('#obfuscate', false)) - domain_block if domain_block.valid? + if domain_block.invalid? + flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: domain_block.errors.full_messages.join(', ')) + next + end + + domain_block + rescue ArgumentError => e + flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: e.message) + next end @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 7c44e88b7b..5194057263 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -49,7 +49,7 @@ module Admin private def set_instance - @instance = Instance.find(params[:id]) + @instance = Instance.find(TagManager.instance.normalize_domain(params[:id]&.strip)) end def set_instances diff --git a/app/controllers/admin/reports/actions_controller.rb b/app/controllers/admin/reports/actions_controller.rb index 5cb5c744f2..554f7906f8 100644 --- a/app/controllers/admin/reports/actions_controller.rb +++ b/app/controllers/admin/reports/actions_controller.rb @@ -3,6 +3,11 @@ class Admin::Reports::ActionsController < Admin::BaseController before_action :set_report + def preview + authorize @report, :show? + @moderation_action = action_from_button + end + def create authorize @report, :show? @@ -13,7 +18,8 @@ class Admin::Reports::ActionsController < Admin::BaseController status_ids: @report.status_ids, current_account: current_account, report_id: @report.id, - send_email_notification: !@report.spam? + send_email_notification: !@report.spam?, + text: params[:text] ) status_batch_action.save! @@ -23,13 +29,16 @@ class Admin::Reports::ActionsController < Admin::BaseController report_id: @report.id, target_account: @report.target_account, current_account: current_account, - send_email_notification: !@report.spam? + send_email_notification: !@report.spam?, + text: params[:text] ) account_action.save! + else + return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button) end - redirect_to admin_reports_path + redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: @report.id) end private @@ -47,6 +56,8 @@ class Admin::Reports::ActionsController < Admin::BaseController 'silence' elsif params[:suspend] 'suspend' + elsif params[:moderation_action] + params[:moderation_action] end end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 64b5cb747c..94b707771f 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) + params.permit( + :display_name, + :note, + :avatar, + :header, + :locked, + :bot, + :discoverable, + :hide_collections, + fields_attributes: [:name, :value] + ) end def user_settings_params diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index f3c0c4b6b4..e77df30216 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -3,6 +3,14 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController before_action -> { authorize_if_got_token! :'admin:read' } + def index + if current_user&.can?(:manage_taxonomies) + render json: @tags, each_serializer: REST::Admin::TagSerializer + else + super + end + end + private def enabled? diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 96c38ff555..5814f7c409 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -81,6 +81,7 @@ class Api::V1::StatusesController < Api::BaseController current_account.id, text: status_params[:status], media_ids: status_params[:media_ids], + media_attributes: status_params[:media_attributes], sensitive: status_params[:sensitive], language: status_params[:language], spoiler_text: status_params[:spoiler_text], @@ -133,6 +134,12 @@ class Api::V1::StatusesController < Api::BaseController :quote_id, :content_type, media_ids: [], + media_attributes: [ + :id, + :thumbnail, + :description, + :focus, + ], poll: [ :multiple, :hide_totals, diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin_export_controller_concern.rb index b40c76557f..4ac48a04b7 100644 --- a/app/controllers/concerns/admin_export_controller_concern.rb +++ b/app/controllers/concerns/admin_export_controller_concern.rb @@ -26,14 +26,4 @@ module AdminExportControllerConcern def import_params params.require(:admin_import).permit(:data) end - - def import_data_path - params[:admin_import][:data].path - end - - def parse_import_data!(default_headers) - data = CSV.read(import_data_path, headers: true, encoding: 'UTF-8') - data = CSV.read(import_data_path, headers: default_headers, encoding: 'UTF-8') unless data.headers&.first&.strip&.include?(default_headers[0]) - @data = data.reject(&:blank?) - end end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 4502da698c..a9950d21fb 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -46,11 +46,11 @@ module SignatureVerification end def require_account_signature! - render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account + render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end def require_actor_signature! - render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor + render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor end def signed_request? @@ -97,11 +97,11 @@ module SignatureVerification actor = stoplight_wrap_request { actor_refresh_key!(actor) } - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? + raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? return actor unless verify_signature(actor, signature, compare_signed_string).nil? - fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" + fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] rescue SignatureVerificationError => e fail_with! e.message rescue HTTP::Error, OpenSSL::SSL::SSLError => e @@ -118,8 +118,8 @@ module SignatureVerification private - def fail_with!(message) - @signature_verification_failure_reason = message + def fail_with!(message, **options) + @signature_verification_failure_reason = { error: message }.merge(options) @signed_request_actor = nil end @@ -209,8 +209,8 @@ module SignatureVerification end expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present? - rescue ArgumentError - return false + rescue ArgumentError => e + raise SignatureVerificationError, "Invalid Date header: #{e.message}" end expires_time ||= created_time + 5.minutes unless created_time.nil? diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index b6050c9138..7ba7a57e3d 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -4,22 +4,17 @@ module WebAppControllerConcern extend ActiveSupport::Concern included do + prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack - before_action :redirect_unauthenticated_to_permalinks! before_action :set_app_body_class - before_action :set_referrer_policy_header end def set_app_body_class @body_classes = 'app-body' end - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'origin' - end - def redirect_unauthenticated_to_permalinks! - return if user_signed_in? + return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in redirect_path = PermalinkRedirector.new(request.path).redirect_path diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 215ecea0d7..4018ef6b1c 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -20,7 +20,7 @@ module Admin::ActionLogsHelper when 'Status' link_to log.human_identifier, log.permalink when 'AccountWarning' - link_to log.human_identifier, admin_account_path(log.target_id) + link_to log.human_identifier, disputes_strike_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) when 'IpBlock', 'Instance', 'CustomEmoji' diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 3175ff5607..ac1b2f95fb 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -194,7 +194,7 @@ ready(() => { } document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { - const domain = document.getElementById('by_domain')?.value; + const domain = document.querySelector('input[type="text"]#by_domain')?.value; if (domain) { const url = new URL(event.target.href); diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index edc332a84f..017b686934 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -203,6 +203,18 @@ export function submitCompose(routerHistory) { dispatch(submitComposeRequest()); + // If we're editing a post with media attachments, those have not + // necessarily been changed on the server. Do it now in the same + // API call. + let media_attributes; + if (statusId !== null) { + media_attributes = media.map(item => ({ + id: item.get('id'), + description: item.get('description'), + focus: item.get('focus'), + })); + } + api(getState).request({ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, method: statusId === null ? 'post' : 'put', @@ -212,6 +224,7 @@ export function submitCompose(routerHistory) { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), quote_id: getState().getIn(['compose', 'quote_id'], null), media_ids: media.map(item => item.get('id')), + media_attributes, sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), spoiler_text: spoilerText, visibility: getState().getIn(['compose', 'privacy']), @@ -438,11 +451,31 @@ export function changeUploadCompose(id, params) { return (dispatch, getState) => { dispatch(changeUploadComposeRequest()); - api(getState).put(`/api/v1/media/${id}`, params).then(response => { - dispatch(changeUploadComposeSuccess(response.data)); - }).catch(error => { - dispatch(changeUploadComposeFail(id, error)); - }); + let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id); + + // Editing already-attached media is deferred to editing the post itself. + // For simplicity's sake, fake an API reply. + if (media && !media.get('unattached')) { + let { description, focus } = params; + const data = media.toJS(); + + if (description) { + data.description = description; + } + + if (focus) { + focus = focus.split(','); + data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; + } + + dispatch(changeUploadComposeSuccess(data, true)); + } else { + api(getState).put(`/api/v1/media/${id}`, params).then(response => { + dispatch(changeUploadComposeSuccess(response.data, false)); + }).catch(error => { + dispatch(changeUploadComposeFail(id, error)); + }); + } }; }; @@ -453,10 +486,11 @@ export function changeUploadComposeRequest() { }; }; -export function changeUploadComposeSuccess(media) { +export function changeUploadComposeSuccess(media, attached) { return { type: COMPOSE_UPLOAD_CHANGE_SUCCESS, media: media, + attached: attached, skipLoading: true, }; }; diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js index fb6e55612e..023151d4bf 100644 --- a/app/javascript/flavours/glitch/actions/dropdown_menu.js +++ b/app/javascript/flavours/glitch/actions/dropdown_menu.js @@ -1,8 +1,8 @@ export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; -export function openDropdownMenu(id, placement, keyboard, scroll_key) { - return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key }; +export function openDropdownMenu(id, keyboard, scroll_key) { + return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key }; } export function closeDropdownMenu(id) { diff --git a/app/javascript/flavours/glitch/actions/tags.js b/app/javascript/flavours/glitch/actions/tags.js index 37e79d4cba..08a08cda31 100644 --- a/app/javascript/flavours/glitch/actions/tags.js +++ b/app/javascript/flavours/glitch/actions/tags.js @@ -1,9 +1,17 @@ -import api from '../api'; +import api, { getLinks } from '../api'; export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; +export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; +export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; +export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; +export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; +export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; + export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; @@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({ error, }); +export const fetchFollowedHashtags = () => (dispatch, getState) => { + dispatch(fetchFollowedHashtagsRequest()); + + api(getState).get('/api/v1/followed_tags').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchFollowedHashtagsFail(err)); + }); +}; + +export function fetchFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_FETCH_REQUEST, + }; +}; + +export function fetchFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, + followed_tags, + next, + }; +}; + +export function fetchFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_FETCH_FAIL, + error, + }; +}; + +export function expandFollowedHashtags() { + return (dispatch, getState) => { + const url = getState().getIn(['followed_tags', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowedHashtagsRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFollowedHashtagsFail(error)); + }); + }; +}; + +export function expandFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, + }; +}; + +export function expandFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + followed_tags, + next, + }; +}; + +export function expandFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_FAIL, + error, + }; +}; + export const followHashtag = name => (dispatch, getState) => { dispatch(followHashtagRequest(name)); diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js index 4c17b69a08..774bf36e6e 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.js +++ b/app/javascript/flavours/glitch/components/admin/Trends.js @@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent { day.uses)} diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.js index b40a2ff350..c7b0246523 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.js +++ b/app/javascript/flavours/glitch/components/autosuggest_input.js @@ -50,6 +50,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { id: PropTypes.string, searchTokens: PropTypes.arrayOf(PropTypes.string), maxLength: PropTypes.number, + lang: PropTypes.string, }; static defaultProps = { @@ -185,7 +186,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang } = this.props; const { suggestionsHidden } = this.state; return ( @@ -210,6 +211,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { id={id} className={className} maxLength={maxLength} + lang={lang} /> diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index 967c593af2..68c0834336 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -48,6 +48,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onKeyDown: PropTypes.func, onPaste: PropTypes.func.isRequired, autoFocus: PropTypes.bool, + lang: PropTypes.string, }; static defaultProps = { @@ -192,7 +193,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props; const { suggestionsHidden } = this.state; return [ @@ -216,6 +217,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onPaste={this.onPaste} dir='auto' aria-autocomplete='list' + lang={lang} /> diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 036e0b9090..7c70f750fd 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; @@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent { scrollable: PropTypes.bool, onClose: PropTypes.func.isRequired, style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, renderHeader: PropTypes.func, @@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent { static defaultProps = { style: {}, - placement: 'bottom', - }; - - state = { - mounted: false, }; handleDocumentClick = e => { @@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent { if (this.focusedItem && this.props.openedViaKeyboard) { this.focusedItem.focus({ preventScroll: true }); } - - this.setState({ mounted: true }); } componentWillUnmount () { @@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent { } render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; - const { mounted } = this.state; + const { items, scrollable, renderHeader, loading } = this.props; let renderItem = this.props.renderItem || this.renderItem; return ( - - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays -
-
+
+ {loading && ( + + )} -
- {loading && ( - - )} - - {!loading && renderHeader && ( -
- {renderHeader(items)} -
- )} - - {!loading && ( -
    - {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} -
- )} -
+ {!loading && renderHeader && ( +
+ {renderHeader(items)}
)} - + + {!loading && ( +
    + {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} +
+ )} +
); } @@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent { isUserTouching: PropTypes.func, onOpen: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, @@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent { id: id++, }; - handleClick = ({ target, type }) => { + handleClick = ({ type }) => { if (this.state.id === this.props.openDropdownId) { this.handleClose(); } else { - const { top } = target.getBoundingClientRect(); - const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); + this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click'); } } @@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent { disabled, loading, scrollable, - dropdownPlacement, openDropdownId, openedViaKeyboard, children, @@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; const button = children ? React.cloneElement(React.Children.only(children), { - ref: this.setTargetRef, onClick: this.handleClick, onMouseDown: this.handleMouseDown, onKeyDown: this.handleButtonKeyDown, @@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent { active={open} disabled={disabled} size={size} - ref={this.setTargetRef} onClick={this.handleClick} onMouseDown={this.handleMouseDown} onKeyDown={this.handleButtonKeyDown} @@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent { return ( - {button} - - - + + {button} + + + {({ props, arrowProps, placement }) => ( +
+
+
+ +
+
+ )} ); diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js index 8b73663d44..a1519757de 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -4,7 +4,6 @@ import { fetchHistory } from 'flavours/glitch/actions/history'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; const mapStateToProps = (state, { statusId }) => ({ - dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), items: state.getIn(['history', statusId, 'items']), @@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({ const mapDispatchToProps = (dispatch, { statusId }) => ({ - onOpen (id, onItemClick, dropdownPlacement, keyboard) { + onOpen (id, onItemClick, keyboard) { dispatch(fetchHistory(statusId)); - dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); + dispatch(openDropdownMenu(id, keyboard)); }, onClose (id) { diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 41a95e92f7..2485f0f480 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -30,6 +30,7 @@ export default class IconButton extends React.PureComponent { counter: PropTypes.number, obfuscateCount: PropTypes.bool, href: PropTypes.string, + ariaHidden: PropTypes.bool, }; static defaultProps = { @@ -39,6 +40,7 @@ export default class IconButton extends React.PureComponent { animate: false, overlay: false, tabIndex: '0', + ariaHidden: false, }; state = { @@ -115,6 +117,7 @@ export default class IconButton extends React.PureComponent { counter, obfuscateCount, href, + ariaHidden, } = this.props; const { @@ -155,6 +158,7 @@ export default class IconButton extends React.PureComponent { -
+
+
+ + +
-
- {results.map(this.renderItem)} -
-
- )} - +
+ {results.map(this.renderItem)} +
+
); } @@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent { placement: 'bottom', }; - handleToggle = ({ target }) => { - const { top } = target.getBoundingClientRect(); - + handleToggle = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent { onChange(value); } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + render () { const { value, intl, frequentlyUsedLanguages } = this.props; const { open, placement } = this.state; return ( -
-
+
+
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 9f90a767d0..e5874de75e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -3,13 +3,12 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import spring from 'react-motion/lib/spring'; import { injectIntl, FormattedMessage, defineMessages, } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; // Components. import Icon from 'flavours/glitch/components/icon'; @@ -17,7 +16,6 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; import { searchEnabled } from 'flavours/glitch/initial_state'; -import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -26,31 +24,20 @@ const messages = defineMessages({ class SearchPopout extends React.PureComponent { - static propTypes = { - style: PropTypes.object, - }; - render () { - const { style } = this.props; const extraInformation = searchEnabled ? : ; return ( -
- - {({ opacity, scaleX, scaleY }) => ( -
-

+
+

-
    -
  • #example
  • -
  • @username@domain
  • -
  • URL
  • -
  • URL
  • -
+
    +
  • #example
  • +
  • @username@domain
  • +
  • URL
  • +
  • URL
  • +
- {extraInformation} -
- )} - + {extraInformation}
); } @@ -136,6 +123,10 @@ class Search extends React.PureComponent { } } + findTarget = () => { + return this.searchForm; + } + render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; @@ -161,8 +152,14 @@ class Search extends React.PureComponent {
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js index 6528bbc84c..cd45245409 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -43,13 +43,13 @@ export default class Upload extends ImmutablePureComponent { {({ scale }) => (
- - {!!media.get('unattached') && ()} + +
- {(media.get('description') || '').length === 0 && !!media.get('unattached') && ( + {(media.get('description') || '').length === 0 && (
- +
)}
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index d12c98c01d..8f2947672b 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -70,6 +70,7 @@ function mapStateToProps (state) { mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']), preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']), isInReply: state.getIn(['compose', 'in_reply_to']) !== null, + lang: state.getIn(['compose', 'language']), }; }; diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index 6c3db8173d..5de9f5419d 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -46,7 +46,7 @@ const mapDispatchToProps = (dispatch) => ({ }, onDoodleOpen() { - dispatch(openModal('DOODLE', { noEsc: true })); + dispatch(openModal('DOODLE', { noEsc: true, noClose: true })); }, }); diff --git a/app/javascript/flavours/glitch/features/followed_tags/index.js b/app/javascript/flavours/glitch/features/followed_tags/index.js new file mode 100644 index 0000000000..4a23afc2d4 --- /dev/null +++ b/app/javascript/flavours/glitch/features/followed_tags/index.js @@ -0,0 +1,89 @@ +import { debounce } from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import Column from 'flavours/glitch/features/ui/components/column'; +import { Helmet } from 'react-helmet'; +import Hashtag from 'flavours/glitch/components/hashtag'; +import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags'; + +const messages = defineMessages({ + heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' }, +}); + +const mapStateToProps = state => ({ + hashtags: state.getIn(['followed_tags', 'items']), + isLoading: state.getIn(['followed_tags', 'isLoading'], true), + hasMore: !!state.getIn(['followed_tags', 'next']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class FollowedTags extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hashtags: ImmutablePropTypes.list, + isLoading: PropTypes.bool, + hasMore: PropTypes.bool, + multiColumn: PropTypes.bool, + }; + + componentDidMount() { + this.props.dispatch(fetchFollowedHashtags()); + }; + + handleLoadMore = debounce(() => { + this.props.dispatch(expandFollowedHashtags()); + }, 300, { leading: true }); + + render () { + const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props; + + const emptyMessage = ; + + return ( + + + + + {hashtags.map((hashtag) => ( + day.get('uses')).toArray()} + /> + ))} + + + + + + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index d01eec811e..d1573da9c2 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; // Our imports -import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state'; +import { expandSpoilers } from 'flavours/glitch/initial_state'; import { preferenceLink } from 'flavours/glitch/utils/backend_links'; import LocalSettingsPageItem from './item'; import DeprecatedLocalSettingsPageItem from './deprecated_item'; @@ -406,6 +406,18 @@ class LocalSettingsPage extends React.PureComponent { > + + +

diff --git a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js index 6b24e41435..86da640ba7 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js @@ -14,6 +14,7 @@ export default class LocalSettingsPageItem extends React.PureComponent { id: PropTypes.string.isRequired, item: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, + inputProps: PropTypes.object, options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string.isRequired, message: PropTypes.string.isRequired, @@ -34,7 +35,7 @@ export default class LocalSettingsPageItem extends React.PureComponent { render () { const { handleChange } = this; - const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder, disabled } = this.props; + const { settings, item, id, inputProps, options, children, dependsOn, dependsOnNot, placeholder, disabled } = this.props; let enabled = !disabled; if (dependsOn) { @@ -54,14 +55,17 @@ export default class LocalSettingsPageItem extends React.PureComponent { let optionId = `${id}--${opt.value}`; return ( @@ -103,7 +108,8 @@ export default class LocalSettingsPageItem extends React.PureComponent { checked={settings.getIn(item)} onChange={handleChange} disabled={!enabled} - /> + {...inputProps} + /> {children}
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index d676a4207c..d1aea1b21d 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -124,6 +124,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} @@ -146,6 +147,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} @@ -168,6 +170,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} @@ -190,6 +193,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} @@ -212,6 +216,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index c0aae505a4..b31ab47249 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -7,7 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { me } from 'flavours/glitch/initial_state'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; -import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -35,6 +35,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); @@ -183,19 +184,19 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { + if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - if (accountAdminLink !== undefined) { - menu.push({ - text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), - href: accountAdminLink(status.getIn(['account', 'id'])), - }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if (accountAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) }); + } + if (statusAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) }); + } } - if (statusAdminLink !== undefined) { - menu.push({ - text: intl.formatMessage(messages.admin_status), - href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')), - }); + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = status.getIn(['account', 'acct']).split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); } } } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 52df1c145f..13f11226f8 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent { domain: PropTypes.string.isRequired, compact: PropTypes.bool, showMedia: PropTypes.bool, - usingPiP: PropTypes.bool, + pictureInPicture: ImmutablePropTypes.contains({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), onToggleMediaVisibility: PropTypes.func, intl: PropTypes.object.isRequired, }; @@ -120,7 +123,7 @@ class DetailedStatus extends ImmutablePureComponent { render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; - const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props; + const { expanded, onToggleHidden, settings, pictureInPicture, intl } = this.props; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; @@ -153,7 +156,7 @@ class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } - if (usingPiP) { + if (pictureInPicture.get('inUse')) { media.push(); mediaIcons.push('video-camera'); } else if (status.get('media_attachments').size > 0) { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index e1fbc98241..0327c34309 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -42,7 +42,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { initBoostModal } from 'flavours/glitch/actions/boosts'; -import { makeGetStatus } from 'flavours/glitch/selectors'; +import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ColumnHeader from '../../components/column_header'; @@ -73,6 +73,7 @@ const messages = defineMessages({ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); + const getPictureInPicture = makeGetPictureInPicture(); const getAncestorsIds = createSelector([ (_, { id }) => id, @@ -130,11 +131,12 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => { const status = getStatus(state, { id: props.params.statusId }); - let ancestorsIds = Immutable.List(); + + let ancestorsIds = Immutable.List(); let descendantsIds = Immutable.List(); if (status) { - ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); + ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); descendantsIds = getDescendantsIds(state, { id: status.get('id') }); } @@ -146,7 +148,7 @@ const makeMapStateToProps = () => { settings: state.get('local_settings'), askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), - usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, + pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), }; }; @@ -191,7 +193,10 @@ class Status extends ImmutablePureComponent { askReplyConfirmation: PropTypes.bool, multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, - usingPiP: PropTypes.bool, + pictureInPicture: ImmutablePropTypes.contains({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), }; state = { @@ -620,7 +625,7 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; + const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props; const { fullscreen } = this.state; if (isLoading) { @@ -698,7 +703,7 @@ class Status extends ImmutablePureComponent { domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} - usingPiP={usingPiP} + pictureInPicture={pictureInPicture} /> - +
diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index bbf25c8a85..eefbdca804 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -75,6 +75,7 @@ * @property {boolean} timeline_preview * @property {string} title * @property {boolean} trends + * @property {boolean} trends_as_landing_page * @property {boolean} unfollow_modal * @property {boolean} use_blurhash * @property {boolean=} use_pending_items @@ -134,6 +135,7 @@ export const singleUserMode = getMeta('single_user_mode'); export const source_url = getMeta('source_url'); export const timelinePreview = getMeta('timeline_preview'); export const title = getMeta('title'); +export const trendsAsLanding = getMeta('trends_as_landing_page'); export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); diff --git a/app/javascript/flavours/glitch/load_polyfills.js b/app/javascript/flavours/glitch/load_polyfills.js index cc5bcd18f1..f5a897f754 100644 --- a/app/javascript/flavours/glitch/load_polyfills.js +++ b/app/javascript/flavours/glitch/load_polyfills.js @@ -23,15 +23,14 @@ function loadPolyfills() { ); // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback and object-fit CSS property. + // Edge does not have requestIdleCallback. // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( window.AbortController && window.IntersectionObserver && window.IntersectionObserverEntry && 'isIntersecting' in IntersectionObserverEntry.prototype && - window.requestIdleCallback && - 'object-fit' in (new Image()).style + window.requestIdleCallback ); return Promise.all([ diff --git a/app/javascript/flavours/glitch/locales/de.json b/app/javascript/flavours/glitch/locales/de.json index c5e3cdb35e..a8f189bf42 100644 --- a/app/javascript/flavours/glitch/locales/de.json +++ b/app/javascript/flavours/glitch/locales/de.json @@ -103,6 +103,7 @@ "settings.auto_collapse_all": "Alles", "settings.auto_collapse_lengthy": "Lange Toots", "settings.auto_collapse_media": "Toots mit Anhängen", + "settings.auto_collapse_height": "Höhe (in Pixeln), ab der ein Toot als lang gilt", "settings.auto_collapse_notifications": "Benachrichtigungen", "settings.auto_collapse_reblogs": "Geteilte Toots", "settings.auto_collapse_replies": "Antworten", diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 59f2f74b1e..7c21f69c3c 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -103,6 +103,7 @@ "settings.auto_collapse_all": "Everything", "settings.auto_collapse_lengthy": "Lengthy toots", "settings.auto_collapse_media": "Toots with media", + "settings.auto_collapse_height": "Height (in pixels) for a toot to be considered lengthy", "settings.auto_collapse_notifications": "Notifications", "settings.auto_collapse_reblogs": "Boosts", "settings.auto_collapse_replies": "Replies", diff --git a/app/javascript/flavours/glitch/locales/eo.json b/app/javascript/flavours/glitch/locales/eo.json index 4d243f94c2..87fe6c657f 100644 --- a/app/javascript/flavours/glitch/locales/eo.json +++ b/app/javascript/flavours/glitch/locales/eo.json @@ -1,6 +1,52 @@ { + "account.add_account_note": "Aldoni noton por @{name}", + "account_note.cancel": "Nuligi", + "account_note.edit": "Redakti", + "account_note.save": "Konservi", + "column.reblogged_by": "Diskonigita de", + "column.subheading": "Diversaj agordoj", + "column_header.profile": "Profilo", + "column_subheading.lists": "Listoj", + "compose.attach": "Aldoni…", + "compose.attach.doodle": "Desegni ion", + "compose.attach.upload": "Alŝuti dosieron", + "compose.content-type.html": "HTML", + "compose.content-type.markdown": "Markdown", + "confirmations.unfilter.author": "Aŭtoro", + "confirmations.unfilter.confirm": "Montri", + "confirmations.unfilter.edit_filter": "Redakti filtrilon", + "navigation_bar.keyboard_shortcuts": "Fulmoklavoj", + "notification_purge.btn_all": "Selekti ĉiujn", + "notification_purge.btn_apply": "Forigi selektajn", + "notification_purge.btn_invert": "Inverti selekton", + "notification_purge.btn_none": "Elekti neniun", + "notifications.marked_clear": "Forigi selektajn sciigojn", + "onboarding.next": "Sekva", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_six.almost_done": "Preskaŭ finita…", + "onboarding.page_six.apps_available": "Estas {apps} disponeblaj por iOS, Android kaj aliaj sistemoj.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.various_app": "poŝtelefonaj aplikaĵoj", + "settings.auto_collapse_all": "Ĉiuj", + "settings.auto_collapse_lengthy": "Longaj afiŝoj", + "settings.auto_collapse_media": "Afiŝoj kun aŭdovidaĵoj", + "settings.auto_collapse_notifications": "Sciigoj", + "settings.auto_collapse_reblogs": "Diskonigoj", + "settings.auto_collapse_replies": "Respondoj", + "settings.close": "Fermi", "settings.content_warnings": "Content warnings", - "settings.preferences": "Preferences" + "settings.content_warnings.regexp": "Regula esprimo", + "settings.preferences": "Preferences", + "settings.shared_settings_link": "preferoj de uzanto", + "settings.side_arm": "Duaranga butono por afiŝi:", + "settings.side_arm.none": "Neniu", + "settings.status_icons": "Ikonoj sur la afiŝoj", + "settings.status_icons_language": "Indikilo de lingvo", + "settings.status_icons_media": "Indikilo de aŭdovidaĵojn kaj balotenketo", + "settings.status_icons_reply": "Indikilo de respondoj", + "settings.status_icons_visibility": "Indikilo de privateco de afiŝo", + "web_app_crash.change_your_settings": "Ŝanĝi viajn {settings}", + "web_app_crash.reload": "Reŝarĝi", + "web_app_crash.reload_page": "{reload} la nunan paĝon", + "web_app_crash.settings": "agordojn" } diff --git a/app/javascript/flavours/glitch/locales/pt-BR.json b/app/javascript/flavours/glitch/locales/pt-BR.json index 4d243f94c2..0bc0d2beae 100644 --- a/app/javascript/flavours/glitch/locales/pt-BR.json +++ b/app/javascript/flavours/glitch/locales/pt-BR.json @@ -1,6 +1,200 @@ { + "about.fork_disclaimer": "O Glitch-soc é um software gratuito de código aberto bifurcado a partir do Mastodon.", + "account.add_account_note": "Adicionar nota para @{name}", + "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.", + "account.follows": "Seguidores", + "account.joined": "Entrou em {date}", + "account.suspended_disclaimer_full": "Este usuário foi suspenso por um moderador.", + "account.view_full_profile": "Ver o perfil completo", + "account_note.cancel": "Cancelar", + "account_note.edit": "Editar", + "account_note.glitch_placeholder": "Nenhum comentário fornecido", + "account_note.save": "Salvar", + "advanced_options.icon_title": "Opções avançadas", + "advanced_options.local-only.long": "Não publicar em outras instâncias", + "advanced_options.local-only.short": "Apenas localmente", + "advanced_options.local-only.tooltip": "Este post é somente local", + "advanced_options.threaded_mode.long": "Abrir automaticamente uma resposta ao postar", + "advanced_options.threaded_mode.short": "Modo de discussão", + "advanced_options.threaded_mode.tooltip": "Modo de discussão ativado", + "boost_modal.missing_description": "Este toot contém algumas mídias sem descrição", + "column.favourited_by": "Favoritado por", + "column.heading": "Diversos", + "column.reblogged_by": "Inpulsionado por", + "column.subheading": "Opções diversas", + "column_header.profile": "Perfil", + "column_subheading.lists": "Listas", + "column_subheading.navigation": "Navegação", + "community.column_settings.allow_local_only": "Mostrar os toots apenas locais", + "compose.attach": "Anexar...", + "compose.attach.doodle": "Desenhe algo", + "compose.attach.upload": "Enviar um arquivo", + "compose.content-type.html": "HTML", + "compose.content-type.markdown": "Markdown", + "compose.content-type.plain": "Texto sem formatação", + "compose_form.poll.multiple_choices": "Permitir múltipla escolha", + "compose_form.poll.single_choice": "Permitir uma escolha", + "compose_form.spoiler": "Ocultar texto atrás do aviso", + "confirmation_modal.do_not_ask_again": "Não pedir confirmação novamente", + "confirmations.deprecated_settings.confirm": "Usar preferências do Mastodon", + "confirmations.deprecated_settings.message": "Alguns dos {app_settings} específicos do dispositivo que você está usando foram substituídos por Mastodon {preferences} e serão substituídos:", + "confirmations.missing_media_description.confirm": "Enviar mesmo assim", + "confirmations.missing_media_description.edit": "Editar mídia", + "confirmations.missing_media_description.message": "Pelo menos um anexo de mídia não tem uma descrição. Considere descrever todos os anexos de mídia para deficientes visuais antes de enviar seu toot.", + "confirmations.unfilter.author": "Autor", + "confirmations.unfilter.confirm": "Exibir", + "confirmations.unfilter.edit_filter": "Editar filtro", + "confirmations.unfilter.filters": "Correspondência de {count, plural, one {filtro} other {filtros}}", + "content-type.change": "Tipo de conteúdo", + "direct.group_by_conversations": "Agrupar por conversa", + "endorsed_accounts_editor.endorsed_accounts": "Contas em destaque", + "favourite_modal.combo": "Você pode pressionar {combo} para pular isso da próxima vez", + "getting_started.onboarding": "Mostre-me ao redor", + "home.column_settings.advanced": "Avançado", + "home.column_settings.filter_regex": "Filtrar com uma expressão regular", + "home.column_settings.show_direct": "Mostrar DMs", + "home.settings": "Configurações da coluna", + "keyboard_shortcuts.bookmark": "para marcar", + "keyboard_shortcuts.secondary_toot": "para enviar toot usando a configuração de privacidade secundária", + "keyboard_shortcuts.toggle_collapse": "para recolher/mostrar toots", + "layout.auto": "Automático", + "layout.desktop": "Área de trabalho", + "layout.hint.auto": "Escolher automaticamente o layout baseado na configuração \"Habilitar interface web avançada\" e o tamanho da tela.", + "layout.hint.desktop": "Use o layout de várias colunas independentemente da configuração \"Habilitar interface web avançada\" ou do tamanho da tela.", + "layout.hint.single": "Use o layout de uma coluna independentemente da configuração \"Habilitar interface web avançada\" ou do tamanho da tela.", + "layout.single": "Celular", + "media_gallery.sensitive": "Sensível", + "moved_to_warning": "Esta conta foi como movida para {moved_to_link} e, portanto, pode não aceitar novos seguidores.", + "navigation_bar.app_settings": "Configurações do aplicativo", + "navigation_bar.featured_users": "Usuários em destaque", + "navigation_bar.info": "Informação estendida", + "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", + "navigation_bar.misc": "Diversos", + "notification.markForDeletion": "Marcar para exclusão", + "notification_purge.btn_all": "Selecionar\ntudo", + "notification_purge.btn_apply": "Limpar\nselecionados", + "notification_purge.btn_invert": "Inverter\nseleção", + "notification_purge.btn_none": "Selecionar\nnenhum", + "notification_purge.start": "Entrar no modo de limpeza de notificação", + "notifications.marked_clear": "Limpar as notificações selecionadas", + "notifications.marked_clear_confirmation": "Tem certeza que deseja limpar todas as notificações selecionadas permanentemente?", + "onboarding.done": "Feito", + "onboarding.next": "Próximo", + "onboarding.page_five.public_timelines": "A linha do tempo local mostra publicações públicas de todos em {domain}. A linha do tempo federada mostra publicações públicas de todos que as pessoas seguem em {domain}. Estas são as linhas do tempo públicas, uma ótima maneira de descobrir novas pessoas.", + "onboarding.page_four.home": "A linha do tempo da casa mostra publicações de pessoas que você segue.", + "onboarding.page_four.notifications": "A coluna de notificações mostra quando alguém interage com você.", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", + "onboarding.page_one.handle": "Você está em {domain}, então o seu identificador completo é {handle}", + "onboarding.page_one.welcome": "Bem-vindo ao {domain}!", + "onboarding.page_six.admin": "O administrador da sua instância é {admin}.", + "onboarding.page_six.almost_done": "Quase pronto...", + "onboarding.page_six.appetoot": "Bom Appetoot!", + "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", + "onboarding.page_six.guidelines": "diretrizes da comunidade", + "onboarding.page_six.read_guidelines": "Por favor, leia {domain} {guidelines}!", + "onboarding.page_six.various_app": "aplicativos móveis", + "onboarding.page_three.profile": "Edite seu perfil para alterar seu avatar, bio e nome de exibição. Lá você também encontrará outras preferências.", + "onboarding.page_three.search": "Use a barra de busca para encontrar pessoas e procure hashtags, tais como {illustration} e {introductions}. Para procurar uma pessoa que não esteja neste caso, use o identificador completo.", + "onboarding.page_two.compose": "Escreva as postagens a partir da coluna de composição. Você pode enviar imagens, alterar as configurações de privacidade e adicionar avisos de conteúdo com os ícones abaixo.", + "onboarding.skip": "Pular", + "settings.always_show_spoilers_field": "Sempre ativar o campo Aviso de Conteúdo", + "settings.auto_collapse": "Colapso automático", + "settings.auto_collapse_all": "Tudo", + "settings.auto_collapse_lengthy": "Toots longos", + "settings.auto_collapse_media": "Toots com mídia", + "settings.auto_collapse_notifications": "Notificações", + "settings.auto_collapse_reblogs": "Impulsos", + "settings.auto_collapse_replies": "Respostas", + "settings.close": "Fechar", + "settings.collapsed_statuses": "Toots recolhidos", + "settings.compose_box_opts": "Caixa de composição", + "settings.confirm_before_clearing_draft": "Mostrar diálogo de confirmação antes de sobrescrever a mensagem que está sendo composta", + "settings.confirm_boost_missing_media_description": "Mostrar diálogo antes de inpulsionar os toots sem descrições de mídia", + "settings.confirm_missing_media_description": "Mostrar diálogo antes de enviar toots sem descrições de mídia", "settings.content_warnings": "Content warnings", - "settings.preferences": "Preferences" + "settings.content_warnings.regexp": "Expressão regular", + "settings.content_warnings_filter": "Avisos de conteúdo para não revelar automaticamente:", + "settings.content_warnings_media_outside": "Exibir anexos de mídia fora avisos de conteúdo", + "settings.content_warnings_media_outside_hint": "Reproduzir o comportamento do Mastodonte, fazendo com que a alternância do Aviso de Conteúdo não afete os anexos de mídia", + "settings.content_warnings_shared_state": "Mostrar/ocultar o conteúdo de todas as cópias de uma só vez", + "settings.content_warnings_shared_state_hint": "Reproduzir o comportamento do Mastodonte fazendo com que o botão de Aviso de Conteúdo afete todas as cópias de um post de uma só vez. Isto evitará o colapso automático de qualquer cópia de um toon com Aviso de Conteúdo revelado", + "settings.content_warnings_unfold_opts": "Opções de auto-revelar", + "settings.deprecated_setting": "Essa configuração agora é controlada pelo {settings_page_link} do Mastodon", + "settings.enable_collapsed": "Habilitar toots recolhidos", + "settings.enable_collapsed_hint": "Posts recolhidos têm partes dos seus conteúdos ocultos para ocupar menos espaço na tela. Isto é diferente do recurso 'Aviso de Conteúdo'", + "settings.enable_content_warnings_auto_unfold": "Revelar automaticamente os avisos de conteúdo", + "settings.general": "Geral", + "settings.hicolor_privacy_icons": "Ícones de privacidade com cores de alto contraste", + "settings.hicolor_privacy_icons.hint": "Exibir ícones de privacidade em cores brilhantes e facilmente distinguíveis", + "settings.image_backgrounds": "Fundos de imagem", + "settings.image_backgrounds_media": "Pré-visualização da mídia de toots colapsados", + "settings.image_backgrounds_media_hint": "Se o post tiver algum anexo de mídia, use o primeiro em um plano de fundo", + "settings.image_backgrounds_users": "Dar a toots recolhidos uma imagem de fundo", + "settings.inline_preview_cards": "Cartões de pré-visualização em linha para links externos", + "settings.layout": "Layout:", + "settings.layout_opts": "Opções de layout", + "settings.media": "Mídia", + "settings.media_fullwidth": "Pré-visualização da mídia em largura total", + "settings.media_letterbox": "Caixa de mensagens", + "settings.media_letterbox_hint": "Escala para baixo para encher os recipientes de imagem em vez de esticá-los e cortá-los", + "settings.media_reveal_behind_cw": "Revelar mídia sensível por trás de um Aviso de Conteúdo por padrão", + "settings.notifications.favicon_badge": "Notificações não lidas como emblema do favicon", + "settings.notifications.favicon_badge.hint": "Adicionar um emblema para notificações não lidas ao favicon", + "settings.notifications.tab_badge": "Emblema de notificações não lidas", + "settings.notifications.tab_badge.hint": "Exibir um emblema para notificações não lidas nos ícones de coluna quando a coluna de notificações não estiver aberta", + "settings.notifications_opts": "Opções de notificações", + "settings.pop_in_left": "Esquerda", + "settings.pop_in_player": "Ativar player pop-in", + "settings.pop_in_position": "Posição do player:", + "settings.pop_in_right": "Direita", + "settings.preferences": "Preferences", + "settings.prepend_cw_re": "Preparar \"re: \" para avisos de conteúdo quando responder", + "settings.preselect_on_reply": "Nome de usuário pré-selecionado na resposta", + "settings.preselect_on_reply_hint": "Ao responder a uma conversa com vários participantes, pré-selecionar nomes de usuários após o primeiro", + "settings.rewrite_mentions": "Reescrever as menções nos status exibidos", + "settings.rewrite_mentions_acct": "Reescrever com nome de usuário e domínio (quando a conta for remota)", + "settings.rewrite_mentions_no": "Não reescrever menções", + "settings.rewrite_mentions_username": "Reescreva com nome de usuário", + "settings.shared_settings_link": "preferências do usuário", + "settings.show_action_bar": "Mostrar botões de ação em toots recolhidos", + "settings.show_content_type_choice": "Exibir opção do tipo de conteúdo ao autorar toots", + "settings.show_reply_counter": "Exibir uma estimativa da contagem de respostas", + "settings.side_arm": "Botão de toot secundário:", + "settings.side_arm.none": "Nenhum", + "settings.side_arm_reply_mode": "Ao responder a um toot, o botão secundário de toot deve:", + "settings.side_arm_reply_mode.copy": "Copiar configuração de privacidade do toot sendo respondido a", + "settings.side_arm_reply_mode.keep": "Mantenha sua privacidade definida", + "settings.side_arm_reply_mode.restrict": "Restringir configuração de privacidade ao toot sendo respondido a", + "settings.status_icons": "Ícones de toot", + "settings.status_icons_language": "Indicador de idioma", + "settings.status_icons_local_only": "Indicador somente local", + "settings.status_icons_media": "Indicadores de mídia e enquete", + "settings.status_icons_reply": "Indicador de resposta", + "settings.status_icons_visibility": "Indicador de privacidade", + "settings.swipe_to_change_columns": "Permitir deslizar para alterar colunas (apenas celular)", + "settings.tag_misleading_links": "Marcar links enganosos", + "settings.tag_misleading_links.hint": "Acrescentar uma indicação visual com o link hospedeiro alvo a cada link que não o mencione explicitamente", + "settings.wide_view": "Visualização ampla (apenas no Modo desktop)", + "settings.wide_view_hint": "Estica as colunas para preencher melhor o espaço disponível.", + "status.collapse": "Recolher", + "status.has_audio": "Possui um arquivo de áudio anexado", + "status.has_pictures": "Possui uma imagem anexada", + "status.has_preview_card": "Possui uma pré-visualização anexada", + "status.has_video": "Possui um vídeo anexado", + "status.in_reply_to": "Este toot é uma resposta", + "status.is_poll": "Este toot é uma enquete", + "status.local_only": "Visível apenas em sua instância", + "status.sensitive_toggle": "Clique para ver", + "status.uncollapse": "Revelar", + "web_app_crash.change_your_settings": "Altere suas {settings}", + "web_app_crash.content": "Você poderia tentar qualquer uma das seguintes opções:", + "web_app_crash.debug_info": "Informações de depuração", + "web_app_crash.disable_addons": "Desativar complementos do navegador ou ferramentas de tradução integradas", + "web_app_crash.issue_tracker": "rastreador de problemas", + "web_app_crash.reload": "Recarregar", + "web_app_crash.reload_page": "{reload} a página atual", + "web_app_crash.report_issue": "Relatar um erro no {issuetracker}", + "web_app_crash.settings": "configurações", + "web_app_crash.title": "Desculpe, mas algo deu errado com o aplicativo Mastodon." } diff --git a/app/javascript/flavours/glitch/permissions.js b/app/javascript/flavours/glitch/permissions.js index 752ddd6c53..9ea149e5f8 100644 --- a/app/javascript/flavours/glitch/permissions.js +++ b/app/javascript/flavours/glitch/permissions.js @@ -1,3 +1,4 @@ -export const PERMISSION_INVITE_USERS = 0x0000000000010000; -export const PERMISSION_MANAGE_USERS = 0x0000000000000400; -export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; +export const PERMISSION_INVITE_USERS = 0x0000000000010000; +export const PERMISSION_MANAGE_USERS = 0x0000000000000400; +export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020; +export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 6162ab6e2d..c4ee082f66 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -575,7 +575,7 @@ export default function compose(state = initialState, action) { .setIn(['media_modal', 'dirty'], false) .update('media_attachments', list => list.map(item => { if (item.get('id') === action.media.id) { - return fromJS(action.media).set('unattached', true); + return fromJS(action.media).set('unattached', !action.attached); } return item; diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js index a78a11acca..51bf9375bf 100644 --- a/app/javascript/flavours/glitch/reducers/dropdown_menu.js +++ b/app/javascript/flavours/glitch/reducers/dropdown_menu.js @@ -4,12 +4,12 @@ import { DROPDOWN_MENU_CLOSE, } from '../actions/dropdown_menu'; -const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false, scroll_key: null }); +const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null }); export default function dropdownMenu(state = initialState, action) { switch (action.type) { case DROPDOWN_MENU_OPEN: - return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard, scroll_key: action.scroll_key }); + return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key }); case DROPDOWN_MENU_CLOSE: return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state; default: diff --git a/app/javascript/flavours/glitch/reducers/followed_tags.js b/app/javascript/flavours/glitch/reducers/followed_tags.js new file mode 100644 index 0000000000..4109b0b10d --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/followed_tags.js @@ -0,0 +1,42 @@ +import { + FOLLOWED_HASHTAGS_FETCH_REQUEST, + FOLLOWED_HASHTAGS_FETCH_SUCCESS, + FOLLOWED_HASHTAGS_FETCH_FAIL, + FOLLOWED_HASHTAGS_EXPAND_REQUEST, + FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + FOLLOWED_HASHTAGS_EXPAND_FAIL, +} from 'flavours/glitch/actions/tags'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, + next: null, +}); + +export default function followed_tags(state = initialState, action) { + switch(action.type) { + case FOLLOWED_HASHTAGS_FETCH_REQUEST: + return state.set('isLoading', true); + case FOLLOWED_HASHTAGS_FETCH_SUCCESS: + return state.withMutations(map => { + map.set('items', fromJS(action.followed_tags)); + map.set('isLoading', false); + map.set('next', action.next); + }); + case FOLLOWED_HASHTAGS_FETCH_FAIL: + return state.set('isLoading', false); + case FOLLOWED_HASHTAGS_EXPAND_REQUEST: + return state.set('isLoading', true); + case FOLLOWED_HASHTAGS_EXPAND_SUCCESS: + return state.withMutations(map => { + map.update('items', set => set.concat(fromJS(action.followed_tags))); + map.set('isLoading', false); + map.set('next', action.next); + }); + case FOLLOWED_HASHTAGS_EXPAND_FAIL: + return state.set('isLoading', false); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 09c08a3629..5b7bdbf699 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -42,6 +42,7 @@ import picture_in_picture from './picture_in_picture'; import accounts_map from './accounts_map'; import history from './history'; import tags from './tags'; +import followed_tags from './followed_tags'; const reducers = { announcements, @@ -87,6 +88,7 @@ const reducers = { picture_in_picture, history, tags, + followed_tags, }; export default combineReducers(reducers); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 81ab1cb0d8..9075146f3e 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -37,6 +37,7 @@ const initialState = ImmutableMap({ reblogs : false, replies : false, media : false, + height : 400, }), backgrounds : ImmutableMap({ user_backgrounds : false, diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index df46b58a8e..83f8783d9b 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -1,6 +1,6 @@ import escapeTextContentForBrowser from 'escape-html'; import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, Map as ImmutableMap, is } from 'immutable'; import { toServerSideType } from 'flavours/glitch/utils/filters'; import { me } from 'flavours/glitch/initial_state'; @@ -74,6 +74,16 @@ export const makeGetStatus = () => { ); }; +export const makeGetPictureInPicture = () => { + return createSelector([ + (state, { id }) => state.get('picture_in_picture').statusId === id, + (state) => state.getIn(['meta', 'layout']) !== 'mobile', + ], (inUse, available) => ImmutableMap({ + inUse: inUse && available, + available, + })); +}; + const getAlertsBase = state => state.get('alerts'); export const getAlerts = createSelector([getAlertsBase], (base) => { diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index cdc506cf45..2158a691ff 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -214,7 +214,7 @@ font-size: 12px; line-height: 12px; font-weight: 500; - color: var(--user-role-accent, $ui-secondary-color); + color: $ui-secondary-color; background-color: var(--user-role-background, rgba($ui-secondary-color, 0.1)); border: 1px solid var(--user-role-border, rgba($ui-secondary-color, 0.5)); diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 7d5b28a12c..9aa2318ce5 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -254,10 +254,8 @@ $content-width: 840px; &__actions { display: inline-flex; - - & > :not(:first-child) { - margin-left: 5px; - } + flex-flow: wrap; + gap: 5px; } h2 small { @@ -1590,6 +1588,15 @@ a.sparkline { margin-bottom: 0; } } + + a { + color: $highlight-text-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } } &__actions { diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 5b3e1db1bc..c2a6593b1a 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -533,14 +533,22 @@ &__tabs { display: flex; - align-items: flex-start; + align-items: flex-end; justify-content: space-between; padding: 7px 10px; - margin-top: -55px; - gap: 8px; + margin-top: -81px; + height: 130px; overflow: hidden; margin-left: -2px; // aligns the pfp with content below + .account-role { + margin: 0 2px; + padding: 4px 0; + box-sizing: border-box; + min-width: 90px; + text-align: center; + } + &__buttons { display: flex; align-items: center; diff --git a/app/javascript/flavours/glitch/styles/components/compose_form.scss b/app/javascript/flavours/glitch/styles/components/compose_form.scss index 00d395ce99..8c6fc63438 100644 --- a/app/javascript/flavours/glitch/styles/components/compose_form.scss +++ b/app/javascript/flavours/glitch/styles/components/compose_form.scss @@ -118,7 +118,7 @@ &.active { border-color: $highlight-text-color; - background: $highlight-text-color; + background: $highlight-text-color url("data:image/svg+xml;utf8,") center center no-repeat; } } } @@ -590,7 +590,6 @@ } .privacy-dropdown__dropdown { - position: absolute; border-radius: 4px; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); background: $simple-background-color; @@ -657,7 +656,6 @@ .language-dropdown { &__dropdown { - position: absolute; background: $simple-background-color; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); border-radius: 4px; diff --git a/app/javascript/flavours/glitch/styles/components/emoji.scss b/app/javascript/flavours/glitch/styles/components/emoji.scss index 9dfee346a6..c037e03f97 100644 --- a/app/javascript/flavours/glitch/styles/components/emoji.scss +++ b/app/javascript/flavours/glitch/styles/components/emoji.scss @@ -13,7 +13,7 @@ .emoji-picker-dropdown__menu { background: $simple-background-color; - position: absolute; + position: relative; box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); border-radius: 4px; margin-top: 5px; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b7a54cd2b4..d4c0d77c84 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -346,9 +346,8 @@ } } -.dropdown-menu { - position: absolute; - transform-origin: 50% 0; +body > [data-popper-placement] { + z-index: 3; } .invisible { @@ -532,6 +531,42 @@ } } +.dropdown-animation { + animation: dropdown 300ms cubic-bezier(0.1, 0.7, 0.1, 1); + + @keyframes dropdown { + from { + opacity: 0; + transform: scaleX(0.85) scaleY(0.75); + } + + to { + opacity: 1; + transform: scaleX(1) scaleY(1); + } + } + + &.top { + transform-origin: bottom; + } + + &.right { + transform-origin: left; + } + + &.bottom { + transform-origin: top; + } + + &.left { + transform-origin: right; + } + + .reduce-motion & { + animation: none; + } +} + .dropdown { display: inline-block; } @@ -600,36 +635,42 @@ .dropdown-menu__arrow { position: absolute; - width: 0; - height: 0; - border: 0 solid transparent; - &.left { - right: -5px; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: $ui-secondary-color; + &::before { + content: ''; + display: block; + width: 14px; + height: 5px; + background-color: $ui-secondary-color; + mask-image: url("data:image/svg+xml;utf8,"); } &.top { bottom: -5px; - margin-left: -7px; - border-width: 5px 7px 0; - border-top-color: $ui-secondary-color; + + &::before { + transform: rotate(180deg); + } + } + + &.right { + left: -9px; + + &::before { + transform: rotate(-90deg); + } } &.bottom { top: -5px; - margin-left: -7px; - border-width: 0 7px 5px; - border-bottom-color: $ui-secondary-color; } - &.right { - left: -5px; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: $ui-secondary-color; + &.left { + right: -9px; + + &::before { + transform: rotate(90deg); + } } } @@ -1544,14 +1585,14 @@ button.icon-button.active i.fa-retweet { align-items: center; background: rgba($base-overlay-background, 0.8); display: flex; - height: 100%; + height: 100vh; justify-content: center; left: 0; opacity: 0; - position: absolute; + position: fixed; top: 0; visibility: hidden; - width: 100%; + width: 100vw; z-index: 2000; * { diff --git a/app/javascript/flavours/glitch/styles/components/local_settings.scss b/app/javascript/flavours/glitch/styles/components/local_settings.scss index db2b9f154e..f36b21e1cd 100644 --- a/app/javascript/flavours/glitch/styles/components/local_settings.scss +++ b/app/javascript/flavours/glitch/styles/components/local_settings.scss @@ -110,6 +110,10 @@ text-decoration: none; } } + + #mastodon-settings--collapsed-auto-height { + width: calc(4ch + 20px); + } } .glitch.local-settings__page__item.string, diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 8ba8bec102..972e01e7d3 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -37,7 +37,6 @@ .modal-root__modal { pointer-events: auto; display: flex; - z-index: 9999; } .media-modal__zoom-button { diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 01b7a11e95..6a73b3a2ce 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -157,6 +157,15 @@ code { padding: 0.2em 0.4em; background: darken($ui-base-color, 12%); } + + li { + list-style: disc; + margin-left: 18px; + } + } + + ul.hint { + margin-bottom: 15px; } span.hint { diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 00f507c186..06c1f2ba4f 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -12,6 +12,14 @@ html { &.button-alternative-2 { color: $white; } + + &.button-tertiary { + color: $highlight-text-color; + } +} + +.simple_form .button.button-tertiary { + color: $highlight-text-color; } .status-card__actions button, @@ -286,22 +294,8 @@ html { .dropdown-menu { background: $white; - &__arrow { - &.left { - border-left-color: $white; - } - - &.top { - border-top-color: $white; - } - - &.bottom { - border-bottom-color: $white; - } - - &.right { - border-right-color: $white; - } + &__arrow::before { + background-color: $white; } &__item { @@ -558,25 +552,6 @@ html { } } -.directory__tag.active > a, -.directory__tag.active > div { - border-color: $ui-highlight-color; - - &, - h4, - h4 small, - .fa, - .trends__item__current { - color: $white; - } - - &:hover, - &:active, - &:focus { - background: $ui-highlight-color; - } -} - .batch-table { &__toolbar, &__row, diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 0847c8f4ce..407d718880 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -117,7 +117,7 @@ width: 18px; height: 18px; flex: 0 0 auto; - margin-right: 10px; + margin-inline-end: 10px; top: -1px; border-radius: 50%; vertical-align: middle; @@ -212,7 +212,7 @@ .button { height: 36px; padding: 0 16px; - margin-right: 10px; + margin-inline-end: 10px; font-size: 14px; } } @@ -250,7 +250,7 @@ line-height: inherit; color: $action-button-color; border-color: $action-button-color; - margin-right: 5px; + margin-inline-end: 5px; } li { @@ -260,7 +260,7 @@ .poll__option { flex: 0 0 auto; width: calc(100% - (23px + 6px)); - margin-right: 6px; + margin-inline-end: 6px; } } @@ -289,10 +289,10 @@ color: $dark-text-color; &__chart { - background: rgba(darken($ui-primary-color, 14%), 0.2); + background: rgba(darken($ui-primary-color, 14%), 0.7); &.leading { - background: rgba($ui-highlight-color, 0.2); + background: rgba($ui-highlight-color, 0.5); } } } diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 1811287462..2a2cf30b53 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -12,10 +12,10 @@ pack: home: filename: packs/home.js preload: - - flavours/glitch/async/compose - - flavours/glitch/async/getting_started - - flavours/glitch/async/home_timeline - - flavours/glitch/async/notifications + - flavours/glitch/async/compose + - flavours/glitch/async/getting_started + - flavours/glitch/async/home_timeline + - flavours/glitch/async/notifications mailer: modal: public: packs/public.js diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index 5cb76b721c..3f0b278993 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -12,10 +12,10 @@ pack: home: filename: application.js preload: - - features/getting_started - - features/compose - - features/home_timeline - - features/notifications + - features/getting_started + - features/compose + - features/home_timeline + - features/notifications mailer: modal: public: public.js diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 12d25490b4..cc27840d00 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -160,6 +160,18 @@ export function submitCompose(routerHistory) { dispatch(submitComposeRequest()); + // If we're editing a post with media attachments, those have not + // necessarily been changed on the server. Do it now in the same + // API call. + let media_attributes; + if (statusId !== null) { + media_attributes = media.map(item => ({ + id: item.get('id'), + description: item.get('description'), + focus: item.get('focus'), + })); + } + api(getState).request({ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, method: statusId === null ? 'post' : 'put', @@ -167,6 +179,7 @@ export function submitCompose(routerHistory) { status, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), + media_attributes, sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: getState().getIn(['compose', 'privacy']), @@ -377,11 +390,31 @@ export function changeUploadCompose(id, params) { return (dispatch, getState) => { dispatch(changeUploadComposeRequest()); - api(getState).put(`/api/v1/media/${id}`, params).then(response => { - dispatch(changeUploadComposeSuccess(response.data)); - }).catch(error => { - dispatch(changeUploadComposeFail(id, error)); - }); + let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id); + + // Editing already-attached media is deferred to editing the post itself. + // For simplicity's sake, fake an API reply. + if (media && !media.get('unattached')) { + let { description, focus } = params; + const data = media.toJS(); + + if (description) { + data.description = description; + } + + if (focus) { + focus = focus.split(','); + data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; + } + + dispatch(changeUploadComposeSuccess(data, true)); + } else { + api(getState).put(`/api/v1/media/${id}`, params).then(response => { + dispatch(changeUploadComposeSuccess(response.data, false)); + }).catch(error => { + dispatch(changeUploadComposeFail(id, error)); + }); + } }; } @@ -392,10 +425,11 @@ export function changeUploadComposeRequest() { }; } -export function changeUploadComposeSuccess(media) { +export function changeUploadComposeSuccess(media, attached) { return { type: COMPOSE_UPLOAD_CHANGE_SUCCESS, media: media, + attached: attached, skipLoading: true, }; } diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js index fb6e55612e..023151d4bf 100644 --- a/app/javascript/mastodon/actions/dropdown_menu.js +++ b/app/javascript/mastodon/actions/dropdown_menu.js @@ -1,8 +1,8 @@ export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; -export function openDropdownMenu(id, placement, keyboard, scroll_key) { - return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key }; +export function openDropdownMenu(id, keyboard, scroll_key) { + return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key }; } export function closeDropdownMenu(id) { diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js index 37e79d4cba..08a08cda31 100644 --- a/app/javascript/mastodon/actions/tags.js +++ b/app/javascript/mastodon/actions/tags.js @@ -1,9 +1,17 @@ -import api from '../api'; +import api, { getLinks } from '../api'; export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; +export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; +export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; +export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; +export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; +export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; + export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; @@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({ error, }); +export const fetchFollowedHashtags = () => (dispatch, getState) => { + dispatch(fetchFollowedHashtagsRequest()); + + api(getState).get('/api/v1/followed_tags').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchFollowedHashtagsFail(err)); + }); +}; + +export function fetchFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_FETCH_REQUEST, + }; +}; + +export function fetchFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, + followed_tags, + next, + }; +}; + +export function fetchFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_FETCH_FAIL, + error, + }; +}; + +export function expandFollowedHashtags() { + return (dispatch, getState) => { + const url = getState().getIn(['followed_tags', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowedHashtagsRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFollowedHashtagsFail(error)); + }); + }; +}; + +export function expandFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, + }; +}; + +export function expandFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + followed_tags, + next, + }; +}; + +export function expandFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_FAIL, + error, + }; +}; + export const followHashtag = name => (dispatch, getState) => { dispatch(followHashtagRequest(name)); diff --git a/app/javascript/mastodon/components/admin/Trends.js b/app/javascript/mastodon/components/admin/Trends.js index 9530c2a5be..d01b8437ed 100644 --- a/app/javascript/mastodon/components/admin/Trends.js +++ b/app/javascript/mastodon/components/admin/Trends.js @@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent { day.uses)} diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js index 12d44b5d0b..b8f8c6f45f 100644 --- a/app/javascript/mastodon/components/autosuggest_input.js +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -50,6 +50,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { id: PropTypes.string, searchTokens: PropTypes.arrayOf(PropTypes.string), maxLength: PropTypes.number, + lang: PropTypes.string, }; static defaultProps = { @@ -185,7 +186,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang } = this.props; const { suggestionsHidden } = this.state; return ( @@ -210,6 +211,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { id={id} className={className} maxLength={maxLength} + lang={lang} /> diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 08b9cd80bb..b6c590916e 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -48,6 +48,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onKeyDown: PropTypes.func, onPaste: PropTypes.func.isRequired, autoFocus: PropTypes.bool, + lang: PropTypes.string, }; static defaultProps = { @@ -192,7 +193,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props; const { suggestionsHidden } = this.state; return [ @@ -216,6 +217,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onPaste={this.onPaste} dir='auto' aria-autocomplete='list' + lang={lang} />
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 4b4ad83555..5897aada89 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { CircularProgress } from 'mastodon/components/loading_indicator'; @@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent { scrollable: PropTypes.bool, onClose: PropTypes.func.isRequired, style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, renderHeader: PropTypes.func, @@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent { static defaultProps = { style: {}, - placement: 'bottom', - }; - - state = { - mounted: false, }; handleDocumentClick = e => { @@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent { if (this.focusedItem && this.props.openedViaKeyboard) { this.focusedItem.focus({ preventScroll: true }); } - - this.setState({ mounted: true }); } componentWillUnmount () { @@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent { } render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; - const { mounted } = this.state; + const { items, scrollable, renderHeader, loading } = this.props; let renderItem = this.props.renderItem || this.renderItem; return ( - - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays -
-
+
+ {loading && ( + + )} -
- {loading && ( - - )} - - {!loading && renderHeader && ( -
- {renderHeader(items)} -
- )} - - {!loading && ( -
    - {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} -
- )} -
+ {!loading && renderHeader && ( +
+ {renderHeader(items)}
)} - + + {!loading && ( +
    + {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} +
+ )} +
); } @@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent { isUserTouching: PropTypes.func, onOpen: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, @@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent { id: id++, }; - handleClick = ({ target, type }) => { + handleClick = ({ type }) => { if (this.state.id === this.props.openDropdownId) { this.handleClose(); } else { - const { top } = target.getBoundingClientRect(); - const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); + this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click'); } } @@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent { disabled, loading, scrollable, - dropdownPlacement, openDropdownId, openedViaKeyboard, children, @@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; const button = children ? React.cloneElement(React.Children.only(children), { - ref: this.setTargetRef, onClick: this.handleClick, onMouseDown: this.handleMouseDown, onKeyDown: this.handleButtonKeyDown, @@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent { active={open} disabled={disabled} size={size} - ref={this.setTargetRef} onClick={this.handleClick} onMouseDown={this.handleMouseDown} onKeyDown={this.handleButtonKeyDown} @@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent { return ( - {button} - - - + + {button} + + + {({ props, arrowProps, placement }) => ( +
+
+
+ +
+
+ )} ); diff --git a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js index e30c18372e..16fe77a73b 100644 --- a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js @@ -4,7 +4,6 @@ import { fetchHistory } from 'mastodon/actions/history'; import DropdownMenu from 'mastodon/components/dropdown_menu'; const mapStateToProps = (state, { statusId }) => ({ - dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), items: state.getIn(['history', statusId, 'items']), @@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({ const mapDispatchToProps = (dispatch, { statusId }) => ({ - onOpen (id, onItemClick, dropdownPlacement, keyboard) { + onOpen (id, onItemClick, keyboard) { dispatch(fetchHistory(statusId)); - dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); + dispatch(openDropdownMenu(id, keyboard)); }, onClose (id) { diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 49858f2e23..b7daf82a47 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -27,6 +27,7 @@ export default class IconButton extends React.PureComponent { counter: PropTypes.number, obfuscateCount: PropTypes.bool, href: PropTypes.string, + ariaHidden: PropTypes.bool, }; static defaultProps = { @@ -36,6 +37,7 @@ export default class IconButton extends React.PureComponent { animate: false, overlay: false, tabIndex: '0', + ariaHidden: false, }; state = { @@ -102,6 +104,7 @@ export default class IconButton extends React.PureComponent { counter, obfuscateCount, href, + ariaHidden, } = this.props; const { @@ -142,6 +145,7 @@ export default class IconButton extends React.PureComponent { type='button' aria-label={title} aria-expanded={expanded} + aria-hidden={ariaHidden} title={title} className={classes} onClick={this.handleClick} diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index bf7982ceab..e4a8be338e 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -345,7 +345,7 @@ class MediaGallery extends React.PureComponent { ); } else if (visible) { - spoilerButton = ; + spoilerButton = ; } else { spoilerButton = ( -
+
+
+ + +
-
- {results.map(this.renderItem)} -
-
- )} - +
+ {results.map(this.renderItem)} +
+
); } @@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent { placement: 'bottom', }; - handleToggle = ({ target }) => { - const { top } = target.getBoundingClientRect(); - + handleToggle = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent { onChange(value); } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + render () { const { value, intl, frequentlyUsedLanguages } = this.props; const { open, placement } = this.state; return ( -
-
+
+
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 1f0e998d36..545b67edac 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; @@ -29,15 +27,10 @@ class PrivacyDropdownMenu extends React.PureComponent { style: PropTypes.object, items: PropTypes.array.isRequired, value: PropTypes.string.isRequired, - placement: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, }; - state = { - mounted: false, - }; - handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); @@ -101,7 +94,6 @@ class PrivacyDropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - this.setState({ mounted: true }); } componentWillUnmount () { @@ -118,31 +110,23 @@ class PrivacyDropdownMenu extends React.PureComponent { } render () { - const { mounted } = this.state; - const { style, items, placement, value } = this.props; + const { style, items, value } = this.props; return ( - - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays -
- {items.map(item => ( -
-
- -
+
+ {items.map(item => ( +
+
+ +
-
- {item.text} - {item.meta} -
-
- ))} +
+ {item.text} + {item.meta} +
- )} - + ))} +
); } @@ -168,7 +152,7 @@ class PrivacyDropdown extends React.PureComponent { placement: 'bottom', }; - handleToggle = ({ target }) => { + handleToggle = () => { if (this.props.isUserTouching && this.props.isUserTouching()) { if (this.state.open) { this.props.onModalClose(); @@ -179,11 +163,9 @@ class PrivacyDropdown extends React.PureComponent { }); } } else { - const { top } = target.getBoundingClientRect(); if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } } @@ -247,6 +229,18 @@ class PrivacyDropdown extends React.PureComponent { } } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + render () { const { value, container, disabled, intl } = this.props; const { open, placement } = this.state; @@ -255,7 +249,7 @@ class PrivacyDropdown extends React.PureComponent { return (
-
+
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 8254fb607a..5820f8ca2e 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -1,9 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { searchEnabled } from '../../../initial_state'; import Icon from 'mastodon/components/icon'; @@ -14,31 +12,20 @@ const messages = defineMessages({ class SearchPopout extends React.PureComponent { - static propTypes = { - style: PropTypes.object, - }; - render () { - const { style } = this.props; const extraInformation = searchEnabled ? : ; return ( -
- - {({ opacity, scaleX, scaleY }) => ( -
-

+
+

-
    -
  • #example
  • -
  • @username@domain
  • -
  • URL
  • -
  • URL
  • -
+
    +
  • #example
  • +
  • @username@domain
  • +
  • URL
  • +
  • URL
  • +
- {extraInformation} -
- )} - + {extraInformation}
); } @@ -115,6 +102,10 @@ class Search extends React.PureComponent { this.setState({ expanded: false }); } + findTarget = () => { + return this.searchForm; + } + render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; @@ -140,8 +131,14 @@ class Search extends React.PureComponent {
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index b08307adee..af06ce1bf5 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -43,10 +43,10 @@ export default class Upload extends ImmutablePureComponent {
- {!!media.get('unattached') && ()} +
- {(media.get('description') || '').length === 0 && !!media.get('unattached') && ( + {(media.get('description') || '').length === 0 && (
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 14cf9230bc..2b76422376 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -26,6 +26,7 @@ const mapStateToProps = state => ({ isUploading: state.getIn(['compose', 'is_uploading']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, isInReply: state.getIn(['compose', 'in_reply_to']) !== null, + lang: state.getIn(['compose', 'language']), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/javascript/mastodon/features/followed_tags/index.js b/app/javascript/mastodon/features/followed_tags/index.js new file mode 100644 index 0000000000..0a62ca76d0 --- /dev/null +++ b/app/javascript/mastodon/features/followed_tags/index.js @@ -0,0 +1,89 @@ +import { debounce } from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import ColumnHeader from 'mastodon/components/column_header'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import Column from 'mastodon/features/ui/components/column'; +import { Helmet } from 'react-helmet'; +import Hashtag from 'mastodon/components/hashtag'; +import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags'; + +const messages = defineMessages({ + heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' }, +}); + +const mapStateToProps = state => ({ + hashtags: state.getIn(['followed_tags', 'items']), + isLoading: state.getIn(['followed_tags', 'isLoading'], true), + hasMore: !!state.getIn(['followed_tags', 'next']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class FollowedTags extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hashtags: ImmutablePropTypes.list, + isLoading: PropTypes.bool, + hasMore: PropTypes.bool, + multiColumn: PropTypes.bool, + }; + + componentDidMount() { + this.props.dispatch(fetchFollowedHashtags()); + }; + + handleLoadMore = debounce(() => { + this.props.dispatch(expandFollowedHashtags()); + }, 300, { leading: true }); + + render () { + const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props; + + const emptyMessage = ; + + return ( + + + + + {hashtags.map((hashtag) => ( + day.get('uses')).toArray()} + /> + ))} + + + + + + + ); + } + +} diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index ea2c9c0a42..746d085c6e 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -246,7 +246,11 @@ class Notification extends ImmutablePureComponent { } renderStatus (notification, link) { - const { intl, unread } = this.props; + const { intl, unread, status } = this.props; + + if (!status) { + return null; + } return ( @@ -264,6 +268,7 @@ class Notification extends ImmutablePureComponent {