Merge commit '41a505513fb36f7c28c8d8a4270d5ee192169462' into glitch-soc/merge-upstream
Conflicts: - `app/serializers/initial_state_serializer.rb`: Upstream renamed an initial state parameter, where we had extra ones. Renamed as upstream did. - `app/workers/feed_insert_worker.rb`: Upstream wrapped database query in a block, we had extra database queries because of the DM timeline. Moved everything in the block.main-unfiltered
commit
82eebd0482
|
@ -812,7 +812,6 @@ Style/FrozenStringLiteralComment:
|
||||||
- 'config/initializers/httplog.rb'
|
- 'config/initializers/httplog.rb'
|
||||||
- 'config/initializers/inflections.rb'
|
- 'config/initializers/inflections.rb'
|
||||||
- 'config/initializers/mail_delivery_job.rb'
|
- 'config/initializers/mail_delivery_job.rb'
|
||||||
- 'config/initializers/makara.rb'
|
|
||||||
- 'config/initializers/mime_types.rb'
|
- 'config/initializers/mime_types.rb'
|
||||||
- 'config/initializers/oj.rb'
|
- 'config/initializers/oj.rb'
|
||||||
- 'config/initializers/omniauth.rb'
|
- 'config/initializers/omniauth.rb'
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -11,7 +11,6 @@ gem 'rack', '~> 2.2.7'
|
||||||
|
|
||||||
gem 'haml-rails', '~>2.0'
|
gem 'haml-rails', '~>2.0'
|
||||||
gem 'pg', '~> 1.5'
|
gem 'pg', '~> 1.5'
|
||||||
gem 'makara', '~> 0.5'
|
|
||||||
gem 'pghero'
|
gem 'pghero'
|
||||||
gem 'dotenv-rails', '~> 2.8'
|
gem 'dotenv-rails', '~> 2.8'
|
||||||
|
|
||||||
|
|
|
@ -399,8 +399,6 @@ GEM
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
makara (0.5.1)
|
|
||||||
activerecord (>= 5.2.0)
|
|
||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
|
@ -815,7 +813,6 @@ DEPENDENCIES
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
makara (~> 0.5)
|
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
mime-types (~> 3.4.1)
|
mime-types (~> 3.4.1)
|
||||||
|
|
|
@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
|
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @statuses,
|
render json: @statuses,
|
||||||
each_serializer: REST::StatusSerializer,
|
each_serializer: REST::StatusSerializer,
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
relationships: @relationships,
|
||||||
status: account_home_feed.regenerating? ? 206 : 200
|
status: account_home_feed.regenerating? ? 206 : 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
export const ALERT_NOOP = 'ALERT_NOOP';
|
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||||
|
|
||||||
export function dismissAlert(alert) {
|
export const dismissAlert = alert => ({
|
||||||
return {
|
|
||||||
type: ALERT_DISMISS,
|
type: ALERT_DISMISS,
|
||||||
alert,
|
alert,
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAlert() {
|
export const clearAlert = () => ({
|
||||||
return {
|
|
||||||
type: ALERT_CLEAR,
|
type: ALERT_CLEAR,
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
|
export const showAlert = alert => ({
|
||||||
return {
|
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
alert,
|
||||||
message,
|
});
|
||||||
message_values,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showAlertForError(error, skipNotFound = false) {
|
export const showAlertForError = (error, skipNotFound = false) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText, headers } = error.response;
|
const { data, status, statusText, headers } = error.response;
|
||||||
|
|
||||||
if (skipNotFound && (status === 404 || status === 410)) {
|
|
||||||
// Skip these errors as they are reflected in the UI
|
// Skip these errors as they are reflected in the UI
|
||||||
|
if (skipNotFound && (status === 404 || status === 410)) {
|
||||||
return { type: ALERT_NOOP };
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limit errors
|
||||||
if (status === 429 && headers['x-ratelimit-reset']) {
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
const reset_date = new Date(headers['x-ratelimit-reset']);
|
return showAlert({
|
||||||
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
|
title: messages.rateLimitedTitle,
|
||||||
|
message: messages.rateLimitedMessage,
|
||||||
|
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = statusText;
|
return showAlert({
|
||||||
let title = `${status}`;
|
title: `${status}`,
|
||||||
|
message: data.error || statusText,
|
||||||
if (data.error) {
|
});
|
||||||
message = data.error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return showAlert(title, message);
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return showAlert();
|
|
||||||
}
|
return showAlert({
|
||||||
|
title: messages.unexpectedTitle,
|
||||||
|
message: messages.unexpectedMessage,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||||
|
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
|
@ -242,6 +244,13 @@ export function submitCompose(routerHistory) {
|
||||||
}
|
}
|
||||||
insertIfOnline(`account:${response.data.account.id}`);
|
insertIfOnline(`account:${response.data.account.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(showAlert({
|
||||||
|
message: messages.published,
|
||||||
|
action: messages.open,
|
||||||
|
dismissAfter: 10000,
|
||||||
|
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||||
|
}));
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
});
|
});
|
||||||
|
@ -274,15 +283,16 @@ export function uploadCompose(files) {
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||||
const progress = new Array(files.length).fill(0);
|
const progress = new Array(files.length).fill(0);
|
||||||
|
|
||||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||||
|
|
||||||
if (files.length + media.size + pending > uploadLimit) {
|
if (files.length + media.size + pending > uploadLimit) {
|
||||||
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
dispatch(showAlert({ message: messages.uploadErrorLimit }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getState().getIn(['compose', 'poll'])) {
|
if (getState().getIn(['compose', 'poll'])) {
|
||||||
dispatch(showAlert(undefined, messages.uploadErrorPoll));
|
dispatch(showAlert({ message: messages.uploadErrorPoll }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -237,7 +237,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
|
|
||||||
const anonymousAccess = !signedIn;
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
|
@ -263,6 +262,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||||
|
@ -331,6 +331,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let replyIcon;
|
let replyIcon;
|
||||||
let replyTitle;
|
let replyTitle;
|
||||||
|
@ -371,7 +372,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<div className='status__action-bar__dropdown'>
|
<div className='status__action-bar__dropdown'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
disabled={anonymousAccess}
|
|
||||||
status={status}
|
status={status}
|
||||||
items={menu}
|
items={menu}
|
||||||
icon='ellipsis-h'
|
icon='ellipsis-h'
|
||||||
|
|
|
@ -290,7 +290,6 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
||||||
menu.push(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('share' in navigator) {
|
if ('share' in navigator) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import Search from 'mastodon/features/compose/containers/search_container';
|
import Search from 'mastodon/features/compose/containers/search_container';
|
||||||
import { showTrends } from 'mastodon/initial_state';
|
import { trendsEnabled } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import Links from './links';
|
import Links from './links';
|
||||||
import SearchResults from './results';
|
import SearchResults from './results';
|
||||||
|
@ -26,7 +26,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
layout: state.getIn(['meta', 'layout']),
|
layout: state.getIn(['meta', 'layout']),
|
||||||
isSearching: state.getIn(['search', 'submitted']) || !showTrends,
|
isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
class Explore extends PureComponent {
|
class Explore extends PureComponent {
|
||||||
|
|
|
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changePushNotifications(path.slice(1), checked));
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert(undefined, messages.permissionDenied));
|
dispatch(showAlert({ message: messages.permissionDenied }));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert(undefined, messages.permissionDenied));
|
dispatch(showAlert({ message: messages.permissionDenied }));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -195,21 +195,23 @@ class ActionBar extends PureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && isRemote) {
|
||||||
if (isRemote) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
|
|
||||||
if ('share' in navigator) {
|
if (publicStatus && 'share' in navigator) {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
menu.push(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
menu.push(null);
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
if (pinnableStatus) {
|
if (pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
|
@ -263,6 +265,7 @@ class ActionBar extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let replyIcon;
|
let replyIcon;
|
||||||
if (status.get('in_reply_to_id', null) === null) {
|
if (status.get('in_reply_to_id', null) === null) {
|
||||||
|
@ -292,7 +295,7 @@ class ActionBar extends PureComponent {
|
||||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||||
|
|
||||||
<div className='detailed-status__action-bar-dropdown'>
|
<div className='detailed-status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
import NavigationPortal from 'mastodon/components/navigation_portal';
|
import NavigationPortal from 'mastodon/components/navigation_portal';
|
||||||
import { timelinePreview, showTrends } from 'mastodon/initial_state';
|
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
import ColumnLink from './column_link';
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
import DisabledAccountBanner from './disabled_account_banner';
|
||||||
|
@ -65,7 +65,7 @@ class NavigationPanel extends Component {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showTrends ? (
|
{trendsEnabled ? (
|
||||||
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
|
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
|
||||||
) : (
|
) : (
|
||||||
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
|
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
|
||||||
|
|
|
@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification';
|
||||||
import { dismissAlert } from '../../../actions/alerts';
|
import { dismissAlert } from '../../../actions/alerts';
|
||||||
import { getAlerts } from '../../../selectors';
|
import { getAlerts } from '../../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { intl }) => {
|
const formatIfNeeded = (intl, message, values) => {
|
||||||
const notifications = getAlerts(state);
|
if (typeof message === 'object') {
|
||||||
|
return intl.formatMessage(message, values);
|
||||||
notifications.forEach(notification => ['title', 'message'].forEach(key => {
|
|
||||||
const value = notification[key];
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
return { notifications };
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
return {
|
notifications: getAlerts(state).map(alert => ({
|
||||||
onDismiss: alert => {
|
...alert,
|
||||||
|
action: formatIfNeeded(intl, alert.action, alert.values),
|
||||||
|
title: formatIfNeeded(intl, alert.title, alert.values),
|
||||||
|
message: formatIfNeeded(intl, alert.message, alert.values),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onDismiss (alert) {
|
||||||
dispatch(dismissAlert(alert));
|
dispatch(dismissAlert(alert));
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
|
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
|
@ -170,7 +170,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
}
|
}
|
||||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||||
} else if (showTrends && trendsAsLanding) {
|
} else if (trendsEnabled && trendsAsLanding) {
|
||||||
redirect = <Redirect from='/' to='/explore' exact />;
|
redirect = <Redirect from='/' to='/explore' exact />;
|
||||||
} else {
|
} else {
|
||||||
redirect = <Redirect from='/' to='/about' exact />;
|
redirect = <Redirect from='/' to='/about' exact />;
|
||||||
|
|
|
@ -69,12 +69,13 @@
|
||||||
* @property {boolean} reduce_motion
|
* @property {boolean} reduce_motion
|
||||||
* @property {string} repository
|
* @property {string} repository
|
||||||
* @property {boolean} search_enabled
|
* @property {boolean} search_enabled
|
||||||
|
* @property {boolean} trends_enabled
|
||||||
* @property {boolean} single_user_mode
|
* @property {boolean} single_user_mode
|
||||||
* @property {string} source_url
|
* @property {string} source_url
|
||||||
* @property {string} streaming_api_base_url
|
* @property {string} streaming_api_base_url
|
||||||
* @property {boolean} timeline_preview
|
* @property {boolean} timeline_preview
|
||||||
* @property {string} title
|
* @property {string} title
|
||||||
* @property {boolean} trends
|
* @property {boolean} show_trends
|
||||||
* @property {boolean} trends_as_landing_page
|
* @property {boolean} trends_as_landing_page
|
||||||
* @property {boolean} unfollow_modal
|
* @property {boolean} unfollow_modal
|
||||||
* @property {boolean} use_blurhash
|
* @property {boolean} use_blurhash
|
||||||
|
@ -122,7 +123,8 @@ export const reduceMotion = getMeta('reduce_motion');
|
||||||
export const registrationsOpen = getMeta('registrations_open');
|
export const registrationsOpen = getMeta('registrations_open');
|
||||||
export const repository = getMeta('repository');
|
export const repository = getMeta('repository');
|
||||||
export const searchEnabled = getMeta('search_enabled');
|
export const searchEnabled = getMeta('search_enabled');
|
||||||
export const showTrends = getMeta('trends');
|
export const trendsEnabled = getMeta('trends_enabled');
|
||||||
|
export const showTrends = getMeta('show_trends');
|
||||||
export const singleUserMode = getMeta('single_user_mode');
|
export const singleUserMode = getMeta('single_user_mode');
|
||||||
export const source_url = getMeta('source_url');
|
export const source_url = getMeta('source_url');
|
||||||
export const timelinePreview = getMeta('timeline_preview');
|
export const timelinePreview = getMeta('timeline_preview');
|
||||||
|
|
|
@ -135,6 +135,8 @@
|
||||||
"community.column_settings.remote_only": "Remote only",
|
"community.column_settings.remote_only": "Remote only",
|
||||||
"compose.language.change": "Change language",
|
"compose.language.change": "Change language",
|
||||||
"compose.language.search": "Search languages...",
|
"compose.language.search": "Search languages...",
|
||||||
|
"compose.published.body": "Post published.",
|
||||||
|
"compose.published.open": "Open",
|
||||||
"compose_form.direct_message_warning_learn_more": "Learn more",
|
"compose_form.direct_message_warning_learn_more": "Learn more",
|
||||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
||||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALERT_SHOW,
|
ALERT_SHOW,
|
||||||
|
@ -8,17 +8,20 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableList([]);
|
const initialState = ImmutableList([]);
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
const addAlert = (state, alert) =>
|
||||||
|
state.push({
|
||||||
|
key: id++,
|
||||||
|
...alert,
|
||||||
|
});
|
||||||
|
|
||||||
export default function alerts(state = initialState, action) {
|
export default function alerts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ALERT_SHOW:
|
case ALERT_SHOW:
|
||||||
return state.push(ImmutableMap({
|
return addAlert(state, action.alert);
|
||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
|
||||||
title: action.title,
|
|
||||||
message: action.message,
|
|
||||||
message_values: action.message_values,
|
|
||||||
}));
|
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.key === action.alert.key);
|
||||||
case ALERT_CLEAR:
|
case ALERT_CLEAR:
|
||||||
return state.clear();
|
return state.clear();
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -26,7 +26,6 @@ import lists from './lists';
|
||||||
import markers from './markers';
|
import markers from './markers';
|
||||||
import media_attachments from './media_attachments';
|
import media_attachments from './media_attachments';
|
||||||
import meta from './meta';
|
import meta from './meta';
|
||||||
import { missedUpdatesReducer } from './missed_updates';
|
|
||||||
import { modalReducer } from './modal';
|
import { modalReducer } from './modal';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
@ -82,7 +81,6 @@ const reducers = {
|
||||||
suggestions,
|
suggestions,
|
||||||
polls,
|
polls,
|
||||||
trends,
|
trends,
|
||||||
missed_updates: missedUpdatesReducer,
|
|
||||||
markers,
|
markers,
|
||||||
picture_in_picture,
|
picture_in_picture,
|
||||||
history,
|
history,
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { Record } from 'immutable';
|
|
||||||
|
|
||||||
import type { Action } from 'redux';
|
|
||||||
|
|
||||||
import { focusApp, unfocusApp } from '../actions/app';
|
|
||||||
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
|
|
||||||
|
|
||||||
interface MissedUpdatesState {
|
|
||||||
focused: boolean;
|
|
||||||
unread: number;
|
|
||||||
}
|
|
||||||
const initialState = Record<MissedUpdatesState>({
|
|
||||||
focused: true,
|
|
||||||
unread: 0,
|
|
||||||
})();
|
|
||||||
|
|
||||||
export function missedUpdatesReducer(
|
|
||||||
state = initialState,
|
|
||||||
action: Action<string>
|
|
||||||
) {
|
|
||||||
switch (action.type) {
|
|
||||||
case focusApp.type:
|
|
||||||
return state.set('focused', true).set('unread', 0);
|
|
||||||
case unfocusApp.type:
|
|
||||||
return state.set('focused', false);
|
|
||||||
case NOTIFICATIONS_UPDATE:
|
|
||||||
return state.get('focused')
|
|
||||||
? state
|
|
||||||
: state.update('unread', (x) => x + 1);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlertsBase = state => state.get('alerts');
|
const ALERT_DEFAULTS = {
|
||||||
|
|
||||||
export const getAlerts = createSelector([getAlertsBase], (base) => {
|
|
||||||
let arr = [];
|
|
||||||
|
|
||||||
base.forEach(item => {
|
|
||||||
arr.push({
|
|
||||||
message: item.get('message'),
|
|
||||||
message_values: item.get('message_values'),
|
|
||||||
title: item.get('title'),
|
|
||||||
key: item.get('key'),
|
|
||||||
dismissAfter: 5000,
|
dismissAfter: 5000,
|
||||||
barStyle: {
|
style: false,
|
||||||
zIndex: 200,
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return arr;
|
export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
|
||||||
});
|
alerts.map(item => ({
|
||||||
|
...ALERT_DEFAULTS,
|
||||||
|
...item,
|
||||||
|
})).toArray());
|
||||||
|
|
||||||
export const makeGetNotification = () => createSelector([
|
export const makeGetNotification = () => createSelector([
|
||||||
(_, base) => base,
|
(_, base) => base,
|
||||||
|
|
|
@ -9077,3 +9077,62 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-list {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
inset-inline-start: -100%;
|
||||||
|
width: auto;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0;
|
||||||
|
color: $primary-text-color;
|
||||||
|
background: rgba($black, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(lighten($ui-base-color, 4%), 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
|
||||||
|
0 4px 6px -4px rgba($base-shadow-color, 0.25);
|
||||||
|
cursor: default;
|
||||||
|
transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
|
||||||
|
transform: translateZ(0);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 21px;
|
||||||
|
|
||||||
|
&.notification-bar-active {
|
||||||
|
inset-inline-start: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-title {
|
||||||
|
margin-inline-end: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-title,
|
||||||
|
.notification-bar-action {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-action {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-inline-start: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $highlight-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: rgba($ui-base-color, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,13 +4,14 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
return if skip_reports?
|
return if skip_reports?
|
||||||
|
|
||||||
target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }.select(&:local?)
|
target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }
|
||||||
target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.select(&:local?).group_by(&:account_id)
|
target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.group_by(&:account_id)
|
||||||
|
|
||||||
target_accounts.each do |target_account|
|
target_accounts.each do |target_account|
|
||||||
target_statuses = target_statuses_by_account[target_account.id]
|
target_statuses = target_statuses_by_account[target_account.id]
|
||||||
|
replied_to_accounts = Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id))
|
||||||
|
|
||||||
next if target_account.suspended?
|
next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?)
|
||||||
|
|
||||||
ReportService.new.call(
|
ReportService.new.call(
|
||||||
@account,
|
@account,
|
||||||
|
|
|
@ -39,7 +39,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
limited_federation_mode: Rails.configuration.x.whitelist_mode,
|
limited_federation_mode: Rails.configuration.x.whitelist_mode,
|
||||||
mascot: instance_presenter.mascot&.file&.url,
|
mascot: instance_presenter.mascot&.file&.url,
|
||||||
profile_directory: Setting.profile_directory,
|
profile_directory: Setting.profile_directory,
|
||||||
trends: Setting.trends,
|
trends_enabled: Setting.trends,
|
||||||
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
|
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
|
||||||
timeline_preview: Setting.timeline_preview,
|
timeline_preview: Setting.timeline_preview,
|
||||||
activity_api_enabled: Setting.activity_api_enabled,
|
activity_api_enabled: Setting.activity_api_enabled,
|
||||||
|
@ -62,9 +62,9 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
||||||
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
||||||
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
||||||
store[:trends] = Setting.trends && object.current_account.user.setting_trends
|
|
||||||
store[:default_content_type] = object.current_account.user.setting_default_content_type
|
store[:default_content_type] = object.current_account.user.setting_default_content_type
|
||||||
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
|
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
|
||||||
|
store[:show_trends] = Setting.trends && object.current_account.user.setting_trends
|
||||||
store[:crop_images] = object.current_account.user.setting_crop_images
|
store[:crop_images] = object.current_account.user.setting_crop_images
|
||||||
else
|
else
|
||||||
store[:auto_play_gif] = Setting.auto_play_gif
|
store[:auto_play_gif] = Setting.auto_play_gif
|
||||||
|
|
|
@ -45,11 +45,15 @@ class ReportService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward_to_origin!
|
def forward_to_origin!
|
||||||
ActivityPub::DeliveryWorker.perform_async(
|
# Send report to the server where the account originates from
|
||||||
payload,
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, @target_account.inbox_url)
|
||||||
some_local_account.id,
|
|
||||||
@target_account.inbox_url
|
# Send report to servers to which the account was replying to, so they also have a chance to act
|
||||||
)
|
inbox_urls = Account.remote.where(id: Status.where(id: reported_status_ids).where.not(in_reply_to_account_id: nil).select(:in_reply_to_account_id)).inboxes - [@target_account.inbox_url]
|
||||||
|
|
||||||
|
inbox_urls.each do |inbox_url|
|
||||||
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward?
|
def forward?
|
||||||
|
|
|
@ -4,6 +4,7 @@ class FeedInsertWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(status_id, id, type = 'home', options = {})
|
def perform(status_id, id, type = 'home', options = {})
|
||||||
|
ApplicationRecord.connected_to(role: :primary) do
|
||||||
@type = type.to_sym
|
@type = type.to_sym
|
||||||
@status = Status.find(status_id)
|
@status = Status.find(status_id)
|
||||||
@options = options.symbolize_keys
|
@options = options.symbolize_keys
|
||||||
|
@ -17,8 +18,11 @@ class FeedInsertWorker
|
||||||
when :direct
|
when :direct
|
||||||
@account = Account.find(id)
|
@account = Account.find(id)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
|
||||||
check_and_insert
|
check_and_insert
|
||||||
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,7 @@ test:
|
||||||
port: <%= ENV['DB_PORT'] %>
|
port: <%= ENV['DB_PORT'] %>
|
||||||
|
|
||||||
production:
|
production:
|
||||||
|
primary:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
|
database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
|
||||||
username: <%= ENV['DB_USER'] || 'mastodon' %>
|
username: <%= ENV['DB_USER'] || 'mastodon' %>
|
||||||
|
@ -34,3 +35,12 @@ production:
|
||||||
host: <%= ENV['DB_HOST'] || 'localhost' %>
|
host: <%= ENV['DB_HOST'] || 'localhost' %>
|
||||||
port: <%= ENV['DB_PORT'] || 5432 %>
|
port: <%= ENV['DB_PORT'] || 5432 %>
|
||||||
prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
|
prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
|
||||||
|
read:
|
||||||
|
<<: *default
|
||||||
|
database: <%= ENV['DB_REPLICA_NAME'] ||ENV['DB_NAME'] || 'mastodon_production' %>
|
||||||
|
username: <%= ENV['DB_REPLICA_USER'] ||ENV['DB_USER'] || 'mastodon' %>
|
||||||
|
password: <%= (ENV['DB_REPLICA_PASS'] || ENV['DB_PASS'] || '').to_json %>
|
||||||
|
host: <%= ENV['DB_REPLICA_HOST'] ||ENV['DB_HOST'] || 'localhost' %>
|
||||||
|
port: <%= ENV['DB_REPLICA_PORT'] ||ENV['DB_PORT'] || 5432 %>
|
||||||
|
prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
|
||||||
|
replica: true
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
Makara::Cookie::DEFAULT_OPTIONS[:same_site] = :lax
|
|
||||||
Makara::Cookie::DEFAULT_OPTIONS[:secure] = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'
|
|
|
@ -17,24 +17,45 @@ RSpec.describe ReportService, type: :service do
|
||||||
|
|
||||||
context 'with a remote account' do
|
context 'with a remote account' do
|
||||||
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
|
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
|
||||||
|
let(:forward) { false }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when forward is true' do
|
||||||
|
let(:forward) { true }
|
||||||
|
|
||||||
it 'sends ActivityPub payload when forward is true' do
|
it 'sends ActivityPub payload when forward is true' do
|
||||||
subject.call(source_account, remote_account, forward: true)
|
subject.call(source_account, remote_account, forward: forward)
|
||||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
|
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not send anything when forward is false' do
|
it 'has an uri' do
|
||||||
subject.call(source_account, remote_account, forward: false)
|
report = subject.call(source_account, remote_account, forward: forward)
|
||||||
expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
|
expect(report.uri).to_not be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has an uri' do
|
context 'when reporting a reply' do
|
||||||
report = subject.call(source_account, remote_account, forward: true)
|
let(:remote_thread_account) { Fabricate(:account, domain: 'foo.com', protocol: :activitypub, inbox_url: 'http://foo.com/inbox') }
|
||||||
expect(report.uri).to_not be_nil
|
let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:post, 'http://foo.com/inbox').to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends ActivityPub payload to the author of the replied-to post' do
|
||||||
|
subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward)
|
||||||
|
expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when forward is false' do
|
||||||
|
it 'does not send anything' do
|
||||||
|
subject.call(source_account, remote_account, forward: forward)
|
||||||
|
expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue