From a30cdfd4d4fdde3245174210b9f7c0a0be7bc122 Mon Sep 17 00:00:00 2001 From: zunda Date: Mon, 26 Feb 2024 12:43:07 -1000 Subject: [PATCH 01/13] Specify 410 for code when responding as json while self-destruction (#29420) --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5f8725f6fc..a046ea19c9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -178,7 +178,7 @@ class ApplicationController < ActionController::Base respond_to do |format| format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] } - format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code } + format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 } end end From 213c87ae595cc1ddcb618516106712b0aae789bd Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Tue, 27 Feb 2024 12:46:58 +0200 Subject: [PATCH 02/13] Fix filters title and keywords overflow (#29396) --- app/javascript/styles/mastodon/admin.scss | 1 + app/javascript/styles/mastodon/forms.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 5625cdd5ec..fcd630c23c 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1044,6 +1044,7 @@ a.name-tag, display: flex; justify-content: space-between; margin-bottom: 0; + word-break: break-word; } &__permissions { diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 555d43cc1c..3ac5c3df95 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1078,6 +1078,7 @@ code { &__type { color: $darker-text-color; + word-break: break-word; } } From 9fa7338b6e5921e184cd23c21df40a08940d03de Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 27 Feb 2024 05:48:38 -0500 Subject: [PATCH 03/13] Use `github` reporter on `haml-lint` runs on CI (#29375) --- .github/workflows/lint-haml.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 8dcab845ee..25615b720d 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -36,4 +36,4 @@ jobs: - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bundle exec haml-lint + bundle exec haml-lint --reporter github From 90573c3abbc0783c25cb4e3aa8c298e10259cb57 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Feb 2024 12:41:19 +0100 Subject: [PATCH 04/13] Change behavior of privacy dropdown to only change value on validation (#29406) --- .../compose/components/language_dropdown.jsx | 1 + .../compose/components/privacy_dropdown.jsx | 124 +---------------- .../components/privacy_dropdown_menu.jsx | 128 ++++++++++++++++++ 3 files changed, 131 insertions(+), 122 deletions(-) create mode 100644 app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index 85057799be..c3bd908a4e 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx @@ -141,6 +141,7 @@ class LanguageDropdownMenu extends PureComponent { case 'Escape': onClose(); break; + case ' ': case 'Enter': this.handleClick(e); break; diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx index 82f9027388..071f0a6fab 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx @@ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; -import { supportsPassiveEvents } from 'detect-passive-events'; import Overlay from 'react-overlays/Overlay'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import { Icon } from 'mastodon/components/icon'; +import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; + const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, @@ -28,126 +28,6 @@ const messages = defineMessages({ unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; - -class PrivacyDropdownMenu extends PureComponent { - - static propTypes = { - style: PropTypes.object, - items: PropTypes.array.isRequired, - value: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - e.stopPropagation(); - } - }; - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - render () { - const { style, items, value } = this.props; - - return ( -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
- - {item.extra && ( -
- -
- )} -
- ))} -
- ); - } - -} - class PrivacyDropdown extends PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx new file mode 100644 index 0000000000..1a5ff1fa80 --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown_menu.jsx @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; + +import { supportsPassiveEvents } from 'detect-passive-events'; + +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import { Icon } from 'mastodon/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; + +export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => { + const nodeRef = useRef(null); + const focusedItemRef = useRef(null); + const [currentValue, setCurrentValue] = useState(value); + + const handleDocumentClick = useCallback((e) => { + if (nodeRef.current && !nodeRef.current.contains(e.target)) { + onClose(); + e.stopPropagation(); + } + }, [nodeRef, onClose]); + + const handleClick = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + onClose(); + onChange(value); + }, [onClose, onChange]); + + const handleKeyDown = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => (item.value === value)); + + let element = null; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case ' ': + case 'Enter': + handleClick(e); + break; + case 'ArrowDown': + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + break; + case 'ArrowUp': + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + } else { + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + } + break; + case 'Home': + element = nodeRef.current.firstChild; + break; + case 'End': + element = nodeRef.current.lastChild; + break; + } + + if (element) { + element.focus(); + setCurrentValue(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }, [nodeRef, items, onClose, handleClick, setCurrentValue]); + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, { capture: true }); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + focusedItemRef.current?.focus({ preventScroll: true }); + + return () => { + document.removeEventListener('click', handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', handleDocumentClick, listenerOptions); + }; + }, [handleDocumentClick]); + + return ( +
    + {items.map(item => ( +
  • +
    + +
    + +
    + {item.text} + {item.meta} +
    + + {item.extra && ( +
    + +
    + )} +
  • + ))} +
+ ); +}; + +PrivacyDropdownMenu.propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; From 54e3a82f1d5248ff91a297d6d06de9f86e9cd208 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 12:48:42 +0100 Subject: [PATCH 05/13] Update dependency thor to v1.3.1 (#29421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 090697e709..076cf915d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -744,7 +744,7 @@ GEM terrapin (1.0.1) climate_control test-prof (1.3.1) - thor (1.3.0) + thor (1.3.1) tilt (2.3.0) timeout (0.4.1) tpm-key_attestation (0.12.0) From 9e78129e6e45e9485f567a88cb4b3dbc0b25a7d2 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 27 Feb 2024 06:50:21 -0500 Subject: [PATCH 06/13] Use "cacheable response" shared example in more places (#29419) --- spec/controllers/custom_css_controller_spec.rb | 13 +------------ .../instance_actors_controller_spec.rb | 13 ++++--------- spec/controllers/manifests_controller_spec.rb | 13 +------------ spec/controllers/tags_controller_spec.rb | 16 ++-------------- spec/support/examples/cache.rb | 2 +- 5 files changed, 9 insertions(+), 48 deletions(-) diff --git a/spec/controllers/custom_css_controller_spec.rb b/spec/controllers/custom_css_controller_spec.rb index 99d36d21b9..405fa0bcf3 100644 --- a/spec/controllers/custom_css_controller_spec.rb +++ b/spec/controllers/custom_css_controller_spec.rb @@ -14,17 +14,6 @@ describe CustomCssController do expect(response).to have_http_status(200) end - it 'returns public cache control header' do - expect(response.headers['Cache-Control']).to include('public') - end - - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - expect(session).to be_empty - end + it_behaves_like 'cacheable response' end end diff --git a/spec/controllers/instance_actors_controller_spec.rb b/spec/controllers/instance_actors_controller_spec.rb index be1eefa7b2..70aaff9d65 100644 --- a/spec/controllers/instance_actors_controller_spec.rb +++ b/spec/controllers/instance_actors_controller_spec.rb @@ -12,23 +12,18 @@ RSpec.describe InstanceActorsController do get :show, params: { format: format } end - it 'returns http success with correct media type, headers, and session values' do + it 'returns http success with correct media type and body' do expect(response) .to have_http_status(200) .and have_attributes( - media_type: eq('application/activity+json'), - cookies: be_empty + media_type: eq('application/activity+json') ) - expect(response.headers) - .to include('Cache-Control' => include('public')) - .and not_include('Set-Cookies') - - expect(session).to be_empty - expect(body_as_json) .to include(:id, :type, :preferredUsername, :inbox, :publicKey, :inbox, :outbox, :url) end + + it_behaves_like 'cacheable response' end before do diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb index d0699c438b..9279fae024 100644 --- a/spec/controllers/manifests_controller_spec.rb +++ b/spec/controllers/manifests_controller_spec.rb @@ -14,17 +14,6 @@ describe ManifestsController do expect(response).to have_http_status(200) end - it 'returns public cache control header' do - expect(response.headers['Cache-Control']).to include('public') - end - - it 'does not set cookies' do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - end - - it 'does not set sessions' do - expect(session).to be_empty - end + it_behaves_like 'cacheable response' end end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index d41e707d43..2bb0c8de3b 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -20,13 +20,7 @@ RSpec.describe TagsController do expect(response).to have_http_status(200) end - it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' end context 'when requested as JSON' do @@ -36,13 +30,7 @@ RSpec.describe TagsController do expect(response).to have_http_status(200) end - it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns public Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'public' - end + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' end end diff --git a/spec/support/examples/cache.rb b/spec/support/examples/cache.rb index afbee66b2d..60e522f426 100644 --- a/spec/support/examples/cache.rb +++ b/spec/support/examples/cache.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true shared_examples 'cacheable response' do |expects_vary: false| - it 'sets correct cache and vary headers and does not set cookies or session' do + it 'sets correct cache and vary headers and does not set cookies or session', :aggregate_failures do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be_nil From 76d256138e6029d848da7cfdf5b8682832c44033 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 27 Feb 2024 06:52:37 -0500 Subject: [PATCH 07/13] Wrap media attachment size calculation in `COALESCE` (#29415) --- .../admin/metrics/measure/instance_media_attachments_measure.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 2d4b5f56b0..1d2dbbe414 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -50,7 +50,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: WHERE date_trunc('day', media_attachments.created_at)::date = axis.period AND #{account_domain_sql(params[:include_subdomains])} ) - SELECT SUM(size) FROM new_media_attachments + SELECT COALESCE(SUM(size), 0) FROM new_media_attachments ) AS value FROM ( SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period From bc4c5ed91883439e5de9ef034b6875c36f4b4241 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:53:53 +0100 Subject: [PATCH 08/13] New Crowdin Translations (automated) (#29423) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/nn.json | 30 ++++++++++++++++--------- config/locales/devise.nn.yml | 19 ++++++++-------- config/locales/nn.yml | 22 +++++++++--------- config/locales/simple_form.nn.yml | 18 ++++++++------- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 2118eb5739..3cc537f54f 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -40,7 +40,7 @@ "account.following_counter": "{count, plural, one {Fylgjer {counter}} other {Fylgjer {counter}}}", "account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.", "account.go_to_profile": "Gå til profil", - "account.hide_reblogs": "Skjul framhevingar frå @{name}", + "account.hide_reblogs": "Gøym framhevingar frå @{name}", "account.in_memoriam": "Til minne om.", "account.joined_short": "Vart med", "account.languages": "Endre språktingingar", @@ -113,7 +113,7 @@ "column.community": "Lokal tidsline", "column.direct": "Private omtaler", "column.directory": "Sjå gjennom profilar", - "column.domain_blocks": "Skjulte domene", + "column.domain_blocks": "Blokkerte domene", "column.favourites": "Favorittar", "column.firehose": "Tidslinjer", "column.follow_requests": "Fylgjeførespurnadar", @@ -124,7 +124,7 @@ "column.pins": "Festa tut", "column.public": "Samla tidsline", "column_back_button.label": "Attende", - "column_header.hide_settings": "Gøym innstillingar", + "column_header.hide_settings": "Gøym innstillingane", "column_header.moveLeft_settings": "Flytt kolonne til venstre", "column_header.moveRight_settings": "Flytt kolonne til høgre", "column_header.pin": "Fest", @@ -171,14 +171,14 @@ "confirmations.delete_list.message": "Er du sikker på at du vil sletta denne lista for alltid?", "confirmations.discard_edit_media.confirm": "Forkast", "confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkasta dei likevel?", - "confirmations.domain_block.confirm": "Skjul alt frå domenet", + "confirmations.domain_block.confirm": "Blokker heile domenet", "confirmations.domain_block.message": "Er du heilt, heilt sikker på at du vil skjula heile {domain}? I dei fleste tilfelle er det godt nok og føretrekt med nokre få målretta blokkeringar eller målbindingar. Du kjem ikkje til å sjå innhald frå domenet i fødererte tidsliner eller i varsla dine. Fylgjarane dine frå domenet vert fjerna.", "confirmations.edit.confirm": "Rediger", "confirmations.edit.message": "Å redigera no vil overskriva den meldinga du er i ferd med å skriva. Er du sikker på at du vil halda fram?", "confirmations.logout.confirm": "Logg ut", "confirmations.logout.message": "Er du sikker på at du vil logga ut?", "confirmations.mute.confirm": "Målbind", - "confirmations.mute.explanation": "Dette vil skjula innlegg som kjem frå og som nemner dei, men vil framleis la dei sjå innlegga dine og fylgje deg.", + "confirmations.mute.explanation": "Dette vil gøyma innlegga deira og innlegg som nemner dei, men dei vil framleis kunna sjå innlegga dine og fylgja deg.", "confirmations.mute.message": "Er du sikker på at du vil målbinda {name}?", "confirmations.redraft.confirm": "Slett & skriv på nytt", "confirmations.redraft.message": "Er du sikker på at du vil sletta denne statusen og skriva han på nytt? Då misser du favorittar og framhevingar, og svar til det opprinnelege innlegget vert foreldrelause.", @@ -230,7 +230,7 @@ "empty_column.bookmarked_statuses": "Du har ikkje lagra noko bokmerke enno. Når du set bokmerke på eit innlegg, dukkar det opp her.", "empty_column.community": "Den lokale tidslina er tom. Skriv noko offentleg å få ballen til å rulle!", "empty_column.direct": "Du har ingen private omtaler enda. Etter du har sendt eller mottatt en, så vil den dukke opp her.", - "empty_column.domain_blocks": "Det er ingen skjulte domene til no.", + "empty_column.domain_blocks": "Det er ingen blokkerte domene enno.", "empty_column.explore_statuses": "Ingenting er i støytet nett no. Prøv igjen seinare!", "empty_column.favourited_statuses": "Du har ingen favoritt-statusar ennå. Når du merkjer ein som favoritt, dukkar han opp her.", "empty_column.favourites": "Ingen har merkt denne statusen som favoritt enno. Når nokon gjer det, dukkar dei opp her.", @@ -277,7 +277,13 @@ "follow_request.authorize": "Autoriser", "follow_request.reject": "Avvis", "follow_requests.unlocked_explanation": "Sjølv om kontoen din ikkje er låst tenkte dei som driv {domain} at du kanskje ville gå gjennom førespurnadar frå desse kontoane manuelt.", + "follow_suggestions.curated_suggestion": "Utvalt av staben", "follow_suggestions.dismiss": "Ikkje vis igjen", + "follow_suggestions.hints.featured": "Denne profilen er handplukka av folka på {domain}.", + "follow_suggestions.hints.friends_of_friends": "Denne profilen er populær hjå dei du fylgjer.", + "follow_suggestions.hints.most_followed": "Mange på {domain} fylgjer denne profilen.", + "follow_suggestions.hints.most_interactions": "Denne profilen har nyss fått mykje merksemd på {domain}.", + "follow_suggestions.hints.similar_to_recently_followed": "Denne profilen liknar på dei andre profilane du har fylgt i det siste.", "follow_suggestions.personalized_suggestion": "Personleg forslag", "follow_suggestions.popular_suggestion": "Populært forslag", "follow_suggestions.view_all": "Vis alle", @@ -395,7 +401,7 @@ "media_gallery.toggle_visible": "{number, plural, one {Skjul bilete} other {Skjul bilete}}", "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.", "mute_modal.duration": "Varigheit", - "mute_modal.hide_notifications": "Skjul varsel frå denne brukaren?", + "mute_modal.hide_notifications": "Gøym varsel frå denne brukaren?", "mute_modal.indefinite": "På ubestemt tid", "navigation_bar.about": "Om", "navigation_bar.advanced_interface": "Opne i avansert nettgrensesnitt", @@ -479,7 +485,8 @@ "onboarding.follows.empty": "Me kan ikkje visa deg nokon resultat no. Du kan prøva å søkja eller bla gjennom utforsk-sida for å finna folk å fylgja, eller du kan prøva att seinare.", "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.follows.title": "Popular on Mastodon", - "onboarding.profile.discoverable": "Gjør min profil synlig", + "onboarding.profile.discoverable": "Gjer profilen min synleg", + "onboarding.profile.discoverable_hint": "Når du vel å gjera profilen din synleg på Mastodon, vil innlegga dine syna i søkjeresultat og populære innlegg, og profilen din kan bli føreslegen for folk med liknande interesser som deg.", "onboarding.profile.display_name": "Synleg namn", "onboarding.profile.display_name_hint": "Det fulle namnet eller kallenamnet ditt…", "onboarding.profile.lead": "Du kan alltid fullføra dette seinare i innstillingane, og der er det endå fleire tilpassingsalternativ.", @@ -528,11 +535,12 @@ "privacy.private.short": "Følgjarar", "privacy.public.long": "Kven som helst på og av Mastodon", "privacy.public.short": "Offentleg", + "privacy.unlisted.additional": "Dette er akkurat som offentleg, bortsett frå at innlegga ikkje dukkar opp i direktestraumar eller merkelappar, i oppdagingar eller Mastodon-søk, sjølv om du har sagt ja til at kontoen skal vera synleg.", "privacy.unlisted.long": "Færre algoritmiske fanfarar", "privacy.unlisted.short": "Stille offentleg", "privacy_policy.last_updated": "Sist oppdatert {date}", "privacy_policy.title": "Personvernsreglar", - "recommended": "Anbefalt", + "recommended": "Tilrådd", "refresh": "Oppdater", "regeneration_indicator.label": "Lastar…", "regeneration_indicator.sublabel": "Heimetidslina di vert førebudd!", @@ -605,7 +613,7 @@ "search.quick_action.status_search": "Innlegg som samsvarer med {x}", "search.search_or_paste": "Søk eller lim inn URL", "search_popout.full_text_search_disabled_message": "Ikkje tilgjengeleg på {domain}.", - "search_popout.full_text_search_logged_out_message": "Bare tilgjengelig når man er logget inn.", + "search_popout.full_text_search_logged_out_message": "Berre tilgjengeleg når du er logga inn.", "search_popout.language_code": "ISO-språkkode", "search_popout.options": "Søkjealternativ", "search_popout.quick_actions": "Hurtighandlinger", @@ -654,7 +662,7 @@ "status.load_more": "Last inn meir", "status.media.open": "Klikk for å opne", "status.media.show": "Klikk for å vise", - "status.media_hidden": "Medium gøymd", + "status.media_hidden": "Mediet er gøymt", "status.mention": "Nemn @{name}", "status.more": "Meir", "status.mute": "Målbind @{name}", diff --git a/config/locales/devise.nn.yml b/config/locales/devise.nn.yml index 96920d42b5..01d6e5a468 100644 --- a/config/locales/devise.nn.yml +++ b/config/locales/devise.nn.yml @@ -12,6 +12,7 @@ nn: last_attempt: Du har eitt forsøk igjen før kontoen din vert låst. locked: Kontoen din er låst. not_found_in_database: Ugyldig %{authentication_keys} eller passord. + omniauth_user_creation_failure: Greidde ikkje laga konto for denne identiteten. pending: Kontoen din er vert gjennomgått enno. timeout: Økta di er utgått. Logg inn omatt for å halde fram. unauthenticated: Du må logge inn eller registere deg før du kan halde fram. @@ -47,19 +48,19 @@ nn: subject: 'Mastodon: Instuksjonar for å endra passord' title: Attstilling av passord two_factor_disabled: - explanation: Innlogging er nå mulig med kun e-postadresse og passord. + explanation: No kan du logga inn med berre epostadresse og passord. subject: 'Mastodon: To-faktor-autentisering deaktivert' - subtitle: To-faktor autentisering for din konto har blitt deaktivert. + subtitle: Tofaktorinnlogging for denne kontoen er skrudd av. title: 2FA deaktivert two_factor_enabled: - explanation: En token generert av den sammenkoblede TOTP-appen vil være påkrevd for innlogging. + explanation: Du treng ein kode frå den tilkopla tofaktor-appen din for å logga inn. subject: 'Mastodon: To-faktor-autentisering aktivert' - subtitle: Tofaktorautentisering er aktivert for din konto. + subtitle: Tofaktorpålogging er skrudd på for kontoen din. title: 2FA aktivert two_factor_recovery_codes_changed: explanation: Dei førre gjenopprettingskodane er ugyldige og nye er genererte. subject: 'Mastodon: To-faktor-gjenopprettingskodar har vorte genererte på nytt' - subtitle: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert. + subtitle: Dei førre innloggingskodane er ikkje gyldige lenger, og nye kodar er laga. title: 2FA-gjenopprettingskodane er endra unlock_instructions: subject: 'Mastodon: Instruksjonar for å opne kontoen igjen' @@ -73,13 +74,13 @@ nn: subject: 'Mastodon: Sikkerheitsnøkkel sletta' title: Ein av sikkerheitsnøklane dine har blitt sletta webauthn_disabled: - explanation: Autentisering med sikkerhetsnøkler er deaktivert for kontoen din. - extra: Innlogging er nå mulig med kun tilgangstoken generert av den sammenkoblede TOTP-appen. + explanation: Innlogging med tryggingsnykjel er skrudd av for kontoen din. + extra: No kan du logga inn med berre kodane som er laga av den tilkopla tofaktor-appen din. subject: 'Mastodon: Autentisering med sikkerheitsnøklar vart skrudd av' title: Sikkerheitsnøklar deaktivert webauthn_enabled: - explanation: Sikkerhetsnøkkelautentisering har blitt aktivert for kontoen din. - extra: Sikkerhetsnøkkelen din kan nå bli brukt for innlogging. + explanation: Innlogging med tryggingsnyklar er skrudd på for kontoen din. + extra: No kan du bruka tryggingsnykjelen din for å logga inn. subject: 'Mastodon: Sikkerheitsnøkkelsautentisering vart skrudd på' title: Sikkerheitsnøklar aktivert omniauth_callbacks: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index b1ae928997..1524b6f7c1 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -31,7 +31,7 @@ nn: created_msg: Moderatormerknad er laga! destroyed_msg: Moderatormerknad er utsletta! accounts: - add_email_domain_block: Gøym e-postdomene + add_email_domain_block: Blokker e-postdomene approve: Godtak approved_msg: Godkjende %{username} sin registreringssøknad are_you_sure: Er du sikker? @@ -767,13 +767,15 @@ nn: disabled: Til ingen users: Til lokale brukarar som er logga inn registrations: + moderation_recommandation: Pass på at du har mange og kjappe redaktørar og moderatorar på laget ditt før du opnar for allmenn registrering! preamble: Kontroller kven som kan oppretta konto på tenaren din. title: Registreringar registrations_mode: modes: - approved: Godkjenning kreves for påmelding + approved: Godkjenning krevst for å registrera seg none: Ingen kan melda seg inn open: Kven som helst kan melda seg inn + warning_hint: Me rår til at du bruker "Godkjenning krevst for å registrera seg" viss du ikkje er sikker på at moderatorane kan handtera søppel og illmeinte registreringar kvikt. security: authorized_fetch: Krev autentisering frå fødererte tenarar authorized_fetch_hint: Krav om autentisering frå fødererte tenarar gjer det mogleg med strengare handheving av blokkering, både på brukar- og tenar-nivå. Likevel, dette har ein kostnad når det gjeld yting, reduserer rekkevidda til svara dine og kan medføra kompabilitetsproblem med enkelte fødererte tenester. Dette vil heller ikkje hindra dei som verkeleg vil i å henta dei offentlege innlegga eller kontoane dine. @@ -1450,7 +1452,7 @@ nn: moderation: title: Moderasjon move_handler: - carry_blocks_over_text: Denne brukaren flytta frå %{acct}, som du gøymde. + carry_blocks_over_text: Denne brukaren flytta frå %{acct}, som du hadde blokkert. carry_mutes_over_text: Denne brukeren flyttet fra %{acct}, som du hadde dempet. copy_account_note_text: 'Denne brukeren flyttet fra %{acct}, her var dine tidligere notater om dem:' navigation: @@ -1537,7 +1539,7 @@ nn: privacy: hint_html: "Tilpass korleis du vil at andre skal finna profilen og innlegga dine. Mastodon har fleire funksjonar du kan ta i bruk for å få kontakt med eit større publikum. Sjå gjerne gjennom innstillingane slik at du er sikker på at dei passar til deg og din bruk." privacy: Personvern - privacy_hint_html: Ha kontroll over kor mykje du vil dela. Folk finn interessante profilar og fine appar ved å sjå gjennom kva andre fylgjer og kva appar dei legg ut innlegg med, men det kan henda du vil gøyma desse opplysingane. + privacy_hint_html: Kontroller kor mykje du vil dela. Folk finn interessante profilar og fine appar ved å sjå gjennom kva andre fylgjer og kva appar dei legg ut innlegg med, men det kan henda du vil gøyma desse opplysingane. reach: Nå andre reach_hint_html: Hald styring med om du vil at andre skal kunna oppdaga og fylgja deg. Vil du at innlegga dine skal stå på Utforsk-sida? Vil du at andre skal sjå deg i tilrådingane for kven dei skal fylgja? Vil du ta imot nye fylgjarar automatisk, eller vil du kontrollera kvar einskild fylgjar? search: Søk @@ -1550,8 +1552,8 @@ nn: limit_reached: Grensen for forskjellige reaksjoner nådd unrecognized_emoji: er ikke en gjenkjent emoji redirects: - prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette. - title: Du forlater %{instance}. + prompt: Viss du stolar på denne lenka, klikkar du på ho for å halda fram. + title: No forlèt du %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Er du sikker på at du ynskjer å fylgja dei valde fylgjarane? @@ -1781,7 +1783,7 @@ nn: webauthn: Sikkerhetsnøkler user_mailer: appeal_approved: - action: Kontoinnstillinger + action: Kontoinnstillingar explanation: Apellen på prikken mot din kontor på %{strike_date} som du la inn på %{appeal_date} har blitt godkjend. Din konto er nok ein gong i god stand. subject: Din klage fra %{date} er godkjent subtitle: Kontoen din er tilbake i god stand. @@ -1789,11 +1791,11 @@ nn: appeal_rejected: explanation: Klagen på advarselen mot din konto den %{strike_date} som du sendte inn den %{appeal_date} har blitt avvist. subject: Din klage fra %{date} er avvist - subtitle: Anken din har blitt avvist. + subtitle: Klaga di vart avvist. title: Anke avvist backup_ready: - explanation: Du etterspurte en fullstendig sikkerhetskopi av din Mastodon-konto. - extra: Den er nå klar for nedlasting! + explanation: Du ba om ein fullstendig tryggingskopi av Mastodon-kontoen din. + extra: No kan du lasta han ned! subject: Arkivet ditt er klart til å lastes ned title: Nedlasting av arkiv failed_2fa: diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml index 1991cb2dbb..98cc372be7 100644 --- a/config/locales/simple_form.nn.yml +++ b/config/locales/simple_form.nn.yml @@ -39,12 +39,14 @@ nn: text: Ei åtvaring kan kun ankast ein gong defaults: autofollow: Folk som lagar ein konto gjennom innbydinga fylgjer deg automatisk + avatar: WEBP, PNG, GIF eller JPG. Maks %{size}. Blir forminska til %{dimensions}pkt bot: Denne kontoen utfører i hovedsak automatiserte handlinger og blir kanskje ikke holdt øye med context: En eller flere sammenhenger der filteret skal gjelde current_password: For sikkerhetsgrunner, vennligst oppgi passordet til den nåværende bruker current_username: Skriv inn brukarnamnet til den noverande kontoen for å stadfesta digest: Kun sendt etter en lang periode med inaktivitet og bare dersom du har mottatt noen personlige meldinger mens du var borte email: Du får snart ein stadfestings-e-post + header: WEBP, PNG, GIF eller JPG. Maks %{size}. Blir forminska til %{dimensions}pkt inbox_url: Kopier URLen fra forsiden til overgangen du vil bruke irreversible: Filtrerte tut vil verta borte for evig, sjølv om filteret vert fjerna seinare locale: Språket til brukargrensesnittet, e-postar og push-varsel @@ -53,8 +55,8 @@ nn: scopes: API-ane som programmet vil få tilgjenge til. Ettersom du vel eit toppnivåomfang tarv du ikkje velja einskilde API-ar. setting_aggregate_reblogs: Ikkje vis nye framhevingar for tut som nyleg har vorte heva fram (Påverkar berre nylege framhevingar) setting_always_send_emails: Vanlegvis vil ikkje e-postvarsel bli sendt når du brukar Mastodon aktivt - setting_default_sensitive: Nærtakande media vert gøymd som standard og kan synast med eit klikk - setting_display_media_default: Gøym media som er merka som nærtakande + setting_default_sensitive: Sensitive media vert gøymde som standard, og du syner dei ved å klikka på dei + setting_display_media_default: Gøym media som er merka som sensitive setting_display_media_hide_all: Alltid skjul alt media setting_display_media_show_all: Vis alltid media setting_use_blurhash: Overgangar er basert på fargane til skjulte grafikkelement, men gjer detaljar utydelege @@ -218,7 +220,7 @@ nn: setting_theme: Sidetema setting_trends: Vis kva som er populært i dag setting_unfollow_modal: Vis stadfesting før du sluttar å fylgja nokon - setting_use_blurhash: Vis fargerike overgangar for gøymt media + setting_use_blurhash: Vis fargerike overgangar for gøymde medium setting_use_pending_items: Saktemodus severity: Alvorsgrad sign_in_token_attempt: Trygdenykel @@ -233,8 +235,8 @@ nn: name: Emneknagg filters: actions: - hide: Gøym totalt - warn: Gøym med ei advarsel + hide: Gøym heilt + warn: Gøym med ei åtvaring form_admin_settings: activity_api_enabled: Legg ut samla statistikk om brukaraktiviteten i APIet backups_retention_period: Arkiveringsperiode for brukararkiv @@ -264,9 +266,9 @@ nn: trends: Aktiver trendar trends_as_landing_page: Bruk trendar som startside interactions: - must_be_follower: Gøym varslingar frå folk som ikkje fylgjer deg - must_be_following: Gøym varslingar frå folk du ikkje fylgjer - must_be_following_dm: Gøym direktemeldinger frå folk du ikkje fylgjer + must_be_follower: Blokker varsel frå folk som ikkje fylgjer deg + must_be_following: Blokker varsel frå folk du ikkje fylgjer + must_be_following_dm: Blokker direktemeldinger frå folk du ikkje fylgjer invite: comment: Kommentar invite_request: From 036f5a05e372b9b1899907195e48001eec079854 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Tue, 27 Feb 2024 15:59:20 +0100 Subject: [PATCH 09/13] Convert the streaming server to ESM (#29389) Co-authored-by: Claire --- streaming/{.eslintrc.js => .eslintrc.cjs} | 12 ++--- streaming/errors.js | 15 ++---- streaming/index.js | 60 +++++++++++++---------- streaming/logging.js | 23 ++++----- streaming/metrics.js | 6 +-- streaming/package.json | 1 + streaming/tsconfig.json | 6 +-- streaming/utils.js | 20 +++----- 8 files changed, 65 insertions(+), 78 deletions(-) rename streaming/{.eslintrc.js => .eslintrc.cjs} (79%) diff --git a/streaming/.eslintrc.js b/streaming/.eslintrc.cjs similarity index 79% rename from streaming/.eslintrc.js rename to streaming/.eslintrc.cjs index 188ebb512d..e25cff7df0 100644 --- a/streaming/.eslintrc.js +++ b/streaming/.eslintrc.cjs @@ -1,4 +1,8 @@ +/* eslint-disable import/no-commonjs */ + // @ts-check + +// @ts-ignore - This needs to be a CJS file (eslint does not yet support ESM configs), and TS is complaining we use require const { defineConfig } = require('eslint-define-config'); module.exports = defineConfig({ @@ -22,22 +26,18 @@ module.exports = defineConfig({ // to maintain. 'no-delete-var': 'off', - // The streaming server is written in commonjs, not ESM for now: - 'import/no-commonjs': 'off', - // This overrides the base configuration for this rule to pick up // dependencies for the streaming server from the correct package.json file. 'import/no-extraneous-dependencies': [ 'error', { - devDependencies: [ - 'streaming/.eslintrc.js', - ], + devDependencies: ['streaming/.eslintrc.cjs'], optionalDependencies: false, peerDependencies: false, includeTypes: true, packageDir: __dirname, }, ], + 'import/extensions': ['error', 'always'], }, }); diff --git a/streaming/errors.js b/streaming/errors.js index 9a641180ba..6c44d2cb8f 100644 --- a/streaming/errors.js +++ b/streaming/errors.js @@ -5,15 +5,14 @@ * override it in let statements. * @type {string} */ -const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error occurred'; -exports.UNKNOWN_ERROR_MESSAGE = UNEXPECTED_ERROR_MESSAGE; +export const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error occurred'; /** * Extracts the status and message properties from the error object, if * available for public use. The `unknown` is for catch statements * @param {Error | AuthenticationError | RequestError | unknown} err */ -exports.extractStatusAndMessage = function(err) { +export function extractStatusAndMessage(err) { let statusCode = 500; let errorMessage = UNEXPECTED_ERROR_MESSAGE; if (err instanceof AuthenticationError || err instanceof RequestError) { @@ -22,9 +21,9 @@ exports.extractStatusAndMessage = function(err) { } return { statusCode, errorMessage }; -}; +} -class RequestError extends Error { +export class RequestError extends Error { /** * @param {string} message */ @@ -35,9 +34,7 @@ class RequestError extends Error { } } -exports.RequestError = RequestError; - -class AuthenticationError extends Error { +export class AuthenticationError extends Error { /** * @param {string} message */ @@ -47,5 +44,3 @@ class AuthenticationError extends Error { this.status = 401; } } - -exports.AuthenticationError = AuthenticationError; diff --git a/streaming/index.js b/streaming/index.js index 1c312ebd70..fa30260a3a 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -1,32 +1,36 @@ // @ts-check -const fs = require('fs'); -const http = require('http'); -const path = require('path'); -const url = require('url'); +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import url from 'node:url'; -const cors = require('cors'); -const dotenv = require('dotenv'); -const express = require('express'); -const { Redis } = require('ioredis'); -const { JSDOM } = require('jsdom'); -const pg = require('pg'); -const dbUrlToConfig = require('pg-connection-string').parse; -const WebSocket = require('ws'); +import cors from 'cors'; +import dotenv from 'dotenv'; +import express from 'express'; +import { Redis } from 'ioredis'; +import { JSDOM } from 'jsdom'; +import pg from 'pg'; +import pgConnectionString from 'pg-connection-string'; +import WebSocket from 'ws'; -const errors = require('./errors'); -const { AuthenticationError, RequestError } = require('./errors'); -const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging'); -const { setupMetrics } = require('./metrics'); -const { isTruthy, normalizeHashtag, firstParam } = require("./utils"); +import { AuthenticationError, RequestError, extractStatusAndMessage as extractErrorStatusAndMessage } from './errors.js'; +import { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } from './logging.js'; +import { setupMetrics } from './metrics.js'; +import { isTruthy, normalizeHashtag, firstParam } from './utils.js'; const environment = process.env.NODE_ENV || 'development'; // Correctly detect and load .env or .env.production file based on environment: const dotenvFile = environment === 'production' ? '.env.production' : '.env'; +const dotenvFilePath = path.resolve( + url.fileURLToPath( + new URL(path.join('..', dotenvFile), import.meta.url) + ) +); dotenv.config({ - path: path.resolve(__dirname, path.join('..', dotenvFile)) + path: dotenvFilePath }); initializeLogLevel(process.env, environment); @@ -143,7 +147,7 @@ const pgConfigFromEnv = (env) => { let baseConfig = {}; if (env.DATABASE_URL) { - const parsedUrl = dbUrlToConfig(env.DATABASE_URL); + const parsedUrl = pgConnectionString.parse(env.DATABASE_URL); // The result of dbUrlToConfig from pg-connection-string is not type // compatible with pg.PoolConfig, since parts of the connection URL may be @@ -326,7 +330,7 @@ const startServer = async () => { // Unfortunately for using the on('upgrade') setup, we need to manually // write a HTTP Response to the Socket to close the connection upgrade // attempt, so the following code is to handle all of that. - const {statusCode, errorMessage } = errors.extractStatusAndMessage(err); + const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); /** @type {Record} */ const headers = { @@ -748,7 +752,7 @@ const startServer = async () => { return; } - const {statusCode, errorMessage } = errors.extractStatusAndMessage(err); + const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: errorMessage })); @@ -1155,7 +1159,7 @@ const startServer = async () => { // @ts-ignore streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering); }).catch(err => { - const {statusCode, errorMessage } = errors.extractStatusAndMessage(err); + const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); res.log.info({ err }, 'Eventsource subscription error'); @@ -1353,7 +1357,7 @@ const startServer = async () => { stopHeartbeat, }; }).catch(err => { - const {statusCode, errorMessage } = errors.extractStatusAndMessage(err); + const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); logger.error({ err }, 'Websocket subscription error'); @@ -1482,13 +1486,15 @@ const startServer = async () => { // Decrement the metrics for connected clients: connectedClients.labels({ type: 'websocket' }).dec(); - // We need to delete the session object as to ensure it correctly gets + // We need to unassign the session object as to ensure it correctly gets // garbage collected, without doing this we could accidentally hold on to // references to the websocket, the request, and the logger, causing // memory leaks. - // - // @ts-ignore - delete session; + + // This is commented out because `delete` only operated on object properties + // It needs to be replaced by `session = undefined`, but it requires every calls to + // `session` to check for it, thus a significant refactor + // delete session; }); // Note: immediately after the `error` event is emitted, the `close` event diff --git a/streaming/logging.js b/streaming/logging.js index 64ee474875..e1c552c22e 100644 --- a/streaming/logging.js +++ b/streaming/logging.js @@ -1,6 +1,6 @@ -const { pino } = require('pino'); -const { pinoHttp, stdSerializers: pinoHttpSerializers } = require('pino-http'); -const uuid = require('uuid'); +import { pino } from 'pino'; +import { pinoHttp, stdSerializers as pinoHttpSerializers } from 'pino-http'; +import * as uuid from 'uuid'; /** * Generates the Request ID for logging and setting on responses @@ -36,7 +36,7 @@ function sanitizeRequestLog(req) { return log; } -const logger = pino({ +export const logger = pino({ name: "streaming", // Reformat the log level to a string: formatters: { @@ -59,7 +59,7 @@ const logger = pino({ } }); -const httpLogger = pinoHttp({ +export const httpLogger = pinoHttp({ logger, genReqId: generateRequestId, serializers: { @@ -71,7 +71,7 @@ const httpLogger = pinoHttp({ * Attaches a logger to the request object received by http upgrade handlers * @param {http.IncomingMessage} request */ -function attachWebsocketHttpLogger(request) { +export function attachWebsocketHttpLogger(request) { generateRequestId(request); request.log = logger.child({ @@ -84,7 +84,7 @@ function attachWebsocketHttpLogger(request) { * @param {http.IncomingMessage} request * @param {import('./index.js').ResolvedAccount} resolvedAccount */ -function createWebsocketLogger(request, resolvedAccount) { +export function createWebsocketLogger(request, resolvedAccount) { // ensure the request.id is always present. generateRequestId(request); @@ -98,17 +98,12 @@ function createWebsocketLogger(request, resolvedAccount) { }); } -exports.logger = logger; -exports.httpLogger = httpLogger; -exports.attachWebsocketHttpLogger = attachWebsocketHttpLogger; -exports.createWebsocketLogger = createWebsocketLogger; - /** * Initializes the log level based on the environment * @param {Object} env * @param {string} environment */ -exports.initializeLogLevel = function initializeLogLevel(env, environment) { +export function initializeLogLevel(env, environment) { if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) { logger.level = env.LOG_LEVEL; } else if (environment === 'development') { @@ -116,4 +111,4 @@ exports.initializeLogLevel = function initializeLogLevel(env, environment) { } else { logger.level = 'info'; } -}; +} diff --git a/streaming/metrics.js b/streaming/metrics.js index d05b4c9b16..a029d778fc 100644 --- a/streaming/metrics.js +++ b/streaming/metrics.js @@ -1,6 +1,6 @@ // @ts-check -const metrics = require('prom-client'); +import metrics from 'prom-client'; /** * @typedef StreamingMetrics @@ -18,7 +18,7 @@ const metrics = require('prom-client'); * @param {import('pg').Pool} pgPool * @returns {StreamingMetrics} */ -function setupMetrics(channels, pgPool) { +export function setupMetrics(channels, pgPool) { // Collect metrics from Node.js metrics.collectDefaultMetrics(); @@ -101,5 +101,3 @@ function setupMetrics(channels, pgPool) { messagesSent, }; } - -exports.setupMetrics = setupMetrics; diff --git a/streaming/package.json b/streaming/package.json index 71f204c0fb..efb692578c 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -7,6 +7,7 @@ }, "description": "Mastodon's Streaming Server", "private": true, + "type": "module", "repository": { "type": "git", "url": "https://github.com/mastodon/mastodon.git" diff --git a/streaming/tsconfig.json b/streaming/tsconfig.json index a0cf68ef90..ba5bd51ff7 100644 --- a/streaming/tsconfig.json +++ b/streaming/tsconfig.json @@ -2,11 +2,11 @@ "extends": "../tsconfig.json", "compilerOptions": { "target": "esnext", - "module": "CommonJS", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "noUnusedParameters": false, "tsBuildInfoFile": "../tmp/cache/streaming/tsconfig.tsbuildinfo", "paths": {}, }, - "include": ["./*.js", "./.eslintrc.js"], + "include": ["./*.js", "./.eslintrc.cjs"], } diff --git a/streaming/utils.js b/streaming/utils.js index 7b87a1d14c..4610bf660d 100644 --- a/streaming/utils.js +++ b/streaming/utils.js @@ -16,11 +16,9 @@ const FALSE_VALUES = [ * @param {any} value * @returns {boolean} */ -const isTruthy = value => - value && !FALSE_VALUES.includes(value); - -exports.isTruthy = isTruthy; - +export function isTruthy(value) { + return value && !FALSE_VALUES.includes(value); +} /** * See app/lib/ascii_folder.rb for the canon definitions @@ -33,7 +31,7 @@ const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEe * @param {string} str * @returns {string} */ -function foldToASCII(str) { +export function foldToASCII(str) { const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g'); return str.replace(regex, function(match) { @@ -42,28 +40,22 @@ function foldToASCII(str) { }); } -exports.foldToASCII = foldToASCII; - /** * @param {string} str * @returns {string} */ -function normalizeHashtag(str) { +export function normalizeHashtag(str) { return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, ''); } -exports.normalizeHashtag = normalizeHashtag; - /** * @param {string|string[]} arrayOrString * @returns {string} */ -function firstParam(arrayOrString) { +export function firstParam(arrayOrString) { if (Array.isArray(arrayOrString)) { return arrayOrString[0]; } else { return arrayOrString; } } - -exports.firstParam = firstParam; From 6f7615ba86afda56e1d661442286a1d68467a525 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Feb 2024 16:18:06 +0100 Subject: [PATCH 10/13] Add basic end-to-end test for admin moderation interface (#29424) --- spec/support/stories/profile_stories.rb | 6 +++++ spec/support/streaming_server_manager.rb | 3 +++ spec/system/report_interface_spec.rb | 31 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 spec/system/report_interface_spec.rb diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb index 74342c337d..f5fc9a441f 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -21,6 +21,12 @@ module ProfileStories click_on I18n.t('auth.login') end + def as_a_logged_in_admin + # This is a bit awkward, but this avoids code duplication. + as_a_logged_in_user + bob.update!(role: UserRole.find_by!(name: 'Admin')) + end + def with_alice_as_local_user @alice_bio = '@alice and @bob are fictional characters commonly used as' \ 'placeholder names in #cryptology, as well as #science and' \ diff --git a/spec/support/streaming_server_manager.rb b/spec/support/streaming_server_manager.rb index 3381918299..b702fc77ce 100644 --- a/spec/support/streaming_server_manager.rb +++ b/spec/support/streaming_server_manager.rb @@ -109,6 +109,9 @@ RSpec.configure do |config| # Also needs to be set per-example here because of the database cleaner. Setting.registrations_mode = 'open' + # Load seeds so we have the default roles otherwise cleared by `DatabaseCleaner` + Rails.application.load_seed + example.run end diff --git a/spec/system/report_interface_spec.rb b/spec/system/report_interface_spec.rb new file mode 100644 index 0000000000..6eba552559 --- /dev/null +++ b/spec/system/report_interface_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'report interface', :paperclip_processing do + include ProfileStories + + let(:email) { 'admin@example.com' } + let(:password) { 'password' } + let(:confirmed_at) { Time.zone.now } + let(:finished_onboarding) { true } + + let(:reported_account) { Fabricate(:account) } + let(:reported_status) { Fabricate(:status, account: reported_account) } + let(:media_attachment) { Fabricate(:media_attachment, account: reported_account, status: reported_status, file: attachment_fixture('attachment.jpg')) } + let!(:report) { Fabricate(:report, target_account: reported_account, status_ids: [media_attachment.status.id]) } + + before do + as_a_logged_in_admin + visit admin_report_path(report) + end + + it 'displays the report interface, including the javascript bits' do + # The report category selector React component is properly rendered + expect(page).to have_css('.report-reason-selector') + + # The media React component is properly rendered + page.scroll_to(page.find('.batch-table__row')) + expect(page).to have_css('.spoiler-button__overlay__label') + end +end From 1b219e709bced7fff87a39f602b159fb6c21b133 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Tue, 27 Feb 2024 12:46:58 +0200 Subject: [PATCH 11/13] [Glitch] Fix filters title and keywords overflow Port 213c87ae595cc1ddcb618516106712b0aae789bd to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 1 + app/javascript/flavours/glitch/styles/forms.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 97ea6d98bb..491b116a50 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1065,6 +1065,7 @@ a.name-tag, display: flex; justify-content: space-between; margin-bottom: 0; + word-break: break-word; } &__permissions { diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 33390f4992..5b7247734f 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -1080,6 +1080,7 @@ code { &__type { color: $darker-text-color; + word-break: break-word; } } From 916d78373d84eb3db47ed11f6e6edf78ae918688 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Feb 2024 12:41:19 +0100 Subject: [PATCH 12/13] [Glitch] Change behavior of privacy dropdown to only change value on validation Port 90573c3abbc0783c25cb4e3aa8c298e10259cb57 to glitch-soc Signed-off-by: Claire --- .../compose/components/language_dropdown.jsx | 1 + .../compose/components/privacy_dropdown.jsx | 124 +---------------- .../components/privacy_dropdown_menu.jsx | 128 ++++++++++++++++++ 3 files changed, 131 insertions(+), 122 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx index db1ce9cece..8edf75203f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -141,6 +141,7 @@ class LanguageDropdownMenu extends PureComponent { case 'Escape': onClose(); break; + case ' ': case 'Enter': this.handleClick(e); break; diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx index 8a49f71511..c99f18545b 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx @@ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; -import { supportsPassiveEvents } from 'detect-passive-events'; import Overlay from 'react-overlays/Overlay'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; +import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; + const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, @@ -28,126 +28,6 @@ const messages = defineMessages({ unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; - -class PrivacyDropdownMenu extends PureComponent { - - static propTypes = { - style: PropTypes.object, - items: PropTypes.array.isRequired, - value: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - e.stopPropagation(); - } - }; - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - render () { - const { style, items, value } = this.props; - - return ( -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
- - {item.extra && ( -
- -
- )} -
- ))} -
- ); - } - -} - class PrivacyDropdown extends PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx new file mode 100644 index 0000000000..03a0b76d23 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; + +import { supportsPassiveEvents } from 'detect-passive-events'; + +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; + +export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => { + const nodeRef = useRef(null); + const focusedItemRef = useRef(null); + const [currentValue, setCurrentValue] = useState(value); + + const handleDocumentClick = useCallback((e) => { + if (nodeRef.current && !nodeRef.current.contains(e.target)) { + onClose(); + e.stopPropagation(); + } + }, [nodeRef, onClose]); + + const handleClick = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + onClose(); + onChange(value); + }, [onClose, onChange]); + + const handleKeyDown = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => (item.value === value)); + + let element = null; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case ' ': + case 'Enter': + handleClick(e); + break; + case 'ArrowDown': + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + break; + case 'ArrowUp': + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + } else { + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + } + break; + case 'Home': + element = nodeRef.current.firstChild; + break; + case 'End': + element = nodeRef.current.lastChild; + break; + } + + if (element) { + element.focus(); + setCurrentValue(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }, [nodeRef, items, onClose, handleClick, setCurrentValue]); + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, { capture: true }); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + focusedItemRef.current?.focus({ preventScroll: true }); + + return () => { + document.removeEventListener('click', handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', handleDocumentClick, listenerOptions); + }; + }, [handleDocumentClick]); + + return ( +
    + {items.map(item => ( +
  • +
    + +
    + +
    + {item.text} + {item.meta} +
    + + {item.extra && ( +
    + +
    + )} +
  • + ))} +
+ ); +}; + +PrivacyDropdownMenu.propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; From e8155319c7366f482081a750cd05e01b5a5277ea Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Feb 2024 19:26:26 +0100 Subject: [PATCH 13/13] Take advantage of upstream's refactor and reduce code duplication --- .../components/dropdown_icon_button.jsx | 4 +- .../compose/components/dropdown_menu.jsx | 125 ------------------ 2 files changed, 2 insertions(+), 127 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx index 0ced5a04ad..9774d4260e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx @@ -5,7 +5,7 @@ import Overlay from 'react-overlays/Overlay'; import { IconButton } from 'flavours/glitch/components/icon_button'; -import DropdownMenu from './dropdown_menu'; +import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; export const DropdownIconButton = ({ value, disabled, icon, onChange, iconComponent, title, options }) => { const containerRef = useRef(null); @@ -53,7 +53,7 @@ export const DropdownIconButton = ({ value, disabled, icon, onChange, iconCompon {({ props, placement }) => (
- { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - e.stopPropagation(); - } - }; - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - render () { - const { style, items, value } = this.props; - - return ( -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
-
- ))} -
- ); - } - -} - -export default DropdownMenu;