Merge pull request #2258 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
8b4df95dbe
|
@ -1,5 +1,5 @@
|
||||||
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
|
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
|
||||||
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
|
FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
|
||||||
|
|
||||||
# Install Rails
|
# Install Rails
|
||||||
# RUN gem install rails webdrivers
|
# RUN gem install rails webdrivers
|
||||||
|
|
108
Gemfile.lock
108
Gemfile.lock
|
@ -18,40 +18,40 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.3)
|
actioncable (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.3)
|
actionmailbox (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.3)
|
actionmailer (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.3)
|
actionpack (6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.3)
|
actiontext (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.3)
|
actionview (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -61,22 +61,22 @@ GEM
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (6.1.7.3)
|
activejob (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.3)
|
activemodel (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activerecord (6.1.7.3)
|
activerecord (6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activestorage (6.1.7.3)
|
activestorage (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.3)
|
activesupport (6.1.7.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -412,7 +412,7 @@ GEM
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.2)
|
mini_portile2 (2.8.2)
|
||||||
minitest (5.18.0)
|
minitest (5.18.1)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
@ -511,20 +511,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.3)
|
rails (6.1.7.4)
|
||||||
actioncable (= 6.1.7.3)
|
actioncable (= 6.1.7.4)
|
||||||
actionmailbox (= 6.1.7.3)
|
actionmailbox (= 6.1.7.4)
|
||||||
actionmailer (= 6.1.7.3)
|
actionmailer (= 6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actiontext (= 6.1.7.3)
|
actiontext (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.3)
|
railties (= 6.1.7.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
@ -539,9 +539,9 @@ GEM
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
railties (6.1.7.3)
|
railties (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
|
|
@ -15,9 +15,11 @@ export const ExplorePrompt = () => (
|
||||||
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||||
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__message__actions__wrapper'>
|
||||||
<div className='dismissable-banner__message__actions'>
|
<div className='dismissable-banner__message__actions'>
|
||||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,9 +33,11 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const getHomeFeedSpeed = createSelector([
|
const getHomeFeedSpeed = createSelector([
|
||||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||||
|
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||||
state => state.get('statuses'),
|
state => state.get('statuses'),
|
||||||
], (statusIds, statusMap) => {
|
], (statusIds, pendingStatusIds, statusMap) => {
|
||||||
const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
|
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||||
|
const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||||
|
@ -46,9 +48,14 @@ const getHomeFeedSpeed = createSelector([
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
|
const homeTooSlow = createSelector([
|
||||||
speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||||
|| (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago
|
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
|
getHomeFeedSpeed,
|
||||||
|
], (isLoading, isPartial, speed) =>
|
||||||
|
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||||
|
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||||
|
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { connect } from 'react-redux';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
||||||
import Permalink from 'flavours/glitch/components/permalink';
|
import Permalink from 'flavours/glitch/components/permalink';
|
||||||
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
|
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
|
||||||
|
@ -22,6 +23,10 @@ const Account = connect(state => ({
|
||||||
</Permalink>
|
</Permalink>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||||
});
|
});
|
||||||
|
@ -45,7 +50,8 @@ class Header extends PureComponent {
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
openClosedRegistrationsModal: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
dispatchServer: PropTypes.func
|
dispatchServer: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -55,14 +61,15 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
|
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||||
|
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
|
||||||
<Account />
|
<Account />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -85,6 +92,7 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
|
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||||
{signupButton}
|
{signupButton}
|
||||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
||||||
</>
|
</>
|
||||||
|
@ -107,4 +115,4 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||||
|
|
|
@ -1005,11 +1005,20 @@ $ui-header-height: 55px;
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button-tertiary {
|
.button-tertiary {
|
||||||
background: rgba($ui-base-color, 0.15);
|
background: rgba($ui-base-color, 0.15);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
|
|
|
@ -108,12 +108,13 @@
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid lighten($ui-base-color, 12%);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: lighten($ui-primary-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
border-color: lighten($ui-base-color, 16%);
|
||||||
color: lighten($darker-text-color, 4%);
|
color: lighten($darker-text-color, 4%);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,13 +129,13 @@ export function resetCompose() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (routerHistory, defaultText) => dispatch => {
|
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(routerHistory);
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mentionCompose(account, routerHistory) {
|
export function mentionCompose(account, routerHistory) {
|
||||||
|
|
|
@ -16,9 +16,11 @@ export const ExplorePrompt = () => (
|
||||||
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||||
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__message__actions__wrapper'>
|
||||||
<div className='dismissable-banner__message__actions'>
|
<div className='dismissable-banner__message__actions'>
|
||||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
);
|
);
|
|
@ -33,9 +33,11 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const getHomeFeedSpeed = createSelector([
|
const getHomeFeedSpeed = createSelector([
|
||||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||||
|
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||||
state => state.get('statuses'),
|
state => state.get('statuses'),
|
||||||
], (statusIds, statusMap) => {
|
], (statusIds, pendingStatusIds, statusMap) => {
|
||||||
const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
|
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||||
|
const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||||
|
@ -46,9 +48,14 @@ const getHomeFeedSpeed = createSelector([
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
|
const homeTooSlow = createSelector([
|
||||||
speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||||
|| (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago
|
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
|
getHomeFeedSpeed,
|
||||||
|
], (isLoading, isPartial, speed) =>
|
||||||
|
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||||
|
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||||
|
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { connect } from 'react-redux';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { fetchServer } from 'mastodon/actions/server';
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
||||||
import { registrationsOpen, me } from 'mastodon/initial_state';
|
import { registrationsOpen, me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -21,6 +22,10 @@ const Account = connect(state => ({
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||||
});
|
});
|
||||||
|
@ -44,7 +49,8 @@ class Header extends PureComponent {
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
openClosedRegistrationsModal: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
dispatchServer: PropTypes.func
|
dispatchServer: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -54,14 +60,15 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
|
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||||
|
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
|
||||||
<Account />
|
<Account />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -84,6 +91,7 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
|
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||||
{signupButton}
|
{signupButton}
|
||||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
||||||
</>
|
</>
|
||||||
|
@ -106,4 +114,4 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "Publish",
|
||||||
"compose_form.publish_form": "Publish",
|
"compose_form.publish_form": "New post",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.save_changes": "Save changes",
|
"compose_form.save_changes": "Save changes",
|
||||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
||||||
|
|
|
@ -133,12 +133,13 @@
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid lighten($ui-base-color, 12%);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: lighten($ui-primary-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
border-color: lighten($ui-base-color, 16%);
|
||||||
color: lighten($darker-text-color, 4%);
|
color: lighten($darker-text-color, 4%);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -3146,7 +3147,7 @@ $ui-header-height: 55px;
|
||||||
.column-back-button {
|
.column-back-button {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3154,6 +3155,7 @@ $ui-header-height: 55px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -3166,7 +3168,7 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__back-button {
|
.column-header__back-button {
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
@ -3201,7 +3203,7 @@ $ui-header-height: 55px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
top: -48px;
|
top: -50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle {
|
.react-toggle {
|
||||||
|
@ -3882,7 +3884,8 @@ a.status-card.compact:hover {
|
||||||
.column-header {
|
.column-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3937,7 +3940,7 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__button {
|
.column-header__button {
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3945,16 +3948,15 @@ a.status-card.compact:hover {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($darker-text-color, 7%);
|
color: lighten($darker-text-color, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: lighten($ui-base-color, 8%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3968,6 +3970,7 @@ a.status-card.compact:hover {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -3987,13 +3990,13 @@ a.status-card.compact:hover {
|
||||||
height: 0;
|
height: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid lighten($ui-base-color, 12%);
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__collapsible-inner {
|
.column-header__collapsible-inner {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: $ui-base-color;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4406,17 +4409,13 @@ a.status-card.compact:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: $base-overlay-background;
|
background-color: rgba($black, 0.45);
|
||||||
text-transform: uppercase;
|
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
text-transform: uppercase;
|
||||||
padding: 4px;
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-toggle {
|
.setting-toggle {
|
||||||
|
@ -4476,6 +4475,7 @@ a.status-card.compact:hover {
|
||||||
|
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
contain: initial;
|
contain: initial;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
@ -6160,6 +6160,7 @@ a.status-card.compact:hover {
|
||||||
display: block;
|
display: block;
|
||||||
color: $white;
|
color: $white;
|
||||||
background: rgba($black, 0.65);
|
background: rgba($black, 0.65);
|
||||||
|
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
@ -6837,24 +6838,6 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.directory__section-headline {
|
|
||||||
background: darken($ui-base-color, 2%);
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
&.active {
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-color: transparent transparent darken($ui-base-color, 7%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-form {
|
.filter-form {
|
||||||
|
@ -7369,7 +7352,6 @@ noscript {
|
||||||
|
|
||||||
.account__header {
|
.account__header {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -7391,6 +7373,7 @@ noscript {
|
||||||
height: 145px;
|
height: 145px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
@ -7404,7 +7387,7 @@ noscript {
|
||||||
&__bar {
|
&__bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 12%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -7413,7 +7396,7 @@ noscript {
|
||||||
|
|
||||||
.account__avatar {
|
.account__avatar {
|
||||||
background: darken($ui-base-color, 8%);
|
background: darken($ui-base-color, 8%);
|
||||||
border: 2px solid lighten($ui-base-color, 4%);
|
border: 2px solid $ui-base-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8785,11 +8768,20 @@ noscript {
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button-tertiary {
|
.button-tertiary {
|
||||||
background: rgba($ui-base-color, 0.15);
|
background: rgba($ui-base-color, 0.15);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AttachmentBatch
|
||||||
|
# Maximum amount of objects you can delete in an S3 API call. It's
|
||||||
|
# important to remember that this does not correspond to the number
|
||||||
|
# of records in the batch, since records can have multiple attachments
|
||||||
|
LIMIT = 1_000
|
||||||
|
|
||||||
|
# Attributes generated and maintained by Paperclip (not all of them
|
||||||
|
# are always used on every class, however)
|
||||||
|
NULLABLE_ATTRIBUTES = %w(
|
||||||
|
file_name
|
||||||
|
content_type
|
||||||
|
file_size
|
||||||
|
fingerprint
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
# Styles that are always present even when not explicitly defined
|
||||||
|
BASE_STYLES = %i(original).freeze
|
||||||
|
|
||||||
|
attr_reader :klass, :records, :storage_mode
|
||||||
|
|
||||||
|
def initialize(klass, records)
|
||||||
|
@klass = klass
|
||||||
|
@records = records
|
||||||
|
@storage_mode = Paperclip::Attachment.default_options[:storage]
|
||||||
|
@attachment_names = klass.attachment_definitions.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
remove_files
|
||||||
|
batch.delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
remove_files
|
||||||
|
batch.update_all(nullified_attributes) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def batch
|
||||||
|
klass.where(id: records.map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_files
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
logger.debug { "Preparing to delete attachments for #{records.size} records" }
|
||||||
|
|
||||||
|
records.each do |record|
|
||||||
|
@attachment_names.each do |attachment_name|
|
||||||
|
attachment = record.public_send(attachment_name)
|
||||||
|
styles = BASE_STYLES | attachment.styles.keys
|
||||||
|
|
||||||
|
next if attachment.blank?
|
||||||
|
|
||||||
|
styles.each do |style|
|
||||||
|
case @storage_mode
|
||||||
|
when :s3
|
||||||
|
logger.debug { "Adding #{attachment.path(style)} to batch for deletion" }
|
||||||
|
keys << attachment.style_name_as_path(style)
|
||||||
|
when :filesystem
|
||||||
|
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||||
|
path = attachment.path(style)
|
||||||
|
FileUtils.remove_file(path, true)
|
||||||
|
|
||||||
|
begin
|
||||||
|
FileUtils.rmdir(File.dirname(path), parents: true)
|
||||||
|
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
|
||||||
|
# Ignore failure to delete a directory, with the same ignored errors
|
||||||
|
# as Paperclip
|
||||||
|
end
|
||||||
|
when :fog
|
||||||
|
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||||
|
attachment.directory.files.new(key: attachment.path(style)).destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless storage_mode == :s3
|
||||||
|
|
||||||
|
# We can batch deletes over S3, but there is a limit of how many
|
||||||
|
# objects can be processed at once, so we have to potentially
|
||||||
|
# separate them into multiple calls.
|
||||||
|
|
||||||
|
keys.each_slice(LIMIT) do |keys_slice|
|
||||||
|
logger.debug { "Deleting #{keys_slice.size} objects" }
|
||||||
|
|
||||||
|
bucket.delete_objects(delete: {
|
||||||
|
objects: keys_slice.map { |key| { key: key } },
|
||||||
|
quiet: true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def bucket
|
||||||
|
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
||||||
|
end
|
||||||
|
|
||||||
|
def nullified_attributes
|
||||||
|
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def logger
|
||||||
|
Rails.logger
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,15 +15,15 @@ class Vacuum::MediaAttachmentsVacuum
|
||||||
private
|
private
|
||||||
|
|
||||||
def vacuum_cached_files!
|
def vacuum_cached_files!
|
||||||
media_attachments_past_retention_period.find_each do |media_attachment|
|
media_attachments_past_retention_period.find_in_batches do |media_attachments|
|
||||||
media_attachment.file.destroy
|
AttachmentBatch.new(MediaAttachment, media_attachments).clear
|
||||||
media_attachment.thumbnail.destroy
|
|
||||||
media_attachment.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def vacuum_orphaned_records!
|
def vacuum_orphaned_records!
|
||||||
orphaned_media_attachments.in_batches.destroy_all
|
orphaned_media_attachments.find_in_batches do |media_attachments|
|
||||||
|
AttachmentBatch.new(MediaAttachment, media_attachments).delete
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_attachments_past_retention_period
|
def media_attachments_past_retention_period
|
||||||
|
|
|
@ -10,14 +10,6 @@ class ClearDomainMediaService < BaseService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def invalidate_association_caches!(status_ids)
|
|
||||||
# Normally, associated models of a status are immutable (except for accounts)
|
|
||||||
# so they are aggressively cached. After updating the media attachments to no
|
|
||||||
# longer point to a local file, we need to clear the cache to make those
|
|
||||||
# changes appear in the API and UI
|
|
||||||
Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_media!
|
def clear_media!
|
||||||
clear_account_images!
|
clear_account_images!
|
||||||
clear_account_attachments!
|
clear_account_attachments!
|
||||||
|
@ -25,31 +17,21 @@ class ClearDomainMediaService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_images!
|
def clear_account_images!
|
||||||
blocked_domain_accounts.reorder(nil).find_each do |account|
|
blocked_domain_accounts.reorder(nil).find_in_batches do |accounts|
|
||||||
account.avatar.destroy if account.avatar&.exists?
|
AttachmentBatch.new(Account, accounts).clear
|
||||||
account.header.destroy if account.header&.exists?
|
|
||||||
account.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_attachments!
|
def clear_account_attachments!
|
||||||
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
||||||
affected_status_ids = []
|
AttachmentBatch.new(MediaAttachment, attachments).clear
|
||||||
|
|
||||||
attachments.each do |attachment|
|
|
||||||
affected_status_ids << attachment.status_id if attachment.status_id.present?
|
|
||||||
|
|
||||||
attachment.file.destroy if attachment.file&.exists?
|
|
||||||
attachment.type = :unknown
|
|
||||||
attachment.save
|
|
||||||
end
|
|
||||||
|
|
||||||
invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_emojos!
|
def clear_emojos!
|
||||||
emojis_from_blocked_domains.destroy_all
|
emojis_from_blocked_domains.find_in_batches do |custom_emojis|
|
||||||
|
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocked_domain
|
def blocked_domain
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
= t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||||
|
|
||||||
= simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f|
|
= simple_form_for @domain_block, url: admin_domain_blocks_path, method: :post do |f|
|
||||||
|
|
||||||
%p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
%p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||||
%ul.hint
|
%ul.hint
|
||||||
|
|
|
@ -53,7 +53,7 @@ describe 'blocking domains through the moderation interface' do
|
||||||
# Confirming updates the block
|
# Confirming updates the block
|
||||||
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
||||||
|
|
||||||
expect(domain_block.reload.severity).to eq 'silence'
|
expect(domain_block.reload.severity).to eq 'suspend'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ describe 'blocking domains through the moderation interface' do
|
||||||
# Confirming updates the block
|
# Confirming updates the block
|
||||||
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
||||||
|
|
||||||
expect(domain_block.reload.severity).to eq 'silence'
|
expect(domain_block.reload.severity).to eq 'suspend'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
70
yarn.lock
70
yarn.lock
|
@ -5841,9 +5841,9 @@ glob-parent@^6.0.2:
|
||||||
is-glob "^4.0.3"
|
is-glob "^4.0.3"
|
||||||
|
|
||||||
glob@^10.2.5, glob@^10.2.6:
|
glob@^10.2.5, glob@^10.2.6:
|
||||||
version "10.2.7"
|
version "10.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.7.tgz#9dd2828cd5bc7bd861e7738d91e7113dda41d7d8"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.0.tgz#763d02a894f3cdfc521b10bbbbc8e0309e750cce"
|
||||||
integrity sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==
|
integrity sha512-AQ1/SB9HH0yCx1jXAT4vmCbTOPe5RQ+kCurjbel5xSCGhebumUv+GJZfa1rEqor3XIViqwSEmlkZCQD43RWrBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child "^3.1.0"
|
foreground-child "^3.1.0"
|
||||||
jackspeak "^2.0.3"
|
jackspeak "^2.0.3"
|
||||||
|
@ -8042,9 +8042,9 @@ minimatch@^5.0.1:
|
||||||
brace-expansion "^2.0.1"
|
brace-expansion "^2.0.1"
|
||||||
|
|
||||||
minimatch@^9.0.1:
|
minimatch@^9.0.1:
|
||||||
version "9.0.1"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.2.tgz#397e387fff22f6795844d00badc903a3d5de7057"
|
||||||
integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==
|
integrity sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^2.0.1"
|
brace-expansion "^2.0.1"
|
||||||
|
|
||||||
|
@ -8769,15 +8769,20 @@ performance-now@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
||||||
|
|
||||||
pg-cloudflare@^1.1.0:
|
pg-cloudflare@^1.1.1:
|
||||||
version "1.1.0"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0"
|
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
|
||||||
integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==
|
integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==
|
||||||
|
|
||||||
pg-connection-string@^2.6.0:
|
pg-connection-string@^2.6.0:
|
||||||
version "2.6.0"
|
version "2.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8"
|
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
|
||||||
integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==
|
integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==
|
||||||
|
|
||||||
|
pg-connection-string@^2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
|
||||||
|
integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==
|
||||||
|
|
||||||
pg-int8@1.0.1:
|
pg-int8@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
@ -8789,10 +8794,10 @@ pg-numeric@1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a"
|
resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a"
|
||||||
integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==
|
integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==
|
||||||
|
|
||||||
pg-pool@^3.6.0:
|
pg-pool@^3.6.1:
|
||||||
version "3.6.0"
|
version "3.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
|
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7"
|
||||||
integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==
|
integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==
|
||||||
|
|
||||||
pg-protocol@*, pg-protocol@^1.6.0:
|
pg-protocol@*, pg-protocol@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
|
@ -8824,19 +8829,19 @@ pg-types@^4.0.1:
|
||||||
postgres-range "^1.1.1"
|
postgres-range "^1.1.1"
|
||||||
|
|
||||||
pg@^8.5.0:
|
pg@^8.5.0:
|
||||||
version "8.11.0"
|
version "8.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.0.tgz#a37e534e94b57a7ed811e926f23a7c56385f55d9"
|
resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.1.tgz#297e0eb240306b1e9e4f55af8a3bae76ae4810b1"
|
||||||
integrity sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==
|
integrity sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-writer "2.0.0"
|
buffer-writer "2.0.0"
|
||||||
packet-reader "1.0.0"
|
packet-reader "1.0.0"
|
||||||
pg-connection-string "^2.6.0"
|
pg-connection-string "^2.6.1"
|
||||||
pg-pool "^3.6.0"
|
pg-pool "^3.6.1"
|
||||||
pg-protocol "^1.6.0"
|
pg-protocol "^1.6.0"
|
||||||
pg-types "^2.1.0"
|
pg-types "^2.1.0"
|
||||||
pgpass "1.x"
|
pgpass "1.x"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
pg-cloudflare "^1.1.0"
|
pg-cloudflare "^1.1.1"
|
||||||
|
|
||||||
pgpass@1.x:
|
pgpass@1.x:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
|
@ -9582,9 +9587,9 @@ react-redux-loading-bar@^5.0.4:
|
||||||
react-lifecycles-compat "^3.0.4"
|
react-lifecycles-compat "^3.0.4"
|
||||||
|
|
||||||
react-redux@^8.0.4:
|
react-redux@^8.0.4:
|
||||||
version "8.1.0"
|
version "8.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.0.tgz#4e147339f00bbaac7196bc42bc99e6fc412846e7"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.1.tgz#8e740f3fd864a4cd0de5ba9cdc8ad39cc9e7c81a"
|
||||||
integrity sha512-CtHZzAOxi7GQvTph4dVLWwZHAWUjV2kMEQtk50OrN8z3gKxpWg3Tz7JfDw32N3Rpd7fh02z73cF6yZkK467gbQ==
|
integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.1"
|
"@babel/runtime" "^7.12.1"
|
||||||
"@types/hoist-non-react-statics" "^3.3.1"
|
"@types/hoist-non-react-statics" "^3.3.1"
|
||||||
|
@ -9702,9 +9707,9 @@ react-test-renderer@^18.2.0:
|
||||||
scheduler "^0.23.0"
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
react-textarea-autosize@*, react-textarea-autosize@^8.4.1:
|
react-textarea-autosize@*, react-textarea-autosize@^8.4.1:
|
||||||
version "8.4.1"
|
version "8.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz#bcfc5462727014b808b14ee916c01e275e8a8335"
|
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.0.tgz#bb0f7faf9849850f1c20b6e7fac0309d4b92f87b"
|
||||||
integrity sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==
|
integrity sha512-cp488su3U9RygmHmGpJp0KEt0i/+57KCK33XVPH+50swVRBhIZYh0fGduz2YLKXwl9vSKBZ9HUXcg9PQXUXqIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.13"
|
"@babel/runtime" "^7.20.13"
|
||||||
use-composed-ref "^1.3.0"
|
use-composed-ref "^1.3.0"
|
||||||
|
@ -10171,9 +10176,9 @@ sass-loader@^10.2.0:
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
|
|
||||||
sass@^1.62.1:
|
sass@^1.62.1:
|
||||||
version "1.63.4"
|
version "1.63.6"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.4.tgz#caf60643321044c61f6a0fe638a07abbd31cfb5d"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.6.tgz#481610e612902e0c31c46b46cf2dad66943283ea"
|
||||||
integrity sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ==
|
integrity sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
|
@ -10846,7 +10851,6 @@ stringz@^2.1.0:
|
||||||
char-regex "^1.0.2"
|
char-regex "^1.0.2"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
name strip-ansi-cjs
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
|
Loading…
Reference in New Issue