diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 98867160ec..e291c32782 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -135,7 +135,7 @@ Lint/UselessAssignment:
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
- Max: 143
+ Max: 146
# Configuration parameters: CountBlocks, Max.
Metrics/BlockNesting:
diff --git a/Gemfile b/Gemfile
index 478d2cfe99..a1eba4efe0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -58,6 +58,7 @@ gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15'
+gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
diff --git a/Gemfile.lock b/Gemfile.lock
index 151a372017..a44538d16f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,6 +7,17 @@ GIT
hkdf (~> 0.2)
jwt (~> 2.0)
+GIT
+ remote: https://github.com/jhawthorn/nsa.git
+ revision: e020fcc3a54d993ab45b7194d89ab720296c111b
+ ref: e020fcc3a54d993ab45b7194d89ab720296c111b
+ specs:
+ nsa (0.2.8)
+ activesupport (>= 4.2, < 7.2)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ sidekiq (>= 3.5)
+ statsd-ruby (~> 1.4, >= 1.4.0)
+
GIT
remote: https://github.com/mastodon/rails-settings-cached.git
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
@@ -103,8 +114,8 @@ GEM
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
- aws-partitions (1.791.0)
- aws-sdk-core (3.178.0)
+ aws-partitions (1.793.0)
+ aws-sdk-core (3.180.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@@ -112,8 +123,8 @@ GEM
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.131.0)
- aws-sdk-core (~> 3, >= 3.177.0)
+ aws-sdk-s3 (1.132.0)
+ aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
@@ -407,7 +418,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
- lograge (0.12.0)
+ lograge (0.13.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@@ -434,8 +445,8 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_mime (1.1.2)
- mini_portile2 (2.8.2)
- minitest (5.18.1)
+ mini_portile2 (2.8.4)
+ minitest (5.19.0)
msgpack (1.7.1)
multi_json (1.15.0)
multipart-post (2.3.0)
@@ -520,9 +531,9 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.1)
- rack (2.2.7)
- rack-attack (6.6.1)
- rack (>= 1.0, < 3)
+ rack (2.2.8)
+ rack-attack (6.7.0)
+ rack (>= 1.0, < 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
@@ -553,8 +564,9 @@ GEM
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
+ rails-dom-testing (2.1.1)
+ activesupport (>= 5.0.0)
+ minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
@@ -661,7 +673,7 @@ GEM
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
- selenium-webdriver (4.9.1)
+ selenium-webdriver (4.11.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@@ -705,6 +717,7 @@ GEM
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.25)
+ statsd-ruby (1.5.0)
stoplight (3.0.1)
redlock (~> 1.0)
strong_migrations (0.8.0)
@@ -719,7 +732,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
- test-prof (1.2.1)
+ test-prof (1.2.2)
thor (1.2.2)
tilt (2.2.0)
timeout (0.4.0)
@@ -786,7 +799,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.6.8)
+ zeitwerk (2.6.11)
PLATFORMS
ruby
@@ -856,6 +869,7 @@ DEPENDENCIES
net-http (~> 0.3.2)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
+ nsa!
oj (~> 3.14)
omniauth (~> 1.9)
omniauth-cas (~> 2.0)
diff --git a/app/controllers/api/v1/instances/languages_controller.rb b/app/controllers/api/v1/instances/languages_controller.rb
new file mode 100644
index 0000000000..17509e748c
--- /dev/null
+++ b/app/controllers/api/v1/instances/languages_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::Instances::LanguagesController < Api::BaseController
+ skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
+ skip_around_action :set_locale
+
+ before_action :set_languages
+
+ vary_by ''
+
+ def show
+ cache_even_if_authenticated!
+ render json: @languages, each_serializer: REST::LanguageSerializer
+ end
+
+ private
+
+ def set_languages
+ @languages = LanguagesHelper::SUPPORTED_LOCALES.keys.map { |code| LanguagePresenter.new(code) }
+ end
+end
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
index 6cd32a377c..3a40ea3823 100644
--- a/app/controllers/concerns/web_app_controller_concern.rb
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -11,7 +11,7 @@ module WebAppControllerConcern
end
def skip_csrf_meta_tags?
- current_user.nil?
+ !(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
end
def set_app_body_class
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 96dc4a2a1e..08561c71f4 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -145,7 +145,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
-export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
+export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx
index 92eb41cff8..80dd5200fc 100644
--- a/app/javascript/mastodon/features/explore/components/story.jsx
+++ b/app/javascript/mastodon/features/explore/components/story.jsx
@@ -22,6 +22,7 @@ export default class Story extends PureComponent {
author: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
+ thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
@@ -33,7 +34,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () {
- const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
+ const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
const { thumbnailLoaded } = this.state;
@@ -49,7 +50,7 @@ export default class Story extends PureComponent {
{thumbnail ? (
<>
-
+
>
) : }
diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx
index 489ab6dd61..663aa6d80f 100644
--- a/app/javascript/mastodon/features/explore/links.jsx
+++ b/app/javascript/mastodon/features/explore/links.jsx
@@ -67,6 +67,7 @@ class Links extends PureComponent {
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')}
+ thumbnailDescription={link.get('image_description')}
blurhash={link.get('blurhash')}
/>
))}
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx
new file mode 100644
index 0000000000..46050309ff
--- /dev/null
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import Button from 'mastodon/components/button';
+import { ShortNumber } from 'mastodon/components/short_number';
+
+const messages = defineMessages({
+ followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
+ unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
+});
+
+const usesRenderer = (displayNumber, pluralReady) => (
+ {displayNumber},
+ }}
+ />
+);
+
+const peopleRenderer = (displayNumber, pluralReady) => (
+ {displayNumber},
+ }}
+ />
+);
+
+const usesTodayRenderer = (displayNumber, pluralReady) => (
+ {displayNumber},
+ }}
+ />
+);
+
+export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
+ if (!tag) {
+ return null;
+ }
+
+ const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
+ const dividingCircle = {' · '};
+
+ return (
+
+
+
#{tag.get('name')}
+
+
+
+
+
+ {dividingCircle}
+
+ {dividingCircle}
+
+
+
+ );
+});
+
+HashtagHeader.propTypes = {
+ tag: ImmutablePropTypes.map,
+ disabled: PropTypes.bool,
+ onClick: PropTypes.func,
+ intl: PropTypes.object,
+};
\ No newline at end of file
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.jsx b/app/javascript/mastodon/features/hashtag_timeline/index.jsx
index b6ed29d8de..d00890cb37 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.jsx
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.jsx
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -17,17 +16,12 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/t
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
-import { Icon } from 'mastodon/components/icon';
import StatusListContainer from '../ui/containers/status_list_container';
+import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container';
-const messages = defineMessages({
- followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
- unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
-});
-
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]),
@@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
- intl: PropTypes.object,
};
handlePin = () => {
@@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent {
};
render () {
- const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
+ const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
- let followButton;
-
- if (tag) {
- const following = tag.get('following');
-
- const classes = classNames('column-header__button', {
- active: following,
- });
-
- followButton = (
-
- );
- }
-
return (
{columnId && }
}
+ alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {
}
-export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
+export default connect(mapStateToProps)(HashtagTimeline);
diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx
index d5db58ad2d..85ef0511db 100644
--- a/app/javascript/mastodon/features/interaction_modal/index.jsx
+++ b/app/javascript/mastodon/features/interaction_modal/index.jsx
@@ -13,7 +13,7 @@ import { openModal, closeModal } from 'mastodon/actions/modal';
import api from 'mastodon/api';
import Button from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
-import { registrationsOpen } from 'mastodon/initial_state';
+import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
const messages = defineMessages({
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
@@ -336,18 +336,36 @@ class InteractionModal extends React.PureComponent {
}
let signupButton;
+ let signUpOrSignInButton;
- if (registrationsOpen) {
- signupButton = (
-
-
+ if (sso_redirect) {
+ signUpOrSignInButton = (
+
+
- );
+ )
} else {
- signupButton = (
-
+ if(registrationsOpen) {
+ signupButton = (
+
+
+
+ );
+ } else {
+ signupButton = (
+
+ );
+ }
+
+ signUpOrSignInButton = (
+ <>
+
+
+
+ {signupButton}
+ >
);
}
@@ -358,6 +376,13 @@ class InteractionModal extends React.PureComponent {
{actionDescription}
+
+
+
+ {signUpOrSignInButton}
+
+
+
diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx
index 6ac3c1d0f2..07fb1db9e2 100644
--- a/app/javascript/mastodon/features/status/components/card.jsx
+++ b/app/javascript/mastodon/features/status/components/card.jsx
@@ -167,7 +167,8 @@ export default class Card extends PureComponent {
/>
);
- let thumbnail =
;
+ const thumbnailDescription = card.get('image_description');
+ const thumbnail =
;
let spoilerButton = (