Compare commits

..

2 Commits

Author SHA1 Message Date
beatrix-bitrot
1ba51a4c35 add test 2017-10-19 22:00:03 +00:00
beatrix-bitrot
b10608299e hide mentions of muted accounts (in home col)
also cleans up some old crap
2017-10-19 21:38:41 +00:00
84 changed files with 631 additions and 1639 deletions

View File

@@ -29,11 +29,6 @@ settings:
import/ignore:
- node_modules
- \\.(css|scss|json)$
import/resolver:
node:
moduleDirectory:
- node_modules
- app/javascript
rules:
brace-style: warn

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

2
Vagrantfile vendored
View File

@@ -83,7 +83,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "4096"]
vb.customize ["modifyvm", :id, "--memory", "2048"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172

View File

@@ -22,14 +22,6 @@ module Admin
end
end
def update
if @custom_emoji.update(resource_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
end
def destroy
@custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
@@ -44,7 +36,7 @@ module Admin
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page])
redirect_to admin_custom_emojis_path(params[:page])
end
def enable
@@ -64,7 +56,7 @@ module Admin
end
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
params.require(:custom_emoji).permit(:shortcode, :image)
end
def filtered_custom_emojis

View File

@@ -1,60 +0,0 @@
# 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

View File

@@ -1,64 +0,0 @@
# 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

View File

@@ -9,10 +9,6 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
def as_array(value)
value.is_a?(Array) ? value : [value]
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end

View File

@@ -1,2 +0,0 @@
module Settings::KeywordMutesHelper
end

View File

@@ -47,9 +47,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
// Our imports //
import ComposeAdvancedOptionsToggle from './toggle';
import ComposeDropdown from '../dropdown/index';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -75,6 +77,11 @@ const messages = defineMessages({
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
});
const iconStyle = {
height : null,
lineHeight : '27px',
};
/*
Implementation:
@@ -93,6 +100,67 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
intl : PropTypes.object.isRequired,
};
state = {
open: false,
};
/*
### `onToggleDropdown()`
This function toggles the opening and closing of the advanced options
dropdown.
*/
onToggleDropdown = () => {
this.setState({ open: !this.state.open });
};
/*
### `onGlobalClick(e)`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
}
/*
### `componentDidMount()`, `componentWillUnmount()`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
/*
### `setRef(c)`
`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
*/
setRef = (c) => {
this.node = c;
}
/*
@@ -103,6 +171,7 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
*/
render () {
const { open } = this.state;
const { intl, values } = this.props;
/*
@@ -149,14 +218,23 @@ toggle as its `key` so that React can keep track of it.
Finally, we can render our component.
*/
return (
<ComposeDropdown
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='home'
highlight={anyEnabled}
>
{optionElems}
</ComposeDropdown>
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className='advanced-options-dropdown__value'
title={intl.formatMessage(messages.advanced_options_icon_title)}
icon='ellipsis-h' active={open || anyEnabled}
size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>
);
}

View File

@@ -1,133 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
// Our imports //
import ComposeDropdown from '../dropdown/index';
import { uploadCompose } from '../../../../mastodon/actions/compose';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { openModal } from '../../../../mastodon/actions/modal';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
upload :
{ id: 'compose.attach.upload', defaultMessage: 'Upload a file' },
doodle :
{ id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
attach :
{ id: 'compose.attach', defaultMessage: 'Attach...' },
});
const mapStateToProps = state => ({
// This horrible expression is copied from vanilla upload_button_container
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
const mapDispatchToProps = dispatch => ({
onSelectFile (files) {
dispatch(uploadCompose(files));
},
onOpenDoodle () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
export default class ComposeAttachOptions extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes.object.isRequired,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
onOpenDoodle: PropTypes.func.isRequired,
};
handleItemClick = bt => {
if (bt === 'upload') {
this.fileElement.click();
}
if (bt === 'doodle') {
this.props.onOpenDoodle();
}
this.dropdown.setState({ open: false });
};
handleFileChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
setFileRef = (c) => {
this.fileElement = c;
}
setDropdownRef = (c) => {
this.dropdown = c;
}
render () {
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
const options = [
{ icon: 'cloud-upload', text: messages.upload, name: 'upload' },
{ icon: 'paint-brush', text: messages.doodle, name: 'doodle' },
];
const optionElems = options.map((item) => {
const hdl = () => this.handleItemClick(item.name);
return (
<div
role='button'
tabIndex='0'
key={item.name}
onClick={hdl}
className='privacy-dropdown__option'
>
<div className='privacy-dropdown__option__icon'>
<i className={`fa fa-fw fa-${item.icon}`} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{intl.formatMessage(item.text)}</strong>
</div>
</div>
);
});
return (
<div>
<ComposeDropdown
title={intl.formatMessage(messages.attach)}
icon='paperclip'
disabled={disabled}
ref={this.setDropdownRef}
>
{optionElems}
</ComposeDropdown>
<input
key={resetFileKey}
ref={this.setFileRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleFileChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</div>
);
}
}

View File

@@ -1,77 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
const iconStyle = {
height : null,
lineHeight : '27px',
};
export default class ComposeDropdown extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string,
highlight: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};
state = {
open: false,
};
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
};
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
onToggleDropdown = () => {
if (this.props.disabled) return;
this.setState({ open: !this.state.open });
};
setRef = (c) => {
this.node = c;
};
render () {
const { open } = this.state;
let { highlight, title, icon, disabled } = this.props;
if (!icon) icon = 'ellipsis-h';
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className={'inverted'}
title={title}
icon={icon} active={open || highlight}
size={18}
style={iconStyle}
disabled={disabled}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{this.props.children}
</div>
</div>
);
}
}

View File

@@ -124,16 +124,6 @@ export default class LocalSettingsPage extends React.PureComponent {
>
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'reblogs']}
id='mastodon-settings--collapsed-auto-reblogs'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'replies']}

View File

@@ -287,7 +287,6 @@ properly and our intersection observer is good to go.
muted,
id,
intersectionObserverWrapper,
prepend,
} = this.props;
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
@@ -300,9 +299,6 @@ properly and our intersection observer is good to go.
node.clientHeight > (
status.get('media_attachments').size && !muted ? 650 : 400
)
) || (
autoCollapseSettings.get('reblogs') &&
prepend === 'reblogged_by'
) || (
autoCollapseSettings.get('replies') &&
status.get('in_reply_to_id', null) !== null

View File

@@ -14,7 +14,6 @@
"settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_reblogs": "Boosts",
"settings.auto_collapse_replies": "Replies",
"settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots",

View File

@@ -59,7 +59,6 @@ const initialState = ImmutableMap({
all : false,
notifications : true,
lengthy : true,
reblogs : false,
replies : false,
media : false,
}),

View File

@@ -69,10 +69,6 @@ functions are:
easier to read and to maintain. I leave it to the future readers of
this code to determine the extent of my successes in this endeavor.
UPDATE 19 Oct 2017: We no longer allow character escapes inside our
double-quoted strings for ease of processing. We now internally use
the name "ƔAML" in our code to clarify that this is Not Quite YAML.
Sending love + warmth eternal,
- kibigo [@kibi@glitch.social]
@@ -100,7 +96,10 @@ const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec.
compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]'
);
const WHITE_SPACE = /[ \t]/;
const INDENTATION = / */; // Indentation must be only spaces.
const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/;
const ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/;
const HEXADECIMAL_CHARS = /[0-9a-fA-F]/;
const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/;
const FLOW_CHAR = /[,[\]{}]/;
@@ -122,7 +121,7 @@ const NEW_LINE = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK)
);
const SOME_NEW_LINES = unirex(
'(?:' + rexstr(NEW_LINE) + ')+'
'(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+'
);
const POSSIBLE_STARTS = unirex(
rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?'
@@ -132,13 +131,22 @@ const POSSIBLE_ENDS = unirex(
rexstr(DOCUMENT_END) + '|' +
rexstr(/<\/p>/)
);
const QUOTE_CHAR = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]'
const CHARACTER_ESCAPE = unirex(
rexstr(/\\/) +
'(?:' +
rexstr(ESCAPE_CHAR) + '|' +
rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' +
rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' +
rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' +
')'
);
const ANY_QUOTE_CHAR = unirex(
rexstr(QUOTE_CHAR) + '*'
const ESCAPED_CHAR = unirex(
rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' +
rexstr(CHARACTER_ESCAPE)
);
const ANY_ESCAPED_CHARS = unirex(
rexstr(ESCAPED_CHAR) + '*'
);
const ESCAPED_APOS = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/)
);
@@ -182,76 +190,120 @@ const LATER_VALUE_CHAR = unirex(
/* YAML CONSTRUCTS */
const ƔAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + '---'
const YAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/---/)
);
const ƔAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)'
const YAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/)
);
const ƔAML_LOOKAHEAD = unirex(
const YAML_LOOKAHEAD = unirex(
'(?=' +
rexstr(ƔAML_START) +
rexstr(YAML_START) +
rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) +
')'
);
const ƔAML_DOUBLE_QUOTE = unirex(
'"' + rexstr(ANY_QUOTE_CHAR) + '"'
const YAML_DOUBLE_QUOTE = unirex(
rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/)
);
const ƔAML_SINGLE_QUOTE = unirex(
'\'' + rexstr(ANY_ESCAPED_APOS) + '\''
const YAML_SINGLE_QUOTE = unirex(
rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/)
);
const ƔAML_SIMPLE_KEY = unirex(
const YAML_SIMPLE_KEY = unirex(
rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*'
);
const ƔAML_SIMPLE_VALUE = unirex(
const YAML_SIMPLE_VALUE = unirex(
rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*'
);
const ƔAML_KEY = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_KEY)
const YAML_KEY = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_KEY)
);
const ƔAML_VALUE = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_VALUE)
const YAML_VALUE = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_VALUE)
);
const ƔAML_SEPARATOR = unirex(
const YAML_SEPARATOR = unirex(
rexstr(ANY_WHITE_SPACE) +
':' + rexstr(WHITE_SPACE) +
rexstr(ANY_WHITE_SPACE)
);
const ƔAML_LINE = unirex(
'(' + rexstr(ƔAML_KEY) + ')' +
rexstr(ƔAML_SEPARATOR) +
'(' + rexstr(ƔAML_VALUE) + ')'
const YAML_LINE = unirex(
'(' + rexstr(YAML_KEY) + ')' +
rexstr(YAML_SEPARATOR) +
'(' + rexstr(YAML_VALUE) + ')'
);
/* FRONTMATTER REGEX */
const ƔAML_FRONTMATTER = unirex(
const YAML_FRONTMATTER = unirex(
rexstr(POSSIBLE_STARTS) +
rexstr(ƔAML_LOOKAHEAD) +
rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) +
rexstr(YAML_LOOKAHEAD) +
rexstr(YAML_START) + rexstr(SOME_NEW_LINES) +
'(?:' +
rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,5}' +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS)
'(' + rexstr(INDENTATION) + ')' +
rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
'(?:' +
'\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,4}' +
')?' +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS)
);
/* SEARCHES */
const FIND_ƔAML_LINE = unirex(
rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE)
const FIND_YAML_LINES = unirex(
rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE)
);
/* STRING PROCESSING */
function processString (str) {
function processString(str) {
switch (str.charAt(0)) {
case '"':
return str.substring(1, str.length - 1);
return str
.substring(1, str.length - 1)
.replace(/\\0/g, '\x00')
.replace(/\\a/g, '\x07')
.replace(/\\b/g, '\x08')
.replace(/\\t/g, '\x09')
.replace(/\\\x09/g, '\x09')
.replace(/\\n/g, '\x0a')
.replace(/\\v/g, '\x0b')
.replace(/\\f/g, '\x0c')
.replace(/\\r/g, '\x0d')
.replace(/\\e/g, '\x1b')
.replace(/\\ /g, '\x20')
.replace(/\\"/g, '\x22')
.replace(/\\\//g, '\x2f')
.replace(/\\\\/g, '\x5c')
.replace(/\\N/g, '\x85')
.replace(/\\_/g, '\xa0')
.replace(/\\L/g, '\u2028')
.replace(/\\P/g, '\u2029')
.replace(
new RegExp(
unirex(
rexstr(/\\x/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{2})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\u/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{4})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\U/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{8})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
);
case '\'':
return str
.substring(1, str.length - 1)
@@ -269,18 +321,15 @@ export function processBio(content) {
text: content,
metadata: [],
};
let ɣaml = content.match(ƔAML_FRONTMATTER);
if (!ɣaml) {
return result;
} else {
ɣaml = ɣaml[0];
}
const start = content.search(ƔAML_START);
const end = start + ɣaml.length - ɣaml.search(ƔAML_START);
result.text = content.substr(end);
let yaml = content.match(YAML_FRONTMATTER);
if (!yaml) return result;
else yaml = yaml[0];
let start = content.search(YAML_START);
let end = start + yaml.length - yaml.search(YAML_START);
result.text = content.substr(0, start) + content.substr(end);
let metadata = null;
let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings
while ((metadata = query.exec(ɣaml))) {
let query = new RegExp(FIND_YAML_LINES, 'g');
while ((metadata = query.exec(yaml))) {
result.metadata.push([
processString(metadata[1]),
processString(metadata[2]),
@@ -303,23 +352,63 @@ export function createBio(note, data) {
let val = '' + data[i][1];
// Key processing
if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"';
if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\'';
else {
key = key
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
key = '\'' + key + '\'';
.replace(/\x00/g, '\\0')
.replace(/\x07/g, '\\a')
.replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = key.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
key = key.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
key = '"' + key + '"';
}
// Value processing
if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"';
if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\'';
else {
key = key
.replace(/'/g, '\'\'')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
key = '\'' + key + '\'';
val = val
.replace(/\x00/g, '\\0')
.replace(/\x07/g, '\\a')
.replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = val.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
val = val.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
val = '"' + val + '"';
}
frontmatter += key + ': ' + val + '\n';

View File

@@ -8,7 +8,6 @@ import {
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
refreshDirectTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
@@ -134,8 +133,6 @@ export function submitCompose() {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
} else if (response.data.visibility === 'direct') {
insertOrRefresh('direct', refreshDirectTimeline);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));

View File

@@ -92,4 +92,3 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');

View File

@@ -115,7 +115,6 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
@@ -156,7 +155,6 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);

View File

@@ -112,19 +112,3 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
foo
</button>
`;
exports[`<Button /> renders title if props.title is given 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
title="foo"
/>
`;

View File

@@ -72,11 +72,4 @@ describe('<Button />', () => {
expect(tree).toMatchSnapshot();
});
it('renders title if props.title is given', () => {
const component = renderer.create(<Button title='foo' />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -14,7 +14,6 @@ export default class Button extends React.PureComponent {
className: PropTypes.string,
style: PropTypes.object,
children: PropTypes.node,
title: PropTypes.string,
};
static defaultProps = {
@@ -36,26 +35,26 @@ export default class Button extends React.PureComponent {
}
render () {
let attrs = {
className: classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
}),
disabled: this.props.disabled,
onClick: this.handleClick,
ref: this.setRef,
style: {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
},
const style = {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
};
if (this.props.title) attrs.title = this.props.title;
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
});
return (
<button {...attrs}>
<button
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
style={style}
>
{this.props.text || this.props.children}
</button>
);

View File

@@ -175,9 +175,7 @@ export default class ColumnHeader extends React.PureComponent {
<div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<span className='column-header__title'>
{title}
</span>
{title}
<div className='column-header__buttons'>
{backButton}
{ notifCleaning ? (

View File

@@ -5,6 +5,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import UploadButtonContainer from '../containers/upload_button_container';
import DoodleButtonContainer from '../containers/doodle_button_container';
import { defineMessages, injectIntl } from 'react-intl';
import Collapsable from '../../../components/collapsable';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
@@ -18,7 +20,6 @@ import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -164,8 +165,6 @@ export default class ComposeForm extends ImmutablePureComponent {
let publishText = '';
let publishText2 = '';
let title = '';
let title2 = '';
const privacyIcons = {
none: '',
@@ -175,10 +174,7 @@ export default class ComposeForm extends ImmutablePureComponent {
direct: 'envelope',
};
title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`;
if (showSideArm) {
// Enhanced behavior with dual toot buttons
publishText = (
<span>
{
@@ -190,15 +186,13 @@ export default class ComposeForm extends ImmutablePureComponent {
</span>
);
title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`;
publishText2 = (
<i
className={`fa fa-${privacyIcons[secondaryVisibility]}`}
aria-label={title2}
aria-label={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`}
/>
);
} else {
// Original vanilla behavior - no icon if public or unlisted
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
@@ -247,12 +241,12 @@ export default class ComposeForm extends ImmutablePureComponent {
</div>
<div className='compose-form__buttons'>
<ComposeAttachOptions />
<SensitiveButtonContainer />
<div className='compose-form__buttons-separator' />
<UploadButtonContainer />
<DoodleButtonContainer />
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
<ComposeAdvancedOptionsContainer />
<SensitiveButtonContainer />
<SpoilerButtonContainer />
</div>
<div className='compose-form__publish'>
@@ -263,7 +257,6 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button
className='compose-form__publish__side-arm'
text={publishText2}
title={title2}
onClick={this.handleSubmit2}
disabled={submitDisabled}
/> : ''
@@ -271,7 +264,6 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button
className='compose-form__publish__primary'
text={publishText}
title={title}
onClick={this.handleSubmit}
disabled={submitDisabled}
/>

View File

@@ -0,0 +1,41 @@
import React from 'react';
import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
doodle: { id: 'doodle_button.label', defaultMessage: 'Add a drawing' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
@injectIntl
export default class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
onOpenCanvas: PropTypes.func.isRequired,
style: PropTypes.object,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onOpenCanvas();
}
render () {
const { intl, disabled } = this.props;
return (
<div className='compose-form__upload-button'>
<IconButton icon='pencil' title={intl.formatMessage(messages.doodle)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
</div>
);
}
}

View File

@@ -68,7 +68,7 @@ export default class Upload extends ImmutablePureComponent {
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} />
<div className={classNames('compose-form__upload-description', { active })}>

View File

@@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import DoodleButton from '../components/doodle_button';
import { openModal } from '../../../actions/modal';
const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
});
const mapDispatchToProps = dispatch => ({
onOpenCanvas () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DoodleButton);

View File

@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
], emojis => emojis.sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();

View File

@@ -47,7 +47,7 @@ class SensitiveButton extends React.PureComponent {
'compose-form__sensitive-button--visible': visible,
});
return (
<div className={className} style={{ transform: `scale(${scale})` }}>
<div className={className} style={{ transform: `translateZ(0) scale(${scale})` }}>
<IconButton
className='compose-form__sensitive-button__icon'
title={intl.formatMessage(messages.title)}

View File

@@ -1,17 +0,0 @@
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);

View File

@@ -1,107 +0,0 @@
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>
);
}
}

View File

@@ -17,7 +17,6 @@ const messages = defineMessages({
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@@ -79,22 +78,18 @@ export default class GettingStarted extends ImmutablePureComponent {
}
}
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
}
navItems = navItems.concat([
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
navItems.push(<ColumnLink key='6' 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' />,
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]);
return (

View File

@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
@@ -23,7 +23,6 @@ const componentMap = {
'PUBLIC': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
};

View File

@@ -40,7 +40,7 @@ export default class UploadArea extends React.PureComponent {
{({ backgroundOpacity, backgroundScale }) =>
<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
<div className='upload-area__drop'>
<div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
<div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />
<div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
</div>
</div>

View File

@@ -29,7 +29,6 @@ import {
Following,
Reblogs,
Favourites,
DirectTimeline,
HashtagTimeline,
Notifications,
FollowRequests,
@@ -72,7 +71,6 @@ const keyMap = {
goToNotifications: 'g n',
goToLocal: 'g l',
goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s',
goToFavourites: 'g f',
goToPinned: 'g p',
@@ -304,10 +302,6 @@ export default class UI extends React.Component {
this.context.router.history.push('/timelines/public');
}
handleHotkeyGoToDirect = () => {
this.context.router.history.push('/timelines/direct');
}
handleHotkeyGoToStart = () => {
this.context.router.history.push('/getting-started');
}
@@ -363,7 +357,6 @@ export default class UI extends React.Component {
goToNotifications: this.handleHotkeyGoToNotifications,
goToLocal: this.handleHotkeyGoToLocal,
goToFederated: this.handleHotkeyGoToFederated,
goToDirect: this.handleHotkeyGoToDirect,
goToStart: this.handleHotkeyGoToStart,
goToFavourites: this.handleHotkeyGoToFavourites,
goToPinned: this.handleHotkeyGoToPinned,
@@ -384,7 +377,6 @@ export default class UI extends React.Component {
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} />

View File

@@ -26,10 +26,6 @@ export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
}
export function DirectTimeline() {
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
}
export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status');
}

View File

@@ -755,19 +755,6 @@
],
"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": [
{
@@ -829,10 +816,6 @@
"defaultMessage": "Local timeline",
"id": "navigation_bar.community_timeline"
},
{
"defaultMessage": "Direct messages",
"id": "navigation_bar.direct"
},
{
"defaultMessage": "Preferences",
"id": "navigation_bar.preferences"

View File

@@ -28,7 +28,6 @@
"bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users",
"column.community": "Local timeline",
"column.direct": "Direct messages",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
@@ -81,7 +80,6 @@
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
@@ -108,7 +106,6 @@
"missing_indicator.label": "Not found",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.direct": "Direct messages",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",

View File

@@ -1,221 +1,221 @@
{
"account.block": "Bloki @{name}",
"account.block_domain": "Kaŝi ĉion el {domain}",
"account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.edit_profile": "Redakti la profilon",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.follows": "Sekvatoj",
"account.follows_you": "Sekvas vin",
"account.media": "Sonbildaĵoj",
"account.media": "Media",
"account.mention": "Mencii @{name}",
"account.mute": "Silentigi @{name}",
"account.mute": "Mute @{name}",
"account.posts": "Mesaĝoj",
"account.report": "Signali @{name}",
"account.report": "Report @{name}",
"account.requested": "Atendas aprobon",
"account.share": "Diskonigi la profilon de @{name}",
"account.share": "Share @{name}'s profile",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Malkaŝi {domain}",
"account.unfollow": "Ne plus sekvi",
"account.unmute": "Malsilentigi @{name}",
"account.view_full_profile": "Vidi plenan profilon",
"boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
"bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_column_error.retry": "Bonvolu reprovi",
"bundle_column_error.title": "Reta eraro",
"bundle_modal_error.close": "Fermi",
"bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
"bundle_modal_error.retry": "Bonvolu reprovi",
"column.blocks": "Blokitaj uzantoj",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Malsekvi",
"account.unmute": "Unmute @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users",
"column.community": "Loka tempolinio",
"column.favourites": "Favoritoj",
"column.follow_requests": "Abonpetoj",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Hejmo",
"column.mutes": "Silentigitaj uzantoj",
"column.mutes": "Muted users",
"column.notifications": "Sciigoj",
"column.pins": "Alpinglitaj pepoj",
"column.pins": "Pinned toot",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Kaŝi agordojn",
"column_header.moveLeft_settings": "Movi kolumnon maldekstren",
"column_header.moveRight_settings": "Movi kolumnon dekstren",
"column_header.pin": "Alpingli",
"column_header.show_settings": "Malkaŝi agordojn",
"column_header.unpin": "Depingli",
"column_subheading.navigation": "Navigado",
"column_subheading.settings": "Agordoj",
"compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
"compose_form.lock_disclaimer.lock": "ŝlosita",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
"compose_form.spoiler_placeholder": "Skribu tie vian averton",
"confirmation_modal.cancel": "Malfari",
"confirmations.block.confirm": "Bloki",
"confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
"confirmations.delete.confirm": "Malaperigi",
"confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
"confirmations.domain_block.confirm": "Kaŝi la tutan reton",
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
"confirmations.mute.confirm": "Silentigi",
"confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
"confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
"embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
"embed.preview": "Ĝi aperos tiel:",
"emoji_button.activity": "Aktivecoj",
"emoji_button.custom": "Personaj",
"emoji_button.flags": "Flagoj",
"emoji_button.food": "Manĝi kaj trinki",
"emoji_button.label": "Enmeti mieneton",
"emoji_button.nature": "Naturo",
"emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objektoj",
"emoji_button.people": "Homoj",
"emoji_button.recent": "Ofte uzataj",
"emoji_button.search": "Serĉo…",
"emoji_button.search_results": "Rezultatoj de serĉo",
"emoji_button.symbols": "Simboloj",
"emoji_button.travel": "Vojaĝoj & lokoj",
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
"empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
"empty_column.home.public_timeline": "la publika tempolinio",
"empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
"follow_request.authorize": "Akcepti",
"follow_request.reject": "Rifuzi",
"getting_started.appsshort": "Aplikaĵoj",
"getting_started.faq": "Oftaj demandoj",
"compose_form.spoiler_placeholder": "Content warning",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.heading": "Por komenci",
"getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
"getting_started.userguide": "Gvidilo de uzo",
"home.column_settings.advanced": "Precizaj agordoj",
"home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.filter_regex": "Forfiltri per regulesprimo",
"home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn",
"home.settings": "Agordoj de la kolumno",
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}.",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advanced",
"home.column_settings.basic": "Basic",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"lightbox.close": "Fermi",
"lightbox.next": "Malantaŭa",
"lightbox.previous": "Antaŭa",
"loading_indicator.label": "Ŝarganta",
"media_gallery.toggle_visible": "Baskuli videblecon",
"missing_indicator.label": "Ne trovita",
"navigation_bar.blocks": "Blokitaj uzantoj",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Ŝarĝanta...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.edit_profile": "Redakti la profilon",
"navigation_bar.favourites": "Favoritaj",
"navigation_bar.follow_requests": "Abonpetoj",
"navigation_bar.info": "Plia informo",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.pins": "Alpinglitaj pepoj",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.follow": "{name} sekvis vin",
"notification.mention": "{name} menciis vin",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notifications.clear": "Forviŝi la sciigojn",
"notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Retumilaj atentigoj",
"notifications.column_settings.favourite": "Favoritoj:",
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.push": "Puŝsciigoj",
"notifications.column_settings.push_meta": "Tiu ĉi aparato",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.sound": "Eligi sonon",
"onboarding.done": "Farita",
"onboarding.next": "Malantaŭa",
"onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.",
"onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
"onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
"onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
"onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}",
"onboarding.page_one.welcome": "Bonvenon al Mastodono!",
"onboarding.page_six.admin": "Via instancestro estas {admin}.",
"onboarding.page_six.almost_done": "Estas preskaŭ finita…",
"onboarding.page_six.appetoot": "Bonan apepiton!",
"onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan apepiton!",
"onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.",
"onboarding.page_six.guidelines": "komunreguloj",
"onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
"onboarding.page_six.various_app": "telefon-aplikaĵoj",
"onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
"onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.",
"onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn («content warning») dank' al la piktogramoj malsupre.",
"onboarding.skip": "Pasigi",
"privacy.change": "Austigi la privateco de la mesaĝo",
"privacy.direct.long": "Vidigi nur al la menciitaj personoj",
"privacy.direct.short": "Rekta",
"privacy.private.long": "Vidigi nur al viaj sekvantoj",
"privacy.private.short": "Nursekvanta",
"privacy.public.long": "Vidigi en publikaj tempolinioj",
"privacy.public.short": "Publika",
"privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
"privacy.unlisted.short": "Nelistigita",
"relative_time.days": "{number}t",
"notifications.column_settings.sound": "Play sound",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "nun",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Malfari",
"report.placeholder": "Pliaj komentoj",
"report.submit": "Sendi",
"report.target": "Signalaĵo",
"reply_indicator.cancel": "Rezigni",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting",
"search.placeholder": "Serĉi",
"search_popout.search_format": "Detala serĉo",
"search_popout.tips.hashtag": "kradvorto",
"search_popout.tips.status": "statkonigo",
"search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
"search_popout.tips.user": "uzanto",
"search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
"standalone.public_title": "Rigardeti…",
"status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Forigi",
"status.embed": "Enmeti",
"status.embed": "Embed",
"status.favourite": "Favori",
"status.load_more": "Ŝargi plie",
"status.media_hidden": "Sonbildaĵo kaŝita",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mencii @{name}",
"status.more": "Pli",
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Disfaldi statkonigon",
"status.pin": "Pingli al la profilo",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Diskonigi",
"status.reblogged_by": "{name} diskonigis",
"status.reblogged_by": "{name} diskonigita",
"status.reply": "Respondi",
"status.replyAll": "Respondi al la fadeno",
"status.report": "Signali @{name}",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.sensitive_toggle": "Alklaki por vidi",
"status.sensitive_warning": "Tikla enhavo",
"status.share": "Diskonigi",
"status.show_less": "Refaldi",
"status.show_more": "Disfaldi",
"status.unmute_conversation": "Malsilentigi konversacion",
"status.unpin": "Depingli de profilo",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.federated_timeline": "Federacia tempolinio",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Hejmo",
"tabs_bar.local_timeline": "Loka tempolinio",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Sciigoj",
"upload_area.title": "Algliti por alŝuti",
"upload_button.label": "Aldoni sonbildaĵon",
"upload_form.description": "Priskribi por la misvidantaj",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.description": "Describe for the visually impaired",
"upload_form.undo": "Malfari",
"upload_progress.label": "Alŝutanta…",
"video.close": "Fermi videon",
"video.exit_fullscreen": "Eliri el plenekrano",
"video.expand": "Vastigi videon",
"video.fullscreen": "Igi plenekrane",
"video.hide": "Kaŝi videon",
"video.mute": "Silentigi",
"video.pause": "Paŭzi",
"video.play": "Legi",
"video.unmute": "Malsilentigi"
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
}

View File

@@ -159,11 +159,11 @@
"privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczny",
"relative_time.days": "{number} dni",
"relative_time.hours": "{number} godz.",
"relative_time.just_now": "teraz",
"relative_time.minutes": "{number} min.",
"relative_time.seconds": "{number} s.",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij",

View File

@@ -63,20 +63,20 @@
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
"confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
"embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.",
"embed.preview": "Так это будет выглядеть:",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Занятия",
"emoji_button.custom": "Собственные",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки",
"emoji_button.label": "Вставить эмодзи",
"emoji_button.nature": "Природа",
"emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предметы",
"emoji_button.people": "Люди",
"emoji_button.recent": "Последние",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Найти...",
"emoji_button.search_results": "Результаты поиска",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Символы",
"emoji_button.travel": "Путешествия",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
@@ -159,34 +159,34 @@
"privacy.public.short": "Публичный",
"privacy.unlisted.long": "Не показывать в лентах",
"privacy.unlisted.short": "Скрытый",
"relative_time.days": "{number}д",
"relative_time.hours": "{number}ч",
"relative_time.just_now": "только что",
"relative_time.minutes": "{number}м",
"relative_time.seconds": "{number}с",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Отмена",
"report.placeholder": "Комментарий",
"report.submit": "Отправить",
"report.target": "Жалуемся на",
"search.placeholder": "Поиск",
"search_popout.search_format": "Продвинутый формат поиска",
"search_popout.tips.hashtag": "хэштег",
"search_popout.tips.status": "статус",
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
"search_popout.tips.user": "пользователь",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
"standalone.public_title": "Прямо сейчас",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Этот статус не может быть продвинут",
"status.delete": "Удалить",
"status.embed": "Встроить",
"status.embed": "Embed",
"status.favourite": "Нравится",
"status.load_more": "Показать еще",
"status.media_hidden": "Медиаконтент скрыт",
"status.mention": "Упомянуть @{name}",
"status.more": "Больше",
"status.more": "More",
"status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус",
"status.pin": "Закрепить в профиле",
"status.pin": "Pin on profile",
"status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)",
"status.reply": "Ответить",
@@ -194,11 +194,11 @@
"status.report": "Пожаловаться",
"status.sensitive_toggle": "Нажмите для просмотра",
"status.sensitive_warning": "Чувствительный контент",
"status.share": "Поделиться",
"status.share": "Share",
"status.show_less": "Свернуть",
"status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда",
"status.unpin": "Открепить от профиля",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",
@@ -206,16 +206,16 @@
"tabs_bar.notifications": "Уведомления",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Добавить медиаконтент",
"upload_form.description": "Описать для людей с нарушениями зрения",
"upload_form.description": "Describe for the visually impaired",
"upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...",
"video.close": "Закрыть видео",
"video.exit_fullscreen": "Покинуть полноэкранный режим",
"video.expand": "Развернуть видео",
"video.fullscreen": "Полноэкранный режим",
"video.hide": "Скрыть видео",
"video.mute": "Заглушить звук",
"video.pause": "Пауза",
"video.play": "Пуск",
"video.unmute": "Включить звук"
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
}

View File

@@ -58,12 +58,6 @@ const initialState = ImmutableMap({
body: '',
}),
}),
direct: ImmutableMap({
regex: ImmutableMap({
body: '',
}),
}),
});
const defaultColumns = fromJS([

View File

@@ -322,11 +322,6 @@
}
}
.compose-form__buttons-separator {
border-left: 1px solid #c3c3c3;
margin: 0 3px;
}
.compose-form__upload-button-icon {
line-height: 27px;
}
@@ -2503,7 +2498,6 @@ button.icon-button.active i.fa-retweet {
}
.column-header {
display: flex;
padding: 15px;
font-size: 16px;
background: lighten($ui-base-color, 4%);
@@ -2529,10 +2523,12 @@ button.icon-button.active i.fa-retweet {
}
.column-header__buttons {
height: 48px;
position: absolute;
right: 0;
top: 0;
height: 100%;
display: flex;
margin: -15px;
margin-left: 0;
height: 48px;
}
.column-header__button {
@@ -2691,14 +2687,6 @@ button.icon-button.active i.fa-retweet {
}
}
.column-header__title {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
}
.text-btn {
display: inline-block;
padding: 0;
@@ -3409,21 +3397,21 @@ button.icon-button.active i.fa-retweet {
}
.fa-search {
transform: rotate(90deg);
transform: translateZ(0) rotate(90deg);
&.active {
pointer-events: none;
transform: rotate(0deg);
transform: translateZ(0) rotate(0deg);
}
}
.fa-times-circle {
top: 11px;
transform: rotate(0deg);
transform: translateZ(0) rotate(0deg);
cursor: pointer;
&.active {
transform: rotate(90deg);
transform: translateZ(0) rotate(90deg);
}
&:hover {
@@ -3472,6 +3460,7 @@ button.icon-button.active i.fa-retweet {
right: 0;
bottom: 0;
background: rgba($base-overlay-background, 0.7);
transform: translateZ(0);
}
.modal-root__container {

View File

@@ -53,9 +53,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_tags(status)
return if @object['tag'].nil?
return unless @object['tag'].is_a?(Array)
as_array(@object['tag']).each do |tag|
@object['tag'].each do |tag|
case tag['type']
when 'Hashtag'
process_hashtag tag, status
@@ -103,9 +103,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_attachments(status)
return if @object['attachment'].nil?
return unless @object['attachment'].is_a?(Array)
as_array(@object['attachment']).each do |attachment|
@object['attachment'].each do |attachment|
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s

View File

@@ -141,8 +141,6 @@ class FeedManager
return false if receiver_id == status.account_id
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
return true if keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id))
check_for_mutes = [status.account_id]
check_for_mutes.concat(status.mentions.pluck(:account_id))
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
@@ -168,18 +166,6 @@ class FeedManager
false
end
def keyword_filter?(status, matcher)
should_filter = matcher =~ status.text
should_filter ||= matcher =~ status.spoiler_text
if status.reblog?
should_filter ||= matcher =~ status.reblog.text
should_filter ||= matcher =~ status.reblog.spoiler_text
end
!!should_filter
end
def filter_from_mentions?(status, receiver_id)
return true if receiver_id == status.account_id
@@ -189,7 +175,6 @@ class FeedManager
should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
should_filter ||= keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id)) # or if the mention contains a muted keyword
should_filter
end

View File

@@ -15,7 +15,6 @@
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
# visible_in_picker :boolean default(TRUE), not null
#
class CustomEmoji < ApplicationRecord

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
module Glitch
def self.table_name_prefix
'glitch_'
end
end

View File

@@ -1,66 +0,0 @@
# 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

View File

@@ -154,14 +154,6 @@ class Status < ApplicationRecord
where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
end
def as_direct_timeline(account)
query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
.where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}")
.where(visibility: [:direct])
apply_timeline_filters(query, account, false)
end
def as_public_timeline(account = nil, local_only = false)
query = timeline_scope(local_only).without_replies

View File

@@ -3,7 +3,7 @@
class REST::CustomEmojiSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :shortcode, :url, :static_url, :visible_in_picker
attributes :shortcode, :url, :static_url
def url
full_asset_url(object.image.url)

View File

@@ -40,7 +40,6 @@ class BatchedRemoveStatusService < BaseService
# Cannot be batched
statuses.each do |status|
unpush_from_public_timelines(status)
unpush_from_direct_timelines(status) if status.direct_visibility?
batch_salmon_slaps(status) if status.local?
end
@@ -101,16 +100,6 @@ class BatchedRemoveStatusService < BaseService
end
end
def unpush_from_direct_timelines(status)
payload = @json_payloads[status.id]
redis.pipelined do
@mentions[status.id].each do |mention|
redis.publish("timeline:direct:#{mention.account.id}", payload) if mention.account.local?
end
redis.publish("timeline:direct:#{status.account.id}", payload) if status.account.local?
end
end
def batch_salmon_slaps(status)
return if @mentions[status.id].empty?

View File

@@ -10,17 +10,15 @@ class FanOutOnWriteService < BaseService
deliver_to_self(status) if status.account.local?
render_anonymous_payload(status)
if status.direct_visibility?
deliver_to_mentioned_followers(status)
deliver_to_direct_timelines(status)
else
deliver_to_followers(status)
end
return if status.account.silenced? || !status.public_visibility? || status.reblog?
render_anonymous_payload(status)
deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account_id != status.account_id
@@ -75,13 +73,4 @@ class FanOutOnWriteService < BaseService
Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local?
end
def deliver_to_direct_timelines(status)
Rails.logger.debug "Delivering status #{status.id} to direct timelines"
status.mentions.includes(:account).each do |mention|
Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
end
Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local?
end
end

View File

@@ -18,7 +18,6 @@ class RemoveStatusService < BaseService
remove_reblogs
remove_from_hashtags
remove_from_public
remove_from_direct if status.direct_visibility?
@status.destroy!
@@ -122,13 +121,6 @@ class RemoveStatusService < BaseService
Redis.current.publish('timeline:public:local', @payload) if @status.local?
end
def remove_from_direct
@mentions.each do |mention|
Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
end
Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
end
def redis
Redis.current
end

View File

@@ -9,12 +9,7 @@
- else
= custom_emoji.domain
%td
- 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
- unless custom_emoji.local?
= table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post
%td
- if custom_emoji.disabled?

View File

@@ -5,7 +5,6 @@
%link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/direct_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)

View File

@@ -1,11 +0,0 @@
.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

View File

@@ -1,10 +0,0 @@
%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') }

View File

@@ -1,6 +0,0 @@
- 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

View File

@@ -1,18 +0,0 @@
- 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') }

View File

@@ -1,6 +0,0 @@
- 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

View File

@@ -97,8 +97,6 @@ Rails.application.configure do
'X-XSS-Protection' => '1; mode=block',
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload'
}
end

View File

@@ -130,15 +130,11 @@ en:
enable: Enable
enabled_msg: Successfully enabled that emoji
image_hint: PNG up to 50KB
listed: Listed
new:
title: Add new custom emoji
shortcode: Shortcode
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
title: Custom emojis
unlisted: Unlisted
update_failed_msg: Could not update that emoji
updated_msg: Emoji successfully updated!
upload: Upload
domain_blocks:
add_new: Add new
@@ -377,14 +373,6 @@ en:
following: Following list
muting: Muting list
upload: Upload
keyword_mutes:
add_keyword: Add keyword
edit: Edit
edit_keyword: Edit keyword
keyword: Keyword
match_whole_word: Match whole word
remove: Remove
remove_all: Remove all
landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse."
landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
media_attachments:
@@ -503,7 +491,6 @@ en:
export: Data export
followers: Authorized followers
import: Import
keyword_mutes: Muted keywords
notifications: Notifications
preferences: Preferences
settings: Settings

View File

@@ -1,77 +1,39 @@
---
ru:
about:
about_hashtag_html: Это публичные статусы, отмеченные хэштегом <strong>#%{hashtag}</strong>. Вы можете взаимодействовать с ними при наличии у Вас аккаунта в глобальной сети Mastodon.
about_mastodon_html: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете &mdash; что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно.
about_this: Об этом узле
closed_registrations: В данный момент регистрация на этом узле закрыта.
contact: Связаться
contact_missing: Не установлено
contact_unavailable: Недоступен
description_headline: Что такое %{domain}?
domain_count_after: другими узлами
domain_count_before: Связан с
extended_description_html: |
<h3>Хорошее место для правил</h3>
<p>Расширенное описание еще не настроено.</p>
features:
humane_approach_body: Наученный ошибками других проектов, Mastodon направлен на выбор этичных решений в борьбе со злоупотреблениями возможностями социальных сетей.
humane_approach_title: Человечный подход
not_a_product_body: Mastodon - не коммерческая сеть. Здесь нет рекламы, сбора данных, отгороженных мест. Здесь нет централизованного управления.
not_a_product_title: Вы - человек, а не продукт
real_conversation_body: С 500 символами в Вашем распоряжении и поддержкой предупреждений о содержании статусов Вы сможете выражать свои мысли так, как Вы этого хотите.
real_conversation_title: Создан для настоящего общения
within_reach_body: Различные приложения для iOS, Android и других платформ, написанные благодаря дружественной к разработчикам экосистеме API, позволят Вам держать связь с Вашими друзьями где угодно.
within_reach_title: Всегда под рукой
find_another_instance: Найти другой узел
generic_description: "%{domain} - один из серверов сети"
hosted_on: Mastodon размещен на %{domain}
learn_more: Узнать больше
other_instances: Другие узлы
source_code: Исходный код
status_count_after: статусов
status_count_before: Опубликовано
user_count_after: пользователей
user_count_before: Здесь живет
what_is_mastodon: Что такое Mastodon?
accounts:
follow: Подписаться
followers: Подписчики
following: Подписан(а)
media: Медиаконтент
nothing_here: Здесь ничего нет!
people_followed_by: Люди, на которых подписан(а) %{name}
people_who_follow: Подписчики %{name}
posts: Посты
posts_with_replies: Посты с ответами
remote_follow: Подписаться на удаленном узле
reserved_username: Имя пользователя зарезервировано
roles:
admin: Администратор
unfollow: Отписаться
admin:
account_moderation_notes:
account: Модератор
create: Создать
created_at: Дата
created_msg: Заметка модератора успешно создана!
delete: Удалить
destroyed_msg: Заметка модератора успешно удалена!
accounts:
are_you_sure: Вы уверены?
confirm: Подтвердить
confirmed: Подтверждено
disable_two_factor_authentication: Отключить 2FA
display_name: Отображаемое имя
domain: Домен
edit: Изменить
email: E-mail
feed_url: URL фида
followers: Подписчики
followers_url: URL подписчиков
follows: Подписки
inbox_url: URL входящих
ip: IP
location:
all: Все
local: Локальные
@@ -83,7 +45,6 @@ ru:
silenced: Заглушенные
suspended: Заблокированные
title: Модерация
moderation_notes: Заметки модератора
most_recent_activity: Последняя активность
most_recent_ip: Последний IP
not_subscribed: Не подписаны
@@ -91,51 +52,19 @@ ru:
alphabetic: По алфавиту
most_recent: По дате
title: Порядок
outbox_url: URL исходящих
perform_full_suspension: Полная блокировка
profile_url: URL профиля
protocol: Протокол
public: Публичный
push_subscription_expires: Подписка PuSH истекает
redownload: Обновить аватар
reset: Сбросить
reset_password: Сбросить пароль
resubscribe: Переподписаться
salmon_url: Salmon URL
search: Поиск
shared_inbox_url: URL общих входящих
show:
created_reports: Жалобы, отправленные этим аккаунтом
report: жалоба
targeted_reports: Жалобы на этот аккаунт
silence: Глушение
statuses: Статусы
subscribe: Подписаться
title: Аккаунты
undo_silenced: Снять глушение
undo_suspension: Снять блокировку
unsubscribe: Отписаться
username: Имя пользователя
web: WWW
custom_emojis:
copied_msg: Локальная копия эмодзи успешно создана
copy: Скопироват
copy_failed_msg: Не удалось создать локальную копию эмодзи
created_msg: Эмодзи успешно создано!
delete: Удалить
destroyed_msg: Эмодзи успешно удалено!
disable: Отключить
disabled_msg: Эмодзи успешно отключено
emoji: Эмодзи
enable: Включить
enabled_msg: Эмодзи успешно включено
image_hint: PNG до 50KB
new:
title: Добавить новое эмодзи
shortcode: Шорткод
shortcode_hint: Как минимум 2 символа, только алфавитно-цифровые символы и подчеркивания
title: Собственные эмодзи
upload: Загрузить
domain_blocks:
add_new: Добавить новую
created_msg: Блокировка домена обрабатывается
@@ -145,15 +74,13 @@ ru:
create: Создать блокировку
hint: Блокировка домена не предотвратит создание новых аккаунтов в базе данных, но ретроактивно и автоматически применит указанные методы модерации для этих аккаунтов.
severity:
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля. Используйте <strong>Ничего</strong>, если хотите только запретить медиаконтент."
noop: Ничего
desc_html: "<strong>Глушение</strong> сделает статусы аккаунта невидимыми для всех, кроме их подписчиков. <strong>Блокировка</strong> удалит весь контент аккаунта, включая мультимедийные вложения и данные профиля."
silence: Глушение
suspend: Блокировка
title: Новая доменная блокировка
reject_media: Запретить медиаконтент
reject_media_hint: Удаляет локально хранимый медиаконтент и запрещает его загрузку в будущем. Не имеет значения в случае блокировки.
severities:
noop: Ничего
silence: Глушение
suspend: Блокировка
severity: Строгость
@@ -170,34 +97,13 @@ ru:
undo: Отменить
title: Доменные блокировки
undo: Отемнить
email_domain_blocks:
add_new: Добавить новую
created_msg: Доменная блокировка еmail успешно создана
delete: Удалить
destroyed_msg: Доменная блокировка еmail успешно удалена
domain: Домен
new:
create: Создать блокировку
title: Новая доменная блокировка еmail
title: Доменная блокировка email
instances:
account_count: Известных аккаунтов
domain_name: Домен
reset: Сбросить
search: Поиск
title: Известные узлы
reports:
action_taken_by: 'Действие предпринято:'
are_you_sure: Вы уверены?
comment:
label: Комментарий
none: Нет
delete: Удалить
id: ID
mark_as_resolved: Отметить как разрешенную
nsfw:
'false': Показать мультимедийные вложения
'true': Скрыть мультимедийные вложения
report: 'Жалоба #%{id}'
reported_account: Аккаунт нарушителя
reported_by: Отправитель жалобы
@@ -210,9 +116,6 @@ ru:
unresolved: Неразрешенные
view: Просмотреть
settings:
bootstrap_timeline_accounts:
desc_html: Разделяйте имена пользователей запятыми. Сработает только для локальных незакрытых аккаунтов. По умолчанию включены все локальные администраторы.
title: Подписки по умолчанию для новых пользователей
contact_information:
email: Введите публичный e-mail
username: Введите имя пользователя
@@ -220,11 +123,7 @@ ru:
closed_message:
desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги
title: Сообщение о закрытой регистрации
deletion:
desc_html: Позволяет всем удалять собственные аккаунты
title: Разрешить удаление аккаунтов
open:
desc_html: Позволяет любому создавать аккаунт
title: Открыть регистрацию
site_description:
desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code>&lt;a&gt;</code> и <code>&lt;em&gt;</code>.
@@ -232,32 +131,8 @@ ru:
site_description_extended:
desc_html: Отображается на странице дополнительной информации<br>Можно использовать HTML-теги
title: Расширенное описание сайта
site_terms:
desc_html: Вы можете добавить сюда собственную политику конфиденциальности, пользовательское соглашение и другие документы. Можно использовать теги HTML.
title: Условия использования
site_title: Название сайта
thumbnail:
desc_html: Используется для предпросмотра с помощью OpenGraph и API. Рекомендуется разрешение 1200x630px
title: Картинка узла
timeline_preview:
desc_html: Показывать публичную ленту на целевой странице
title: Предпросмотр ленты
title: Настройки сайта
statuses:
back_to_account: Назад к странице аккаунта
batch:
delete: Удалить
nsfw_off: Выключить NSFW
nsfw_on: Включить NSFW
execute: Выполнить
failed_to_execute: Не удалось выполнить
media:
hide: Скрыть медиаконтент
show: Показать медиаконтент
title: Медиаконтент
no_media: Без медиаконтента
title: Статусы аккаунта
with_media: С медиаконтентом
subscriptions:
callback_url: Callback URL
confirmed: Подтверждено
@@ -266,31 +141,18 @@ ru:
title: WebSub
topic: Тема
title: Администрирование
admin_mailer:
new_report:
body: "%{reporter} подал(а) жалобу на %{target}"
subject: Новая жалоба, узел %{instance} (#%{id})
application_mailer:
salutation: "%{name},"
settings: 'Изменить настройки e-mail: %{link}'
signature: Уведомления Mastodon от %{instance}
view: 'Просмотр:'
applications:
created: Приложение успешно создано
destroyed: Приложение успешно удалено
invalid_url: Введенный URL неверен
regenerate_token: Повторно сгенерировать токен доступа
token_regenerated: Токен доступа успешно сгенерирован
warning: Будьте очень внимательны с этими данными. Не делитесь ими ни с кем!
your_token: Ваш токен доступа
auth:
agreement_html: Создавая аккаунт, вы соглашаетесь с <a href="%{rules_path}">нашими правилами поведения</a> и <a href="%{terms_path}">политикой конфиденциальности</a>.
change_password: Изменить пароль
delete_account: Удалить аккаунт
delete_account_html: Если Вы хотите удалить свой аккаунт, вы можете <a href="%{path}">перейти сюда</a>. У Вас будет запрошено подтверждение.
didnt_get_confirmation: Не получили инструкцию для подтверждения?
forgot_password: Забыли пароль?
invalid_reset_password_token: Токен сброса пароля неверен или устарел. Пожалуйста, запросите новый.
login: Войти
logout: Выйти
register: Зарегистрироваться
@@ -300,12 +162,6 @@ ru:
authorize_follow:
error: К сожалению, при поиске удаленного аккаунта возникла ошибка
follow: Подписаться
follow_request: 'Вы отправили запрос на подписку:'
following: 'Ура! Теперь Вы подписаны на:'
post_follow:
close: Или просто закрыть это окно.
return: Вернуться к профилю пользователя
web: Перейти к WWW
title: Подписаться на %{acct}
datetime:
distance_in_words:
@@ -337,10 +193,7 @@ ru:
content: Проверка безопасности не удалась. Возможно, Вы блокируете cookies?
title: Проверка безопасности не удалась.
'429': Слишком много запросов
'500':
content: Приносим извинения, но на нашей стороне что-то пошло не так.
title: Страница неверна
noscript_html: Для работы с Mastodon, пожалуйста, включите JavaScript. Кроме того, вы можете использовать одно из <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">приложений</a> Mastodon для Вашей платформы.
noscript_html: Для работы с Mastodon, пожалуйста, включите JavaScript.
exports:
blocks: Список блокировки
csv: CSV
@@ -412,30 +265,23 @@ ru:
number:
human:
decimal_units:
format: "%n %u"
format: "%n%u"
units:
billion: млрд
million: млн
billion: B
million: M
quadrillion: Q
thousand: тыс
trillion: трлн
thousand: K
trillion: T
unit: ''
pagination:
next: След
prev: Пред
truncate: "&hellip;"
preferences:
languages: Языки
other: Другое
publishing: Публикация
web: WWW
push_notifications:
favourite:
title: Ваш статус понравился %{name}
follow:
title: "%{name} теперь подписан(а) на Вас"
group:
title: "%{count} уведомлений"
mention:
action_boost: Продвинуть
action_expand: Развернуть
@@ -489,24 +335,16 @@ ru:
authorized_apps: Авторизованные приложения
back: Назад в Mastodon
delete: Удаление аккаунта
development: Разработка
edit_profile: Изменить профиль
export: Экспорт данных
followers: Авторизованные подписчики
import: Импорт
notifications: Уведомления
preferences: Настройки
settings: Опции
two_factor_authentication: Двухфакторная аутентификация
your_apps: Ваши приложения
statuses:
open_in_web: Открыть в WWW
over_character_limit: превышен лимит символов (%{max})
pin_errors:
limit: Слишком много закрепленных статусов
ownership: Нельзя закрепить чужой статус
private: Нельзя закрепить непубличный статус
reblog: Нельзя закрепить продвинутый статус
show_more: Подробнее
visibilities:
private: Для подписчиков
@@ -521,8 +359,6 @@ ru:
sensitive_content: Чувствительный контент
terms:
title: Условия обслуживания и политика конфиденциальности %{instance}
themes:
default: Mastodon
time:
formats:
default: "%b %d, %Y, %H:%M"
@@ -531,13 +367,11 @@ ru:
description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
disable: Отключить
enable: Включить
enabled: Двухфакторная аутентификация включена
enabled_success: Двухфакторная аутентификация успешно включена
generate_recovery_codes: Сгенерировать коды восстановления
instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
recovery_codes: Коды восстановления
recovery_codes_regenerated: Коды восстановления успешно сгенерированы
recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
setup: Настроить
@@ -545,4 +379,3 @@ ru:
users:
invalid_email: Введенный e-mail неверен
invalid_otp_token: Введен неверный код
signed_in_as: 'Выполнен вход под именем:'

View File

@@ -4,7 +4,6 @@ pt-BR:
hints:
defaults:
avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 120x120px
digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência.
display_name:
one: <span class="name-counter">1</span> caracter restante
other: <span class="name-counter">%{count}</span> caracteres restantes
@@ -14,7 +13,6 @@ pt-BR:
one: <span class="note-counter">1</span> caracter restante
other: <span class="note-counter">%{count}</span> caracteres restantes
setting_noindex: Afeta seu perfil público e as páginas de suas postagens
setting_theme: Afeta a aparência do Mastodon quando em sua conta em qualquer aparelho.
imports:
data: Arquivo CSV exportado de outra instância do Mastodon
sessions:
@@ -44,9 +42,7 @@ pt-BR:
setting_default_sensitive: Sempre marcar mídia como sensível
setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem
setting_noindex: Não quero ser indexado por mecanismos de busca
setting_reduce_motion: Reduz movimento em animações
setting_system_font_ui: Usar a fonte padrão de seu sistema
setting_theme: Tema do site
setting_unfollow_modal: Mostrar diálogo de confirmação antes de deixar de seguir alguém
severity: Gravidade
type: Tipo de importação

View File

@@ -4,7 +4,6 @@ ru:
hints:
defaults:
avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px
digest: Отсылается после долгого периода неактивности с общей информацией упоминаний, полученных в Ваше отсутствие
display_name:
few: Осталось <span class="name-counter">%{count}</span> символа
many: Осталось <span class="name-counter">%{count}</span> символов
@@ -18,7 +17,6 @@ ru:
one: Остался <span class="name-counter">1</span> символ
other: Осталось <span class="name-counter">%{count}</span> символов
setting_noindex: Относится к Вашему публичному профилю и страницам статусов
setting_theme: Влияет на внешний вид Mastodon при выполненном входе в аккаунт.
imports:
data: Файл CSV, экспортированный с другого узла Mastodon
sessions:
@@ -48,8 +46,6 @@ ru:
setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный
setting_delete_modal: Показывать диалог подтверждения перед удалением
setting_noindex: Отказаться от индексации в поисковых машинах
setting_reduce_motion: Уменьшить движение в анимации
setting_site_theme: Тема сайта
setting_system_font_ui: Использовать шрифт системы по умолчанию
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
severity: Строгость

View File

@@ -7,7 +7,6 @@ SimpleNavigation::Configuration.run do |navigation|
primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url
settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
settings.item :keyword_mutes, safe_join([fa_icon('volume-off fw'), t('settings.keyword_mutes')]), settings_keyword_mutes_url
settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url
settings.item :password, safe_join([fa_icon('lock fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}

View File

@@ -66,13 +66,6 @@ Rails.application.routes.draw do
namespace :settings do
resource :profile, only: [:show, :update]
resources :keyword_mutes do
collection do
delete :destroy_all
end
end
resource :preferences, only: [:show, :update]
resource :notifications, only: [:show, :update]
resource :import, only: [:show, :create]
@@ -147,7 +140,7 @@ Rails.application.routes.draw do
resource :two_factor_authentication, only: [:destroy]
end
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do
resources :custom_emojis, only: [:index, :new, :create, :destroy] do
member do
post :copy
post :enable
@@ -200,7 +193,6 @@ Rails.application.routes.draw do
end
namespace :timelines do
resource :direct, only: :show, controller: :direct
resource :home, only: :show, controller: :home
resource :public, only: :show, controller: :public
resources :tag, only: :show

View File

@@ -3,62 +3,48 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
redis = Redis.current
fm = FeedManager.instance
# Old scheme:
# Each user's feed zset had a series of score:value entries,
# where "regular" statuses had the same score and value (their
# ID). Reblogs had a score of the reblogging status' ID, and a
# value of the reblogged status' ID.
# New scheme:
# The feed contains only entries with the same score and value.
# Reblogs result in the reblogging status being added to the
# feed, with an entry in a reblog tracking zset (where the score
# is once again set to the reblogging status' ID, and the value
# is set to the reblogged status' ID). This is safe for Redis'
# float coersion because in this reblog tracking zset, we only
# need the rebloggging status' ID to be able to stop tracking
# entries after they have gotten too far down the feed, which
# does not require an exact value.
# This process reads all feeds and writes 3 times for each reblogs.
# So we use Lua script to avoid overhead between Ruby and Redis.
script = <<-LUA
local timeline_key = KEYS[1]
local reblog_key = KEYS[2]
-- So, first, we iterate over the user's feed to find any reblogs.
local items = redis.call('zrange', timeline_key, 0, -1, 'withscores')
for i = 1, #items, 2 do
local reblogged_id = items[i]
local reblogging_id = items[i + 1]
if (reblogged_id ~= reblogging_id) then
-- The score and value don't match, so this is a reblog.
-- (note that we're transitioning from IDs < 53 bits so we
-- don't have to worry about the loss of precision)
-- Remove the old entry
redis.call('zrem', timeline_key, reblogged_id)
-- Add a new one for the reblogging status
redis.call('zadd', timeline_key, reblogging_id, reblogging_id)
-- Track the fact that this was a reblog
redis.call('zadd', reblog_key, reblogging_id, reblogged_id)
end
end
LUA
script_hash = redis.script(:load, script)
# find_each is batched on the database side.
User.includes(:account).find_each do |user|
account = user.account
# Old scheme:
# Each user's feed zset had a series of score:value entries,
# where "regular" statuses had the same score and value (their
# ID). Reblogs had a score of the reblogging status' ID, and a
# value of the reblogged status' ID.
# New scheme:
# The feed contains only entries with the same score and value.
# Reblogs result in the reblogging status being added to the
# feed, with an entry in a reblog tracking zset (where the score
# is once again set to the reblogging status' ID, and the value
# is set to the reblogged status' ID). This is safe for Redis'
# float coersion because in this reblog tracking zset, we only
# need the rebloggging status' ID to be able to stop tracking
# entries after they have gotten too far down the feed, which
# does not require an exact value.
# So, first, we iterate over the user's feed to find any reblogs.
timeline_key = fm.key(:home, account.id)
reblog_key = fm.key(:home, account.id, 'reblogs')
redis.zrange(timeline_key, 0, -1, with_scores: true).each do |entry|
next if entry[0] == entry[1]
redis.evalsha(script_hash, [timeline_key, reblog_key])
# The score and value don't match, so this is a reblog.
# (note that we're transitioning from IDs < 53 bits so we
# don't have to worry about the loss of precision)
reblogged_id, reblogging_id = entry
# Remove the old entry
redis.zrem(timeline_key, reblogged_id)
# Add a new one for the reblogging status
redis.zadd(timeline_key, reblogging_id, reblogging_id)
# Track the fact that this was a reblog
redis.zadd(reblog_key, reblogging_id, reblogged_id)
end
end
end

View File

@@ -1,12 +0,0 @@
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

View File

@@ -1,7 +0,0 @@
class AddVisibleInPickerToCustomEmoji < ActiveRecord::Migration[5.1]
def change
safety_assured {
add_column :custom_emojis, :visible_in_picker, :boolean, default: true, null: false
}
end
end

View File

@@ -1,7 +0,0 @@
class MoveKeywordMutesIntoGlitchNamespace < ActiveRecord::Migration[5.1]
def change
safety_assured do
rename_table :keyword_mutes, :glitch_keyword_mutes
end
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171021191900) do
ActiveRecord::Schema.define(version: 20171010025614) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -111,7 +111,6 @@ ActiveRecord::Schema.define(version: 20171021191900) do
t.boolean "disabled", default: false, null: false
t.string "uri"
t.string "image_remote_url"
t.boolean "visible_in_picker", default: true, null: false
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
end
@@ -156,15 +155,6 @@ ActiveRecord::Schema.define(version: 20171021191900) do
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
end
create_table "glitch_keyword_mutes", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "keyword", null: false
t.boolean "whole_word", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_glitch_keyword_mutes_on_account_id"
end
create_table "imports", force: :cascade do |t|
t.integer "type", null: false
t.boolean "approved", default: false, null: false
@@ -482,7 +472,6 @@ ActiveRecord::Schema.define(version: 20171021191900) do
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
add_foreign_key "media_attachments", "statuses", on_delete: :nullify

View File

@@ -10,7 +10,6 @@ module Paperclip
unless options[:style] == :original && num_frames > 1
tmp_file = Paperclip::TempfileFactory.new.generate(attachment.instance.file_file_name)
tmp_file << file.read
tmp_file.flush
return tmp_file
end

View File

@@ -135,5 +135,22 @@
},
"optionalDependencies": {
"fsevents": "*"
},
"jest": {
"projects": [
"<rootDir>/app/javascript/mastodon"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/vendor/",
"<rootDir>/config/",
"<rootDir>/log/",
"<rootDir>/public/",
"<rootDir>/tmp/"
],
"setupFiles": [
"raf/polyfill"
],
"setupTestFrameworkScriptFile": "<rootDir>/app/javascript/mastodon/test_setup.js"
}
}

View File

@@ -1,5 +0,0 @@
require 'rails_helper'
RSpec.describe Settings::KeywordMutesController, type: :controller do
end

View File

@@ -1,2 +0,0 @@
Fabricator('Glitch::KeywordMute') do
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,15 +0,0 @@
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

View File

@@ -119,44 +119,6 @@ RSpec.describe FeedManager do
reblog = Fabricate(:status, reblog: status, account: jeff)
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
end
it 'returns true for a status containing a muted keyword' do
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
status = Fabricate(:status, text: 'This is a hot take', account: bob)
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
end
it 'returns true for a reply containing a muted keyword' do
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
s1 = Fabricate(:status, text: 'Something', account: alice)
s2 = Fabricate(:status, text: 'This is a hot take', thread: s1, account: bob)
expect(FeedManager.instance.filter?(:home, s2, alice.id)).to be true
end
it 'returns true for a status whose spoiler text contains a muted keyword' do
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob)
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
end
it 'returns true for a reblog containing a muted keyword' do
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
status = Fabricate(:status, text: 'This is a hot take', account: bob)
reblog = Fabricate(:status, reblog: status, account: jeff)
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
end
it 'returns true for a reblog whose spoiler text contains a muted keyword' do
Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take')
status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob)
reblog = Fabricate(:status, reblog: status, account: jeff)
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
end
end
context 'for mentions feed' do
@@ -185,13 +147,6 @@ RSpec.describe FeedManager do
bob.follow!(alice)
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
end
it 'returns true for status that contains a muted keyword' do
Fabricate('Glitch::KeywordMute', account: bob, keyword: 'take')
status = Fabricate(:status, text: 'This is a hot take', account: alice)
bob.follow!(alice)
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
end
end
end

View File

@@ -1,89 +0,0 @@
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

View File

@@ -20,29 +20,20 @@ RSpec.describe MediaAttachment, type: :model do
end
describe 'non-animated gif non-conversion' do
fixtures = [
{ filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 },
{ filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 },
]
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.gif')) }
fixtures.each do |fixture|
context fixture[:filename] do
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) }
it 'sets type to image' do
expect(media.type).to eq 'image'
end
it 'sets type to image' do
expect(media.type).to eq 'image'
end
it 'leaves original file as-is' do
expect(media.file_content_type).to eq 'image/gif'
end
it 'leaves original file as-is' do
expect(media.file_content_type).to eq 'image/gif'
end
it 'sets meta' do
expect(media.file.meta["original"]["width"]).to eq fixture[:width]
expect(media.file.meta["original"]["height"]).to eq fixture[:height]
expect(media.file.meta["original"]["aspect"]).to eq fixture[:aspect]
end
end
it 'sets meta' do
expect(media.file.meta["original"]["width"]).to eq 600
expect(media.file.meta["original"]["height"]).to eq 400
expect(media.file.meta["original"]["aspect"]).to eq 1.5
end
end

View File

@@ -232,55 +232,6 @@ RSpec.describe Status, type: :model do
end
end
describe '.as_direct_timeline' do
let(:account) { Fabricate(:account) }
let(:followed) { Fabricate(:account) }
let(:not_followed) { Fabricate(:account) }
before do
Fabricate(:follow, account: account, target_account: followed)
@self_public_status = Fabricate(:status, account: account, visibility: :public)
@self_direct_status = Fabricate(:status, account: account, visibility: :direct)
@followed_public_status = Fabricate(:status, account: followed, visibility: :public)
@followed_direct_status = Fabricate(:status, account: followed, visibility: :direct)
@not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct)
@results = Status.as_direct_timeline(account)
end
it 'does not include public statuses from self' do
expect(@results).to_not include(@self_public_status)
end
it 'includes direct statuses from self' do
expect(@results).to include(@self_direct_status)
end
it 'does not include public statuses from followed' do
expect(@results).to_not include(@followed_public_status)
end
it 'includes direct statuses mentioning recipient from followed' do
Fabricate(:mention, account: account, status: @followed_direct_status)
expect(@results).to include(@followed_direct_status)
end
it 'does not include direct statuses not mentioning recipient from followed' do
expect(@results).to_not include(@followed_direct_status)
end
it 'includes direct statuses mentioning recipient from non-followed' do
Fabricate(:mention, account: account, status: @not_followed_direct_status)
expect(@results).to include(@not_followed_direct_status)
end
it 'does not include direct statuses not mentioning recipient from non-followed' do
expect(@results).to_not include(@not_followed_direct_status)
end
end
describe '.as_public_timeline' do
it 'only includes statuses with public visibility' do
public_status = Fabricate(:status, visibility: :public)

View File

@@ -402,10 +402,6 @@ const startWorker = (workerId) => {
streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true);
});
app.get('/api/v1/streaming/direct', (req, res) => {
streamFrom(`timeline:direct:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
});
app.get('/api/v1/streaming/hashtag', (req, res) => {
streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
});
@@ -441,9 +437,6 @@ const startWorker = (workerId) => {
case 'public:local':
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'direct':
streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'hashtag':
streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;