mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 07:49:29 +00:00
Compare commits
48 Commits
with-masto
...
autocollap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0298d66f8 | ||
|
|
73bf0ea7d1 | ||
|
|
254b74c71f | ||
|
|
870d71b78b | ||
|
|
781105293c | ||
|
|
0cb329f63a | ||
|
|
0129f5eada | ||
|
|
656f5b6f87 | ||
|
|
22da775a85 | ||
|
|
dd28b94cf0 | ||
|
|
d556be2968 | ||
|
|
4f337c020a | ||
|
|
02f7f3619a | ||
|
|
a2612d0d38 | ||
|
|
31814ddda0 | ||
|
|
42f2045c21 | ||
|
|
5f0268ab84 | ||
|
|
20fee786b1 | ||
|
|
74777599cf | ||
|
|
1ba3725473 | ||
|
|
e40fe4092d | ||
|
|
d9485e6497 | ||
|
|
d5c8ebe205 | ||
|
|
d03b48cea0 | ||
|
|
9226257a1b | ||
|
|
641f90e73a | ||
|
|
f5a3283976 | ||
|
|
8410d33b49 | ||
|
|
a76b024228 | ||
|
|
3db80f75a6 | ||
|
|
af8f06413e | ||
|
|
1a60445a5f | ||
|
|
4c84513e04 | ||
|
|
4b68e82a19 | ||
|
|
19826774f0 | ||
|
|
ad86c86fa8 | ||
|
|
670e6a33f8 | ||
|
|
cd04e3df58 | ||
|
|
4a64181461 | ||
|
|
2e03a10059 | ||
|
|
4fa2f7e82d | ||
|
|
b4b657eb1d | ||
|
|
693c66dfde | ||
|
|
a4851100fd | ||
|
|
9f609bc94e | ||
|
|
603cf02b70 | ||
|
|
4745d6eeca | ||
|
|
9093e2de7a |
@@ -22,6 +22,14 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @custom_emoji.update(resource_params)
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
||||
else
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_emoji.destroy
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
||||
@@ -36,7 +44,7 @@ module Admin
|
||||
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||
end
|
||||
|
||||
redirect_to admin_custom_emojis_path(params[:page])
|
||||
redirect_to admin_custom_emojis_path(page: params[:page])
|
||||
end
|
||||
|
||||
def enable
|
||||
@@ -56,7 +64,7 @@ module Admin
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:custom_emoji).permit(:shortcode, :image)
|
||||
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
|
||||
end
|
||||
|
||||
def filtered_custom_emojis
|
||||
|
||||
60
app/controllers/api/v1/timelines/direct_controller.rb
Normal file
60
app/controllers/api/v1/timelines/direct_controller.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::DirectController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read }, only: [:show]
|
||||
before_action :require_user!, only: [:show]
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_statuses
|
||||
cached_direct_statuses
|
||||
end
|
||||
|
||||
def cached_direct_statuses
|
||||
cache_collection direct_statuses, Status
|
||||
end
|
||||
|
||||
def direct_statuses
|
||||
direct_timeline_statuses.paginate_by_max_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def direct_timeline_statuses
|
||||
Status.as_direct_timeline(current_account)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.permit(:local, :limit).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
||||
64
app/controllers/settings/keyword_mutes_controller.rb
Normal file
64
app/controllers/settings/keyword_mutes_controller.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::KeywordMutesController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@keyword_mutes = paginated_keyword_mutes_for_account
|
||||
end
|
||||
|
||||
def new
|
||||
@keyword_mute = keyword_mutes_for_account.build
|
||||
end
|
||||
|
||||
def create
|
||||
@keyword_mute = keyword_mutes_for_account.create(keyword_mute_params)
|
||||
|
||||
if @keyword_mute.persisted?
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @keyword_mute.update(keyword_mute_params)
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@keyword_mute.destroy!
|
||||
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
keyword_mutes_for_account.delete_all
|
||||
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyword_mutes_for_account
|
||||
Glitch::KeywordMute.where(account: current_account)
|
||||
end
|
||||
|
||||
def load_keyword_mute
|
||||
@keyword_mute = keyword_mutes_for_account.find(params[:id])
|
||||
end
|
||||
|
||||
def keyword_mute_params
|
||||
params.require(:keyword_mute).permit(:keyword, :whole_word)
|
||||
end
|
||||
|
||||
def paginated_keyword_mutes_for_account
|
||||
keyword_mutes_for_account.order(:keyword).page params[:page]
|
||||
end
|
||||
end
|
||||
@@ -9,6 +9,10 @@ module JsonLdHelper
|
||||
value.is_a?(Array) ? value.first : value
|
||||
end
|
||||
|
||||
def as_array(value)
|
||||
value.is_a?(Array) ? value : [value]
|
||||
end
|
||||
|
||||
def value_or_id(value)
|
||||
value.is_a?(String) || value.nil? ? value : value['id']
|
||||
end
|
||||
|
||||
2
app/helpers/settings/keyword_mutes_helper.rb
Normal file
2
app/helpers/settings/keyword_mutes_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module Settings::KeywordMutesHelper
|
||||
end
|
||||
@@ -124,6 +124,16 @@ export default class LocalSettingsPage extends React.PureComponent {
|
||||
>
|
||||
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
|
||||
</LocalSettingsPageItem>
|
||||
<LocalSettingsPageItem
|
||||
settings={settings}
|
||||
item={['collapsed', 'auto', 'reblogs']}
|
||||
id='mastodon-settings--collapsed-auto-reblogs'
|
||||
onChange={onChange}
|
||||
dependsOn={[['collapsed', 'enabled']]}
|
||||
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
||||
>
|
||||
<FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' />
|
||||
</LocalSettingsPageItem>
|
||||
<LocalSettingsPageItem
|
||||
settings={settings}
|
||||
item={['collapsed', 'auto', 'replies']}
|
||||
|
||||
@@ -287,6 +287,7 @@ properly and our intersection observer is good to go.
|
||||
muted,
|
||||
id,
|
||||
intersectionObserverWrapper,
|
||||
prepend,
|
||||
} = this.props;
|
||||
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
|
||||
|
||||
@@ -299,6 +300,9 @@ properly and our intersection observer is good to go.
|
||||
node.clientHeight > (
|
||||
status.get('media_attachments').size && !muted ? 650 : 400
|
||||
)
|
||||
) || (
|
||||
autoCollapseSettings.get('reblogs') &&
|
||||
prepend === 'reblogged_by'
|
||||
) || (
|
||||
autoCollapseSettings.get('replies') &&
|
||||
status.get('in_reply_to_id', null) !== null
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"settings.auto_collapse_lengthy": "Lengthy toots",
|
||||
"settings.auto_collapse_media": "Toots with media",
|
||||
"settings.auto_collapse_notifications": "Notifications",
|
||||
"settings.auto_collapse_reblogs": "Boosts",
|
||||
"settings.auto_collapse_replies": "Replies",
|
||||
"settings.close": "Close",
|
||||
"settings.collapsed_statuses": "Collapsed toots",
|
||||
|
||||
@@ -59,6 +59,7 @@ const initialState = ImmutableMap({
|
||||
all : false,
|
||||
notifications : true,
|
||||
lengthy : true,
|
||||
reblogs : false,
|
||||
replies : false,
|
||||
media : false,
|
||||
}),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
refreshHomeTimeline,
|
||||
refreshCommunityTimeline,
|
||||
refreshPublicTimeline,
|
||||
refreshDirectTimeline,
|
||||
} from './timelines';
|
||||
|
||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||
@@ -133,6 +134,8 @@ export function submitCompose() {
|
||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||
insertOrRefresh('community', refreshCommunityTimeline);
|
||||
insertOrRefresh('public', refreshPublicTimeline);
|
||||
} else if (response.data.visibility === 'direct') {
|
||||
insertOrRefresh('direct', refreshDirectTimeline);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
|
||||
@@ -92,3 +92,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
|
||||
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
|
||||
export const connectPublicStream = () => connectTimelineStream('public', 'public');
|
||||
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
|
||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
|
||||
@@ -115,6 +115,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
|
||||
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
|
||||
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
||||
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
|
||||
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
||||
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
||||
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
||||
@@ -155,6 +156,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
||||
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
|
||||
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
||||
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
|
||||
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
||||
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
||||
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
||||
|
||||
@@ -175,7 +175,9 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
<div className={wrapperClassName}>
|
||||
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
{title}
|
||||
<span className='column-header__title'>
|
||||
{title}
|
||||
</span>
|
||||
<div className='column-header__buttons'>
|
||||
{backButton}
|
||||
{ notifCleaning ? (
|
||||
|
||||
@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
|
||||
|
||||
const getCustomEmojis = createSelector([
|
||||
state => state.get('custom_emojis'),
|
||||
], emojis => emojis.sort((a, b) => {
|
||||
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
|
||||
const aShort = a.get('shortcode').toLowerCase();
|
||||
const bShort = b.get('shortcode').toLowerCase();
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../../community_timeline/components/column_settings';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'direct']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['direct', ...key], checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||
107
app/javascript/mastodon/features/direct_timeline/index.js
Normal file
107
app/javascript/mastodon/features/direct_timeline/index.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
refreshDirectTimeline,
|
||||
expandDirectTimeline,
|
||||
} from '../../actions/timelines';
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { connectDirectStream } from '../../actions/streaming';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
@injectIntl
|
||||
export default class DirectTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('DIRECT', {}));
|
||||
}
|
||||
}
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(refreshDirectTimeline());
|
||||
this.disconnect = dispatch(connectDirectStream());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.props.dispatch(expandDirectTimeline());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
<Column ref={this.setRef}>
|
||||
<ColumnHeader
|
||||
icon='envelope'
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`direct_timeline-${columnId}`}
|
||||
timelineId='direct'
|
||||
loadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const messages = defineMessages({
|
||||
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
|
||||
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
|
||||
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
@@ -78,18 +79,22 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
navItems = navItems.concat([
|
||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
||||
]);
|
||||
|
||||
if (me.get('locked')) {
|
||||
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
|
||||
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
|
||||
}
|
||||
|
||||
navItems = navItems.concat([
|
||||
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
||||
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
|
||||
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
||||
]);
|
||||
|
||||
if (me.get('locked')) {
|
||||
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
navItems = navItems.concat([
|
||||
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
||||
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
|
||||
import ColumnLoading from './column_loading';
|
||||
import DrawerLoading from './drawer_loading';
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
|
||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from '../../ui/util/async-components';
|
||||
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import { scrollRight } from '../../../scroll';
|
||||
@@ -23,6 +23,7 @@ const componentMap = {
|
||||
'PUBLIC': PublicTimeline,
|
||||
'COMMUNITY': CommunityTimeline,
|
||||
'HASHTAG': HashtagTimeline,
|
||||
'DIRECT': DirectTimeline,
|
||||
'FAVOURITES': FavouritedStatuses,
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Following,
|
||||
Reblogs,
|
||||
Favourites,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
Notifications,
|
||||
FollowRequests,
|
||||
@@ -71,6 +72,7 @@ const keyMap = {
|
||||
goToNotifications: 'g n',
|
||||
goToLocal: 'g l',
|
||||
goToFederated: 'g t',
|
||||
goToDirect: 'g d',
|
||||
goToStart: 'g s',
|
||||
goToFavourites: 'g f',
|
||||
goToPinned: 'g p',
|
||||
@@ -302,6 +304,10 @@ export default class UI extends React.Component {
|
||||
this.context.router.history.push('/timelines/public');
|
||||
}
|
||||
|
||||
handleHotkeyGoToDirect = () => {
|
||||
this.context.router.history.push('/timelines/direct');
|
||||
}
|
||||
|
||||
handleHotkeyGoToStart = () => {
|
||||
this.context.router.history.push('/getting-started');
|
||||
}
|
||||
@@ -357,6 +363,7 @@ export default class UI extends React.Component {
|
||||
goToNotifications: this.handleHotkeyGoToNotifications,
|
||||
goToLocal: this.handleHotkeyGoToLocal,
|
||||
goToFederated: this.handleHotkeyGoToFederated,
|
||||
goToDirect: this.handleHotkeyGoToDirect,
|
||||
goToStart: this.handleHotkeyGoToStart,
|
||||
goToFavourites: this.handleHotkeyGoToFavourites,
|
||||
goToPinned: this.handleHotkeyGoToPinned,
|
||||
@@ -377,6 +384,7 @@ export default class UI extends React.Component {
|
||||
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||
|
||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
||||
|
||||
@@ -26,6 +26,10 @@ export function HashtagTimeline () {
|
||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
||||
}
|
||||
|
||||
export function DirectTimeline() {
|
||||
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
}
|
||||
|
||||
@@ -755,6 +755,19 @@
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/compose/index.json"
|
||||
},
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
"defaultMessage": "Direct messages",
|
||||
"id": "column.direct"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||
"id": "empty_column.direct"
|
||||
}
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/direct_timeline/index.json"
|
||||
},
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
@@ -816,6 +829,10 @@
|
||||
"defaultMessage": "Local timeline",
|
||||
"id": "navigation_bar.community_timeline"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Direct messages",
|
||||
"id": "navigation_bar.direct"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Preferences",
|
||||
"id": "navigation_bar.preferences"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"column.blocks": "Blocked users",
|
||||
"column.community": "Local timeline",
|
||||
"column.direct": "Direct messages",
|
||||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.home": "Home",
|
||||
@@ -80,6 +81,7 @@
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"emoji_button.travel": "Travel & Places",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
|
||||
"empty_column.home.public_timeline": "the public timeline",
|
||||
@@ -106,6 +108,7 @@
|
||||
"missing_indicator.label": "Not found",
|
||||
"navigation_bar.blocks": "Blocked users",
|
||||
"navigation_bar.community_timeline": "Local timeline",
|
||||
"navigation_bar.direct": "Direct messages",
|
||||
"navigation_bar.edit_profile": "Edit profile",
|
||||
"navigation_bar.favourites": "Favourites",
|
||||
"navigation_bar.follow_requests": "Follow requests",
|
||||
|
||||
@@ -1,221 +1,221 @@
|
||||
{
|
||||
"account.block": "Bloki @{name}",
|
||||
"account.block_domain": "Hide everything from {domain}",
|
||||
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
|
||||
"account.block_domain": "Kaŝi ĉion el {domain}",
|
||||
"account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
|
||||
"account.edit_profile": "Redakti la profilon",
|
||||
"account.follow": "Sekvi",
|
||||
"account.followers": "Sekvantoj",
|
||||
"account.follows": "Sekvatoj",
|
||||
"account.follows_you": "Sekvas vin",
|
||||
"account.media": "Media",
|
||||
"account.media": "Sonbildaĵoj",
|
||||
"account.mention": "Mencii @{name}",
|
||||
"account.mute": "Mute @{name}",
|
||||
"account.mute": "Silentigi @{name}",
|
||||
"account.posts": "Mesaĝoj",
|
||||
"account.report": "Report @{name}",
|
||||
"account.report": "Signali @{name}",
|
||||
"account.requested": "Atendas aprobon",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.share": "Diskonigi la profilon de @{name}",
|
||||
"account.unblock": "Malbloki @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unfollow": "Malsekvi",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.view_full_profile": "View full profile",
|
||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"column.blocks": "Blocked users",
|
||||
"account.unblock_domain": "Malkaŝi {domain}",
|
||||
"account.unfollow": "Ne plus sekvi",
|
||||
"account.unmute": "Malsilentigi @{name}",
|
||||
"account.view_full_profile": "Vidi plenan profilon",
|
||||
"boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
|
||||
"bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
|
||||
"bundle_column_error.retry": "Bonvolu reprovi",
|
||||
"bundle_column_error.title": "Reta eraro",
|
||||
"bundle_modal_error.close": "Fermi",
|
||||
"bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
|
||||
"bundle_modal_error.retry": "Bonvolu reprovi",
|
||||
"column.blocks": "Blokitaj uzantoj",
|
||||
"column.community": "Loka tempolinio",
|
||||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.favourites": "Favoritoj",
|
||||
"column.follow_requests": "Abonpetoj",
|
||||
"column.home": "Hejmo",
|
||||
"column.mutes": "Muted users",
|
||||
"column.mutes": "Silentigitaj uzantoj",
|
||||
"column.notifications": "Sciigoj",
|
||||
"column.pins": "Pinned toot",
|
||||
"column.pins": "Alpinglitaj pepoj",
|
||||
"column.public": "Fratara tempolinio",
|
||||
"column_back_button.label": "Reveni",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
"column_header.moveLeft_settings": "Move column to the left",
|
||||
"column_header.moveRight_settings": "Move column to the right",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.show_settings": "Show settings",
|
||||
"column_header.unpin": "Unpin",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"column_subheading.settings": "Settings",
|
||||
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
||||
"compose_form.lock_disclaimer.lock": "locked",
|
||||
"column_header.hide_settings": "Kaŝi agordojn",
|
||||
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
|
||||
"column_header.moveRight_settings": "Movi kolumnon dekstren",
|
||||
"column_header.pin": "Alpingli",
|
||||
"column_header.show_settings": "Malkaŝi agordojn",
|
||||
"column_header.unpin": "Depingli",
|
||||
"column_subheading.navigation": "Navigado",
|
||||
"column_subheading.settings": "Agordoj",
|
||||
"compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
|
||||
"compose_form.lock_disclaimer.lock": "ŝlosita",
|
||||
"compose_form.placeholder": "Pri kio vi pensas?",
|
||||
"compose_form.publish": "Hup",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
|
||||
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
|
||||
"compose_form.spoiler_placeholder": "Content warning",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.block.confirm": "Block",
|
||||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
||||
"confirmations.delete.confirm": "Delete",
|
||||
"confirmations.delete.message": "Are you sure you want to delete this status?",
|
||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||
"confirmations.mute.confirm": "Mute",
|
||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
||||
"embed.preview": "Here is what it will look like:",
|
||||
"emoji_button.activity": "Activity",
|
||||
"emoji_button.custom": "Custom",
|
||||
"emoji_button.flags": "Flags",
|
||||
"emoji_button.food": "Food & Drink",
|
||||
"emoji_button.label": "Insert emoji",
|
||||
"emoji_button.nature": "Nature",
|
||||
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
|
||||
"emoji_button.objects": "Objects",
|
||||
"emoji_button.people": "People",
|
||||
"emoji_button.recent": "Frequently used",
|
||||
"emoji_button.search": "Search...",
|
||||
"emoji_button.search_results": "Search results",
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"emoji_button.travel": "Travel & Places",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
|
||||
"empty_column.home.public_timeline": "the public timeline",
|
||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
|
||||
"follow_request.authorize": "Authorize",
|
||||
"follow_request.reject": "Reject",
|
||||
"getting_started.appsshort": "Apps",
|
||||
"getting_started.faq": "FAQ",
|
||||
"compose_form.spoiler_placeholder": "Skribu tie vian averton",
|
||||
"confirmation_modal.cancel": "Malfari",
|
||||
"confirmations.block.confirm": "Bloki",
|
||||
"confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
|
||||
"confirmations.delete.confirm": "Malaperigi",
|
||||
"confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
|
||||
"confirmations.domain_block.confirm": "Kaŝi la tutan reton",
|
||||
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
|
||||
"confirmations.mute.confirm": "Silentigi",
|
||||
"confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
|
||||
"confirmations.unfollow.confirm": "Ne plu sekvi",
|
||||
"confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
|
||||
"embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
|
||||
"embed.preview": "Ĝi aperos tiel:",
|
||||
"emoji_button.activity": "Aktivecoj",
|
||||
"emoji_button.custom": "Personaj",
|
||||
"emoji_button.flags": "Flagoj",
|
||||
"emoji_button.food": "Manĝi kaj trinki",
|
||||
"emoji_button.label": "Enmeti mieneton",
|
||||
"emoji_button.nature": "Naturo",
|
||||
"emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
|
||||
"emoji_button.objects": "Objektoj",
|
||||
"emoji_button.people": "Homoj",
|
||||
"emoji_button.recent": "Ofte uzataj",
|
||||
"emoji_button.search": "Serĉo…",
|
||||
"emoji_button.search_results": "Rezultatoj de serĉo",
|
||||
"emoji_button.symbols": "Simboloj",
|
||||
"emoji_button.travel": "Vojaĝoj & lokoj",
|
||||
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
|
||||
"empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
|
||||
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
|
||||
"empty_column.home.public_timeline": "la publika tempolinio",
|
||||
"empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
|
||||
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
|
||||
"follow_request.authorize": "Akcepti",
|
||||
"follow_request.reject": "Rifuzi",
|
||||
"getting_started.appsshort": "Aplikaĵoj",
|
||||
"getting_started.faq": "Oftaj demandoj",
|
||||
"getting_started.heading": "Por komenci",
|
||||
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}.",
|
||||
"getting_started.userguide": "User Guide",
|
||||
"home.column_settings.advanced": "Advanced",
|
||||
"home.column_settings.basic": "Basic",
|
||||
"home.column_settings.filter_regex": "Filter out by regular expressions",
|
||||
"home.column_settings.show_reblogs": "Show boosts",
|
||||
"home.column_settings.show_replies": "Show replies",
|
||||
"home.settings": "Column settings",
|
||||
"getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
|
||||
"getting_started.userguide": "Gvidilo de uzo",
|
||||
"home.column_settings.advanced": "Precizaj agordoj",
|
||||
"home.column_settings.basic": "Bazaj agordoj",
|
||||
"home.column_settings.filter_regex": "Forfiltri per regulesprimo",
|
||||
"home.column_settings.show_reblogs": "Montri diskonigojn",
|
||||
"home.column_settings.show_replies": "Montri respondojn",
|
||||
"home.settings": "Agordoj de la kolumno",
|
||||
"lightbox.close": "Fermi",
|
||||
"lightbox.next": "Next",
|
||||
"lightbox.previous": "Previous",
|
||||
"loading_indicator.label": "Ŝarĝanta...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"missing_indicator.label": "Not found",
|
||||
"navigation_bar.blocks": "Blocked users",
|
||||
"lightbox.next": "Malantaŭa",
|
||||
"lightbox.previous": "Antaŭa",
|
||||
"loading_indicator.label": "Ŝarganta…",
|
||||
"media_gallery.toggle_visible": "Baskuli videblecon",
|
||||
"missing_indicator.label": "Ne trovita",
|
||||
"navigation_bar.blocks": "Blokitaj uzantoj",
|
||||
"navigation_bar.community_timeline": "Loka tempolinio",
|
||||
"navigation_bar.edit_profile": "Redakti la profilon",
|
||||
"navigation_bar.favourites": "Favourites",
|
||||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.info": "Extended information",
|
||||
"navigation_bar.favourites": "Favoritaj",
|
||||
"navigation_bar.follow_requests": "Abonpetoj",
|
||||
"navigation_bar.info": "Plia informo",
|
||||
"navigation_bar.logout": "Elsaluti",
|
||||
"navigation_bar.mutes": "Muted users",
|
||||
"navigation_bar.pins": "Pinned toots",
|
||||
"navigation_bar.mutes": "Silentigitaj uzantoj",
|
||||
"navigation_bar.pins": "Alpinglitaj pepoj",
|
||||
"navigation_bar.preferences": "Preferoj",
|
||||
"navigation_bar.public_timeline": "Fratara tempolinio",
|
||||
"notification.favourite": "{name} favoris vian mesaĝon",
|
||||
"notification.follow": "{name} sekvis vin",
|
||||
"notification.mention": "{name} menciis vin",
|
||||
"notification.reblog": "{name} diskonigis vian mesaĝon",
|
||||
"notifications.clear": "Clear notifications",
|
||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||
"notifications.clear": "Forviŝi la sciigojn",
|
||||
"notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
|
||||
"notifications.column_settings.alert": "Retumilaj atentigoj",
|
||||
"notifications.column_settings.favourite": "Favoroj:",
|
||||
"notifications.column_settings.favourite": "Favoritoj:",
|
||||
"notifications.column_settings.follow": "Novaj sekvantoj:",
|
||||
"notifications.column_settings.mention": "Mencioj:",
|
||||
"notifications.column_settings.push": "Push notifications",
|
||||
"notifications.column_settings.push_meta": "This device",
|
||||
"notifications.column_settings.push": "Puŝsciigoj",
|
||||
"notifications.column_settings.push_meta": "Tiu ĉi aparato",
|
||||
"notifications.column_settings.reblog": "Diskonigoj:",
|
||||
"notifications.column_settings.show": "Montri en kolono",
|
||||
"notifications.column_settings.sound": "Play sound",
|
||||
"onboarding.done": "Done",
|
||||
"onboarding.next": "Next",
|
||||
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
|
||||
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
|
||||
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
|
||||
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
|
||||
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
|
||||
"onboarding.page_one.welcome": "Welcome to Mastodon!",
|
||||
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
|
||||
"onboarding.page_six.almost_done": "Almost done...",
|
||||
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
||||
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
|
||||
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
|
||||
"onboarding.page_six.guidelines": "community guidelines",
|
||||
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
|
||||
"onboarding.page_six.various_app": "mobile apps",
|
||||
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
|
||||
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
|
||||
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
|
||||
"onboarding.skip": "Skip",
|
||||
"privacy.change": "Adjust status privacy",
|
||||
"privacy.direct.long": "Post to mentioned users only",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.private.long": "Post to followers only",
|
||||
"privacy.private.short": "Followers-only",
|
||||
"privacy.public.long": "Post to public timelines",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.unlisted.long": "Do not show in public timelines",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"relative_time.days": "{number}d",
|
||||
"notifications.column_settings.sound": "Eligi sonon",
|
||||
"onboarding.done": "Farita",
|
||||
"onboarding.next": "Malantaŭa",
|
||||
"onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.",
|
||||
"onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
|
||||
"onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
|
||||
"onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
|
||||
"onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}",
|
||||
"onboarding.page_one.welcome": "Bonvenon al Mastodono!",
|
||||
"onboarding.page_six.admin": "Via instancestro estas {admin}.",
|
||||
"onboarding.page_six.almost_done": "Estas preskaŭ finita…",
|
||||
"onboarding.page_six.appetoot": "Bonan a‘pepi’ton!",
|
||||
"onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan a‘pepi’ton!",
|
||||
"onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.",
|
||||
"onboarding.page_six.guidelines": "komunreguloj",
|
||||
"onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
|
||||
"onboarding.page_six.various_app": "telefon-aplikaĵoj",
|
||||
"onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
|
||||
"onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.",
|
||||
"onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn (« content warning ») dank' al la piktogramoj malsupre.",
|
||||
"onboarding.skip": "Pasigi",
|
||||
"privacy.change": "Alĝustigi la privateco de la mesaĝo",
|
||||
"privacy.direct.long": "Vidigi nur al la menciitaj personoj",
|
||||
"privacy.direct.short": "Rekta",
|
||||
"privacy.private.long": "Vidigi nur al viaj sekvantoj",
|
||||
"privacy.private.short": "Nursekvanta",
|
||||
"privacy.public.long": "Vidigi en publikaj tempolinioj",
|
||||
"privacy.public.short": "Publika",
|
||||
"privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
|
||||
"privacy.unlisted.short": "Nelistigita",
|
||||
"relative_time.days": "{number}t",
|
||||
"relative_time.hours": "{number}h",
|
||||
"relative_time.just_now": "now",
|
||||
"relative_time.just_now": "nun",
|
||||
"relative_time.minutes": "{number}m",
|
||||
"relative_time.seconds": "{number}s",
|
||||
"reply_indicator.cancel": "Rezigni",
|
||||
"report.placeholder": "Additional comments",
|
||||
"report.submit": "Submit",
|
||||
"report.target": "Reporting",
|
||||
"reply_indicator.cancel": "Malfari",
|
||||
"report.placeholder": "Pliaj komentoj",
|
||||
"report.submit": "Sendi",
|
||||
"report.target": "Signalaĵo",
|
||||
"search.placeholder": "Serĉi",
|
||||
"search_popout.search_format": "Advanced search format",
|
||||
"search_popout.tips.hashtag": "hashtag",
|
||||
"search_popout.tips.status": "status",
|
||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
||||
"search_popout.tips.user": "user",
|
||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
||||
"standalone.public_title": "A look inside...",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
"search_popout.search_format": "Detala serĉo",
|
||||
"search_popout.tips.hashtag": "kradvorto",
|
||||
"search_popout.tips.status": "statkonigo",
|
||||
"search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
|
||||
"search_popout.tips.user": "uzanto",
|
||||
"search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
|
||||
"standalone.public_title": "Rigardeti…",
|
||||
"status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
|
||||
"status.delete": "Forigi",
|
||||
"status.embed": "Embed",
|
||||
"status.embed": "Enmeti",
|
||||
"status.favourite": "Favori",
|
||||
"status.load_more": "Load more",
|
||||
"status.media_hidden": "Media hidden",
|
||||
"status.load_more": "Ŝargi plie",
|
||||
"status.media_hidden": "Sonbildaĵo kaŝita",
|
||||
"status.mention": "Mencii @{name}",
|
||||
"status.more": "More",
|
||||
"status.mute_conversation": "Mute conversation",
|
||||
"status.open": "Expand this status",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.more": "Pli",
|
||||
"status.mute_conversation": "Silentigi konversacion",
|
||||
"status.open": "Disfaldi statkonigon",
|
||||
"status.pin": "Pingli al la profilo",
|
||||
"status.reblog": "Diskonigi",
|
||||
"status.reblogged_by": "{name} diskonigita",
|
||||
"status.reblogged_by": "{name} diskonigis",
|
||||
"status.reply": "Respondi",
|
||||
"status.replyAll": "Reply to thread",
|
||||
"status.report": "Report @{name}",
|
||||
"status.replyAll": "Respondi al la fadeno",
|
||||
"status.report": "Signali @{name}",
|
||||
"status.sensitive_toggle": "Alklaki por vidi",
|
||||
"status.sensitive_warning": "Tikla enhavo",
|
||||
"status.share": "Share",
|
||||
"status.show_less": "Show less",
|
||||
"status.show_more": "Show more",
|
||||
"status.unmute_conversation": "Unmute conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.share": "Diskonigi",
|
||||
"status.show_less": "Refaldi",
|
||||
"status.show_more": "Disfaldi",
|
||||
"status.unmute_conversation": "Malsilentigi konversacion",
|
||||
"status.unpin": "Depingli de profilo",
|
||||
"tabs_bar.compose": "Ekskribi",
|
||||
"tabs_bar.federated_timeline": "Federated",
|
||||
"tabs_bar.federated_timeline": "Federacia tempolinio",
|
||||
"tabs_bar.home": "Hejmo",
|
||||
"tabs_bar.local_timeline": "Local",
|
||||
"tabs_bar.local_timeline": "Loka tempolinio",
|
||||
"tabs_bar.notifications": "Sciigoj",
|
||||
"upload_area.title": "Drag & drop to upload",
|
||||
"upload_button.label": "Aldoni enhavaĵon",
|
||||
"upload_form.description": "Describe for the visually impaired",
|
||||
"upload_area.title": "Algliti por alŝuti",
|
||||
"upload_button.label": "Aldoni sonbildaĵon",
|
||||
"upload_form.description": "Priskribi por la misvidantaj",
|
||||
"upload_form.undo": "Malfari",
|
||||
"upload_progress.label": "Uploading...",
|
||||
"video.close": "Close video",
|
||||
"video.exit_fullscreen": "Exit full screen",
|
||||
"video.expand": "Expand video",
|
||||
"video.fullscreen": "Full screen",
|
||||
"video.hide": "Hide video",
|
||||
"video.mute": "Mute sound",
|
||||
"video.pause": "Pause",
|
||||
"video.play": "Play",
|
||||
"video.unmute": "Unmute sound"
|
||||
"upload_progress.label": "Alŝutanta…",
|
||||
"video.close": "Fermi videon",
|
||||
"video.exit_fullscreen": "Eliri el plenekrano",
|
||||
"video.expand": "Vastigi videon",
|
||||
"video.fullscreen": "Igi plenekrane",
|
||||
"video.hide": "Kaŝi videon",
|
||||
"video.mute": "Silentigi",
|
||||
"video.pause": "Paŭzi",
|
||||
"video.play": "Legi",
|
||||
"video.unmute": "Malsilentigi"
|
||||
}
|
||||
|
||||
@@ -63,20 +63,20 @@
|
||||
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
||||
"confirmations.unfollow.confirm": "Отписаться",
|
||||
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
|
||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
||||
"embed.preview": "Here is what it will look like:",
|
||||
"embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.",
|
||||
"embed.preview": "Так это будет выглядеть:",
|
||||
"emoji_button.activity": "Занятия",
|
||||
"emoji_button.custom": "Custom",
|
||||
"emoji_button.custom": "Собственные",
|
||||
"emoji_button.flags": "Флаги",
|
||||
"emoji_button.food": "Еда и напитки",
|
||||
"emoji_button.label": "Вставить эмодзи",
|
||||
"emoji_button.nature": "Природа",
|
||||
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
|
||||
"emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
|
||||
"emoji_button.objects": "Предметы",
|
||||
"emoji_button.people": "Люди",
|
||||
"emoji_button.recent": "Frequently used",
|
||||
"emoji_button.recent": "Последние",
|
||||
"emoji_button.search": "Найти...",
|
||||
"emoji_button.search_results": "Search results",
|
||||
"emoji_button.search_results": "Результаты поиска",
|
||||
"emoji_button.symbols": "Символы",
|
||||
"emoji_button.travel": "Путешествия",
|
||||
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
|
||||
@@ -159,34 +159,34 @@
|
||||
"privacy.public.short": "Публичный",
|
||||
"privacy.unlisted.long": "Не показывать в лентах",
|
||||
"privacy.unlisted.short": "Скрытый",
|
||||
"relative_time.days": "{number}d",
|
||||
"relative_time.hours": "{number}h",
|
||||
"relative_time.just_now": "now",
|
||||
"relative_time.minutes": "{number}m",
|
||||
"relative_time.seconds": "{number}s",
|
||||
"relative_time.days": "{number}д",
|
||||
"relative_time.hours": "{number}ч",
|
||||
"relative_time.just_now": "только что",
|
||||
"relative_time.minutes": "{number}м",
|
||||
"relative_time.seconds": "{number}с",
|
||||
"reply_indicator.cancel": "Отмена",
|
||||
"report.placeholder": "Комментарий",
|
||||
"report.submit": "Отправить",
|
||||
"report.target": "Жалуемся на",
|
||||
"search.placeholder": "Поиск",
|
||||
"search_popout.search_format": "Advanced search format",
|
||||
"search_popout.tips.hashtag": "hashtag",
|
||||
"search_popout.tips.status": "status",
|
||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
||||
"search_popout.tips.user": "user",
|
||||
"search_popout.search_format": "Продвинутый формат поиска",
|
||||
"search_popout.tips.hashtag": "хэштег",
|
||||
"search_popout.tips.status": "статус",
|
||||
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
|
||||
"search_popout.tips.user": "пользователь",
|
||||
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
|
||||
"standalone.public_title": "A look inside...",
|
||||
"standalone.public_title": "Прямо сейчас",
|
||||
"status.cannot_reblog": "Этот статус не может быть продвинут",
|
||||
"status.delete": "Удалить",
|
||||
"status.embed": "Embed",
|
||||
"status.embed": "Встроить",
|
||||
"status.favourite": "Нравится",
|
||||
"status.load_more": "Показать еще",
|
||||
"status.media_hidden": "Медиаконтент скрыт",
|
||||
"status.mention": "Упомянуть @{name}",
|
||||
"status.more": "More",
|
||||
"status.more": "Больше",
|
||||
"status.mute_conversation": "Заглушить тред",
|
||||
"status.open": "Развернуть статус",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pin": "Закрепить в профиле",
|
||||
"status.reblog": "Продвинуть",
|
||||
"status.reblogged_by": "{name} продвинул(а)",
|
||||
"status.reply": "Ответить",
|
||||
@@ -194,11 +194,11 @@
|
||||
"status.report": "Пожаловаться",
|
||||
"status.sensitive_toggle": "Нажмите для просмотра",
|
||||
"status.sensitive_warning": "Чувствительный контент",
|
||||
"status.share": "Share",
|
||||
"status.share": "Поделиться",
|
||||
"status.show_less": "Свернуть",
|
||||
"status.show_more": "Развернуть",
|
||||
"status.unmute_conversation": "Снять глушение с треда",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.unpin": "Открепить от профиля",
|
||||
"tabs_bar.compose": "Написать",
|
||||
"tabs_bar.federated_timeline": "Глобальная",
|
||||
"tabs_bar.home": "Главная",
|
||||
@@ -206,16 +206,16 @@
|
||||
"tabs_bar.notifications": "Уведомления",
|
||||
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||
"upload_button.label": "Добавить медиаконтент",
|
||||
"upload_form.description": "Describe for the visually impaired",
|
||||
"upload_form.description": "Описать для людей с нарушениями зрения",
|
||||
"upload_form.undo": "Отменить",
|
||||
"upload_progress.label": "Загрузка...",
|
||||
"video.close": "Close video",
|
||||
"video.exit_fullscreen": "Exit full screen",
|
||||
"video.expand": "Expand video",
|
||||
"video.fullscreen": "Full screen",
|
||||
"video.hide": "Hide video",
|
||||
"video.mute": "Mute sound",
|
||||
"video.pause": "Pause",
|
||||
"video.play": "Play",
|
||||
"video.unmute": "Unmute sound"
|
||||
"video.close": "Закрыть видео",
|
||||
"video.exit_fullscreen": "Покинуть полноэкранный режим",
|
||||
"video.expand": "Развернуть видео",
|
||||
"video.fullscreen": "Полноэкранный режим",
|
||||
"video.hide": "Скрыть видео",
|
||||
"video.mute": "Заглушить звук",
|
||||
"video.pause": "Пауза",
|
||||
"video.play": "Пуск",
|
||||
"video.unmute": "Включить звук"
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ const initialState = ImmutableMap({
|
||||
body: '',
|
||||
}),
|
||||
}),
|
||||
|
||||
direct: ImmutableMap({
|
||||
regex: ImmutableMap({
|
||||
body: '',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const defaultColumns = fromJS([
|
||||
|
||||
@@ -2503,6 +2503,7 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
@@ -2528,12 +2529,10 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
|
||||
.column-header__buttons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
margin: -15px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.column-header__button {
|
||||
@@ -2692,6 +2691,14 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__title {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
@@ -3465,7 +3472,6 @@ button.icon-button.active i.fa-retweet {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba($base-overlay-background, 0.7);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.modal-root__container {
|
||||
|
||||
@@ -53,9 +53,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
|
||||
def process_tags(status)
|
||||
return unless @object['tag'].is_a?(Array)
|
||||
return if @object['tag'].nil?
|
||||
|
||||
@object['tag'].each do |tag|
|
||||
as_array(@object['tag']).each do |tag|
|
||||
case tag['type']
|
||||
when 'Hashtag'
|
||||
process_hashtag tag, status
|
||||
@@ -103,9 +103,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
|
||||
def process_attachments(status)
|
||||
return unless @object['attachment'].is_a?(Array)
|
||||
return if @object['attachment'].nil?
|
||||
|
||||
@object['attachment'].each do |attachment|
|
||||
as_array(@object['attachment']).each do |attachment|
|
||||
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
|
||||
|
||||
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
||||
|
||||
@@ -141,6 +141,8 @@ class FeedManager
|
||||
return false if receiver_id == status.account_id
|
||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
|
||||
return true if keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id))
|
||||
|
||||
check_for_mutes = [status.account_id]
|
||||
check_for_mutes.concat(status.mentions.pluck(:account_id))
|
||||
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
|
||||
@@ -166,6 +168,18 @@ class FeedManager
|
||||
false
|
||||
end
|
||||
|
||||
def keyword_filter?(status, matcher)
|
||||
should_filter = matcher =~ status.text
|
||||
should_filter ||= matcher =~ status.spoiler_text
|
||||
|
||||
if status.reblog?
|
||||
should_filter ||= matcher =~ status.reblog.text
|
||||
should_filter ||= matcher =~ status.reblog.spoiler_text
|
||||
end
|
||||
|
||||
!!should_filter
|
||||
end
|
||||
|
||||
def filter_from_mentions?(status, receiver_id)
|
||||
return true if receiver_id == status.account_id
|
||||
|
||||
@@ -175,6 +189,7 @@ class FeedManager
|
||||
|
||||
should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
|
||||
should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
|
||||
should_filter ||= keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id)) # or if the mention contains a muted keyword
|
||||
|
||||
should_filter
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# disabled :boolean default(FALSE), not null
|
||||
# uri :string
|
||||
# image_remote_url :string
|
||||
# visible_in_picker :boolean default(TRUE), not null
|
||||
#
|
||||
|
||||
class CustomEmoji < ApplicationRecord
|
||||
|
||||
7
app/models/glitch.rb
Normal file
7
app/models/glitch.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Glitch
|
||||
def self.table_name_prefix
|
||||
'glitch_'
|
||||
end
|
||||
end
|
||||
66
app/models/glitch/keyword_mute.rb
Normal file
66
app/models/glitch/keyword_mute.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: glitch_keyword_mutes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# account_id :integer not null
|
||||
# keyword :string not null
|
||||
# whole_word :boolean default(TRUE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Glitch::KeywordMute < ApplicationRecord
|
||||
belongs_to :account, required: true
|
||||
|
||||
validates_presence_of :keyword
|
||||
|
||||
after_commit :invalidate_cached_matcher
|
||||
|
||||
def self.matcher_for(account_id)
|
||||
Matcher.new(account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invalidate_cached_matcher
|
||||
Rails.cache.delete("keyword_mutes:regex:#{account_id}")
|
||||
end
|
||||
|
||||
class Matcher
|
||||
attr_reader :account_id
|
||||
attr_reader :regex
|
||||
|
||||
def initialize(account_id)
|
||||
@account_id = account_id
|
||||
regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account }
|
||||
@regex = /#{regex_text}/i
|
||||
end
|
||||
|
||||
def =~(str)
|
||||
regex =~ str
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keywords
|
||||
Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word)
|
||||
end
|
||||
|
||||
def regex_text_for_account
|
||||
kws = keywords.find_each.with_object([]) do |kw, a|
|
||||
a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword)
|
||||
end
|
||||
|
||||
Regexp.union(kws).source
|
||||
end
|
||||
|
||||
def boundary_regex_for_keyword(keyword)
|
||||
sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
|
||||
eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
|
||||
|
||||
/#{sb}#{Regexp.escape(keyword)}#{eb}/
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -154,6 +154,14 @@ class Status < ApplicationRecord
|
||||
where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
|
||||
end
|
||||
|
||||
def as_direct_timeline(account)
|
||||
query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
|
||||
.where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}")
|
||||
.where(visibility: [:direct])
|
||||
|
||||
apply_timeline_filters(query, account, false)
|
||||
end
|
||||
|
||||
def as_public_timeline(account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).without_replies
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class REST::CustomEmojiSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :shortcode, :url, :static_url
|
||||
attributes :shortcode, :url, :static_url, :visible_in_picker
|
||||
|
||||
def url
|
||||
full_asset_url(object.image.url)
|
||||
|
||||
@@ -40,6 +40,7 @@ class BatchedRemoveStatusService < BaseService
|
||||
# Cannot be batched
|
||||
statuses.each do |status|
|
||||
unpush_from_public_timelines(status)
|
||||
unpush_from_direct_timelines(status) if status.direct_visibility?
|
||||
batch_salmon_slaps(status) if status.local?
|
||||
end
|
||||
|
||||
@@ -100,6 +101,16 @@ class BatchedRemoveStatusService < BaseService
|
||||
end
|
||||
end
|
||||
|
||||
def unpush_from_direct_timelines(status)
|
||||
payload = @json_payloads[status.id]
|
||||
redis.pipelined do
|
||||
@mentions[status.id].each do |mention|
|
||||
redis.publish("timeline:direct:#{mention.account.id}", payload) if mention.account.local?
|
||||
end
|
||||
redis.publish("timeline:direct:#{status.account.id}", payload) if status.account.local?
|
||||
end
|
||||
end
|
||||
|
||||
def batch_salmon_slaps(status)
|
||||
return if @mentions[status.id].empty?
|
||||
|
||||
|
||||
@@ -10,15 +10,17 @@ class FanOutOnWriteService < BaseService
|
||||
|
||||
deliver_to_self(status) if status.account.local?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
|
||||
if status.direct_visibility?
|
||||
deliver_to_mentioned_followers(status)
|
||||
deliver_to_direct_timelines(status)
|
||||
else
|
||||
deliver_to_followers(status)
|
||||
end
|
||||
|
||||
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
deliver_to_hashtags(status)
|
||||
|
||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
@@ -73,4 +75,13 @@ class FanOutOnWriteService < BaseService
|
||||
Redis.current.publish('timeline:public', @payload)
|
||||
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||
end
|
||||
|
||||
def deliver_to_direct_timelines(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to direct timelines"
|
||||
|
||||
status.mentions.includes(:account).each do |mention|
|
||||
Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
|
||||
end
|
||||
Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,7 @@ class RemoveStatusService < BaseService
|
||||
remove_reblogs
|
||||
remove_from_hashtags
|
||||
remove_from_public
|
||||
remove_from_direct if status.direct_visibility?
|
||||
|
||||
@status.destroy!
|
||||
|
||||
@@ -121,6 +122,13 @@ class RemoveStatusService < BaseService
|
||||
Redis.current.publish('timeline:public:local', @payload) if @status.local?
|
||||
end
|
||||
|
||||
def remove_from_direct
|
||||
@mentions.each do |mention|
|
||||
Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
|
||||
end
|
||||
Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
|
||||
end
|
||||
|
||||
def redis
|
||||
Redis.current
|
||||
end
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
- else
|
||||
= custom_emoji.domain
|
||||
%td
|
||||
- unless custom_emoji.local?
|
||||
- if custom_emoji.local?
|
||||
- if custom_emoji.visible_in_picker
|
||||
= table_link_to 'eye', t('admin.custom_emojis.listed'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: false }), method: :patch
|
||||
- else
|
||||
= table_link_to 'eye-slash', t('admin.custom_emojis.unlisted'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: true }), method: :patch
|
||||
- else
|
||||
= table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post
|
||||
%td
|
||||
- if custom_emoji.disabled?
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
%link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
|
||||
%link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
|
||||
%link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
|
||||
%link{ href: asset_pack_path('features/direct_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
|
||||
%meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
|
||||
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
|
||||
|
||||
|
||||
11
app/views/settings/keyword_mutes/_fields.html.haml
Normal file
11
app/views/settings/keyword_mutes/_fields.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
.fields-group
|
||||
= f.input :keyword
|
||||
= f.check_box :whole_word
|
||||
= f.label :whole_word, t('keyword_mutes.match_whole_word')
|
||||
|
||||
.actions
|
||||
- if f.object.persisted?
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
= link_to t('keyword_mutes.remove'), settings_keyword_mute_path(f.object), class: 'negative button', method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
- else
|
||||
= f.button :button, t('keyword_mutes.add_keyword'), type: :submit
|
||||
10
app/views/settings/keyword_mutes/_keyword_mute.html.haml
Normal file
10
app/views/settings/keyword_mutes/_keyword_mute.html.haml
Normal file
@@ -0,0 +1,10 @@
|
||||
%tr
|
||||
%td
|
||||
= keyword_mute.keyword
|
||||
%td
|
||||
- if keyword_mute.whole_word
|
||||
%i.fa.fa-check
|
||||
%td
|
||||
= table_link_to 'edit', t('keyword_mutes.edit'), edit_settings_keyword_mute_path(keyword_mute)
|
||||
%td
|
||||
= table_link_to 'times', t('keyword_mutes.remove'), settings_keyword_mute_path(keyword_mute), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
6
app/views/settings/keyword_mutes/edit.html.haml
Normal file
6
app/views/settings/keyword_mutes/edit.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
- content_for :page_title do
|
||||
= t('keyword_mutes.edit_keyword')
|
||||
|
||||
= simple_form_for @keyword_mute, url: settings_keyword_mute_path(@keyword_mute), as: :keyword_mute do |f|
|
||||
= render 'shared/error_messages', object: @keyword_mute
|
||||
= render 'fields', f: f
|
||||
18
app/views/settings/keyword_mutes/index.html.haml
Normal file
18
app/views/settings/keyword_mutes/index.html.haml
Normal file
@@ -0,0 +1,18 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.keyword_mutes')
|
||||
|
||||
.table-wrapper
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th= t('keyword_mutes.keyword')
|
||||
%th= t('keyword_mutes.match_whole_word')
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
= render partial: 'keyword_mute', collection: @keyword_mutes, as: :keyword_mute
|
||||
|
||||
= paginate @keyword_mutes
|
||||
.simple_form
|
||||
= link_to t('keyword_mutes.add_keyword'), new_settings_keyword_mute_path, class: 'button'
|
||||
= link_to t('keyword_mutes.remove_all'), destroy_all_settings_keyword_mutes_path, class: 'button negative', method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
6
app/views/settings/keyword_mutes/new.html.haml
Normal file
6
app/views/settings/keyword_mutes/new.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
- content_for :page_title do
|
||||
= t('keyword_mutes.add_keyword')
|
||||
|
||||
= simple_form_for @keyword_mute, url: settings_keyword_mutes_path, as: :keyword_mute do |f|
|
||||
= render 'shared/error_messages', object: @keyword_mute
|
||||
= render 'fields', f: f
|
||||
@@ -97,6 +97,8 @@ Rails.application.configure do
|
||||
'X-XSS-Protection' => '1; mode=block',
|
||||
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
|
||||
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
|
||||
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload'
|
||||
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
|
||||
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
@@ -130,11 +130,15 @@ en:
|
||||
enable: Enable
|
||||
enabled_msg: Successfully enabled that emoji
|
||||
image_hint: PNG up to 50KB
|
||||
listed: Listed
|
||||
new:
|
||||
title: Add new custom emoji
|
||||
shortcode: Shortcode
|
||||
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
|
||||
title: Custom emojis
|
||||
unlisted: Unlisted
|
||||
update_failed_msg: Could not update that emoji
|
||||
updated_msg: Emoji successfully updated!
|
||||
upload: Upload
|
||||
domain_blocks:
|
||||
add_new: Add new
|
||||
@@ -373,6 +377,14 @@ en:
|
||||
following: Following list
|
||||
muting: Muting list
|
||||
upload: Upload
|
||||
keyword_mutes:
|
||||
add_keyword: Add keyword
|
||||
edit: Edit
|
||||
edit_keyword: Edit keyword
|
||||
keyword: Keyword
|
||||
match_whole_word: Match whole word
|
||||
remove: Remove
|
||||
remove_all: Remove all
|
||||
landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse."
|
||||
landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
|
||||
media_attachments:
|
||||
@@ -491,6 +503,7 @@ en:
|
||||
export: Data export
|
||||
followers: Authorized followers
|
||||
import: Import
|
||||
keyword_mutes: Muted keywords
|
||||
notifications: Notifications
|
||||
preferences: Preferences
|
||||
settings: Settings
|
||||
|
||||
@@ -1,39 +1,77 @@
|
||||
---
|
||||
ru:
|
||||
about:
|
||||
about_hashtag_html: Это публичные статусы, отмеченные хэштегом <strong>#%{hashtag}</strong>. Вы можете взаимодействовать с ними при наличии у Вас аккаунта в глобальной сети Mastodon.
|
||||
about_mastodon_html: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
|
||||
about_this: Об этом узле
|
||||
closed_registrations: В данный момент регистрация на этом узле закрыта.
|
||||
contact: Связаться
|
||||
contact_missing: Не установлено
|
||||
contact_unavailable: Недоступен
|
||||
description_headline: Что такое %{domain}?
|
||||
domain_count_after: другими узлами
|
||||
domain_count_before: Связан с
|
||||
extended_description_html: |
|
||||
<h3>Хорошее место для правил</h3>
|
||||
<p>Расширенное описание еще не настроено.</p>
|
||||
features:
|
||||
humane_approach_body: Наученный ошибками других проектов, Mastodon направлен на выбор этичных решений в борьбе со злоупотреблениями возможностями социальных сетей.
|
||||
humane_approach_title: Человечный подход
|
||||
not_a_product_body: Mastodon - не коммерческая сеть. Здесь нет рекламы, сбора данных, отгороженных мест. Здесь нет централизованного управления.
|
||||
not_a_product_title: Вы - человек, а не продукт
|
||||
real_conversation_body: С 500 символами в Вашем распоряжении и поддержкой предупреждений о содержании статусов Вы сможете выражать свои мысли так, как Вы этого хотите.
|
||||
real_conversation_title: Создан для настоящего общения
|
||||
within_reach_body: Различные приложения для iOS, Android и других платформ, написанные благодаря дружественной к разработчикам экосистеме API, позволят Вам держать связь с Вашими друзьями где угодно.
|
||||
within_reach_title: Всегда под рукой
|
||||
find_another_instance: Найти другой узел
|
||||
generic_description: "%{domain} - один из серверов сети"
|
||||
hosted_on: Mastodon размещен на %{domain}
|
||||
learn_more: Узнать больше
|
||||
other_instances: Другие узлы
|
||||
source_code: Исходный код
|
||||
status_count_after: статусов
|
||||
status_count_before: Опубликовано
|
||||
user_count_after: пользователей
|
||||
user_count_before: Здесь живет
|
||||
what_is_mastodon: Что такое Mastodon?
|
||||
accounts:
|
||||
follow: Подписаться
|
||||
followers: Подписчики
|
||||
following: Подписан(а)
|
||||
media: Медиаконтент
|
||||
nothing_here: Здесь ничего нет!
|
||||
people_followed_by: Люди, на которых подписан(а) %{name}
|
||||
people_who_follow: Подписчики %{name}
|
||||
posts: Посты
|
||||
posts_with_replies: Посты с ответами
|
||||
remote_follow: Подписаться на удаленном узле
|
||||
reserved_username: Имя пользователя зарезервировано
|
||||
roles:
|
||||
admin: Администратор
|
||||
unfollow: Отписаться
|
||||
admin:
|
||||
account_moderation_notes:
|
||||
account: Модератор
|
||||
create: Создать
|
||||
created_at: Дата
|
||||
created_msg: Заметка модератора успешно создана!
|
||||
delete: Удалить
|
||||
destroyed_msg: Заметка модератора успешно удалена!
|
||||
accounts:
|
||||
are_you_sure: Вы уверены?
|
||||
confirm: Подтвердить
|
||||
confirmed: Подтверждено
|
||||
disable_two_factor_authentication: Отключить 2FA
|
||||
display_name: Отображаемое имя
|
||||
domain: Домен
|
||||
edit: Изменить
|
||||
email: E-mail
|
||||
feed_url: URL фида
|
||||
followers: Подписчики
|
||||
followers_url: URL подписчиков
|
||||
follows: Подписки
|
||||
inbox_url: URL входящих
|
||||
ip: IP
|
||||
location:
|
||||
all: Все
|
||||
local: Локальные
|
||||
@@ -45,6 +83,7 @@ ru:
|
||||
silenced: Заглушенные
|
||||
suspended: Заблокированные
|
||||
title: Модерация
|
||||
moderation_notes: Заметки модератора
|
||||
most_recent_activity: Последняя активность
|
||||
most_recent_ip: Последний IP
|
||||
not_subscribed: Не подписаны
|
||||
@@ -52,19 +91,51 @@ ru:
|
||||
alphabetic: По алфавиту
|
||||
most_recent: По дате
|
||||
title: Порядок
|
||||
outbox_url: URL исходящих
|
||||
perform_full_suspension: Полная блокировка
|
||||
profile_url: URL профиля
|
||||
protocol: Протокол
|
||||
public: Публичный
|
||||
push_subscription_expires: Подписка PuSH истекает
|
||||
redownload: Обновить аватар
|
||||
reset: Сбросить
|
||||
reset_password: Сбросить пароль
|
||||
resubscribe: Переподписаться
|
||||
salmon_url: Salmon URL
|
||||
search: Поиск
|
||||
shared_inbox_url: URL общих входящих
|
||||
show:
|
||||
created_reports: Жалобы, отправленные этим аккаунтом
|
||||
report: жалоба
|
||||
targeted_reports: Жалобы на этот аккаунт
|
||||
silence: Глушение
|
||||
statuses: Статусы
|
||||
subscribe: Подписаться
|
||||
title: Аккаунты
|
||||
undo_silenced: Снять глушение
|
||||
undo_suspension: Снять блокировку
|
||||
unsubscribe: Отписаться
|
||||
username: Имя пользователя
|
||||
web: WWW
|
||||
custom_emojis:
|
||||
copied_msg: Локальная копия эмодзи успешно создана
|
||||
copy: Скопироват
|
||||
copy_failed_msg: Не удалось создать локальную копию эмодзи
|
||||
created_msg: Эмодзи успешно создано!
|
||||
delete: Удалить
|
||||
destroyed_msg: Эмодзи успешно удалено!
|
||||
disable: Отключить
|
||||
disabled_msg: Эмодзи успешно отключено
|
||||
emoji: Эмодзи
|
||||
enable: Включить
|
||||
enabled_msg: Эмодзи успешно включено
|
||||
image_hint: PNG до 50KB
|
||||
new:
|
||||
title: Добавить новое эмодзи
|
||||
shortcode: Шорткод
|
||||
shortcode_hint: Как минимум 2 символа, только алфавитно-цифровые символы и подчеркивания
|
||||
title: Собственные эмодзи
|
||||
upload: Загрузить
|
||||
domain_blocks:
|
||||
add_new: Добавить новую
|
||||
created_msg: Блокировка домена обрабатывается
|
||||
@@ -74,13 +145,15 @@ ru:
|
||||
create: Создать блокировку
|
||||
hint: Блокировка домена не предотвратит создание новых аккаунтов в базе данных, но ретроактивно и автоматически применит указанные методы модерации для этих аккаунтов.
|
||||
severity:
|
||||
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля."
|
||||
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля. Используйте <strong>Ничего</strong>, если хотите только запретить медиаконтент."
|
||||
noop: Ничего
|
||||
silence: Глушение
|
||||
suspend: Блокировка
|
||||
title: Новая доменная блокировка
|
||||
reject_media: Запретить медиаконтент
|
||||
reject_media_hint: Удаляет локально хранимый медиаконтент и запрещает его загрузку в будущем. Не имеет значения в случае блокировки.
|
||||
severities:
|
||||
noop: Ничего
|
||||
silence: Глушение
|
||||
suspend: Блокировка
|
||||
severity: Строгость
|
||||
@@ -97,13 +170,34 @@ ru:
|
||||
undo: Отменить
|
||||
title: Доменные блокировки
|
||||
undo: Отемнить
|
||||
email_domain_blocks:
|
||||
add_new: Добавить новую
|
||||
created_msg: Доменная блокировка еmail успешно создана
|
||||
delete: Удалить
|
||||
destroyed_msg: Доменная блокировка еmail успешно удалена
|
||||
domain: Домен
|
||||
new:
|
||||
create: Создать блокировку
|
||||
title: Новая доменная блокировка еmail
|
||||
title: Доменная блокировка email
|
||||
instances:
|
||||
account_count: Известных аккаунтов
|
||||
domain_name: Домен
|
||||
reset: Сбросить
|
||||
search: Поиск
|
||||
title: Известные узлы
|
||||
reports:
|
||||
action_taken_by: 'Действие предпринято:'
|
||||
are_you_sure: Вы уверены?
|
||||
comment:
|
||||
label: Комментарий
|
||||
none: Нет
|
||||
delete: Удалить
|
||||
id: ID
|
||||
mark_as_resolved: Отметить как разрешенную
|
||||
nsfw:
|
||||
'false': Показать мультимедийные вложения
|
||||
'true': Скрыть мультимедийные вложения
|
||||
report: 'Жалоба #%{id}'
|
||||
reported_account: Аккаунт нарушителя
|
||||
reported_by: Отправитель жалобы
|
||||
@@ -116,6 +210,9 @@ ru:
|
||||
unresolved: Неразрешенные
|
||||
view: Просмотреть
|
||||
settings:
|
||||
bootstrap_timeline_accounts:
|
||||
desc_html: Разделяйте имена пользователей запятыми. Сработает только для локальных незакрытых аккаунтов. По умолчанию включены все локальные администраторы.
|
||||
title: Подписки по умолчанию для новых пользователей
|
||||
contact_information:
|
||||
email: Введите публичный e-mail
|
||||
username: Введите имя пользователя
|
||||
@@ -123,7 +220,11 @@ ru:
|
||||
closed_message:
|
||||
desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги
|
||||
title: Сообщение о закрытой регистрации
|
||||
deletion:
|
||||
desc_html: Позволяет всем удалять собственные аккаунты
|
||||
title: Разрешить удаление аккаунтов
|
||||
open:
|
||||
desc_html: Позволяет любому создавать аккаунт
|
||||
title: Открыть регистрацию
|
||||
site_description:
|
||||
desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code><a></code> и <code><em></code>.
|
||||
@@ -131,8 +232,32 @@ ru:
|
||||
site_description_extended:
|
||||
desc_html: Отображается на странице дополнительной информации<br>Можно использовать HTML-теги
|
||||
title: Расширенное описание сайта
|
||||
site_terms:
|
||||
desc_html: Вы можете добавить сюда собственную политику конфиденциальности, пользовательское соглашение и другие документы. Можно использовать теги HTML.
|
||||
title: Условия использования
|
||||
site_title: Название сайта
|
||||
thumbnail:
|
||||
desc_html: Используется для предпросмотра с помощью OpenGraph и API. Рекомендуется разрешение 1200x630px
|
||||
title: Картинка узла
|
||||
timeline_preview:
|
||||
desc_html: Показывать публичную ленту на целевой странице
|
||||
title: Предпросмотр ленты
|
||||
title: Настройки сайта
|
||||
statuses:
|
||||
back_to_account: Назад к странице аккаунта
|
||||
batch:
|
||||
delete: Удалить
|
||||
nsfw_off: Выключить NSFW
|
||||
nsfw_on: Включить NSFW
|
||||
execute: Выполнить
|
||||
failed_to_execute: Не удалось выполнить
|
||||
media:
|
||||
hide: Скрыть медиаконтент
|
||||
show: Показать медиаконтент
|
||||
title: Медиаконтент
|
||||
no_media: Без медиаконтента
|
||||
title: Статусы аккаунта
|
||||
with_media: С медиаконтентом
|
||||
subscriptions:
|
||||
callback_url: Callback URL
|
||||
confirmed: Подтверждено
|
||||
@@ -141,18 +266,31 @@ ru:
|
||||
title: WebSub
|
||||
topic: Тема
|
||||
title: Администрирование
|
||||
admin_mailer:
|
||||
new_report:
|
||||
body: "%{reporter} подал(а) жалобу на %{target}"
|
||||
subject: Новая жалоба, узел %{instance} (#%{id})
|
||||
application_mailer:
|
||||
salutation: "%{name},"
|
||||
settings: 'Изменить настройки e-mail: %{link}'
|
||||
signature: Уведомления Mastodon от %{instance}
|
||||
view: 'Просмотр:'
|
||||
applications:
|
||||
created: Приложение успешно создано
|
||||
destroyed: Приложение успешно удалено
|
||||
invalid_url: Введенный URL неверен
|
||||
regenerate_token: Повторно сгенерировать токен доступа
|
||||
token_regenerated: Токен доступа успешно сгенерирован
|
||||
warning: Будьте очень внимательны с этими данными. Не делитесь ими ни с кем!
|
||||
your_token: Ваш токен доступа
|
||||
auth:
|
||||
agreement_html: Создавая аккаунт, вы соглашаетесь с <a href="%{rules_path}">нашими правилами поведения</a> и <a href="%{terms_path}">политикой конфиденциальности</a>.
|
||||
change_password: Изменить пароль
|
||||
delete_account: Удалить аккаунт
|
||||
delete_account_html: Если Вы хотите удалить свой аккаунт, вы можете <a href="%{path}">перейти сюда</a>. У Вас будет запрошено подтверждение.
|
||||
didnt_get_confirmation: Не получили инструкцию для подтверждения?
|
||||
forgot_password: Забыли пароль?
|
||||
invalid_reset_password_token: Токен сброса пароля неверен или устарел. Пожалуйста, запросите новый.
|
||||
login: Войти
|
||||
logout: Выйти
|
||||
register: Зарегистрироваться
|
||||
@@ -162,6 +300,12 @@ ru:
|
||||
authorize_follow:
|
||||
error: К сожалению, при поиске удаленного аккаунта возникла ошибка
|
||||
follow: Подписаться
|
||||
follow_request: 'Вы отправили запрос на подписку:'
|
||||
following: 'Ура! Теперь Вы подписаны на:'
|
||||
post_follow:
|
||||
close: Или просто закрыть это окно.
|
||||
return: Вернуться к профилю пользователя
|
||||
web: Перейти к WWW
|
||||
title: Подписаться на %{acct}
|
||||
datetime:
|
||||
distance_in_words:
|
||||
@@ -193,7 +337,10 @@ ru:
|
||||
content: Проверка безопасности не удалась. Возможно, Вы блокируете cookies?
|
||||
title: Проверка безопасности не удалась.
|
||||
'429': Слишком много запросов
|
||||
noscript_html: Для работы с Mastodon, пожалуйста, включите JavaScript.
|
||||
'500':
|
||||
content: Приносим извинения, но на нашей стороне что-то пошло не так.
|
||||
title: Страница неверна
|
||||
noscript_html: Для работы с Mastodon, пожалуйста, включите JavaScript. Кроме того, вы можете использовать одно из <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">приложений</a> Mastodon для Вашей платформы.
|
||||
exports:
|
||||
blocks: Список блокировки
|
||||
csv: CSV
|
||||
@@ -265,23 +412,30 @@ ru:
|
||||
number:
|
||||
human:
|
||||
decimal_units:
|
||||
format: "%n%u"
|
||||
format: "%n %u"
|
||||
units:
|
||||
billion: B
|
||||
million: M
|
||||
billion: млрд
|
||||
million: млн
|
||||
quadrillion: Q
|
||||
thousand: K
|
||||
trillion: T
|
||||
thousand: тыс
|
||||
trillion: трлн
|
||||
unit: ''
|
||||
pagination:
|
||||
next: След
|
||||
prev: Пред
|
||||
truncate: "…"
|
||||
preferences:
|
||||
languages: Языки
|
||||
other: Другое
|
||||
publishing: Публикация
|
||||
web: WWW
|
||||
push_notifications:
|
||||
favourite:
|
||||
title: Ваш статус понравился %{name}
|
||||
follow:
|
||||
title: "%{name} теперь подписан(а) на Вас"
|
||||
group:
|
||||
title: "%{count} уведомлений"
|
||||
mention:
|
||||
action_boost: Продвинуть
|
||||
action_expand: Развернуть
|
||||
@@ -335,16 +489,24 @@ ru:
|
||||
authorized_apps: Авторизованные приложения
|
||||
back: Назад в Mastodon
|
||||
delete: Удаление аккаунта
|
||||
development: Разработка
|
||||
edit_profile: Изменить профиль
|
||||
export: Экспорт данных
|
||||
followers: Авторизованные подписчики
|
||||
import: Импорт
|
||||
notifications: Уведомления
|
||||
preferences: Настройки
|
||||
settings: Опции
|
||||
two_factor_authentication: Двухфакторная аутентификация
|
||||
your_apps: Ваши приложения
|
||||
statuses:
|
||||
open_in_web: Открыть в WWW
|
||||
over_character_limit: превышен лимит символов (%{max})
|
||||
pin_errors:
|
||||
limit: Слишком много закрепленных статусов
|
||||
ownership: Нельзя закрепить чужой статус
|
||||
private: Нельзя закрепить непубличный статус
|
||||
reblog: Нельзя закрепить продвинутый статус
|
||||
show_more: Подробнее
|
||||
visibilities:
|
||||
private: Для подписчиков
|
||||
@@ -359,6 +521,8 @@ ru:
|
||||
sensitive_content: Чувствительный контент
|
||||
terms:
|
||||
title: Условия обслуживания и политика конфиденциальности %{instance}
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
@@ -367,11 +531,13 @@ ru:
|
||||
description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
|
||||
disable: Отключить
|
||||
enable: Включить
|
||||
enabled: Двухфакторная аутентификация включена
|
||||
enabled_success: Двухфакторная аутентификация успешно включена
|
||||
generate_recovery_codes: Сгенерировать коды восстановления
|
||||
instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
|
||||
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
|
||||
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
|
||||
recovery_codes: Коды восстановления
|
||||
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
|
||||
recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
|
||||
setup: Настроить
|
||||
@@ -379,3 +545,4 @@ ru:
|
||||
users:
|
||||
invalid_email: Введенный e-mail неверен
|
||||
invalid_otp_token: Введен неверный код
|
||||
signed_in_as: 'Выполнен вход под именем:'
|
||||
|
||||
@@ -4,6 +4,7 @@ pt-BR:
|
||||
hints:
|
||||
defaults:
|
||||
avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 120x120px
|
||||
digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência.
|
||||
display_name:
|
||||
one: <span class="name-counter">1</span> caracter restante
|
||||
other: <span class="name-counter">%{count}</span> caracteres restantes
|
||||
@@ -13,6 +14,7 @@ pt-BR:
|
||||
one: <span class="note-counter">1</span> caracter restante
|
||||
other: <span class="note-counter">%{count}</span> caracteres restantes
|
||||
setting_noindex: Afeta seu perfil público e as páginas de suas postagens
|
||||
setting_theme: Afeta a aparência do Mastodon quando em sua conta em qualquer aparelho.
|
||||
imports:
|
||||
data: Arquivo CSV exportado de outra instância do Mastodon
|
||||
sessions:
|
||||
@@ -42,7 +44,9 @@ pt-BR:
|
||||
setting_default_sensitive: Sempre marcar mídia como sensível
|
||||
setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem
|
||||
setting_noindex: Não quero ser indexado por mecanismos de busca
|
||||
setting_reduce_motion: Reduz movimento em animações
|
||||
setting_system_font_ui: Usar a fonte padrão de seu sistema
|
||||
setting_theme: Tema do site
|
||||
setting_unfollow_modal: Mostrar diálogo de confirmação antes de deixar de seguir alguém
|
||||
severity: Gravidade
|
||||
type: Tipo de importação
|
||||
|
||||
@@ -4,6 +4,7 @@ ru:
|
||||
hints:
|
||||
defaults:
|
||||
avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px
|
||||
digest: Отсылается после долгого периода неактивности с общей информацией упоминаний, полученных в Ваше отсутствие
|
||||
display_name:
|
||||
few: Осталось <span class="name-counter">%{count}</span> символа
|
||||
many: Осталось <span class="name-counter">%{count}</span> символов
|
||||
@@ -17,6 +18,7 @@ ru:
|
||||
one: Остался <span class="name-counter">1</span> символ
|
||||
other: Осталось <span class="name-counter">%{count}</span> символов
|
||||
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
|
||||
setting_theme: Влияет на внешний вид Mastodon при выполненном входе в аккаунт.
|
||||
imports:
|
||||
data: Файл CSV, экспортированный с другого узла Mastodon
|
||||
sessions:
|
||||
@@ -46,6 +48,8 @@ ru:
|
||||
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
|
||||
setting_delete_modal: Показывать диалог подтверждения перед удалением
|
||||
setting_noindex: Отказаться от индексации в поисковых машинах
|
||||
setting_reduce_motion: Уменьшить движение в анимации
|
||||
setting_site_theme: Тема сайта
|
||||
setting_system_font_ui: Использовать шрифт системы по умолчанию
|
||||
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
|
||||
severity: Строгость
|
||||
|
||||
@@ -7,6 +7,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
||||
primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
|
||||
settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url
|
||||
settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
|
||||
settings.item :keyword_mutes, safe_join([fa_icon('volume-off fw'), t('settings.keyword_mutes')]), settings_keyword_mutes_url
|
||||
settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url
|
||||
settings.item :password, safe_join([fa_icon('lock fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
|
||||
settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}
|
||||
|
||||
@@ -66,6 +66,13 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :settings do
|
||||
resource :profile, only: [:show, :update]
|
||||
|
||||
resources :keyword_mutes do
|
||||
collection do
|
||||
delete :destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
resource :preferences, only: [:show, :update]
|
||||
resource :notifications, only: [:show, :update]
|
||||
resource :import, only: [:show, :create]
|
||||
@@ -140,7 +147,7 @@ Rails.application.routes.draw do
|
||||
resource :two_factor_authentication, only: [:destroy]
|
||||
end
|
||||
|
||||
resources :custom_emojis, only: [:index, :new, :create, :destroy] do
|
||||
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do
|
||||
member do
|
||||
post :copy
|
||||
post :enable
|
||||
@@ -193,6 +200,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
namespace :timelines do
|
||||
resource :direct, only: :show, controller: :direct
|
||||
resource :home, only: :show, controller: :home
|
||||
resource :public, only: :show, controller: :public
|
||||
resources :tag, only: :show
|
||||
|
||||
@@ -3,48 +3,62 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
|
||||
redis = Redis.current
|
||||
fm = FeedManager.instance
|
||||
|
||||
# Old scheme:
|
||||
# Each user's feed zset had a series of score:value entries,
|
||||
# where "regular" statuses had the same score and value (their
|
||||
# ID). Reblogs had a score of the reblogging status' ID, and a
|
||||
# value of the reblogged status' ID.
|
||||
|
||||
# New scheme:
|
||||
# The feed contains only entries with the same score and value.
|
||||
# Reblogs result in the reblogging status being added to the
|
||||
# feed, with an entry in a reblog tracking zset (where the score
|
||||
# is once again set to the reblogging status' ID, and the value
|
||||
# is set to the reblogged status' ID). This is safe for Redis'
|
||||
# float coersion because in this reblog tracking zset, we only
|
||||
# need the rebloggging status' ID to be able to stop tracking
|
||||
# entries after they have gotten too far down the feed, which
|
||||
# does not require an exact value.
|
||||
|
||||
# This process reads all feeds and writes 3 times for each reblogs.
|
||||
# So we use Lua script to avoid overhead between Ruby and Redis.
|
||||
script = <<-LUA
|
||||
local timeline_key = KEYS[1]
|
||||
local reblog_key = KEYS[2]
|
||||
|
||||
-- So, first, we iterate over the user's feed to find any reblogs.
|
||||
local items = redis.call('zrange', timeline_key, 0, -1, 'withscores')
|
||||
|
||||
for i = 1, #items, 2 do
|
||||
local reblogged_id = items[i]
|
||||
local reblogging_id = items[i + 1]
|
||||
if (reblogged_id ~= reblogging_id) then
|
||||
|
||||
-- The score and value don't match, so this is a reblog.
|
||||
-- (note that we're transitioning from IDs < 53 bits so we
|
||||
-- don't have to worry about the loss of precision)
|
||||
|
||||
-- Remove the old entry
|
||||
redis.call('zrem', timeline_key, reblogged_id)
|
||||
|
||||
-- Add a new one for the reblogging status
|
||||
redis.call('zadd', timeline_key, reblogging_id, reblogging_id)
|
||||
|
||||
-- Track the fact that this was a reblog
|
||||
redis.call('zadd', reblog_key, reblogging_id, reblogged_id)
|
||||
end
|
||||
end
|
||||
LUA
|
||||
script_hash = redis.script(:load, script)
|
||||
|
||||
# find_each is batched on the database side.
|
||||
User.includes(:account).find_each do |user|
|
||||
account = user.account
|
||||
|
||||
# Old scheme:
|
||||
# Each user's feed zset had a series of score:value entries,
|
||||
# where "regular" statuses had the same score and value (their
|
||||
# ID). Reblogs had a score of the reblogging status' ID, and a
|
||||
# value of the reblogged status' ID.
|
||||
|
||||
# New scheme:
|
||||
# The feed contains only entries with the same score and value.
|
||||
# Reblogs result in the reblogging status being added to the
|
||||
# feed, with an entry in a reblog tracking zset (where the score
|
||||
# is once again set to the reblogging status' ID, and the value
|
||||
# is set to the reblogged status' ID). This is safe for Redis'
|
||||
# float coersion because in this reblog tracking zset, we only
|
||||
# need the rebloggging status' ID to be able to stop tracking
|
||||
# entries after they have gotten too far down the feed, which
|
||||
# does not require an exact value.
|
||||
|
||||
# So, first, we iterate over the user's feed to find any reblogs.
|
||||
timeline_key = fm.key(:home, account.id)
|
||||
reblog_key = fm.key(:home, account.id, 'reblogs')
|
||||
redis.zrange(timeline_key, 0, -1, with_scores: true).each do |entry|
|
||||
next if entry[0] == entry[1]
|
||||
|
||||
# The score and value don't match, so this is a reblog.
|
||||
# (note that we're transitioning from IDs < 53 bits so we
|
||||
# don't have to worry about the loss of precision)
|
||||
|
||||
reblogged_id, reblogging_id = entry
|
||||
|
||||
# Remove the old entry
|
||||
redis.zrem(timeline_key, reblogged_id)
|
||||
|
||||
# Add a new one for the reblogging status
|
||||
redis.zadd(timeline_key, reblogging_id, reblogging_id)
|
||||
|
||||
# Track the fact that this was a reblog
|
||||
redis.zadd(reblog_key, reblogging_id, reblogged_id)
|
||||
end
|
||||
redis.evalsha(script_hash, [timeline_key, reblog_key])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
12
db/migrate/20171009222537_create_keyword_mutes.rb
Normal file
12
db/migrate/20171009222537_create_keyword_mutes.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class CreateKeywordMutes < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :keyword_mutes do |t|
|
||||
t.references :account, null: false
|
||||
t.string :keyword, null: false
|
||||
t.boolean :whole_word, null: false, default: true
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_foreign_key :keyword_mutes, :accounts, on_delete: :cascade
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class AddVisibleInPickerToCustomEmoji < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
safety_assured {
|
||||
add_column :custom_emojis, :visible_in_picker, :boolean, default: true, null: false
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class MoveKeywordMutesIntoGlitchNamespace < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
safety_assured do
|
||||
rename_table :keyword_mutes, :glitch_keyword_mutes
|
||||
end
|
||||
end
|
||||
end
|
||||
13
db/schema.rb
13
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20171010025614) do
|
||||
ActiveRecord::Schema.define(version: 20171021191900) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -111,6 +111,7 @@ ActiveRecord::Schema.define(version: 20171010025614) do
|
||||
t.boolean "disabled", default: false, null: false
|
||||
t.string "uri"
|
||||
t.string "image_remote_url"
|
||||
t.boolean "visible_in_picker", default: true, null: false
|
||||
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
|
||||
end
|
||||
|
||||
@@ -155,6 +156,15 @@ ActiveRecord::Schema.define(version: 20171010025614) do
|
||||
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
|
||||
end
|
||||
|
||||
create_table "glitch_keyword_mutes", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "keyword", null: false
|
||||
t.boolean "whole_word", default: true, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_glitch_keyword_mutes_on_account_id"
|
||||
end
|
||||
|
||||
create_table "imports", force: :cascade do |t|
|
||||
t.integer "type", null: false
|
||||
t.boolean "approved", default: false, null: false
|
||||
@@ -472,6 +482,7 @@ ActiveRecord::Schema.define(version: 20171010025614) do
|
||||
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
|
||||
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||
add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade
|
||||
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
||||
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
|
||||
add_foreign_key "media_attachments", "statuses", on_delete: :nullify
|
||||
|
||||
@@ -10,6 +10,7 @@ module Paperclip
|
||||
unless options[:style] == :original && num_frames > 1
|
||||
tmp_file = Paperclip::TempfileFactory.new.generate(attachment.instance.file_file_name)
|
||||
tmp_file << file.read
|
||||
tmp_file.flush
|
||||
return tmp_file
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Settings::KeywordMutesController, type: :controller do
|
||||
|
||||
end
|
||||
2
spec/fabricators/glitch_keyword_mute_fabricator.rb
Normal file
2
spec/fabricators/glitch_keyword_mute_fabricator.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
Fabricator('Glitch::KeywordMute') do
|
||||
end
|
||||
BIN
spec/fixtures/files/mini-static.gif
vendored
Normal file
BIN
spec/fixtures/files/mini-static.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
15
spec/helpers/settings/keyword_mutes_helper_spec.rb
Normal file
15
spec/helpers/settings/keyword_mutes_helper_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Specs in this file have access to a helper object that includes
|
||||
# the Settings::KeywordMutesHelper. For example:
|
||||
#
|
||||
# describe Settings::KeywordMutesHelper do
|
||||
# describe "string concat" do
|
||||
# it "concats two strings with spaces" do
|
||||
# expect(helper.concat_strings("this","that")).to eq("this that")
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
RSpec.describe Settings::KeywordMutesHelper, type: :helper do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
@@ -119,6 +119,44 @@ RSpec.describe FeedManager do
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a status containing a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
|
||||
status = Fabricate(:status, text: 'This is a hot take', account: bob)
|
||||
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a reply containing a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
|
||||
s1 = Fabricate(:status, text: 'Something', account: alice)
|
||||
s2 = Fabricate(:status, text: 'This is a hot take', thread: s1, account: bob)
|
||||
|
||||
expect(FeedManager.instance.filter?(:home, s2, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a status whose spoiler text contains a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
|
||||
status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob)
|
||||
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a reblog containing a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
|
||||
status = Fabricate(:status, text: 'This is a hot take', account: bob)
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a reblog whose spoiler text contains a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
|
||||
status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob)
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'for mentions feed' do
|
||||
@@ -147,6 +185,13 @@ RSpec.describe FeedManager do
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for status that contains a muted keyword' do
|
||||
Fabricate('Glitch::KeywordMute', account: bob, keyword: 'take')
|
||||
status = Fabricate(:status, text: 'This is a hot take', account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
89
spec/models/glitch/keyword_mute_spec.rb
Normal file
89
spec/models/glitch/keyword_mute_spec.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Glitch::KeywordMute, type: :model do
|
||||
let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) }
|
||||
let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) }
|
||||
|
||||
describe '.matcher_for' do
|
||||
let(:matcher) { Glitch::KeywordMute.matcher_for(alice) }
|
||||
|
||||
describe 'with no mutes' do
|
||||
before do
|
||||
Glitch::KeywordMute.delete_all
|
||||
end
|
||||
|
||||
it 'does not match' do
|
||||
expect(matcher =~ 'This is a hot take').to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with mutes' do
|
||||
it 'does not match keywords set by a different account' do
|
||||
Glitch::KeywordMute.create!(account: bob, keyword: 'take')
|
||||
|
||||
expect(matcher =~ 'This is a hot take').to be_falsy
|
||||
end
|
||||
|
||||
it 'does not match if no keywords match the status text' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'cold')
|
||||
|
||||
expect(matcher =~ 'This is a hot take').to be_falsy
|
||||
end
|
||||
|
||||
it 'considers word boundaries when matching' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true)
|
||||
|
||||
expect(matcher =~ 'bobcats').to be_falsy
|
||||
end
|
||||
|
||||
it 'matches substrings if whole_word is false' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'take', whole_word: false)
|
||||
|
||||
expect(matcher =~ 'This is a shiitake mushroom').to be_truthy
|
||||
end
|
||||
|
||||
it 'matches keywords at the beginning of the text' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'take')
|
||||
|
||||
expect(matcher =~ 'Take this').to be_truthy
|
||||
end
|
||||
|
||||
it 'matches keywords at the end of the text' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'take')
|
||||
|
||||
expect(matcher =~ 'This is a hot take').to be_truthy
|
||||
end
|
||||
|
||||
it 'matches if at least one keyword case-insensitively matches the text' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'hot')
|
||||
|
||||
expect(matcher =~ 'This is a HOT take').to be_truthy
|
||||
end
|
||||
|
||||
it 'matches keywords surrounded by non-alphanumeric ornamentation' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'hot')
|
||||
|
||||
expect(matcher =~ '(hot take)').to be_truthy
|
||||
end
|
||||
|
||||
it 'escapes metacharacters in keywords' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)')
|
||||
|
||||
expect(matcher =~ '(hot take)').to be_truthy
|
||||
end
|
||||
|
||||
it 'uses case-folding rules appropriate for more than just English' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'großeltern')
|
||||
|
||||
expect(matcher =~ 'besuch der grosseltern').to be_truthy
|
||||
end
|
||||
|
||||
it 'matches keywords that are composed of multiple words' do
|
||||
Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake')
|
||||
|
||||
expect(matcher =~ 'This is a shiitake').to be_truthy
|
||||
expect(matcher =~ 'This is shiitake').to_not be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,20 +20,29 @@ RSpec.describe MediaAttachment, type: :model do
|
||||
end
|
||||
|
||||
describe 'non-animated gif non-conversion' do
|
||||
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.gif')) }
|
||||
fixtures = [
|
||||
{ filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 },
|
||||
{ filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 },
|
||||
]
|
||||
|
||||
it 'sets type to image' do
|
||||
expect(media.type).to eq 'image'
|
||||
end
|
||||
fixtures.each do |fixture|
|
||||
context fixture[:filename] do
|
||||
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) }
|
||||
|
||||
it 'leaves original file as-is' do
|
||||
expect(media.file_content_type).to eq 'image/gif'
|
||||
end
|
||||
it 'sets type to image' do
|
||||
expect(media.type).to eq 'image'
|
||||
end
|
||||
|
||||
it 'sets meta' do
|
||||
expect(media.file.meta["original"]["width"]).to eq 600
|
||||
expect(media.file.meta["original"]["height"]).to eq 400
|
||||
expect(media.file.meta["original"]["aspect"]).to eq 1.5
|
||||
it 'leaves original file as-is' do
|
||||
expect(media.file_content_type).to eq 'image/gif'
|
||||
end
|
||||
|
||||
it 'sets meta' do
|
||||
expect(media.file.meta["original"]["width"]).to eq fixture[:width]
|
||||
expect(media.file.meta["original"]["height"]).to eq fixture[:height]
|
||||
expect(media.file.meta["original"]["aspect"]).to eq fixture[:aspect]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -232,6 +232,55 @@ RSpec.describe Status, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '.as_direct_timeline' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:followed) { Fabricate(:account) }
|
||||
let(:not_followed) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
Fabricate(:follow, account: account, target_account: followed)
|
||||
|
||||
@self_public_status = Fabricate(:status, account: account, visibility: :public)
|
||||
@self_direct_status = Fabricate(:status, account: account, visibility: :direct)
|
||||
@followed_public_status = Fabricate(:status, account: followed, visibility: :public)
|
||||
@followed_direct_status = Fabricate(:status, account: followed, visibility: :direct)
|
||||
@not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct)
|
||||
|
||||
@results = Status.as_direct_timeline(account)
|
||||
end
|
||||
|
||||
it 'does not include public statuses from self' do
|
||||
expect(@results).to_not include(@self_public_status)
|
||||
end
|
||||
|
||||
it 'includes direct statuses from self' do
|
||||
expect(@results).to include(@self_direct_status)
|
||||
end
|
||||
|
||||
it 'does not include public statuses from followed' do
|
||||
expect(@results).to_not include(@followed_public_status)
|
||||
end
|
||||
|
||||
it 'includes direct statuses mentioning recipient from followed' do
|
||||
Fabricate(:mention, account: account, status: @followed_direct_status)
|
||||
expect(@results).to include(@followed_direct_status)
|
||||
end
|
||||
|
||||
it 'does not include direct statuses not mentioning recipient from followed' do
|
||||
expect(@results).to_not include(@followed_direct_status)
|
||||
end
|
||||
|
||||
it 'includes direct statuses mentioning recipient from non-followed' do
|
||||
Fabricate(:mention, account: account, status: @not_followed_direct_status)
|
||||
expect(@results).to include(@not_followed_direct_status)
|
||||
end
|
||||
|
||||
it 'does not include direct statuses not mentioning recipient from non-followed' do
|
||||
expect(@results).to_not include(@not_followed_direct_status)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '.as_public_timeline' do
|
||||
it 'only includes statuses with public visibility' do
|
||||
public_status = Fabricate(:status, visibility: :public)
|
||||
|
||||
@@ -402,6 +402,10 @@ const startWorker = (workerId) => {
|
||||
streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true);
|
||||
});
|
||||
|
||||
app.get('/api/v1/streaming/direct', (req, res) => {
|
||||
streamFrom(`timeline:direct:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
|
||||
});
|
||||
|
||||
app.get('/api/v1/streaming/hashtag', (req, res) => {
|
||||
streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
|
||||
});
|
||||
@@ -437,6 +441,9 @@ const startWorker = (workerId) => {
|
||||
case 'public:local':
|
||||
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'direct':
|
||||
streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'hashtag':
|
||||
streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user