mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 00:08:46 +00:00
Compare commits
49 Commits
with-masto
...
update-mas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc8532359b | ||
|
|
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
|
||||||
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
|
def destroy
|
||||||
@custom_emoji.destroy
|
@custom_emoji.destroy
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
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')
|
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to admin_custom_emojis_path(params[:page])
|
redirect_to admin_custom_emojis_path(page: params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
@@ -56,7 +64,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:custom_emoji).permit(:shortcode, :image)
|
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_custom_emojis
|
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
|
value.is_a?(Array) ? value.first : value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def as_array(value)
|
||||||
|
value.is_a?(Array) ? value : [value]
|
||||||
|
end
|
||||||
|
|
||||||
def value_or_id(value)
|
def value_or_id(value)
|
||||||
value.is_a?(String) || value.nil? ? value : value['id']
|
value.is_a?(String) || value.nil? ? value : value['id']
|
||||||
end
|
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' />
|
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
|
||||||
</LocalSettingsPageItem>
|
</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
|
<LocalSettingsPageItem
|
||||||
settings={settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'replies']}
|
item={['collapsed', 'auto', 'replies']}
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ properly and our intersection observer is good to go.
|
|||||||
muted,
|
muted,
|
||||||
id,
|
id,
|
||||||
intersectionObserverWrapper,
|
intersectionObserverWrapper,
|
||||||
|
prepend,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
|
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
|
||||||
|
|
||||||
@@ -299,6 +300,9 @@ properly and our intersection observer is good to go.
|
|||||||
node.clientHeight > (
|
node.clientHeight > (
|
||||||
status.get('media_attachments').size && !muted ? 650 : 400
|
status.get('media_attachments').size && !muted ? 650 : 400
|
||||||
)
|
)
|
||||||
|
) || (
|
||||||
|
autoCollapseSettings.get('reblogs') &&
|
||||||
|
prepend === 'reblogged_by'
|
||||||
) || (
|
) || (
|
||||||
autoCollapseSettings.get('replies') &&
|
autoCollapseSettings.get('replies') &&
|
||||||
status.get('in_reply_to_id', null) !== null
|
status.get('in_reply_to_id', null) !== null
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"settings.auto_collapse_lengthy": "Lengthy toots",
|
"settings.auto_collapse_lengthy": "Lengthy toots",
|
||||||
"settings.auto_collapse_media": "Toots with media",
|
"settings.auto_collapse_media": "Toots with media",
|
||||||
"settings.auto_collapse_notifications": "Notifications",
|
"settings.auto_collapse_notifications": "Notifications",
|
||||||
|
"settings.auto_collapse_reblogs": "Boosts",
|
||||||
"settings.auto_collapse_replies": "Replies",
|
"settings.auto_collapse_replies": "Replies",
|
||||||
"settings.close": "Close",
|
"settings.close": "Close",
|
||||||
"settings.collapsed_statuses": "Collapsed toots",
|
"settings.collapsed_statuses": "Collapsed toots",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const initialState = ImmutableMap({
|
|||||||
all : false,
|
all : false,
|
||||||
notifications : true,
|
notifications : true,
|
||||||
lengthy : true,
|
lengthy : true,
|
||||||
|
reblogs : false,
|
||||||
replies : false,
|
replies : false,
|
||||||
media : false,
|
media : false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
refreshHomeTimeline,
|
refreshHomeTimeline,
|
||||||
refreshCommunityTimeline,
|
refreshCommunityTimeline,
|
||||||
refreshPublicTimeline,
|
refreshPublicTimeline,
|
||||||
|
refreshDirectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
|
|
||||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
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') {
|
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertOrRefresh('community', refreshCommunityTimeline);
|
insertOrRefresh('community', refreshCommunityTimeline);
|
||||||
insertOrRefresh('public', refreshPublicTimeline);
|
insertOrRefresh('public', refreshPublicTimeline);
|
||||||
|
} else if (response.data.visibility === 'direct') {
|
||||||
|
insertOrRefresh('direct', refreshDirectTimeline);
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
|
|||||||
@@ -92,3 +92,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
|
|||||||
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
|
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
|
||||||
export const connectPublicStream = () => connectTimelineStream('public', 'public');
|
export const connectPublicStream = () => connectTimelineStream('public', 'public');
|
||||||
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
|
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 refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
|
||||||
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
||||||
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
|
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 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 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}`);
|
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 expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
|
||||||
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
||||||
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
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 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 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}`);
|
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}>
|
<div className={wrapperClassName}>
|
||||||
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
<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`} />
|
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||||
{title}
|
<span className='column-header__title'>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{backButton}
|
{backButton}
|
||||||
{ notifCleaning ? (
|
{ notifCleaning ? (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
|
|||||||
|
|
||||||
const getCustomEmojis = createSelector([
|
const getCustomEmojis = createSelector([
|
||||||
state => state.get('custom_emojis'),
|
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 aShort = a.get('shortcode').toLowerCase();
|
||||||
const bShort = b.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' },
|
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
|
||||||
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
|
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
|
||||||
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
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' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
@@ -78,18 +79,22 @@ export default class GettingStarted extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems = navItems.concat([
|
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
|
||||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
|
||||||
<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' />);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems = navItems.concat([
|
navItems = navItems.concat([
|
||||||
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||||
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
|
<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 (
|
return (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
|
|||||||
import ColumnLoading from './column_loading';
|
import ColumnLoading from './column_loading';
|
||||||
import DrawerLoading from './drawer_loading';
|
import DrawerLoading from './drawer_loading';
|
||||||
import BundleColumnError from './bundle_column_error';
|
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 detectPassiveEvents from 'detect-passive-events';
|
||||||
import { scrollRight } from '../../../scroll';
|
import { scrollRight } from '../../../scroll';
|
||||||
@@ -23,6 +23,7 @@ const componentMap = {
|
|||||||
'PUBLIC': PublicTimeline,
|
'PUBLIC': PublicTimeline,
|
||||||
'COMMUNITY': CommunityTimeline,
|
'COMMUNITY': CommunityTimeline,
|
||||||
'HASHTAG': HashtagTimeline,
|
'HASHTAG': HashtagTimeline,
|
||||||
|
'DIRECT': DirectTimeline,
|
||||||
'FAVOURITES': FavouritedStatuses,
|
'FAVOURITES': FavouritedStatuses,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
Following,
|
Following,
|
||||||
Reblogs,
|
Reblogs,
|
||||||
Favourites,
|
Favourites,
|
||||||
|
DirectTimeline,
|
||||||
HashtagTimeline,
|
HashtagTimeline,
|
||||||
Notifications,
|
Notifications,
|
||||||
FollowRequests,
|
FollowRequests,
|
||||||
@@ -71,6 +72,7 @@ const keyMap = {
|
|||||||
goToNotifications: 'g n',
|
goToNotifications: 'g n',
|
||||||
goToLocal: 'g l',
|
goToLocal: 'g l',
|
||||||
goToFederated: 'g t',
|
goToFederated: 'g t',
|
||||||
|
goToDirect: 'g d',
|
||||||
goToStart: 'g s',
|
goToStart: 'g s',
|
||||||
goToFavourites: 'g f',
|
goToFavourites: 'g f',
|
||||||
goToPinned: 'g p',
|
goToPinned: 'g p',
|
||||||
@@ -302,6 +304,10 @@ export default class UI extends React.Component {
|
|||||||
this.context.router.history.push('/timelines/public');
|
this.context.router.history.push('/timelines/public');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHotkeyGoToDirect = () => {
|
||||||
|
this.context.router.history.push('/timelines/direct');
|
||||||
|
}
|
||||||
|
|
||||||
handleHotkeyGoToStart = () => {
|
handleHotkeyGoToStart = () => {
|
||||||
this.context.router.history.push('/getting-started');
|
this.context.router.history.push('/getting-started');
|
||||||
}
|
}
|
||||||
@@ -357,6 +363,7 @@ export default class UI extends React.Component {
|
|||||||
goToNotifications: this.handleHotkeyGoToNotifications,
|
goToNotifications: this.handleHotkeyGoToNotifications,
|
||||||
goToLocal: this.handleHotkeyGoToLocal,
|
goToLocal: this.handleHotkeyGoToLocal,
|
||||||
goToFederated: this.handleHotkeyGoToFederated,
|
goToFederated: this.handleHotkeyGoToFederated,
|
||||||
|
goToDirect: this.handleHotkeyGoToDirect,
|
||||||
goToStart: this.handleHotkeyGoToStart,
|
goToStart: this.handleHotkeyGoToStart,
|
||||||
goToFavourites: this.handleHotkeyGoToFavourites,
|
goToFavourites: this.handleHotkeyGoToFavourites,
|
||||||
goToPinned: this.handleHotkeyGoToPinned,
|
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/home' component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
|
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
|
||||||
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} 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='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export function HashtagTimeline () {
|
|||||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DirectTimeline() {
|
||||||
|
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
||||||
|
}
|
||||||
|
|
||||||
export function Status () {
|
export function Status () {
|
||||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -755,6 +755,19 @@
|
|||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/compose/index.json"
|
"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": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
@@ -816,6 +829,10 @@
|
|||||||
"defaultMessage": "Local timeline",
|
"defaultMessage": "Local timeline",
|
||||||
"id": "navigation_bar.community_timeline"
|
"id": "navigation_bar.community_timeline"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Direct messages",
|
||||||
|
"id": "navigation_bar.direct"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Preferences",
|
"defaultMessage": "Preferences",
|
||||||
"id": "navigation_bar.preferences"
|
"id": "navigation_bar.preferences"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"bundle_modal_error.retry": "Try again",
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
|
"column.direct": "Direct messages",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
"emoji_button.symbols": "Symbols",
|
"emoji_button.symbols": "Symbols",
|
||||||
"emoji_button.travel": "Travel & Places",
|
"emoji_button.travel": "Travel & Places",
|
||||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
"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.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": "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.home.public_timeline": "the public timeline",
|
||||||
@@ -106,6 +108,7 @@
|
|||||||
"missing_indicator.label": "Not found",
|
"missing_indicator.label": "Not found",
|
||||||
"navigation_bar.blocks": "Blocked users",
|
"navigation_bar.blocks": "Blocked users",
|
||||||
"navigation_bar.community_timeline": "Local timeline",
|
"navigation_bar.community_timeline": "Local timeline",
|
||||||
|
"navigation_bar.direct": "Direct messages",
|
||||||
"navigation_bar.edit_profile": "Edit profile",
|
"navigation_bar.edit_profile": "Edit profile",
|
||||||
"navigation_bar.favourites": "Favourites",
|
"navigation_bar.favourites": "Favourites",
|
||||||
"navigation_bar.follow_requests": "Follow requests",
|
"navigation_bar.follow_requests": "Follow requests",
|
||||||
|
|||||||
@@ -1,221 +1,221 @@
|
|||||||
{
|
{
|
||||||
"account.block": "Bloki @{name}",
|
"account.block": "Bloki @{name}",
|
||||||
"account.block_domain": "Hide everything from {domain}",
|
"account.block_domain": "Kaŝi ĉion el {domain}",
|
||||||
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
|
"account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
|
||||||
"account.edit_profile": "Redakti la profilon",
|
"account.edit_profile": "Redakti la profilon",
|
||||||
"account.follow": "Sekvi",
|
"account.follow": "Sekvi",
|
||||||
"account.followers": "Sekvantoj",
|
"account.followers": "Sekvantoj",
|
||||||
"account.follows": "Sekvatoj",
|
"account.follows": "Sekvatoj",
|
||||||
"account.follows_you": "Sekvas vin",
|
"account.follows_you": "Sekvas vin",
|
||||||
"account.media": "Media",
|
"account.media": "Sonbildaĵoj",
|
||||||
"account.mention": "Mencii @{name}",
|
"account.mention": "Mencii @{name}",
|
||||||
"account.mute": "Mute @{name}",
|
"account.mute": "Silentigi @{name}",
|
||||||
"account.posts": "Mesaĝoj",
|
"account.posts": "Mesaĝoj",
|
||||||
"account.report": "Report @{name}",
|
"account.report": "Signali @{name}",
|
||||||
"account.requested": "Atendas aprobon",
|
"account.requested": "Atendas aprobon",
|
||||||
"account.share": "Share @{name}'s profile",
|
"account.share": "Diskonigi la profilon de @{name}",
|
||||||
"account.unblock": "Malbloki @{name}",
|
"account.unblock": "Malbloki @{name}",
|
||||||
"account.unblock_domain": "Unhide {domain}",
|
"account.unblock_domain": "Malkaŝi {domain}",
|
||||||
"account.unfollow": "Malsekvi",
|
"account.unfollow": "Ne plus sekvi",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Malsilentigi @{name}",
|
||||||
"account.view_full_profile": "View full profile",
|
"account.view_full_profile": "Vidi plenan profilon",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
|
||||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
"bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
|
||||||
"bundle_column_error.retry": "Try again",
|
"bundle_column_error.retry": "Bonvolu reprovi",
|
||||||
"bundle_column_error.title": "Network error",
|
"bundle_column_error.title": "Reta eraro",
|
||||||
"bundle_modal_error.close": "Close",
|
"bundle_modal_error.close": "Fermi",
|
||||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
"bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
|
||||||
"bundle_modal_error.retry": "Try again",
|
"bundle_modal_error.retry": "Bonvolu reprovi",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blokitaj uzantoj",
|
||||||
"column.community": "Loka tempolinio",
|
"column.community": "Loka tempolinio",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favoritoj",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Abonpetoj",
|
||||||
"column.home": "Hejmo",
|
"column.home": "Hejmo",
|
||||||
"column.mutes": "Muted users",
|
"column.mutes": "Silentigitaj uzantoj",
|
||||||
"column.notifications": "Sciigoj",
|
"column.notifications": "Sciigoj",
|
||||||
"column.pins": "Pinned toot",
|
"column.pins": "Alpinglitaj pepoj",
|
||||||
"column.public": "Fratara tempolinio",
|
"column.public": "Fratara tempolinio",
|
||||||
"column_back_button.label": "Reveni",
|
"column_back_button.label": "Reveni",
|
||||||
"column_header.hide_settings": "Hide settings",
|
"column_header.hide_settings": "Kaŝi agordojn",
|
||||||
"column_header.moveLeft_settings": "Move column to the left",
|
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
|
||||||
"column_header.moveRight_settings": "Move column to the right",
|
"column_header.moveRight_settings": "Movi kolumnon dekstren",
|
||||||
"column_header.pin": "Pin",
|
"column_header.pin": "Alpingli",
|
||||||
"column_header.show_settings": "Show settings",
|
"column_header.show_settings": "Malkaŝi agordojn",
|
||||||
"column_header.unpin": "Unpin",
|
"column_header.unpin": "Depingli",
|
||||||
"column_subheading.navigation": "Navigation",
|
"column_subheading.navigation": "Navigado",
|
||||||
"column_subheading.settings": "Settings",
|
"column_subheading.settings": "Agordoj",
|
||||||
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
"compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
|
||||||
"compose_form.lock_disclaimer.lock": "locked",
|
"compose_form.lock_disclaimer.lock": "ŝlosita",
|
||||||
"compose_form.placeholder": "Pri kio vi pensas?",
|
"compose_form.placeholder": "Pri kio vi pensas?",
|
||||||
"compose_form.publish": "Hup",
|
"compose_form.publish": "Hup",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
|
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
|
||||||
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
|
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
|
||||||
"compose_form.spoiler_placeholder": "Content warning",
|
"compose_form.spoiler_placeholder": "Skribu tie vian averton",
|
||||||
"confirmation_modal.cancel": "Cancel",
|
"confirmation_modal.cancel": "Malfari",
|
||||||
"confirmations.block.confirm": "Block",
|
"confirmations.block.confirm": "Bloki",
|
||||||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
"confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
|
||||||
"confirmations.delete.confirm": "Delete",
|
"confirmations.delete.confirm": "Malaperigi",
|
||||||
"confirmations.delete.message": "Are you sure you want to delete this status?",
|
"confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
|
||||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
"confirmations.domain_block.confirm": "Kaŝi la tutan reton",
|
||||||
"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.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": "Mute",
|
"confirmations.mute.confirm": "Silentigi",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
|
||||||
"confirmations.unfollow.confirm": "Unfollow",
|
"confirmations.unfollow.confirm": "Ne plu sekvi",
|
||||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
"confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
|
||||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
"embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Ĝi aperos tiel:",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Aktivecoj",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "Personaj",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flagoj",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Manĝi kaj trinki",
|
||||||
"emoji_button.label": "Insert emoji",
|
"emoji_button.label": "Enmeti mieneton",
|
||||||
"emoji_button.nature": "Nature",
|
"emoji_button.nature": "Naturo",
|
||||||
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
|
"emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
|
||||||
"emoji_button.objects": "Objects",
|
"emoji_button.objects": "Objektoj",
|
||||||
"emoji_button.people": "People",
|
"emoji_button.people": "Homoj",
|
||||||
"emoji_button.recent": "Frequently used",
|
"emoji_button.recent": "Ofte uzataj",
|
||||||
"emoji_button.search": "Search...",
|
"emoji_button.search": "Serĉo…",
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "Rezultatoj de serĉo",
|
||||||
"emoji_button.symbols": "Symbols",
|
"emoji_button.symbols": "Simboloj",
|
||||||
"emoji_button.travel": "Travel & Places",
|
"emoji_button.travel": "Vojaĝoj & lokoj",
|
||||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
|
||||||
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
|
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
|
||||||
"empty_column.home.public_timeline": "the public timeline",
|
"empty_column.home.public_timeline": "la publika tempolinio",
|
||||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
"empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
|
||||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
|
"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": "Authorize",
|
"follow_request.authorize": "Akcepti",
|
||||||
"follow_request.reject": "Reject",
|
"follow_request.reject": "Rifuzi",
|
||||||
"getting_started.appsshort": "Apps",
|
"getting_started.appsshort": "Aplikaĵoj",
|
||||||
"getting_started.faq": "FAQ",
|
"getting_started.faq": "Oftaj demandoj",
|
||||||
"getting_started.heading": "Por komenci",
|
"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.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
|
||||||
"getting_started.userguide": "User Guide",
|
"getting_started.userguide": "Gvidilo de uzo",
|
||||||
"home.column_settings.advanced": "Advanced",
|
"home.column_settings.advanced": "Precizaj agordoj",
|
||||||
"home.column_settings.basic": "Basic",
|
"home.column_settings.basic": "Bazaj agordoj",
|
||||||
"home.column_settings.filter_regex": "Filter out by regular expressions",
|
"home.column_settings.filter_regex": "Forfiltri per regulesprimo",
|
||||||
"home.column_settings.show_reblogs": "Show boosts",
|
"home.column_settings.show_reblogs": "Montri diskonigojn",
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"home.column_settings.show_replies": "Montri respondojn",
|
||||||
"home.settings": "Column settings",
|
"home.settings": "Agordoj de la kolumno",
|
||||||
"lightbox.close": "Fermi",
|
"lightbox.close": "Fermi",
|
||||||
"lightbox.next": "Next",
|
"lightbox.next": "Malantaŭa",
|
||||||
"lightbox.previous": "Previous",
|
"lightbox.previous": "Antaŭa",
|
||||||
"loading_indicator.label": "Ŝarĝanta...",
|
"loading_indicator.label": "Ŝarganta…",
|
||||||
"media_gallery.toggle_visible": "Toggle visibility",
|
"media_gallery.toggle_visible": "Baskuli videblecon",
|
||||||
"missing_indicator.label": "Not found",
|
"missing_indicator.label": "Ne trovita",
|
||||||
"navigation_bar.blocks": "Blocked users",
|
"navigation_bar.blocks": "Blokitaj uzantoj",
|
||||||
"navigation_bar.community_timeline": "Loka tempolinio",
|
"navigation_bar.community_timeline": "Loka tempolinio",
|
||||||
"navigation_bar.edit_profile": "Redakti la profilon",
|
"navigation_bar.edit_profile": "Redakti la profilon",
|
||||||
"navigation_bar.favourites": "Favourites",
|
"navigation_bar.favourites": "Favoritaj",
|
||||||
"navigation_bar.follow_requests": "Follow requests",
|
"navigation_bar.follow_requests": "Abonpetoj",
|
||||||
"navigation_bar.info": "Extended information",
|
"navigation_bar.info": "Plia informo",
|
||||||
"navigation_bar.logout": "Elsaluti",
|
"navigation_bar.logout": "Elsaluti",
|
||||||
"navigation_bar.mutes": "Muted users",
|
"navigation_bar.mutes": "Silentigitaj uzantoj",
|
||||||
"navigation_bar.pins": "Pinned toots",
|
"navigation_bar.pins": "Alpinglitaj pepoj",
|
||||||
"navigation_bar.preferences": "Preferoj",
|
"navigation_bar.preferences": "Preferoj",
|
||||||
"navigation_bar.public_timeline": "Fratara tempolinio",
|
"navigation_bar.public_timeline": "Fratara tempolinio",
|
||||||
"notification.favourite": "{name} favoris vian mesaĝon",
|
"notification.favourite": "{name} favoris vian mesaĝon",
|
||||||
"notification.follow": "{name} sekvis vin",
|
"notification.follow": "{name} sekvis vin",
|
||||||
"notification.mention": "{name} menciis vin",
|
"notification.mention": "{name} menciis vin",
|
||||||
"notification.reblog": "{name} diskonigis vian mesaĝon",
|
"notification.reblog": "{name} diskonigis vian mesaĝon",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Forviŝi la sciigojn",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
"notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
|
||||||
"notifications.column_settings.alert": "Retumilaj atentigoj",
|
"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.follow": "Novaj sekvantoj:",
|
||||||
"notifications.column_settings.mention": "Mencioj:",
|
"notifications.column_settings.mention": "Mencioj:",
|
||||||
"notifications.column_settings.push": "Push notifications",
|
"notifications.column_settings.push": "Puŝsciigoj",
|
||||||
"notifications.column_settings.push_meta": "This device",
|
"notifications.column_settings.push_meta": "Tiu ĉi aparato",
|
||||||
"notifications.column_settings.reblog": "Diskonigoj:",
|
"notifications.column_settings.reblog": "Diskonigoj:",
|
||||||
"notifications.column_settings.show": "Montri en kolono",
|
"notifications.column_settings.show": "Montri en kolono",
|
||||||
"notifications.column_settings.sound": "Play sound",
|
"notifications.column_settings.sound": "Eligi sonon",
|
||||||
"onboarding.done": "Done",
|
"onboarding.done": "Farita",
|
||||||
"onboarding.next": "Next",
|
"onboarding.next": "Malantaŭa",
|
||||||
"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_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": "The home timeline shows posts from people you follow.",
|
"onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
|
||||||
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
|
"onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
|
||||||
"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.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
|
||||||
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
|
"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": "Welcome to Mastodon!",
|
"onboarding.page_one.welcome": "Bonvenon al Mastodono!",
|
||||||
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
|
"onboarding.page_six.admin": "Via instancestro estas {admin}.",
|
||||||
"onboarding.page_six.almost_done": "Almost done...",
|
"onboarding.page_six.almost_done": "Estas preskaŭ finita…",
|
||||||
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
"onboarding.page_six.appetoot": "Bonan a‘pepi’ton!",
|
||||||
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
|
"onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan a‘pepi’ton!",
|
||||||
"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.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": "community guidelines",
|
"onboarding.page_six.guidelines": "komunreguloj",
|
||||||
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
|
"onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
|
||||||
"onboarding.page_six.various_app": "mobile apps",
|
"onboarding.page_six.various_app": "telefon-aplikaĵoj",
|
||||||
"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.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
|
||||||
"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_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": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
|
"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": "Skip",
|
"onboarding.skip": "Pasigi",
|
||||||
"privacy.change": "Adjust status privacy",
|
"privacy.change": "Alĝustigi la privateco de la mesaĝo",
|
||||||
"privacy.direct.long": "Post to mentioned users only",
|
"privacy.direct.long": "Vidigi nur al la menciitaj personoj",
|
||||||
"privacy.direct.short": "Direct",
|
"privacy.direct.short": "Rekta",
|
||||||
"privacy.private.long": "Post to followers only",
|
"privacy.private.long": "Vidigi nur al viaj sekvantoj",
|
||||||
"privacy.private.short": "Followers-only",
|
"privacy.private.short": "Nursekvanta",
|
||||||
"privacy.public.long": "Post to public timelines",
|
"privacy.public.long": "Vidigi en publikaj tempolinioj",
|
||||||
"privacy.public.short": "Public",
|
"privacy.public.short": "Publika",
|
||||||
"privacy.unlisted.long": "Do not show in public timelines",
|
"privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Nelistigita",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}t",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number}h",
|
||||||
"relative_time.just_now": "now",
|
"relative_time.just_now": "nun",
|
||||||
"relative_time.minutes": "{number}m",
|
"relative_time.minutes": "{number}m",
|
||||||
"relative_time.seconds": "{number}s",
|
"relative_time.seconds": "{number}s",
|
||||||
"reply_indicator.cancel": "Rezigni",
|
"reply_indicator.cancel": "Malfari",
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Pliaj komentoj",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Sendi",
|
||||||
"report.target": "Reporting",
|
"report.target": "Signalaĵo",
|
||||||
"search.placeholder": "Serĉi",
|
"search.placeholder": "Serĉi",
|
||||||
"search_popout.search_format": "Advanced search format",
|
"search_popout.search_format": "Detala serĉo",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "kradvorto",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "statkonigo",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "uzanto",
|
||||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
"search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
|
||||||
"standalone.public_title": "A look inside...",
|
"standalone.public_title": "Rigardeti…",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
|
||||||
"status.delete": "Forigi",
|
"status.delete": "Forigi",
|
||||||
"status.embed": "Embed",
|
"status.embed": "Enmeti",
|
||||||
"status.favourite": "Favori",
|
"status.favourite": "Favori",
|
||||||
"status.load_more": "Load more",
|
"status.load_more": "Ŝargi plie",
|
||||||
"status.media_hidden": "Media hidden",
|
"status.media_hidden": "Sonbildaĵo kaŝita",
|
||||||
"status.mention": "Mencii @{name}",
|
"status.mention": "Mencii @{name}",
|
||||||
"status.more": "More",
|
"status.more": "Pli",
|
||||||
"status.mute_conversation": "Mute conversation",
|
"status.mute_conversation": "Silentigi konversacion",
|
||||||
"status.open": "Expand this status",
|
"status.open": "Disfaldi statkonigon",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pingli al la profilo",
|
||||||
"status.reblog": "Diskonigi",
|
"status.reblog": "Diskonigi",
|
||||||
"status.reblogged_by": "{name} diskonigita",
|
"status.reblogged_by": "{name} diskonigis",
|
||||||
"status.reply": "Respondi",
|
"status.reply": "Respondi",
|
||||||
"status.replyAll": "Reply to thread",
|
"status.replyAll": "Respondi al la fadeno",
|
||||||
"status.report": "Report @{name}",
|
"status.report": "Signali @{name}",
|
||||||
"status.sensitive_toggle": "Alklaki por vidi",
|
"status.sensitive_toggle": "Alklaki por vidi",
|
||||||
"status.sensitive_warning": "Tikla enhavo",
|
"status.sensitive_warning": "Tikla enhavo",
|
||||||
"status.share": "Share",
|
"status.share": "Diskonigi",
|
||||||
"status.show_less": "Show less",
|
"status.show_less": "Refaldi",
|
||||||
"status.show_more": "Show more",
|
"status.show_more": "Disfaldi",
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Malsilentigi konversacion",
|
||||||
"status.unpin": "Unpin from profile",
|
"status.unpin": "Depingli de profilo",
|
||||||
"tabs_bar.compose": "Ekskribi",
|
"tabs_bar.compose": "Ekskribi",
|
||||||
"tabs_bar.federated_timeline": "Federated",
|
"tabs_bar.federated_timeline": "Federacia tempolinio",
|
||||||
"tabs_bar.home": "Hejmo",
|
"tabs_bar.home": "Hejmo",
|
||||||
"tabs_bar.local_timeline": "Local",
|
"tabs_bar.local_timeline": "Loka tempolinio",
|
||||||
"tabs_bar.notifications": "Sciigoj",
|
"tabs_bar.notifications": "Sciigoj",
|
||||||
"upload_area.title": "Drag & drop to upload",
|
"upload_area.title": "Algliti por alŝuti",
|
||||||
"upload_button.label": "Aldoni enhavaĵon",
|
"upload_button.label": "Aldoni sonbildaĵon",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "Priskribi por la misvidantaj",
|
||||||
"upload_form.undo": "Malfari",
|
"upload_form.undo": "Malfari",
|
||||||
"upload_progress.label": "Uploading...",
|
"upload_progress.label": "Alŝutanta…",
|
||||||
"video.close": "Close video",
|
"video.close": "Fermi videon",
|
||||||
"video.exit_fullscreen": "Exit full screen",
|
"video.exit_fullscreen": "Eliri el plenekrano",
|
||||||
"video.expand": "Expand video",
|
"video.expand": "Vastigi videon",
|
||||||
"video.fullscreen": "Full screen",
|
"video.fullscreen": "Igi plenekrane",
|
||||||
"video.hide": "Hide video",
|
"video.hide": "Kaŝi videon",
|
||||||
"video.mute": "Mute sound",
|
"video.mute": "Silentigi",
|
||||||
"video.pause": "Pause",
|
"video.pause": "Paŭzi",
|
||||||
"video.play": "Play",
|
"video.play": "Legi",
|
||||||
"video.unmute": "Unmute sound"
|
"video.unmute": "Malsilentigi"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,20 +63,20 @@
|
|||||||
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
||||||
"confirmations.unfollow.confirm": "Отписаться",
|
"confirmations.unfollow.confirm": "Отписаться",
|
||||||
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
|
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
|
||||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
"embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Так это будет выглядеть:",
|
||||||
"emoji_button.activity": "Занятия",
|
"emoji_button.activity": "Занятия",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "Собственные",
|
||||||
"emoji_button.flags": "Флаги",
|
"emoji_button.flags": "Флаги",
|
||||||
"emoji_button.food": "Еда и напитки",
|
"emoji_button.food": "Еда и напитки",
|
||||||
"emoji_button.label": "Вставить эмодзи",
|
"emoji_button.label": "Вставить эмодзи",
|
||||||
"emoji_button.nature": "Природа",
|
"emoji_button.nature": "Природа",
|
||||||
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
|
"emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
|
||||||
"emoji_button.objects": "Предметы",
|
"emoji_button.objects": "Предметы",
|
||||||
"emoji_button.people": "Люди",
|
"emoji_button.people": "Люди",
|
||||||
"emoji_button.recent": "Frequently used",
|
"emoji_button.recent": "Последние",
|
||||||
"emoji_button.search": "Найти...",
|
"emoji_button.search": "Найти...",
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "Результаты поиска",
|
||||||
"emoji_button.symbols": "Символы",
|
"emoji_button.symbols": "Символы",
|
||||||
"emoji_button.travel": "Путешествия",
|
"emoji_button.travel": "Путешествия",
|
||||||
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
|
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
|
||||||
@@ -159,34 +159,34 @@
|
|||||||
"privacy.public.short": "Публичный",
|
"privacy.public.short": "Публичный",
|
||||||
"privacy.unlisted.long": "Не показывать в лентах",
|
"privacy.unlisted.long": "Не показывать в лентах",
|
||||||
"privacy.unlisted.short": "Скрытый",
|
"privacy.unlisted.short": "Скрытый",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}д",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number}ч",
|
||||||
"relative_time.just_now": "now",
|
"relative_time.just_now": "только что",
|
||||||
"relative_time.minutes": "{number}m",
|
"relative_time.minutes": "{number}м",
|
||||||
"relative_time.seconds": "{number}s",
|
"relative_time.seconds": "{number}с",
|
||||||
"reply_indicator.cancel": "Отмена",
|
"reply_indicator.cancel": "Отмена",
|
||||||
"report.placeholder": "Комментарий",
|
"report.placeholder": "Комментарий",
|
||||||
"report.submit": "Отправить",
|
"report.submit": "Отправить",
|
||||||
"report.target": "Жалуемся на",
|
"report.target": "Жалуемся на",
|
||||||
"search.placeholder": "Поиск",
|
"search.placeholder": "Поиск",
|
||||||
"search_popout.search_format": "Advanced search format",
|
"search_popout.search_format": "Продвинутый формат поиска",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "хэштег",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "статус",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "пользователь",
|
||||||
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
|
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
|
||||||
"standalone.public_title": "A look inside...",
|
"standalone.public_title": "Прямо сейчас",
|
||||||
"status.cannot_reblog": "Этот статус не может быть продвинут",
|
"status.cannot_reblog": "Этот статус не может быть продвинут",
|
||||||
"status.delete": "Удалить",
|
"status.delete": "Удалить",
|
||||||
"status.embed": "Embed",
|
"status.embed": "Встроить",
|
||||||
"status.favourite": "Нравится",
|
"status.favourite": "Нравится",
|
||||||
"status.load_more": "Показать еще",
|
"status.load_more": "Показать еще",
|
||||||
"status.media_hidden": "Медиаконтент скрыт",
|
"status.media_hidden": "Медиаконтент скрыт",
|
||||||
"status.mention": "Упомянуть @{name}",
|
"status.mention": "Упомянуть @{name}",
|
||||||
"status.more": "More",
|
"status.more": "Больше",
|
||||||
"status.mute_conversation": "Заглушить тред",
|
"status.mute_conversation": "Заглушить тред",
|
||||||
"status.open": "Развернуть статус",
|
"status.open": "Развернуть статус",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Закрепить в профиле",
|
||||||
"status.reblog": "Продвинуть",
|
"status.reblog": "Продвинуть",
|
||||||
"status.reblogged_by": "{name} продвинул(а)",
|
"status.reblogged_by": "{name} продвинул(а)",
|
||||||
"status.reply": "Ответить",
|
"status.reply": "Ответить",
|
||||||
@@ -194,11 +194,11 @@
|
|||||||
"status.report": "Пожаловаться",
|
"status.report": "Пожаловаться",
|
||||||
"status.sensitive_toggle": "Нажмите для просмотра",
|
"status.sensitive_toggle": "Нажмите для просмотра",
|
||||||
"status.sensitive_warning": "Чувствительный контент",
|
"status.sensitive_warning": "Чувствительный контент",
|
||||||
"status.share": "Share",
|
"status.share": "Поделиться",
|
||||||
"status.show_less": "Свернуть",
|
"status.show_less": "Свернуть",
|
||||||
"status.show_more": "Развернуть",
|
"status.show_more": "Развернуть",
|
||||||
"status.unmute_conversation": "Снять глушение с треда",
|
"status.unmute_conversation": "Снять глушение с треда",
|
||||||
"status.unpin": "Unpin from profile",
|
"status.unpin": "Открепить от профиля",
|
||||||
"tabs_bar.compose": "Написать",
|
"tabs_bar.compose": "Написать",
|
||||||
"tabs_bar.federated_timeline": "Глобальная",
|
"tabs_bar.federated_timeline": "Глобальная",
|
||||||
"tabs_bar.home": "Главная",
|
"tabs_bar.home": "Главная",
|
||||||
@@ -206,16 +206,16 @@
|
|||||||
"tabs_bar.notifications": "Уведомления",
|
"tabs_bar.notifications": "Уведомления",
|
||||||
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||||
"upload_button.label": "Добавить медиаконтент",
|
"upload_button.label": "Добавить медиаконтент",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "Описать для людей с нарушениями зрения",
|
||||||
"upload_form.undo": "Отменить",
|
"upload_form.undo": "Отменить",
|
||||||
"upload_progress.label": "Загрузка...",
|
"upload_progress.label": "Загрузка...",
|
||||||
"video.close": "Close video",
|
"video.close": "Закрыть видео",
|
||||||
"video.exit_fullscreen": "Exit full screen",
|
"video.exit_fullscreen": "Покинуть полноэкранный режим",
|
||||||
"video.expand": "Expand video",
|
"video.expand": "Развернуть видео",
|
||||||
"video.fullscreen": "Full screen",
|
"video.fullscreen": "Полноэкранный режим",
|
||||||
"video.hide": "Hide video",
|
"video.hide": "Скрыть видео",
|
||||||
"video.mute": "Mute sound",
|
"video.mute": "Заглушить звук",
|
||||||
"video.pause": "Pause",
|
"video.pause": "Пауза",
|
||||||
"video.play": "Play",
|
"video.play": "Пуск",
|
||||||
"video.unmute": "Unmute sound"
|
"video.unmute": "Включить звук"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ const initialState = ImmutableMap({
|
|||||||
body: '',
|
body: '',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
direct: ImmutableMap({
|
||||||
|
regex: ImmutableMap({
|
||||||
|
body: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultColumns = fromJS([
|
const defaultColumns = fromJS([
|
||||||
|
|||||||
@@ -2503,6 +2503,7 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.column-header {
|
.column-header {
|
||||||
|
display: flex;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
@@ -2528,12 +2529,10 @@ button.icon-button.active i.fa-retweet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.column-header__buttons {
|
.column-header__buttons {
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
margin: -15px;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__button {
|
.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 {
|
.text-btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -3465,7 +3472,6 @@ button.icon-button.active i.fa-retweet {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba($base-overlay-background, 0.7);
|
background: rgba($base-overlay-background, 0.7);
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-root__container {
|
.modal-root__container {
|
||||||
|
|||||||
Submodule app/javascript/themes/mastodon-go updated: 1ba394d1d7...74c0293e83
@@ -53,9 +53,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_tags(status)
|
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']
|
case tag['type']
|
||||||
when 'Hashtag'
|
when 'Hashtag'
|
||||||
process_hashtag tag, status
|
process_hashtag tag, status
|
||||||
@@ -103,9 +103,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_attachments(status)
|
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?
|
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
|
||||||
|
|
||||||
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ class FeedManager
|
|||||||
return false if receiver_id == status.account_id
|
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 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 = [status.account_id]
|
||||||
check_for_mutes.concat(status.mentions.pluck(:account_id))
|
check_for_mutes.concat(status.mentions.pluck(:account_id))
|
||||||
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
|
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
|
||||||
@@ -166,6 +168,18 @@ class FeedManager
|
|||||||
false
|
false
|
||||||
end
|
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)
|
def filter_from_mentions?(status, receiver_id)
|
||||||
return true if receiver_id == status.account_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 = 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 ||= (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
|
should_filter
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# disabled :boolean default(FALSE), not null
|
# disabled :boolean default(FALSE), not null
|
||||||
# uri :string
|
# uri :string
|
||||||
# image_remote_url :string
|
# image_remote_url :string
|
||||||
|
# visible_in_picker :boolean default(TRUE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomEmoji < ApplicationRecord
|
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])
|
where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
|
||||||
end
|
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)
|
def as_public_timeline(account = nil, local_only = false)
|
||||||
query = timeline_scope(local_only).without_replies
|
query = timeline_scope(local_only).without_replies
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class REST::CustomEmojiSerializer < ActiveModel::Serializer
|
class REST::CustomEmojiSerializer < ActiveModel::Serializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :shortcode, :url, :static_url
|
attributes :shortcode, :url, :static_url, :visible_in_picker
|
||||||
|
|
||||||
def url
|
def url
|
||||||
full_asset_url(object.image.url)
|
full_asset_url(object.image.url)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
# Cannot be batched
|
# Cannot be batched
|
||||||
statuses.each do |status|
|
statuses.each do |status|
|
||||||
unpush_from_public_timelines(status)
|
unpush_from_public_timelines(status)
|
||||||
|
unpush_from_direct_timelines(status) if status.direct_visibility?
|
||||||
batch_salmon_slaps(status) if status.local?
|
batch_salmon_slaps(status) if status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -100,6 +101,16 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
end
|
end
|
||||||
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)
|
def batch_salmon_slaps(status)
|
||||||
return if @mentions[status.id].empty?
|
return if @mentions[status.id].empty?
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ class FanOutOnWriteService < BaseService
|
|||||||
|
|
||||||
deliver_to_self(status) if status.account.local?
|
deliver_to_self(status) if status.account.local?
|
||||||
|
|
||||||
|
render_anonymous_payload(status)
|
||||||
|
|
||||||
if status.direct_visibility?
|
if status.direct_visibility?
|
||||||
deliver_to_mentioned_followers(status)
|
deliver_to_mentioned_followers(status)
|
||||||
|
deliver_to_direct_timelines(status)
|
||||||
else
|
else
|
||||||
deliver_to_followers(status)
|
deliver_to_followers(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
||||||
|
|
||||||
render_anonymous_payload(status)
|
|
||||||
deliver_to_hashtags(status)
|
deliver_to_hashtags(status)
|
||||||
|
|
||||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
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', @payload)
|
||||||
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class RemoveStatusService < BaseService
|
|||||||
remove_reblogs
|
remove_reblogs
|
||||||
remove_from_hashtags
|
remove_from_hashtags
|
||||||
remove_from_public
|
remove_from_public
|
||||||
|
remove_from_direct if status.direct_visibility?
|
||||||
|
|
||||||
@status.destroy!
|
@status.destroy!
|
||||||
|
|
||||||
@@ -121,6 +122,13 @@ class RemoveStatusService < BaseService
|
|||||||
Redis.current.publish('timeline:public:local', @payload) if @status.local?
|
Redis.current.publish('timeline:public:local', @payload) if @status.local?
|
||||||
end
|
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
|
def redis
|
||||||
Redis.current
|
Redis.current
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
- else
|
- else
|
||||||
= custom_emoji.domain
|
= custom_emoji.domain
|
||||||
%td
|
%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
|
= table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post
|
||||||
%td
|
%td
|
||||||
- if custom_emoji.disabled?
|
- 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/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/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/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}
|
%meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
|
||||||
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
|
%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',
|
'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';" ,
|
'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',
|
'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
|
end
|
||||||
|
|||||||
@@ -130,11 +130,15 @@ en:
|
|||||||
enable: Enable
|
enable: Enable
|
||||||
enabled_msg: Successfully enabled that emoji
|
enabled_msg: Successfully enabled that emoji
|
||||||
image_hint: PNG up to 50KB
|
image_hint: PNG up to 50KB
|
||||||
|
listed: Listed
|
||||||
new:
|
new:
|
||||||
title: Add new custom emoji
|
title: Add new custom emoji
|
||||||
shortcode: Shortcode
|
shortcode: Shortcode
|
||||||
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
|
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
|
||||||
title: Custom emojis
|
title: Custom emojis
|
||||||
|
unlisted: Unlisted
|
||||||
|
update_failed_msg: Could not update that emoji
|
||||||
|
updated_msg: Emoji successfully updated!
|
||||||
upload: Upload
|
upload: Upload
|
||||||
domain_blocks:
|
domain_blocks:
|
||||||
add_new: Add new
|
add_new: Add new
|
||||||
@@ -373,6 +377,14 @@ en:
|
|||||||
following: Following list
|
following: Following list
|
||||||
muting: Muting list
|
muting: Muting list
|
||||||
upload: Upload
|
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_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>.
|
landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
|
||||||
media_attachments:
|
media_attachments:
|
||||||
@@ -491,6 +503,7 @@ en:
|
|||||||
export: Data export
|
export: Data export
|
||||||
followers: Authorized followers
|
followers: Authorized followers
|
||||||
import: Import
|
import: Import
|
||||||
|
keyword_mutes: Muted keywords
|
||||||
notifications: Notifications
|
notifications: Notifications
|
||||||
preferences: Preferences
|
preferences: Preferences
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
|||||||
@@ -1,39 +1,77 @@
|
|||||||
---
|
---
|
||||||
ru:
|
ru:
|
||||||
about:
|
about:
|
||||||
|
about_hashtag_html: Это публичные статусы, отмеченные хэштегом <strong>#%{hashtag}</strong>. Вы можете взаимодействовать с ними при наличии у Вас аккаунта в глобальной сети Mastodon.
|
||||||
about_mastodon_html: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
|
about_mastodon_html: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
|
||||||
about_this: Об этом узле
|
about_this: Об этом узле
|
||||||
closed_registrations: В данный момент регистрация на этом узле закрыта.
|
closed_registrations: В данный момент регистрация на этом узле закрыта.
|
||||||
contact: Связаться
|
contact: Связаться
|
||||||
|
contact_missing: Не установлено
|
||||||
|
contact_unavailable: Недоступен
|
||||||
description_headline: Что такое %{domain}?
|
description_headline: Что такое %{domain}?
|
||||||
domain_count_after: другими узлами
|
domain_count_after: другими узлами
|
||||||
domain_count_before: Связан с
|
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: Другие узлы
|
other_instances: Другие узлы
|
||||||
source_code: Исходный код
|
source_code: Исходный код
|
||||||
status_count_after: статусов
|
status_count_after: статусов
|
||||||
status_count_before: Опубликовано
|
status_count_before: Опубликовано
|
||||||
user_count_after: пользователей
|
user_count_after: пользователей
|
||||||
user_count_before: Здесь живет
|
user_count_before: Здесь живет
|
||||||
|
what_is_mastodon: Что такое Mastodon?
|
||||||
accounts:
|
accounts:
|
||||||
follow: Подписаться
|
follow: Подписаться
|
||||||
followers: Подписчики
|
followers: Подписчики
|
||||||
following: Подписан(а)
|
following: Подписан(а)
|
||||||
|
media: Медиаконтент
|
||||||
nothing_here: Здесь ничего нет!
|
nothing_here: Здесь ничего нет!
|
||||||
people_followed_by: Люди, на которых подписан(а) %{name}
|
people_followed_by: Люди, на которых подписан(а) %{name}
|
||||||
people_who_follow: Подписчики %{name}
|
people_who_follow: Подписчики %{name}
|
||||||
posts: Посты
|
posts: Посты
|
||||||
|
posts_with_replies: Посты с ответами
|
||||||
remote_follow: Подписаться на удаленном узле
|
remote_follow: Подписаться на удаленном узле
|
||||||
|
reserved_username: Имя пользователя зарезервировано
|
||||||
|
roles:
|
||||||
|
admin: Администратор
|
||||||
unfollow: Отписаться
|
unfollow: Отписаться
|
||||||
admin:
|
admin:
|
||||||
|
account_moderation_notes:
|
||||||
|
account: Модератор
|
||||||
|
create: Создать
|
||||||
|
created_at: Дата
|
||||||
|
created_msg: Заметка модератора успешно создана!
|
||||||
|
delete: Удалить
|
||||||
|
destroyed_msg: Заметка модератора успешно удалена!
|
||||||
accounts:
|
accounts:
|
||||||
are_you_sure: Вы уверены?
|
are_you_sure: Вы уверены?
|
||||||
|
confirm: Подтвердить
|
||||||
|
confirmed: Подтверждено
|
||||||
|
disable_two_factor_authentication: Отключить 2FA
|
||||||
display_name: Отображаемое имя
|
display_name: Отображаемое имя
|
||||||
domain: Домен
|
domain: Домен
|
||||||
edit: Изменить
|
edit: Изменить
|
||||||
email: E-mail
|
email: E-mail
|
||||||
feed_url: URL фида
|
feed_url: URL фида
|
||||||
followers: Подписчики
|
followers: Подписчики
|
||||||
|
followers_url: URL подписчиков
|
||||||
follows: Подписки
|
follows: Подписки
|
||||||
|
inbox_url: URL входящих
|
||||||
|
ip: IP
|
||||||
location:
|
location:
|
||||||
all: Все
|
all: Все
|
||||||
local: Локальные
|
local: Локальные
|
||||||
@@ -45,6 +83,7 @@ ru:
|
|||||||
silenced: Заглушенные
|
silenced: Заглушенные
|
||||||
suspended: Заблокированные
|
suspended: Заблокированные
|
||||||
title: Модерация
|
title: Модерация
|
||||||
|
moderation_notes: Заметки модератора
|
||||||
most_recent_activity: Последняя активность
|
most_recent_activity: Последняя активность
|
||||||
most_recent_ip: Последний IP
|
most_recent_ip: Последний IP
|
||||||
not_subscribed: Не подписаны
|
not_subscribed: Не подписаны
|
||||||
@@ -52,19 +91,51 @@ ru:
|
|||||||
alphabetic: По алфавиту
|
alphabetic: По алфавиту
|
||||||
most_recent: По дате
|
most_recent: По дате
|
||||||
title: Порядок
|
title: Порядок
|
||||||
|
outbox_url: URL исходящих
|
||||||
perform_full_suspension: Полная блокировка
|
perform_full_suspension: Полная блокировка
|
||||||
profile_url: URL профиля
|
profile_url: URL профиля
|
||||||
|
protocol: Протокол
|
||||||
public: Публичный
|
public: Публичный
|
||||||
push_subscription_expires: Подписка PuSH истекает
|
push_subscription_expires: Подписка PuSH истекает
|
||||||
|
redownload: Обновить аватар
|
||||||
|
reset: Сбросить
|
||||||
reset_password: Сбросить пароль
|
reset_password: Сбросить пароль
|
||||||
|
resubscribe: Переподписаться
|
||||||
salmon_url: Salmon URL
|
salmon_url: Salmon URL
|
||||||
|
search: Поиск
|
||||||
|
shared_inbox_url: URL общих входящих
|
||||||
|
show:
|
||||||
|
created_reports: Жалобы, отправленные этим аккаунтом
|
||||||
|
report: жалоба
|
||||||
|
targeted_reports: Жалобы на этот аккаунт
|
||||||
silence: Глушение
|
silence: Глушение
|
||||||
statuses: Статусы
|
statuses: Статусы
|
||||||
|
subscribe: Подписаться
|
||||||
title: Аккаунты
|
title: Аккаунты
|
||||||
undo_silenced: Снять глушение
|
undo_silenced: Снять глушение
|
||||||
undo_suspension: Снять блокировку
|
undo_suspension: Снять блокировку
|
||||||
|
unsubscribe: Отписаться
|
||||||
username: Имя пользователя
|
username: Имя пользователя
|
||||||
web: WWW
|
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:
|
domain_blocks:
|
||||||
add_new: Добавить новую
|
add_new: Добавить новую
|
||||||
created_msg: Блокировка домена обрабатывается
|
created_msg: Блокировка домена обрабатывается
|
||||||
@@ -74,13 +145,15 @@ ru:
|
|||||||
create: Создать блокировку
|
create: Создать блокировку
|
||||||
hint: Блокировка домена не предотвратит создание новых аккаунтов в базе данных, но ретроактивно и автоматически применит указанные методы модерации для этих аккаунтов.
|
hint: Блокировка домена не предотвратит создание новых аккаунтов в базе данных, но ретроактивно и автоматически применит указанные методы модерации для этих аккаунтов.
|
||||||
severity:
|
severity:
|
||||||
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля."
|
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля. Используйте <strong>Ничего</strong>, если хотите только запретить медиаконтент."
|
||||||
|
noop: Ничего
|
||||||
silence: Глушение
|
silence: Глушение
|
||||||
suspend: Блокировка
|
suspend: Блокировка
|
||||||
title: Новая доменная блокировка
|
title: Новая доменная блокировка
|
||||||
reject_media: Запретить медиаконтент
|
reject_media: Запретить медиаконтент
|
||||||
reject_media_hint: Удаляет локально хранимый медиаконтент и запрещает его загрузку в будущем. Не имеет значения в случае блокировки.
|
reject_media_hint: Удаляет локально хранимый медиаконтент и запрещает его загрузку в будущем. Не имеет значения в случае блокировки.
|
||||||
severities:
|
severities:
|
||||||
|
noop: Ничего
|
||||||
silence: Глушение
|
silence: Глушение
|
||||||
suspend: Блокировка
|
suspend: Блокировка
|
||||||
severity: Строгость
|
severity: Строгость
|
||||||
@@ -97,13 +170,34 @@ ru:
|
|||||||
undo: Отменить
|
undo: Отменить
|
||||||
title: Доменные блокировки
|
title: Доменные блокировки
|
||||||
undo: Отемнить
|
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:
|
reports:
|
||||||
|
action_taken_by: 'Действие предпринято:'
|
||||||
|
are_you_sure: Вы уверены?
|
||||||
comment:
|
comment:
|
||||||
label: Комментарий
|
label: Комментарий
|
||||||
none: Нет
|
none: Нет
|
||||||
delete: Удалить
|
delete: Удалить
|
||||||
id: ID
|
id: ID
|
||||||
mark_as_resolved: Отметить как разрешенную
|
mark_as_resolved: Отметить как разрешенную
|
||||||
|
nsfw:
|
||||||
|
'false': Показать мультимедийные вложения
|
||||||
|
'true': Скрыть мультимедийные вложения
|
||||||
report: 'Жалоба #%{id}'
|
report: 'Жалоба #%{id}'
|
||||||
reported_account: Аккаунт нарушителя
|
reported_account: Аккаунт нарушителя
|
||||||
reported_by: Отправитель жалобы
|
reported_by: Отправитель жалобы
|
||||||
@@ -116,6 +210,9 @@ ru:
|
|||||||
unresolved: Неразрешенные
|
unresolved: Неразрешенные
|
||||||
view: Просмотреть
|
view: Просмотреть
|
||||||
settings:
|
settings:
|
||||||
|
bootstrap_timeline_accounts:
|
||||||
|
desc_html: Разделяйте имена пользователей запятыми. Сработает только для локальных незакрытых аккаунтов. По умолчанию включены все локальные администраторы.
|
||||||
|
title: Подписки по умолчанию для новых пользователей
|
||||||
contact_information:
|
contact_information:
|
||||||
email: Введите публичный e-mail
|
email: Введите публичный e-mail
|
||||||
username: Введите имя пользователя
|
username: Введите имя пользователя
|
||||||
@@ -123,7 +220,11 @@ ru:
|
|||||||
closed_message:
|
closed_message:
|
||||||
desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги
|
desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги
|
||||||
title: Сообщение о закрытой регистрации
|
title: Сообщение о закрытой регистрации
|
||||||
|
deletion:
|
||||||
|
desc_html: Позволяет всем удалять собственные аккаунты
|
||||||
|
title: Разрешить удаление аккаунтов
|
||||||
open:
|
open:
|
||||||
|
desc_html: Позволяет любому создавать аккаунт
|
||||||
title: Открыть регистрацию
|
title: Открыть регистрацию
|
||||||
site_description:
|
site_description:
|
||||||
desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code><a></code> и <code><em></code>.
|
desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code><a></code> и <code><em></code>.
|
||||||
@@ -131,8 +232,32 @@ ru:
|
|||||||
site_description_extended:
|
site_description_extended:
|
||||||
desc_html: Отображается на странице дополнительной информации<br>Можно использовать HTML-теги
|
desc_html: Отображается на странице дополнительной информации<br>Можно использовать HTML-теги
|
||||||
title: Расширенное описание сайта
|
title: Расширенное описание сайта
|
||||||
|
site_terms:
|
||||||
|
desc_html: Вы можете добавить сюда собственную политику конфиденциальности, пользовательское соглашение и другие документы. Можно использовать теги HTML.
|
||||||
|
title: Условия использования
|
||||||
site_title: Название сайта
|
site_title: Название сайта
|
||||||
|
thumbnail:
|
||||||
|
desc_html: Используется для предпросмотра с помощью OpenGraph и API. Рекомендуется разрешение 1200x630px
|
||||||
|
title: Картинка узла
|
||||||
|
timeline_preview:
|
||||||
|
desc_html: Показывать публичную ленту на целевой странице
|
||||||
|
title: Предпросмотр ленты
|
||||||
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:
|
subscriptions:
|
||||||
callback_url: Callback URL
|
callback_url: Callback URL
|
||||||
confirmed: Подтверждено
|
confirmed: Подтверждено
|
||||||
@@ -141,18 +266,31 @@ ru:
|
|||||||
title: WebSub
|
title: WebSub
|
||||||
topic: Тема
|
topic: Тема
|
||||||
title: Администрирование
|
title: Администрирование
|
||||||
|
admin_mailer:
|
||||||
|
new_report:
|
||||||
|
body: "%{reporter} подал(а) жалобу на %{target}"
|
||||||
|
subject: Новая жалоба, узел %{instance} (#%{id})
|
||||||
application_mailer:
|
application_mailer:
|
||||||
|
salutation: "%{name},"
|
||||||
settings: 'Изменить настройки e-mail: %{link}'
|
settings: 'Изменить настройки e-mail: %{link}'
|
||||||
signature: Уведомления Mastodon от %{instance}
|
signature: Уведомления Mastodon от %{instance}
|
||||||
view: 'Просмотр:'
|
view: 'Просмотр:'
|
||||||
applications:
|
applications:
|
||||||
|
created: Приложение успешно создано
|
||||||
|
destroyed: Приложение успешно удалено
|
||||||
invalid_url: Введенный URL неверен
|
invalid_url: Введенный URL неверен
|
||||||
|
regenerate_token: Повторно сгенерировать токен доступа
|
||||||
|
token_regenerated: Токен доступа успешно сгенерирован
|
||||||
|
warning: Будьте очень внимательны с этими данными. Не делитесь ими ни с кем!
|
||||||
|
your_token: Ваш токен доступа
|
||||||
auth:
|
auth:
|
||||||
|
agreement_html: Создавая аккаунт, вы соглашаетесь с <a href="%{rules_path}">нашими правилами поведения</a> и <a href="%{terms_path}">политикой конфиденциальности</a>.
|
||||||
change_password: Изменить пароль
|
change_password: Изменить пароль
|
||||||
delete_account: Удалить аккаунт
|
delete_account: Удалить аккаунт
|
||||||
delete_account_html: Если Вы хотите удалить свой аккаунт, вы можете <a href="%{path}">перейти сюда</a>. У Вас будет запрошено подтверждение.
|
delete_account_html: Если Вы хотите удалить свой аккаунт, вы можете <a href="%{path}">перейти сюда</a>. У Вас будет запрошено подтверждение.
|
||||||
didnt_get_confirmation: Не получили инструкцию для подтверждения?
|
didnt_get_confirmation: Не получили инструкцию для подтверждения?
|
||||||
forgot_password: Забыли пароль?
|
forgot_password: Забыли пароль?
|
||||||
|
invalid_reset_password_token: Токен сброса пароля неверен или устарел. Пожалуйста, запросите новый.
|
||||||
login: Войти
|
login: Войти
|
||||||
logout: Выйти
|
logout: Выйти
|
||||||
register: Зарегистрироваться
|
register: Зарегистрироваться
|
||||||
@@ -162,6 +300,12 @@ ru:
|
|||||||
authorize_follow:
|
authorize_follow:
|
||||||
error: К сожалению, при поиске удаленного аккаунта возникла ошибка
|
error: К сожалению, при поиске удаленного аккаунта возникла ошибка
|
||||||
follow: Подписаться
|
follow: Подписаться
|
||||||
|
follow_request: 'Вы отправили запрос на подписку:'
|
||||||
|
following: 'Ура! Теперь Вы подписаны на:'
|
||||||
|
post_follow:
|
||||||
|
close: Или просто закрыть это окно.
|
||||||
|
return: Вернуться к профилю пользователя
|
||||||
|
web: Перейти к WWW
|
||||||
title: Подписаться на %{acct}
|
title: Подписаться на %{acct}
|
||||||
datetime:
|
datetime:
|
||||||
distance_in_words:
|
distance_in_words:
|
||||||
@@ -193,7 +337,10 @@ ru:
|
|||||||
content: Проверка безопасности не удалась. Возможно, Вы блокируете cookies?
|
content: Проверка безопасности не удалась. Возможно, Вы блокируете cookies?
|
||||||
title: Проверка безопасности не удалась.
|
title: Проверка безопасности не удалась.
|
||||||
'429': Слишком много запросов
|
'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:
|
exports:
|
||||||
blocks: Список блокировки
|
blocks: Список блокировки
|
||||||
csv: CSV
|
csv: CSV
|
||||||
@@ -265,23 +412,30 @@ ru:
|
|||||||
number:
|
number:
|
||||||
human:
|
human:
|
||||||
decimal_units:
|
decimal_units:
|
||||||
format: "%n%u"
|
format: "%n %u"
|
||||||
units:
|
units:
|
||||||
billion: B
|
billion: млрд
|
||||||
million: M
|
million: млн
|
||||||
quadrillion: Q
|
quadrillion: Q
|
||||||
thousand: K
|
thousand: тыс
|
||||||
trillion: T
|
trillion: трлн
|
||||||
unit: ''
|
unit: ''
|
||||||
pagination:
|
pagination:
|
||||||
next: След
|
next: След
|
||||||
prev: Пред
|
prev: Пред
|
||||||
truncate: "…"
|
truncate: "…"
|
||||||
|
preferences:
|
||||||
|
languages: Языки
|
||||||
|
other: Другое
|
||||||
|
publishing: Публикация
|
||||||
|
web: WWW
|
||||||
push_notifications:
|
push_notifications:
|
||||||
favourite:
|
favourite:
|
||||||
title: Ваш статус понравился %{name}
|
title: Ваш статус понравился %{name}
|
||||||
follow:
|
follow:
|
||||||
title: "%{name} теперь подписан(а) на Вас"
|
title: "%{name} теперь подписан(а) на Вас"
|
||||||
|
group:
|
||||||
|
title: "%{count} уведомлений"
|
||||||
mention:
|
mention:
|
||||||
action_boost: Продвинуть
|
action_boost: Продвинуть
|
||||||
action_expand: Развернуть
|
action_expand: Развернуть
|
||||||
@@ -335,16 +489,24 @@ ru:
|
|||||||
authorized_apps: Авторизованные приложения
|
authorized_apps: Авторизованные приложения
|
||||||
back: Назад в Mastodon
|
back: Назад в Mastodon
|
||||||
delete: Удаление аккаунта
|
delete: Удаление аккаунта
|
||||||
|
development: Разработка
|
||||||
edit_profile: Изменить профиль
|
edit_profile: Изменить профиль
|
||||||
export: Экспорт данных
|
export: Экспорт данных
|
||||||
followers: Авторизованные подписчики
|
followers: Авторизованные подписчики
|
||||||
import: Импорт
|
import: Импорт
|
||||||
|
notifications: Уведомления
|
||||||
preferences: Настройки
|
preferences: Настройки
|
||||||
settings: Опции
|
settings: Опции
|
||||||
two_factor_authentication: Двухфакторная аутентификация
|
two_factor_authentication: Двухфакторная аутентификация
|
||||||
|
your_apps: Ваши приложения
|
||||||
statuses:
|
statuses:
|
||||||
open_in_web: Открыть в WWW
|
open_in_web: Открыть в WWW
|
||||||
over_character_limit: превышен лимит символов (%{max})
|
over_character_limit: превышен лимит символов (%{max})
|
||||||
|
pin_errors:
|
||||||
|
limit: Слишком много закрепленных статусов
|
||||||
|
ownership: Нельзя закрепить чужой статус
|
||||||
|
private: Нельзя закрепить непубличный статус
|
||||||
|
reblog: Нельзя закрепить продвинутый статус
|
||||||
show_more: Подробнее
|
show_more: Подробнее
|
||||||
visibilities:
|
visibilities:
|
||||||
private: Для подписчиков
|
private: Для подписчиков
|
||||||
@@ -359,6 +521,8 @@ ru:
|
|||||||
sensitive_content: Чувствительный контент
|
sensitive_content: Чувствительный контент
|
||||||
terms:
|
terms:
|
||||||
title: Условия обслуживания и политика конфиденциальности %{instance}
|
title: Условия обслуживания и политика конфиденциальности %{instance}
|
||||||
|
themes:
|
||||||
|
default: Mastodon
|
||||||
time:
|
time:
|
||||||
formats:
|
formats:
|
||||||
default: "%b %d, %Y, %H:%M"
|
default: "%b %d, %Y, %H:%M"
|
||||||
@@ -367,11 +531,13 @@ ru:
|
|||||||
description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
|
description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
|
||||||
disable: Отключить
|
disable: Отключить
|
||||||
enable: Включить
|
enable: Включить
|
||||||
|
enabled: Двухфакторная аутентификация включена
|
||||||
enabled_success: Двухфакторная аутентификация успешно включена
|
enabled_success: Двухфакторная аутентификация успешно включена
|
||||||
generate_recovery_codes: Сгенерировать коды восстановления
|
generate_recovery_codes: Сгенерировать коды восстановления
|
||||||
instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
|
instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
|
||||||
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
|
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
|
||||||
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
|
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
|
||||||
|
recovery_codes: Коды восстановления
|
||||||
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
|
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
|
||||||
recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
|
recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
|
||||||
setup: Настроить
|
setup: Настроить
|
||||||
@@ -379,3 +545,4 @@ ru:
|
|||||||
users:
|
users:
|
||||||
invalid_email: Введенный e-mail неверен
|
invalid_email: Введенный e-mail неверен
|
||||||
invalid_otp_token: Введен неверный код
|
invalid_otp_token: Введен неверный код
|
||||||
|
signed_in_as: 'Выполнен вход под именем:'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pt-BR:
|
|||||||
hints:
|
hints:
|
||||||
defaults:
|
defaults:
|
||||||
avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 120x120px
|
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:
|
display_name:
|
||||||
one: <span class="name-counter">1</span> caracter restante
|
one: <span class="name-counter">1</span> caracter restante
|
||||||
other: <span class="name-counter">%{count}</span> caracteres restantes
|
other: <span class="name-counter">%{count}</span> caracteres restantes
|
||||||
@@ -13,6 +14,7 @@ pt-BR:
|
|||||||
one: <span class="note-counter">1</span> caracter restante
|
one: <span class="note-counter">1</span> caracter restante
|
||||||
other: <span class="note-counter">%{count}</span> caracteres restantes
|
other: <span class="note-counter">%{count}</span> caracteres restantes
|
||||||
setting_noindex: Afeta seu perfil público e as páginas de suas postagens
|
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:
|
imports:
|
||||||
data: Arquivo CSV exportado de outra instância do Mastodon
|
data: Arquivo CSV exportado de outra instância do Mastodon
|
||||||
sessions:
|
sessions:
|
||||||
@@ -42,7 +44,9 @@ pt-BR:
|
|||||||
setting_default_sensitive: Sempre marcar mídia como sensível
|
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_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_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_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
|
setting_unfollow_modal: Mostrar diálogo de confirmação antes de deixar de seguir alguém
|
||||||
severity: Gravidade
|
severity: Gravidade
|
||||||
type: Tipo de importação
|
type: Tipo de importação
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ ru:
|
|||||||
hints:
|
hints:
|
||||||
defaults:
|
defaults:
|
||||||
avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px
|
avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px
|
||||||
|
digest: Отсылается после долгого периода неактивности с общей информацией упоминаний, полученных в Ваше отсутствие
|
||||||
display_name:
|
display_name:
|
||||||
few: Осталось <span class="name-counter">%{count}</span> символа
|
few: Осталось <span class="name-counter">%{count}</span> символа
|
||||||
many: Осталось <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> символ
|
one: Остался <span class="name-counter">1</span> символ
|
||||||
other: Осталось <span class="name-counter">%{count}</span> символов
|
other: Осталось <span class="name-counter">%{count}</span> символов
|
||||||
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
|
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
|
||||||
|
setting_theme: Влияет на внешний вид Mastodon при выполненном входе в аккаунт.
|
||||||
imports:
|
imports:
|
||||||
data: Файл CSV, экспортированный с другого узла Mastodon
|
data: Файл CSV, экспортированный с другого узла Mastodon
|
||||||
sessions:
|
sessions:
|
||||||
@@ -46,6 +48,8 @@ ru:
|
|||||||
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
|
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
|
||||||
setting_delete_modal: Показывать диалог подтверждения перед удалением
|
setting_delete_modal: Показывать диалог подтверждения перед удалением
|
||||||
setting_noindex: Отказаться от индексации в поисковых машинах
|
setting_noindex: Отказаться от индексации в поисковых машинах
|
||||||
|
setting_reduce_motion: Уменьшить движение в анимации
|
||||||
|
setting_site_theme: Тема сайта
|
||||||
setting_system_font_ui: Использовать шрифт системы по умолчанию
|
setting_system_font_ui: Использовать шрифт системы по умолчанию
|
||||||
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
|
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
|
||||||
severity: Строгость
|
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|
|
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 :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 :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 :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 :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}
|
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
|
namespace :settings do
|
||||||
resource :profile, only: [:show, :update]
|
resource :profile, only: [:show, :update]
|
||||||
|
|
||||||
|
resources :keyword_mutes do
|
||||||
|
collection do
|
||||||
|
delete :destroy_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resource :preferences, only: [:show, :update]
|
resource :preferences, only: [:show, :update]
|
||||||
resource :notifications, only: [:show, :update]
|
resource :notifications, only: [:show, :update]
|
||||||
resource :import, only: [:show, :create]
|
resource :import, only: [:show, :create]
|
||||||
@@ -140,7 +147,7 @@ Rails.application.routes.draw do
|
|||||||
resource :two_factor_authentication, only: [:destroy]
|
resource :two_factor_authentication, only: [:destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :custom_emojis, only: [:index, :new, :create, :destroy] do
|
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do
|
||||||
member do
|
member do
|
||||||
post :copy
|
post :copy
|
||||||
post :enable
|
post :enable
|
||||||
@@ -193,6 +200,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
namespace :timelines do
|
namespace :timelines do
|
||||||
|
resource :direct, only: :show, controller: :direct
|
||||||
resource :home, only: :show, controller: :home
|
resource :home, only: :show, controller: :home
|
||||||
resource :public, only: :show, controller: :public
|
resource :public, only: :show, controller: :public
|
||||||
resources :tag, only: :show
|
resources :tag, only: :show
|
||||||
|
|||||||
@@ -3,48 +3,62 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
|
|||||||
redis = Redis.current
|
redis = Redis.current
|
||||||
fm = FeedManager.instance
|
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.
|
# find_each is batched on the database side.
|
||||||
User.includes(:account).find_each do |user|
|
User.includes(:account).find_each do |user|
|
||||||
account = user.account
|
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)
|
timeline_key = fm.key(:home, account.id)
|
||||||
reblog_key = fm.key(:home, account.id, 'reblogs')
|
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.
|
redis.evalsha(script_hash, [timeline_key, reblog_key])
|
||||||
# (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
|
|
||||||
end
|
end
|
||||||
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -111,6 +111,7 @@ ActiveRecord::Schema.define(version: 20171010025614) do
|
|||||||
t.boolean "disabled", default: false, null: false
|
t.boolean "disabled", default: false, null: false
|
||||||
t.string "uri"
|
t.string "uri"
|
||||||
t.string "image_remote_url"
|
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
|
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
|
||||||
end
|
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
|
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
|
||||||
end
|
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|
|
create_table "imports", force: :cascade do |t|
|
||||||
t.integer "type", null: false
|
t.integer "type", null: false
|
||||||
t.boolean "approved", default: false, 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 "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", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
||||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", 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 "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", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
|
||||||
add_foreign_key "media_attachments", "statuses", 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
|
unless options[:style] == :original && num_frames > 1
|
||||||
tmp_file = Paperclip::TempfileFactory.new.generate(attachment.instance.file_file_name)
|
tmp_file = Paperclip::TempfileFactory.new.generate(attachment.instance.file_file_name)
|
||||||
tmp_file << file.read
|
tmp_file << file.read
|
||||||
|
tmp_file.flush
|
||||||
return tmp_file
|
return tmp_file
|
||||||
end
|
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)
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'for mentions feed' do
|
context 'for mentions feed' do
|
||||||
@@ -147,6 +185,13 @@ RSpec.describe FeedManager do
|
|||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
||||||
end
|
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
|
||||||
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
|
end
|
||||||
|
|
||||||
describe 'non-animated gif non-conversion' do
|
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
|
fixtures.each do |fixture|
|
||||||
expect(media.type).to eq 'image'
|
context fixture[:filename] do
|
||||||
end
|
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) }
|
||||||
|
|
||||||
it 'leaves original file as-is' do
|
it 'sets type to image' do
|
||||||
expect(media.file_content_type).to eq 'image/gif'
|
expect(media.type).to eq 'image'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets meta' do
|
it 'leaves original file as-is' do
|
||||||
expect(media.file.meta["original"]["width"]).to eq 600
|
expect(media.file_content_type).to eq 'image/gif'
|
||||||
expect(media.file.meta["original"]["height"]).to eq 400
|
end
|
||||||
expect(media.file.meta["original"]["aspect"]).to eq 1.5
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,55 @@ RSpec.describe Status, type: :model do
|
|||||||
end
|
end
|
||||||
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
|
describe '.as_public_timeline' do
|
||||||
it 'only includes statuses with public visibility' do
|
it 'only includes statuses with public visibility' do
|
||||||
public_status = Fabricate(:status, visibility: :public)
|
public_status = Fabricate(:status, visibility: :public)
|
||||||
|
|||||||
@@ -402,6 +402,10 @@ const startWorker = (workerId) => {
|
|||||||
streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true);
|
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) => {
|
app.get('/api/v1/streaming/hashtag', (req, res) => {
|
||||||
streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
|
streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
|
||||||
});
|
});
|
||||||
@@ -437,6 +441,9 @@ const startWorker = (workerId) => {
|
|||||||
case 'public:local':
|
case 'public:local':
|
||||||
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||||
break;
|
break;
|
||||||
|
case 'direct':
|
||||||
|
streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||||
|
break;
|
||||||
case 'hashtag':
|
case 'hashtag':
|
||||||
streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user