Merge branch 'master' into glitch-soc/merge-upstream

Manually-resolved conflicts:
	.circleci/config.yml
	app/controllers/accounts_controller.rb
	app/controllers/auth/passwords_controller.rb
	app/controllers/statuses_controller.rb
	app/javascript/packs/public.js
	app/models/media_attachment.rb
	app/views/stream_entries/_content_spoiler.html.haml
	app/views/stream_entries/_media.html.haml
	config/locales/en.yml
	config/locales/ja.yml
	config/locales/pl.yml
	lib/mastodon/version.rb

Some content from app/javascript/packs/public.js has been split to
app/javascript/core/settings.js.

Translation strings for glitch-soc's keyword mutes were dropped.

Everything else was mostly “take both”.
th-downstream
Thibaut Girka 6 years ago
commit 1159490f00

@ -31,7 +31,7 @@ class AboutController < ApplicationController
end end
def set_body_classes def set_body_classes
@body_classes = 'about-body' @body_classes = 'with-modals'
end end
def initial_state_params def initial_state_params

@ -11,6 +11,7 @@ class AccountsController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
use_pack 'public' use_pack 'public'
@body_classes = 'with-modals'
@pinned_statuses = [] @pinned_statuses = []
if current_account && @account.blocking?(current_account) if current_account && @account.blocking?(current_account)

@ -3,6 +3,7 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth' layout 'auth'
before_action :set_body_classes
before_action :set_user, only: [:finish_signup] before_action :set_user, only: [:finish_signup]
before_action :set_pack before_action :set_pack
@ -28,6 +29,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
@user = current_user @user = current_user
end end
def set_body_classes
@body_classes = 'lighter'
end
def user_params def user_params
params.require(:user).permit(:email) params.require(:user).permit(:email)
end end

@ -3,6 +3,7 @@
class Auth::PasswordsController < Devise::PasswordsController class Auth::PasswordsController < Devise::PasswordsController
before_action :check_validity_of_reset_password_token, only: :edit before_action :check_validity_of_reset_password_token, only: :edit
before_action :set_pack before_action :set_pack
before_action :set_body_classes
layout 'auth' layout 'auth'
@ -15,6 +16,10 @@ class Auth::PasswordsController < Devise::PasswordsController
end end
end end
def set_body_classes
@body_classes = 'lighter'
end
def reset_password_token_is_valid? def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present? resource_class.with_reset_password_token(params[:reset_password_token]).present?
end end

@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_pack before_action :set_pack
before_action :set_sessions, only: [:edit, :update] before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create]
def destroy def destroy
not_found not_found
@ -84,6 +85,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def set_body_classes
@body_classes = 'lighter'
end
def set_invite def set_invite
@invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil @invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
end end

@ -10,6 +10,7 @@ class Auth::SessionsController < Devise::SessionsController
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
prepend_before_action :set_pack prepend_before_action :set_pack
before_action :set_instance_presenter, only: [:new] before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes
def new def new
Devise.omniauth_configs.each do |provider, config| Devise.omniauth_configs.each do |provider, config|
@ -114,6 +115,10 @@ class Auth::SessionsController < Devise::SessionsController
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def set_body_classes
@body_classes = 'lighter'
end
def home_paths(resource) def home_paths(resource)
paths = [about_path] paths = [about_path]
if single_user_mode? && resource.is_a?(User) if single_user_mode? && resource.is_a?(User)

@ -8,6 +8,7 @@ module AccountControllerConcern
included do included do
layout 'public' layout 'public'
before_action :set_account before_action :set_account
before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :check_account_suspension
end end
@ -18,6 +19,10 @@ module AccountControllerConcern
@account = Account.find_local!(params[:account_username]) @account = Account.find_local!(params[:account_username])
end end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [

@ -43,7 +43,7 @@ class InvitesController < ApplicationController
end end
def invites def invites
Invite.where(user: current_user) Invite.where(user: current_user).order(id: :desc)
end end
def resource_params def resource_params

@ -12,6 +12,7 @@ class StatusesController < ApplicationController
before_action :set_account before_action :set_account
before_action :set_status before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :check_account_suspension
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
@ -22,6 +23,8 @@ class StatusesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
use_pack 'public' use_pack 'public'
@body_classes = 'with-modals'
set_ancestors set_ancestors
set_descendants set_descendants
@ -150,6 +153,10 @@ class StatusesController < ApplicationController
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def check_account_suspension def check_account_suspension
gone if @account.suspended? gone if @account.suspended?
end end

@ -38,7 +38,7 @@ class TagsController < ApplicationController
private private
def set_body_classes def set_body_classes
@body_classes = 'tag-body' @body_classes = 'with-modals'
end end
def set_instance_presenter def set_instance_presenter

@ -12,6 +12,52 @@ module StreamEntriesHelper
end end
end end
def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button', data: { method: :post } do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
end
else
link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
end
end
else
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
end
end
end
def account_badge(account)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif Setting.show_staff_badge && account.user_staff?
content_tag(:div, class: 'roles') do
if account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end
def link_to_more(url)
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
end
def nothing_here(extra_classes = '')
content_tag(:div, class: "nothing-here #{extra_classes}") do
t('accounts.nothing_here')
end
end
def account_description(account) def account_description(account)
prepend_str = [ prepend_str = [
[ [

@ -1,6 +1,7 @@
// This file will be loaded on public pages, regardless of theme. // This file will be loaded on public pages, regardless of theme.
const { delegate } = require('rails-ujs'); const { delegate } = require('rails-ujs');
const { length } = require('stringz');
delegate(document, '.webapp-btn', 'click', ({ target, button }) => { delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
if (button !== 0) { if (button !== 0) {

@ -3,24 +3,29 @@
const { length } = require('stringz'); const { length } = require('stringz');
const { delegate } = require('rails-ujs'); const { delegate } = require('rails-ujs');
delegate(document, '.account_display_name', 'input', ({ target }) => { delegate(document, '#account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter'); const nameCounter = document.querySelector('.name-counter');
const name = document.querySelector('.card .display-name strong');
if (nameCounter) { if (nameCounter) {
nameCounter.textContent = 30 - length(target.value); nameCounter.textContent = 30 - length(target.value);
} }
if (name) {
name.innerHTML = emojify(target.value);
}
}); });
delegate(document, '.account_note', 'input', ({ target }) => { delegate(document, '#account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter'); const noteCounter = document.querySelector('.note-counter');
if (noteCounter) { if (noteCounter) {
noteCounter.textContent = 500 - length(target.value); noteCounter.textContent = 160 - length(target.value);
} }
}); });
delegate(document, '#account_avatar', 'change', ({ target }) => { delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card.compact .avatar img'); const avatar = document.querySelector('.card .avatar img');
const [file] = target.files || []; const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
@ -28,9 +33,19 @@ delegate(document, '#account_avatar', 'change', ({ target }) => {
}); });
delegate(document, '#account_header', 'change', ({ target }) => { delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card.compact'); const header = document.querySelector('.card .card__img img');
const [file] = target.files || []; const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.style.backgroundImage = `url(${url})`; header.src = url;
});
delegate(document, '#account_locked', 'change', ({ target }) => {
const lock = document.querySelector('.card .display-name i');
if (target.checked) {
lock.style.display = 'inline';
} else {
lock.style.display = 'none';
}
}); });

@ -60,6 +60,32 @@ const getUnitDelay = units => {
} }
}; };
export const timeAgoString = (intl, date, now, year) => {
const delta = now - date.getTime();
let relativeTime;
if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.just_now);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
} else {
relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
}
return relativeTime;
};
@injectIntl @injectIntl
export default class RelativeTimestamp extends React.Component { export default class RelativeTimestamp extends React.Component {
@ -121,28 +147,8 @@ export default class RelativeTimestamp extends React.Component {
render () { render () {
const { timestamp, intl, year } = this.props; const { timestamp, intl, year } = this.props;
const date = new Date(timestamp); const date = new Date(timestamp);
const delta = this.state.now - date.getTime(); const relativeTime = timeAgoString(intl, date, this.state.now, year);
let relativeTime;
if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.just_now);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
} else {
relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
}
return ( return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>

@ -1,12 +1,12 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container'; import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import LoadGap from './load_gap'; import LoadGap from './load_gap';
import ScrollableList from './scrollable_list'; import ScrollableList from './scrollable_list';
import { FormattedMessage } from 'react-intl';
export default class StatusList extends ImmutablePureComponent { export default class StatusList extends ImmutablePureComponent {
@ -71,7 +71,7 @@ export default class StatusList extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props; const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other; const { isLoading, isPartial } = other;
if (isPartial) { if (isPartial) {
@ -122,7 +122,7 @@ export default class StatusList extends ImmutablePureComponent {
} }
return ( return (
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
{scrollableContent} {scrollableContent}
</ScrollableList> </ScrollableList>
); );

@ -29,19 +29,19 @@ export default class MediaContainer extends PureComponent {
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
document.body.classList.add('media-standalone__body'); document.body.classList.add('with-modals--active');
this.setState({ media, index }); this.setState({ media, index });
} }
handleOpenVideo = (video, time) => { handleOpenVideo = (video, time) => {
const media = ImmutableList([video]); const media = ImmutableList([video]);
document.body.classList.add('media-standalone__body'); document.body.classList.add('with-modals--active');
this.setState({ media, time }); this.setState({ media, time });
} }
handleCloseMedia = () => { handleCloseMedia = () => {
document.body.classList.remove('media-standalone__body'); document.body.classList.remove('with-modals--active');
this.setState({ media: null, index: null, time: null }); this.setState({ media: null, index: null, time: null });
} }

@ -142,17 +142,17 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'> <div className='account__action-bar'>
<div className='account__action-bar-links'> <div className='account__action-bar-links'>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
<span><FormattedMessage id='account.posts' defaultMessage='Toots' /></span> <FormattedMessage id='account.posts' defaultMessage='Toots' />
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <strong>{shortNumberFormat(account.get('statuses_count'))}</strong>
</Link> </Link>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
<span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span> <FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <strong>{shortNumberFormat(account.get('following_count'))}</strong>
</Link> </Link>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
<span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> <FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <strong>{shortNumberFormat(account.get('followers_count'))}</strong>
</Link> </Link>
</div> </div>

@ -23,6 +23,7 @@ const mapStateToProps = (state, props) => ({
class LoadMoreMedia extends ImmutablePureComponent { class LoadMoreMedia extends ImmutablePureComponent {
static propTypes = { static propTypes = {
shouldUpdateScroll: PropTypes.func,
maxId: PropTypes.string, maxId: PropTypes.string,
onLoadMore: PropTypes.func.isRequired, onLoadMore: PropTypes.func.isRequired,
}; };
@ -90,7 +91,7 @@ export default class AccountGallery extends ImmutablePureComponent {
} }
render () { render () {
const { medias, isLoading, hasMore } = this.props; const { medias, shouldUpdateScroll, isLoading, hasMore } = this.props;
let loadOlder = null; let loadOlder = null;
@ -110,7 +111,7 @@ export default class AccountGallery extends ImmutablePureComponent {
<Column> <Column>
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='account_gallery'> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.params.accountId} />

@ -29,6 +29,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
@ -61,7 +62,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props; const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) { if (!statusIds && isLoading) {
return ( return (
@ -83,6 +84,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={hasMore}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -8,8 +10,6 @@ import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import { fetchBlocks, expandBlocks } from '../../actions/blocks'; import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
@ -26,6 +26,7 @@ export default class Blocks extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -43,7 +44,7 @@ export default class Blocks extends ImmutablePureComponent {
} }
render () { render () {
const { intl, accountIds } = this.props; const { intl, accountIds, shouldUpdateScroll } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -56,7 +57,7 @@ export default class Blocks extends ImmutablePureComponent {
return ( return (
<Column icon='ban' heading={intl.formatMessage(messages.heading)}> <Column icon='ban' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollContainer scrollKey='blocks'> <ScrollContainer scrollKey='blocks' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
{accountIds.map(id => {accountIds.map(id =>
<AccountContainer key={id} id={id} /> <AccountContainer key={id} id={id} />

@ -39,6 +39,7 @@ export default class CommunityTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
@ -100,7 +101,7 @@ export default class CommunityTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -124,6 +125,7 @@ export default class CommunityTimeline extends React.PureComponent {
timelineId={`community${onlyMedia ? ':media' : ''}`} timelineId={`community${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -23,6 +23,7 @@ export default class DirectTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
@ -71,7 +72,7 @@ export default class DirectTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, hasUnread, columnId, multiColumn } = this.props; const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -93,6 +94,7 @@ export default class DirectTimeline extends React.PureComponent {
timelineId='direct' timelineId='direct'
onLoadMore={this.handleLoadMore} onLoadMore={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." />} 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." />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import DomainContainer from '../../containers/domain_container'; import DomainContainer from '../../containers/domain_container';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
const messages = defineMessages({ const messages = defineMessages({
@ -28,6 +28,7 @@ export default class Blocks extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
domains: ImmutablePropTypes.orderedSet, domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -41,7 +42,7 @@ export default class Blocks extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, domains } = this.props; const { intl, domains, shouldUpdateScroll } = this.props;
if (!domains) { if (!domains) {
return ( return (
@ -54,7 +55,7 @@ export default class Blocks extends ImmutablePureComponent {
return ( return (
<Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}> <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} shouldUpdateScroll={shouldUpdateScroll}>
{domains.map(domain => {domains.map(domain =>
<DomainContainer key={domain} domain={domain} /> <DomainContainer key={domain} domain={domain} />
)} )}

@ -27,6 +27,7 @@ export default class Favourites extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
@ -67,7 +68,7 @@ export default class Favourites extends ImmutablePureComponent {
}, 300, { leading: true }) }, 300, { leading: true })
render () { render () {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -90,6 +91,7 @@ export default class Favourites extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -8,7 +9,6 @@ import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
@ -20,6 +20,7 @@ export default class Favourites extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
}; };
@ -34,7 +35,7 @@ export default class Favourites extends ImmutablePureComponent {
} }
render () { render () {
const { accountIds } = this.props; const { shouldUpdateScroll, accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -48,7 +49,7 @@ export default class Favourites extends ImmutablePureComponent {
<Column> <Column>
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='favourites'> <ScrollContainer scrollKey='favourites' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable'> <div className='scrollable'>
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
</div> </div>

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -8,8 +10,6 @@ import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountAuthorizeContainer from './containers/account_authorize_container'; import AccountAuthorizeContainer from './containers/account_authorize_container';
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@ -26,6 +26,7 @@ export default class FollowRequests extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -43,7 +44,7 @@ export default class FollowRequests extends ImmutablePureComponent {
} }
render () { render () {
const { intl, accountIds } = this.props; const { intl, shouldUpdateScroll, accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -57,7 +58,7 @@ export default class FollowRequests extends ImmutablePureComponent {
<Column icon='users' heading={intl.formatMessage(messages.heading)}> <Column icon='users' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollContainer scrollKey='follow_requests'> <ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
{accountIds.map(id => {accountIds.map(id =>
<AccountAuthorizeContainer key={id} id={id} /> <AccountAuthorizeContainer key={id} id={id} />

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -14,7 +15,6 @@ import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
@ -27,6 +27,7 @@ export default class Followers extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
}; };
@ -57,7 +58,7 @@ export default class Followers extends ImmutablePureComponent {
} }
render () { render () {
const { accountIds, hasMore } = this.props; const { shouldUpdateScroll, accountIds, hasMore } = this.props;
let loadMore = null; let loadMore = null;
@ -77,7 +78,7 @@ export default class Followers extends ImmutablePureComponent {
<Column> <Column>
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='followers'> <ScrollContainer scrollKey='followers' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<div className='followers'> <div className='followers'>
<HeaderContainer accountId={this.props.params.accountId} hideTabs /> <HeaderContainer accountId={this.props.params.accountId} hideTabs />

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -14,7 +15,6 @@ import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
@ -27,6 +27,7 @@ export default class Following extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
}; };
@ -57,7 +58,7 @@ export default class Following extends ImmutablePureComponent {
} }
render () { render () {
const { accountIds, hasMore } = this.props; const { shouldUpdateScroll, accountIds, hasMore } = this.props;
let loadMore = null; let loadMore = null;
@ -77,7 +78,7 @@ export default class Following extends ImmutablePureComponent {
<Column> <Column>
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='following'> <ScrollContainer scrollKey='following' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable' onScroll={this.handleScroll}> <div className='scrollable' onScroll={this.handleScroll}>
<div className='following'> <div className='following'>
<HeaderContainer accountId={this.props.params.accountId} hideTabs /> <HeaderContainer accountId={this.props.params.accountId} hideTabs />

@ -20,6 +20,7 @@ export default class HashtagTimeline extends React.PureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -83,7 +84,7 @@ export default class HashtagTimeline extends React.PureComponent {
} }
render () { render () {
const { hasUnread, columnId, multiColumn } = this.props; const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
@ -107,6 +108,7 @@ export default class HashtagTimeline extends React.PureComponent {
timelineId={`hashtag:${id}`} timelineId={`hashtag:${id}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -25,6 +25,7 @@ export default class HomeTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
isPartial: PropTypes.bool, isPartial: PropTypes.bool,
@ -93,7 +94,7 @@ export default class HomeTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, hasUnread, columnId, multiColumn } = this.props; const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -117,6 +118,7 @@ export default class HomeTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
timelineId='home' timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -35,6 +35,7 @@ export default class ListTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -112,7 +113,7 @@ export default class ListTimeline extends React.PureComponent {
} }
render () { render () {
const { hasUnread, columnId, multiColumn, list } = this.props; const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const title = list ? list.get('title') : id; const title = list ? list.get('title') : id;
@ -166,6 +167,7 @@ export default class ListTimeline extends React.PureComponent {
timelineId={`list:${id}`} timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />} emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -8,8 +10,6 @@ import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import { fetchMutes, expandMutes } from '../../actions/mutes'; import { fetchMutes, expandMutes } from '../../actions/mutes';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
@ -26,6 +26,7 @@ export default class Mutes extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -43,7 +44,7 @@ export default class Mutes extends ImmutablePureComponent {
} }
render () { render () {
const { intl, accountIds } = this.props; const { intl, shouldUpdateScroll, accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -56,7 +57,7 @@ export default class Mutes extends ImmutablePureComponent {
return ( return (
<Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollContainer scrollKey='mutes'> <ScrollContainer scrollKey='mutes' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable mutes' onScroll={this.handleScroll}> <div className='scrollable mutes' onScroll={this.handleScroll}>
{accountIds.map(id => {accountIds.map(id =>
<AccountContainer key={id} id={id} /> <AccountContainer key={id} id={id} />

@ -24,6 +24,7 @@ export default class PinnedStatuses extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired, hasMore: PropTypes.bool.isRequired,
@ -42,7 +43,7 @@ export default class PinnedStatuses extends ImmutablePureComponent {
} }
render () { render () {
const { intl, statusIds, hasMore } = this.props; const { intl, shouldUpdateScroll, statusIds, hasMore } = this.props;
return ( return (
<Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
@ -51,6 +52,7 @@ export default class PinnedStatuses extends ImmutablePureComponent {
statusIds={statusIds} statusIds={statusIds}
scrollKey='pinned_statuses' scrollKey='pinned_statuses'
hasMore={hasMore} hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -39,6 +39,7 @@ export default class PublicTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -107,7 +108,7 @@ export default class PublicTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -131,6 +132,7 @@ export default class PublicTimeline extends React.PureComponent {
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`} scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
shouldUpdateScroll={shouldUpdateScroll}
/> />
</Column> </Column>
); );

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -8,7 +9,6 @@ import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
@ -20,6 +20,7 @@ export default class Reblogs extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
}; };
@ -34,7 +35,7 @@ export default class Reblogs extends ImmutablePureComponent {
} }
render () { render () {
const { accountIds } = this.props; const { shouldUpdateScroll, accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -48,7 +49,7 @@ export default class Reblogs extends ImmutablePureComponent {
<Column> <Column>
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='reblogs'> <ScrollContainer scrollKey='reblogs' shouldUpdateScroll={shouldUpdateScroll}>
<div className='scrollable reblogs'> <div className='scrollable reblogs'>
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
</div> </div>

@ -42,7 +42,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -370,7 +370,7 @@ export default class Status extends ImmutablePureComponent {
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { status, ancestorsIds, descendantsIds, intl } = this.props; const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
if (status === null) { if (status === null) {
@ -410,7 +410,7 @@ export default class Status extends ImmutablePureComponent {
)} )}
/> />
<ScrollContainer scrollKey='thread'> <ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}>
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
{ancestors} {ancestors}

@ -16,7 +16,7 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
const previewState = 'previewMediaModal'; export const previewState = 'previewMediaModal';
@injectIntl @injectIntl
export default class MediaModal extends ImmutablePureComponent { export default class MediaModal extends ImmutablePureComponent {

@ -41,14 +41,15 @@ export default class ModalRoot extends React.PureComponent {
}; };
getSnapshotBeforeUpdate () { getSnapshotBeforeUpdate () {
const visible = !!this.props.type; return { visible: !!this.props.type };
return {
overflowY: visible ? 'hidden' : null,
};
} }
componentDidUpdate (prevProps, prevState, { overflowY }) { componentDidUpdate (prevProps, prevState, { visible }) {
document.body.style.overflowY = overflowY; if (visible) {
document.body.classList.add('with-modals--active');
} else {
document.body.classList.remove('with-modals--active');
}
} }
renderLoading = modalId => () => { renderLoading = modalId => () => {

@ -1,12 +1,14 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import NotificationsContainer from './containers/notifications_container'; import { HotKeys } from 'react-hotkeys';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container'; import LoadingBarContainer from './containers/loading_bar_container';
import TabsBar from './components/tabs_bar'; import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container'; import ModalContainer from './containers/modal_container';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import { isMobile } from '../../is_mobile'; import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { uploadCompose, resetCompose } from '../../actions/compose'; import { uploadCompose, resetCompose } from '../../actions/compose';
@ -44,9 +46,8 @@ import {
PinnedStatuses, PinnedStatuses,
Lists, Lists,
} from './util/async-components'; } from './util/async-components';
import { HotKeys } from 'react-hotkeys';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
import { defineMessages, injectIntl } from 'react-intl'; import { previewState } from './components/media_modal';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles. // Without this it ends up in ~8 very commonly used bundles.
@ -117,6 +118,10 @@ class SwitchingColumnsArea extends React.PureComponent {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
shouldUpdateScroll (_, { location }) {
return location.state !== previewState;
}
handleResize = debounce(() => { handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate // The cached heights are no longer accurate, invalidate
this.props.onLayoutChange(); this.props.onLayoutChange();
@ -141,36 +146,36 @@ class SwitchingColumnsArea extends React.PureComponent {
{redirect} {redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/media' component={PublicTimeline} content={children} componentParams={{ onlyMedia: true }} /> <WrappedRoute path='/timelines/public/media' component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} /> <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ onlyMedia: true }} /> <WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} /> <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute component={GenericNotFound} content={children} /> <WrappedRoute component={GenericNotFound} content={children} />

@ -1,17 +1,17 @@
{ {
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.block": "Blokovat @{name}", "account.block": "Zablokovat uživatele @{name}",
"account.block_domain": "Skrýt vše z {domain}", "account.block_domain": "Skrýt vše z {domain}",
"account.blocked": "Blokován/a", "account.blocked": "Blokován/a",
"account.direct": "Přímá zpráva pro uživatele @{name}", "account.direct": "Přímá zpráva pro uživatele @{name}",
"account.disclaimer_full": "Níže uvedené informace nemusejí zcela odrážet profil uživatele.", "account.disclaimer_full": "Níže uvedené informace nemusejí zcela odrážet profil uživatele.",
"account.domain_blocked": "Doména skryta", "account.domain_blocked": "Doména skryta",
"account.edit_profile": "Uprav profil", "account.edit_profile": "Upravit profil",
"account.follow": "Sleduj", "account.follow": "Sleduj",
"account.followers": "Sledovatelé", "account.followers": "Sledovatelé",
"account.follows": "Sleduje", "account.follows": "Sleduje",
"account.follows_you": "Sleduje vás", "account.follows_you": "Sleduje vás",
"account.hide_reblogs": "Skrýt povýšení od uživatele @{name}", "account.hide_reblogs": "Skrýt boosty od uživatele @{name}",
"account.media": "Média", "account.media": "Média",
"account.mention": "Zmínit uživatele @{name}", "account.mention": "Zmínit uživatele @{name}",
"account.moved_to": "{name} se přesunul/a na:", "account.moved_to": "{name} se přesunul/a na:",
@ -23,7 +23,7 @@
"account.report": "Nahlásit uživatele @{name}", "account.report": "Nahlásit uživatele @{name}",
"account.requested": "Požadavek čeká na schválení. Kliknutím zrušíte požadavek o sledování", "account.requested": "Požadavek čeká na schválení. Kliknutím zrušíte požadavek o sledování",
"account.share": "Sdílet profil uživatele @{name}", "account.share": "Sdílet profil uživatele @{name}",
"account.show_reblogs": "Zobrazit povýšení od uživatele @{name}", "account.show_reblogs": "Zobrazit boosty od uživatele @{name}",
"account.unblock": "Odblokovat uživatele @{name}", "account.unblock": "Odblokovat uživatele @{name}",
"account.unblock_domain": "Odkrýt doménu {domain}", "account.unblock_domain": "Odkrýt doménu {domain}",
"account.unfollow": "Přestat sledovat", "account.unfollow": "Přestat sledovat",
@ -64,7 +64,7 @@
"compose_form.direct_message_warning_learn_more": "Zjistit více", "compose_form.direct_message_warning_learn_more": "Zjistit více",
"compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.", "compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.",
"compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledovatele.", "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledovatele.",
"compose_form.lock_disclaimer.lock": "zamknutý", "compose_form.lock_disclaimer.lock": "zamčený",
"compose_form.placeholder": "Co máte na mysli?", "compose_form.placeholder": "Co máte na mysli?",
"compose_form.publish": "Tootnout", "compose_form.publish": "Tootnout",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
@ -85,225 +85,225 @@
"confirmations.mute.confirm": "Ignorovat", "confirmations.mute.confirm": "Ignorovat",
"confirmations.mute.message": "Jste si jistý/á, že chcete ignorovat uživatele {name}?", "confirmations.mute.message": "Jste si jistý/á, že chcete ignorovat uživatele {name}?",
"confirmations.redraft.confirm": "Vymazat a přepsat", "confirmations.redraft.confirm": "Vymazat a přepsat",
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", "confirmations.redraft.message": "Jste si jistý/á, že chcete vymazat a přepsat tento status? Ztratíte všechny jeho odpovědi, boosty a oblíbení.",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Přestat sledovat",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "jste si jistý/á, že chcete přestat sledovat uživatele {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Pro přidání statusu na vaši webovou stránku zkopírujte níže uvedený kód.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Takhle to bude vypadat:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Aktivita",
"emoji_button.custom": "Custom", "emoji_button.custom": "Vlastní",
"emoji_button.flags": "Flags", "emoji_button.flags": "Vlajky",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Jídla a nápoje",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Vložit emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Příroda",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", "emoji_button.not_found": "Žádné emoji!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Předměty",
"emoji_button.people": "People", "emoji_button.people": "Lidé",
"emoji_button.recent": "Frequently used", "emoji_button.recent": "Často používané",
"emoji_button.search": "Search...", "emoji_button.search": "Hledat...",
"emoji_button.search_results": "Search results", "emoji_button.search_results": "Výsledky hledání",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symboly",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Cestování a místa",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "Místní časová osa je prázdná. Napište něco veřejně a rozhýbejte to tu!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.direct": "Ještě nemáte žádné přímé zprávy. Pokud nějakou pošlete nebo dostanete, zobrazí se zde.",
"empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.hashtag": "Pod tímto hashtagem ještě nic není.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home": "Vaše domovská časová osa je prázdná! Začněte navštívením {public} nebo použijte hledání a seznamte se s dalšími uživateli.",
"empty_column.home.public_timeline": "the public timeline", "empty_column.home.public_timeline": "veřejné časové osy",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.list": "V tomto seznamu ještě nic není. Pokud budou členové tohoto seznamu psát nové statusy, objeví se zde.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte konverzaci komunikováním s ostatními.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "empty_column.public": "Tady nic není! Napište něco veřejně, nebo manuálně začněte sledovat uživatele z jiných instancí, aby tu něco přibylo",
"follow_request.authorize": "Authorize", "follow_request.authorize": "Autorizovat",
"follow_request.reject": "Reject", "follow_request.reject": "Odmítnout",
"getting_started.developers": "Developers", "getting_started.developers": "Vývojáři",
"getting_started.documentation": "Documentation", "getting_started.documentation": "Documentation",
"getting_started.find_friends": "Find friends from Twitter", "getting_started.find_friends": "Najděte si přátele z Twitteru",
"getting_started.heading": "Getting started", "getting_started.heading": "Začínáme",
"getting_started.invite": "Invite people", "getting_started.invite": "Pozvat lidi",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", "getting_started.open_source_notice": "Mastodon je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {github}.",
"getting_started.security": "Security", "getting_started.security": "Zabezpečení",
"getting_started.terms": "Terms of service", "getting_started.terms": "Podmínky používání",
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Základní",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Zobrazit odpovědi",
"keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.back": "k návratu zpět",
"keyboard_shortcuts.boost": "to boost", "keyboard_shortcuts.boost": "k boostnutí",
"keyboard_shortcuts.column": "to focus a status in one of the columns", "keyboard_shortcuts.column": "k zaměření na status v jednom ze sloupců",
"keyboard_shortcuts.compose": "to focus the compose textarea", "keyboard_shortcuts.compose": "k zaměření na psací prostor",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Popis",
"keyboard_shortcuts.down": "to move down in the list", "keyboard_shortcuts.down": "k přesunutí dolů v seznamu",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "k otevření statusu",
"keyboard_shortcuts.favourite": "to favourite", "keyboard_shortcuts.favourite": "k oblíbení",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Klávesové zkratky",
"keyboard_shortcuts.hotkey": "Hotkey", "keyboard_shortcuts.hotkey": "Horká klávesa",
"keyboard_shortcuts.legend": "to display this legend", "keyboard_shortcuts.legend": "k zobrazení této legendy",
"keyboard_shortcuts.mention": "to mention author", "keyboard_shortcuts.mention": "ke zmínění autora",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "k odpovězení",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "k zaměření na vyhledávání",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "k zobrazení/skrytí textu za CW",
"keyboard_shortcuts.toot": "to start a brand new toot", "keyboard_shortcuts.toot": "k napsání úplně nového tootu",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "ke zrušení soustředění na psací prostor/hledání",
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.up": "k posunutí nahoru v seznamu",
"lightbox.close": "Close", "lightbox.close": "Zavřít",
"lightbox.next": "Next", "lightbox.next": "Další",
"lightbox.previous": "Previous", "lightbox.previous": "Předchozí",
"lists.account.add": "Add to list", "lists.account.add": "Přidat do seznamu",
"lists.account.remove": "Remove from list", "lists.account.remove": "Odebrat ze seznamu",
"lists.delete": "Delete list", "lists.delete": "Smazat seznam",
"lists.edit": "Edit list", "lists.edit": "Upravit seznam",
"lists.new.create": "Add list", "lists.new.create": "Přidat seznam",
"lists.new.title_placeholder": "New list title", "lists.new.title_placeholder": "Název nového seznamu",
"lists.search": "Search among people you follow", "lists.search": "Hledejte mezi uživateli, které sledujete",
"lists.subheading": "Your lists", "lists.subheading": "Vaše seznamy",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Načítám...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Přepínat viditelnost",
"missing_indicator.label": "Not found", "missing_indicator.label": "Nenalezeno",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "Tento zdroj se nepodažilo najít",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Skrýt oznámení před tímto uživatelem?",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blokovaní uživatelé",
"navigation_bar.community_timeline": "Local timeline", "navigation_bar.community_timeline": "Místní časová osa",
"navigation_bar.direct": "Direct messages", "navigation_bar.direct": "Přímé zprávy",
"navigation_bar.discover": "Discover", "navigation_bar.discover": "Objevujte",
"navigation_bar.domain_blocks": "Hidden domains", "navigation_bar.domain_blocks": "Skryté domény",
"navigation_bar.edit_profile": "Edit profile", "navigation_bar.edit_profile": "Upravit profil",
"navigation_bar.favourites": "Favourites", "navigation_bar.favourites": "Oblíbené",
"navigation_bar.filters": "Muted words", "navigation_bar.filters": "Skrytá slova",
"navigation_bar.follow_requests": "Follow requests", "navigation_bar.follow_requests": "Žádosti o sledování",
"navigation_bar.info": "About this instance", "navigation_bar.info": "O této instanci",
"navigation_bar.keyboard_shortcuts": "Hotkeys", "navigation_bar.keyboard_shortcuts": "Klávesové zkratky",
"navigation_bar.lists": "Lists", "navigation_bar.lists": "Seznamy",
"navigation_bar.logout": "Logout", "navigation_bar.logout": "Odhlásit se",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Ignorovaní uživatelé",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Osobní",
"navigation_bar.pins": "Pinned toots", "navigation_bar.pins": "Připnuté tooty",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Předvolby",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federovaná časová osa",
"navigation_bar.security": "Security", "navigation_bar.security": "Zabezpečení",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} označil/a váš status jako oblíbený",
"notification.follow": "{name} followed you", "notification.follow": "{name} vás začal/a sledovat",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} vás zmínil/a",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} vám boostnul/a status",
"notifications.clear": "Clear notifications", "notifications.clear": "Vymazat oznámení",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?",
"notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.alert": "Desktopová oznámení",
"notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.favourite": "Oblíbené:",
"notifications.column_settings.follow": "New followers:", "notifications.column_settings.follow": "Noví sledovatelé:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Zmínky:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push oznámení",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "Toto zařízení",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Show in column", "notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Play sound", "notifications.column_settings.sound": "Přehrát zvuk",
"notifications.group": "{count} notifications", "notifications.group": "{count} oznámení",
"onboarding.done": "Done", "onboarding.done": "Hotovo",
"onboarding.next": "Next", "onboarding.next": "Další",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", "onboarding.page_five.public_timelines": "Místní časová osa zobrazuje veřejné příspěvky od všech lidí na {domain}. Federovaná časová osa zobrazuje veřejné příspěvky ode všech, které lidé na {domain} sledují. Toto jsou veřejné časové osy, výborný způsob, jak objevovat nové lidi.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.home": "Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_four.notifications": "Sloupec oznámení se zobrazí, když s vámi někdo bude komunikovat.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_one.federation": "Mastodon je síť nezávislých serverů, jejichž propojením vzniká jedna velká sociální síť. Těmto serverům říkáme instance.",
"onboarding.page_one.full_handle": "Your full handle", "onboarding.page_one.full_handle": "Vaše celá adresa profilu",
"onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.handle_hint": "Tohle je, co byste řekl/a svým přátelům, aby hledali.",
"onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_one.welcome": "Vítejte na Mastodonu!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.admin": "Administrátorem vaší instance je {admin}.",
"onboarding.page_six.almost_done": "Almost done...", "onboarding.page_six.almost_done": "Skoro hotovo...",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "Bon appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", "onboarding.page_six.apps_available": "Jsou dostupné {apps} pro iOS, Android a jiné platformy.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "onboarding.page_six.github": "Mastodon je svobodný a otevřený software. Na {github} můžete nahlásit chyby, požádat o nové funkce, nebo přispívat ke kódu.",
"onboarding.page_six.guidelines": "community guidelines", "onboarding.page_six.guidelines": "komunitní pravidla",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "Prosím přečtěte si {guidelines} {domain}!",
"onboarding.page_six.various_app": "mobile apps", "onboarding.page_six.various_app": "mobilní aplikace",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", "onboarding.page_three.profile": "Upravte si svůj profil a změňte si svůj avatar, popis profilu a zobrazované jméno. V nastaveních najdete i další možnosti.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", "onboarding.page_three.search": "Pomocí vyhledávacího řádku najděte lidi a podívejte se na hashtagy jako {illustration} a {introductions}. Chcete-li najít někoho, kdo není na této instanci, použijte jeho celou adresu profilu.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", "onboarding.page_two.compose": "Příspěvky pište z pole na komponování. Ikonami níže můžete nahrávat obrázky, změnit nastavení soukromí a přidat varování o obsahu.",
"onboarding.skip": "Skip", "onboarding.skip": "Přeskočit",
"privacy.change": "Adjust status privacy", "privacy.change": "Změnit viditelnost statusu",
"privacy.direct.long": "Post to mentioned users only", "privacy.direct.long": "Odeslat pouze zmíněným uživatelům",
"privacy.direct.short": "Direct", "privacy.direct.short": "Přímé",
"privacy.private.long": "Post to followers only", "privacy.private.long": "Odeslat pouze sledovatelům",
"privacy.private.short": "Followers-only", "privacy.private.short": "Pouze pro sledovatele",
"privacy.public.long": "Post to public timelines", "privacy.public.long": "Odeslat na veřejné časové osy",
"privacy.public.short": "Public", "privacy.public.short": "Veřejné",
"privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted", "privacy.unlisted.short": "Nezobrazované",
"regeneration_indicator.label": "Loading…", "regeneration_indicator.label": "Načítám…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!", "regeneration_indicator.sublabel": "Váš domovský proud se připravuje!",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.hours": "{number}h", "relative_time.hours": "{number}h",
"relative_time.just_now": "now", "relative_time.just_now": "teď",
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancel", "reply_indicator.cancel": "Zrušit",
"report.forward": "Forward to {target}", "report.forward": "Přeposlat k {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", "report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.hint": "Toto nahlášení bude zasláno moderátorům vaší instance. Níže můžete uvést, proč tento účet nahlašujete:",
"report.placeholder": "Additional comments", "report.placeholder": "Další komentáře",
"report.submit": "Submit", "report.submit": "Odeslat",
"report.target": "Report {target}", "report.target": "Nahlásit {target}",
"search.placeholder": "Search", "search.placeholder": "Hledat",
"search_popout.search_format": "Advanced search format", "search_popout.search_format": "Pokročilé vyhledávání",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.full_text": "Jednoduchý textový výpis statusů, které jste napsal/a, oblíbil/a si, povýšil/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, jmen a hashtagů.",
"search_popout.tips.hashtag": "hashtag", "search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status", "search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.text": "Jednoduchý textový výpis odpovídajících jmen, přezdívek a hashtagů",
"search_popout.tips.user": "user", "search_popout.tips.user": "uživatel",
"search_results.accounts": "People", "search_results.accounts": "Lidé",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtagy",
"search_results.statuses": "Toots", "search_results.statuses": "Tooty",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {výsledek} other {výsledků}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "Nahlédnout dovnitř...",
"status.block": "Block @{name}", "status.block": "Zablokovat uživatele @{name}",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Zrušit boost",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
"status.delete": "Delete", "status.delete": "Delete",
"status.direct": "Direct message @{name}", "status.direct": "Poslat přímou zprávu uživateli @{name}",
"status.embed": "Embed", "status.embed": "Vložit",
"status.favourite": "Favourite", "status.favourite": "Oblíbit",
"status.filtered": "Filtered", "status.filtered": "Filtrováno",
"status.load_more": "Load more", "status.load_more": "Zobrazit více",
"status.media_hidden": "Media hidden", "status.media_hidden": "Média skryta",
"status.mention": "Mention @{name}", "status.mention": "Zmínit uživatele @{name}",
"status.more": "More", "status.more": "Více",
"status.mute": "Mute @{name}", "status.mute": "Ignorovat uživatele @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Ignorovat konverzaci",
"status.open": "Expand this status", "status.open": "Otevřít tento status",
"status.pin": "Pin on profile", "status.pin": "Připnout na profil",
"status.pinned": "Pinned toot", "status.pinned": "Připnutý toot",
"status.reblog": "Boost", "status.reblog": "Boostnout",
"status.reblog_private": "Boost to original audience", "status.reblog_private": "Boostnout původnímu publiku",
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boostnul/a",
"status.redraft": "Delete & re-draft", "status.redraft": "Vymazat a přepsat",
"status.reply": "Reply", "status.reply": "Odpovědět",
"status.replyAll": "Reply to thread", "status.replyAll": "Odpovědět na vlákno",
"status.report": "Report @{name}", "status.report": "Nahlásit uživatele @{name}",
"status.sensitive_toggle": "Click to view", "status.sensitive_toggle": "Klikněte pro zobrazení",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Citlivý obsah",
"status.share": "Share", "status.share": "Sdílet",
"status.show_less": "Show less", "status.show_less": "Zobrazit méně",
"status.show_less_all": "Show less for all", "status.show_less_all": "Zobrazit méně pro všechny",
"status.show_more": "Show more", "status.show_more": "Zobrazit více",
"status.show_more_all": "Show more for all", "status.show_more_all": "Zobrazit více pro všechny",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Přestat ignorovat konverzaci",
"status.unpin": "Unpin from profile", "status.unpin": "Odepnout z profilu",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federovaná",
"tabs_bar.home": "Home", "tabs_bar.home": "Domů",
"tabs_bar.local_timeline": "Local", "tabs_bar.local_timeline": "Místní",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Oznámení",
"tabs_bar.search": "Search", "tabs_bar.search": "Hledat",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} other {lidí}} diskutuje",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.",
"upload_area.title": "Drag & drop to upload", "upload_area.title": "Přetažením nahrajete",
"upload_button.label": "Add media", "upload_button.label": "Přidat média",
"upload_form.description": "Describe for the visually impaired", "upload_form.description": "Popis pro zrakově postižené",
"upload_form.focus": "Crop", "upload_form.focus": "Vystřihnout",
"upload_form.undo": "Delete", "upload_form.undo": "Smazat",
"upload_progress.label": "Uploading...", "upload_progress.label": "Nahrávám...",
"video.close": "Close video", "video.close": "Zavřít video",
"video.exit_fullscreen": "Exit full screen", "video.exit_fullscreen": "Ukončit celou obrazovku",
"video.expand": "Expand video", "video.expand": "Otevřít video",
"video.fullscreen": "Full screen", "video.fullscreen": "Celá obrazovka",
"video.hide": "Hide video", "video.hide": "Skrýt video",
"video.mute": "Mute sound", "video.mute": "Vypnout zvuk",
"video.pause": "Pause", "video.pause": "Pauza",
"video.play": "Play", "video.play": "Přehrát",
"video.unmute": "Unmute sound" "video.unmute": "Zapnout zvuk"
} }

@ -167,7 +167,7 @@
"navigation_bar.domain_blocks": "숨겨진 도메인", "navigation_bar.domain_blocks": "숨겨진 도메인",
"navigation_bar.edit_profile": "프로필 편집", "navigation_bar.edit_profile": "프로필 편집",
"navigation_bar.favourites": "즐겨찾기", "navigation_bar.favourites": "즐겨찾기",
"navigation_bar.filters": "Muted words", "navigation_bar.filters": "뮤트",
"navigation_bar.follow_requests": "팔로우 요청", "navigation_bar.follow_requests": "팔로우 요청",
"navigation_bar.info": "이 인스턴스에 대해서", "navigation_bar.info": "이 인스턴스에 대해서",
"navigation_bar.keyboard_shortcuts": "단축키", "navigation_bar.keyboard_shortcuts": "단축키",

@ -129,11 +129,11 @@
"keyboard_shortcuts.boost": "para compartilhar", "keyboard_shortcuts.boost": "para compartilhar",
"keyboard_shortcuts.column": "Focar um status em uma das colunas", "keyboard_shortcuts.column": "Focar um status em uma das colunas",
"keyboard_shortcuts.compose": "para focar a área de redação", "keyboard_shortcuts.compose": "para focar a área de redação",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Descrição",
"keyboard_shortcuts.down": "para mover para baixo na lista", "keyboard_shortcuts.down": "para mover para baixo na lista",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "para expandir um status",
"keyboard_shortcuts.favourite": "para adicionar aos favoritos", "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Atalhos de teclado",
"keyboard_shortcuts.hotkey": "Atalho", "keyboard_shortcuts.hotkey": "Atalho",
"keyboard_shortcuts.legend": "para mostrar essa legenda", "keyboard_shortcuts.legend": "para mostrar essa legenda",
"keyboard_shortcuts.mention": "para mencionar o autor", "keyboard_shortcuts.mention": "para mencionar o autor",

@ -65,7 +65,7 @@
"compose_form.hashtag_warning": "ఈ టూట్ అన్లిస్టెడ్ కాబట్టి ఏ హాష్ ట్యాగ్ క్రిందకూ రాదు. పబ్లిక్ టూట్ లను మాత్రమే హాష్ ట్యాగ్ ద్వారా శోధించవచ్చు.", "compose_form.hashtag_warning": "ఈ టూట్ అన్లిస్టెడ్ కాబట్టి ఏ హాష్ ట్యాగ్ క్రిందకూ రాదు. పబ్లిక్ టూట్ లను మాత్రమే హాష్ ట్యాగ్ ద్వారా శోధించవచ్చు.",
"compose_form.lock_disclaimer": "మీ ఖాతా {locked} చేయబడలేదు. ఎవరైనా మిమ్మల్ని అనుసరించి మీ అనుచరులకు-మాత్రమే పోస్ట్లను వీక్షించవచ్చు.", "compose_form.lock_disclaimer": "మీ ఖాతా {locked} చేయబడలేదు. ఎవరైనా మిమ్మల్ని అనుసరించి మీ అనుచరులకు-మాత్రమే పోస్ట్లను వీక్షించవచ్చు.",
"compose_form.lock_disclaimer.lock": "బిగించబడినది", "compose_form.lock_disclaimer.lock": "బిగించబడినది",
"compose_form.placeholder": "మీ మనస్సులో ఏమి ఉంది?", "compose_form.placeholder": "మీ మనస్సులో ఏమంది?",
"compose_form.publish": "టూట్", "compose_form.publish": "టూట్",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "మీడియా సున్నితమైనదిగా గుర్తించబడింది", "compose_form.sensitive.marked": "మీడియా సున్నితమైనదిగా గుర్తించబడింది",
@ -115,7 +115,7 @@
"follow_request.authorize": "అనుమతించు", "follow_request.authorize": "అనుమతించు",
"follow_request.reject": "తిరస్కరించు", "follow_request.reject": "తిరస్కరించు",
"getting_started.developers": "డెవలపర్లు", "getting_started.developers": "డెవలపర్లు",
"getting_started.documentation": "Documentation", "getting_started.documentation": "డాక్యుమెంటేషన్",
"getting_started.find_friends": "ట్విట్టర్ నుండి స్నేహితులను కనుగొనండి", "getting_started.find_friends": "ట్విట్టర్ నుండి స్నేహితులను కనుగొనండి",
"getting_started.heading": "మొదలుపెడదాం", "getting_started.heading": "మొదలుపెడదాం",
"getting_started.invite": "వ్యక్తులను ఆహ్వానించండి", "getting_started.invite": "వ్యక్తులను ఆహ్వానించండి",
@ -167,7 +167,7 @@
"navigation_bar.domain_blocks": "దాచిన డొమైన్లు", "navigation_bar.domain_blocks": "దాచిన డొమైన్లు",
"navigation_bar.edit_profile": "ప్రొఫైల్ని సవరించండి", "navigation_bar.edit_profile": "ప్రొఫైల్ని సవరించండి",
"navigation_bar.favourites": "ఇష్టపడినవి", "navigation_bar.favourites": "ఇష్టపడినవి",
"navigation_bar.filters": "Muted words", "navigation_bar.filters": "మ్యూట్ చేయబడిన పదాలు",
"navigation_bar.follow_requests": "అనుసరించడానికి అభ్యర్ధనలు", "navigation_bar.follow_requests": "అనుసరించడానికి అభ్యర్ధనలు",
"navigation_bar.info": "ఈ దృష్టాంతం గురించి", "navigation_bar.info": "ఈ దృష్టాంతం గురించి",
"navigation_bar.keyboard_shortcuts": "హాట్ కీలు", "navigation_bar.keyboard_shortcuts": "హాట్ కీలు",
@ -258,7 +258,7 @@
"status.direct": "@{name}కు నేరుగా సందేశం పంపు", "status.direct": "@{name}కు నేరుగా సందేశం పంపు",
"status.embed": "ఎంబెడ్", "status.embed": "ఎంబెడ్",
"status.favourite": "ఇష్టపడు", "status.favourite": "ఇష్టపడు",
"status.filtered": "Filtered", "status.filtered": "వడకట్టబడిన",
"status.load_more": "మరిన్ని లోడ్ చేయి", "status.load_more": "మరిన్ని లోడ్ చేయి",
"status.media_hidden": "మీడియా దాచబడింది", "status.media_hidden": "మీడియా దాచబడింది",
"status.mention": "@{name}ను ప్రస్తావించు", "status.mention": "@{name}ను ప్రస్తావించు",

@ -5,14 +5,16 @@ import { start } from '../mastodon/common';
start(); start();
function main() { function main() {
const IntlRelativeFormat = require('intl-relativeformat').default; const { length } = require('stringz');
const IntlMessageFormat = require('intl-messageformat').default;
const { timeAgoString } = require('../mastodon/components/relative_timestamp');
const { delegate } = require('rails-ujs');
const emojify = require('../mastodon/features/emoji/emoji').default; const emojify = require('../mastodon/features/emoji/emoji').default;
const { getLocale } = require('../mastodon/locales'); const { getLocale } = require('../mastodon/locales');
const { localeData } = getLocale(); const { messages } = getLocale();
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
const Rellax = require('rellax');
localeData.forEach(IntlRelativeFormat.__addLocaleData);
ready(() => { ready(() => {
const locale = document.documentElement.lang; const locale = document.documentElement.lang;
@ -25,8 +27,6 @@ function main() {
minute: 'numeric', minute: 'numeric',
}); });
const relativeFormat = new IntlRelativeFormat(locale);
[].forEach.call(document.querySelectorAll('.emojify'), (content) => { [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML); content.innerHTML = emojify(content.innerHTML);
}); });
@ -41,12 +41,16 @@ function main() {
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
content.title = dateTimeFormat.format(datetime); content.title = dateTimeFormat.format(datetime);
content.textContent = relativeFormat.format(datetime); content.textContent = timeAgoString({
formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, datetime.getFullYear());
}); });
[].forEach.call(document.querySelectorAll('.logo-button'), (content) => { [].forEach.call(document.querySelectorAll('.modal-button'), (content) => {
content.addEventListener('click', (e) => { content.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
window.open(e.target.href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(e.target.href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
@ -64,6 +68,8 @@ function main() {
}) })
.catch(error => console.error(error)); .catch(error => console.error(error));
} }
new Rellax('.parallax', { speed: -1 });
}); });
} }

@ -10,7 +10,7 @@
@import 'mastodon/lists'; @import 'mastodon/lists';
@import 'mastodon/footer'; @import 'mastodon/footer';
@import 'mastodon/compact_header'; @import 'mastodon/compact_header';
@import 'mastodon/landing_strip'; @import 'mastodon/widgets';
@import 'mastodon/forms'; @import 'mastodon/forms';
@import 'mastodon/accounts'; @import 'mastodon/accounts';
@import 'mastodon/stream_entries'; @import 'mastodon/stream_entries';

@ -1115,6 +1115,21 @@ $small-breakpoint: 960px;
} }
&.tag-page { &.tag-page {
@media screen and (max-width: $column-breakpoint) {
padding: 0;
.container {
padding: 0;
}
#mastodon-timeline {
display: block;
width: 100vw;
height: 100vh;
border-radius: 0;
}
}
.grid { .grid {
@media screen and (min-width: $small-breakpoint) { @media screen and (min-width: $small-breakpoint) {
grid-template-columns: 33% 67%; grid-template-columns: 33% 67%;
@ -1146,24 +1161,17 @@ $small-breakpoint: 960px;
@media screen and (max-width: $column-breakpoint) { @media screen and (max-width: $column-breakpoint) {
.grid { .grid {
grid-gap: 0;
.column-1 { .column-1 {
grid-column: 1; grid-column: 1;
grid-row: 2; grid-row: 1;
} }
.column-2 { .column-2 {
grid-column: 1; display: none;
grid-row: 1;
} }
} }
.brand {
margin: 0;
}
.landing-page__features {
display: none;
}
} }
} }
} }

@ -1,243 +1,100 @@
.card { .card {
background-color: $base-shadow-color; & > a {
background-size: cover;
background-position: center;
border-radius: 4px 4px 0 0;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
overflow: hidden;
position: relative;
display: flex;
&::after {
background: rgba(darken($ui-base-color, 8%), 0.5);
display: block; display: block;
content: ""; text-decoration: none;
position: absolute; color: inherit;
left: 0; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
@media screen and (max-width: 740px) {
border-radius: 0;
box-shadow: none;
}
.card__illustration {
padding: 60px 0;
position: relative;
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
}
.card__bio {
max-width: 260px;
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
background: rgba(darken($ui-base-color, 8%), 0.8);
position: relative;
z-index: 2;
}
&.compact {
padding: 30px 0;
border-radius: 4px;
.avatar {
margin-bottom: 0;
img { @media screen and (max-width: $no-gap-breakpoint) {
object-fit: cover; box-shadow: none;
}
} }
}
.name {
display: block;
font-size: 20px;
line-height: 18px * 1.5;
color: $primary-text-color;
padding: 10px 15px;
padding-bottom: 0;
font-weight: 500;
position: relative;
z-index: 2;
margin-bottom: 30px;
overflow: hidden;
text-overflow: ellipsis;
small { &:hover,
display: block; &:active,
font-size: 14px; &:focus {
color: $highlight-text-color; .card__bar {
font-weight: 400; background: lighten($ui-base-color, 8%);
overflow: hidden;
text-overflow: ellipsis;
.fa {
margin-left: 3px;
} }
} }
} }
.avatar { &__img {
width: 120px; height: 130px;
margin: 0 auto;
position: relative; position: relative;
z-index: 2; background: darken($ui-base-color, 12%);
border-radius: 4px 4px 0 0;
img { img {
width: 120px;
height: 120px;
display: block;
border-radius: 120px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
}
.roles {
margin-bottom: 30px;
padding: 0 15px;
}
.details-counters {
margin-top: 30px;
display: flex;
flex-direction: row;
width: 100%;
}
.counter {
width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;
padding: 5px 10px 0;
margin-bottom: 10px;
border-right: 1px solid lighten($ui-base-color, 4%);
cursor: default;
text-align: center;
position: relative;
a {
display: block;
}
&:last-child {
border-right: 0;
}
&::after {
display: block; display: block;
content: "";
position: absolute;
bottom: -10px;
left: 0;
width: 100%; width: 100%;
border-bottom: 4px solid $ui-primary-color; height: 100%;
opacity: 0.5; margin: 0;
transition: all 400ms ease; object-fit: cover;
} border-radius: 4px 4px 0 0;
&.active {
&::after {
border-bottom: 4px solid $highlight-text-color;
opacity: 1;
}
}
&:hover {
&::after {
opacity: 1;
transition-duration: 100ms;
}
} }
a { @media screen and (max-width: 600px) {
text-decoration: none; height: 200px;
color: inherit;
} }
.counter-label { @media screen and (max-width: $no-gap-breakpoint) {
font-size: 12px; display: none;
display: block;
margin-bottom: 5px;
}
.counter-number {
font-weight: 500;
font-size: 18px;
color: $primary-text-color;
font-family: 'mastodon-font-display', sans-serif;
} }
} }
.bio { &__bar {
font-size: 14px; position: relative;
line-height: 18px; padding: 15px;
padding: 0 15px; display: flex;
color: $secondary-text-color; justify-content: flex-start;
} align-items: center;
background: lighten($ui-base-color, 4%);
@media screen and (max-width: 480px) { border-radius: 0 0 4px 4px;
display: block;
.card__bio { @media screen and (max-width: $no-gap-breakpoint) {
max-width: none; border-radius: 0;
} }
.name, .avatar {
.roles { flex: 0 0 auto;
text-align: center; width: 48px;
margin-bottom: 15px; height: 48px;
} padding-top: 2px;
.bio { img {
margin-bottom: 15px; width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: 4px;
background: darken($ui-base-color, 8%);
}
} }
}
}
.card, .display-name {
.account-grid-card { margin-left: 15px;
.controls { text-align: left;
position: absolute;
top: 15px; strong {
left: 15px; font-size: 15px;
z-index: 2; color: $primary-text-color;
font-weight: 500;
.icon-button { overflow: hidden;
color: rgba($white, 0.8); text-overflow: ellipsis;
text-decoration: none;
font-size: 13px;
line-height: 13px;
font-weight: 500;
.fa {
font-weight: 400;
margin-right: 5px;
} }
&:hover, span {
&:active, display: block;
&:focus { font-size: 14px;
color: $white; color: $darker-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
} }
} }
.account-grid-card .controls {
left: auto;
right: 15px;
}
.pagination { .pagination {
padding: 30px 0; padding: 30px 0;
text-align: center; text-align: center;
@ -314,289 +171,23 @@
} }
} }
.accounts-grid {
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
background: darken($simple-background-color, 8%);
border-radius: 0 0 4px 4px;
padding: 20px 5px;
padding-bottom: 10px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
z-index: 2;
position: relative;
&.empty img {
position: absolute;
opacity: 0.2;
height: 200px;
left: 0;
bottom: 0;
pointer-events: none;
}
@media screen and (max-width: 740px) {
border-radius: 0;
box-shadow: none;
}
.account-grid-card {
box-sizing: border-box;
width: 335px;
background: $simple-background-color;
border-radius: 4px;
color: $inverted-text-color;
margin: 0 5px 10px;
position: relative;
@media screen and (max-width: 740px) {
width: calc(100% - 10px);
}
.account-grid-card__header {
overflow: hidden;
height: 100px;
border-radius: 4px 4px 0 0;
background-color: lighten($inverted-text-color, 4%);
background-size: cover;
background-position: center;
position: relative;
&::after {
background: rgba(darken($ui-base-color, 8%), 0.5);
display: block;
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
}
.account-grid-card__avatar {
box-sizing: border-box;
padding: 15px;
position: absolute;
z-index: 2;
top: 100px - (40px + 2px);
left: -2px;
}
.avatar {
width: 80px;
height: 80px;
img {
display: block;
width: 80px;
height: 80px;
border-radius: 80px;
border: 2px solid $simple-background-color;
background: $simple-background-color;
}
}
.name {
padding: 15px;
padding-top: 10px;
padding-left: 15px + 80px + 15px;
a {
display: block;
color: $inverted-text-color;
text-decoration: none;
text-overflow: ellipsis;
overflow: hidden;
font-weight: 500;
&:hover {
.display_name {
text-decoration: underline;
}
}
}
}
.display_name {
font-size: 16px;
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
.username {
color: $lighter-text-color;
font-size: 14px;
font-weight: 400;
}
.account__header__content {
padding: 10px 15px;
padding-top: 15px;
color: $lighter-text-color;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
height: 5.5em;
position: relative;
&::after {
display: block;
content: "";
width: 100%;
height: 100px;
position: absolute;
bottom: 0;
background: linear-gradient(to bottom, rgba($simple-background-color, 0.01) 0%, rgba($simple-background-color, 1) 100%);
left: 0;
border-radius: 0 0 4px 4px;
pointer-events: none;
}
}
}
}
.nothing-here { .nothing-here {
width: 100%; background: $ui-base-color;
display: block; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: $light-text-color; color: $light-text-color;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
padding: 130px 0; display: flex;
padding-top: 125px; justify-content: center;
margin: 0 auto; align-items: center;
cursor: default; cursor: default;
}
.account-card {
border-radius: 4px; border-radius: 4px;
text-align: left; padding: 20px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); min-height: 30vh;
background: $simple-background-color;
&__header {
background: $base-shadow-color;
background-size: cover;
background-position: center center;
height: 90px;
border-radius: 4px 4px 0 0;
}
& > .detailed-status__display-name { &--under-tabs {
display: block; border-radius: 0 0 4px 4px;
overflow: hidden;
display: flex;
align-items: center;
padding: 10px;
&:last-child {
margin-bottom: 0;
}
& > div:first-child {
flex: 0 0 auto;
margin-right: 10px;
width: 48px;
height: 48px;
}
.avatar {
display: block;
border-radius: 4px;
margin: 0;
}
.display-name {
flex: 1 0 auto;
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: default;
& > .detailed-status__display-name {
margin-bottom: 0;
}
strong {
font-weight: 500;
color: $ui-base-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
span {
font-size: 14px;
color: $light-text-color;
}
}
&:hover {
.display-name {
strong {
text-decoration: none;
}
}
}
}
.counter {
box-sizing: border-box;
flex: 0 0 auto;
color: $light-text-color;
padding: 0 10px;
cursor: default;
text-align: center;
position: relative;
line-height: 24px;
.counter-label {
font-size: 12px;
display: block;
text-transform: uppercase;
}
.counter-number {
font-weight: 500;
font-size: 16px;
color: $inverted-text-color;
font-family: 'mastodon-font-display', sans-serif;
}
}
}
.activity-stream-tabs {
background: $simple-background-color;
border-bottom: 1px solid $ui-secondary-color;
position: relative;
z-index: 2;
a {
display: inline-block;
padding: 15px;
text-decoration: none;
color: $highlight-text-color;
text-transform: uppercase;
font-weight: 500;
&:hover,
&:active,
&:focus {
color: lighten($highlight-text-color, 8%);
}
&.active {
color: $inverted-text-color;
cursor: default;
}
} }
} }
@ -629,14 +220,14 @@
padding: 0; padding: 0;
margin: 15px -15px -15px; margin: 15px -15px -15px;
border: 0 none; border: 0 none;
border-top: 1px solid lighten($ui-base-color, 4%); border-top: 1px solid lighten($ui-base-color, 12%);
border-bottom: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 12%);
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
dl { dl {
display: flex; display: flex;
border-bottom: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 12%);
} }
dt, dt,

@ -1,13 +1,10 @@
body { body {
font-family: 'mastodon-font-sans-serif', sans-serif; font-family: 'mastodon-font-sans-serif', sans-serif;
background: $ui-base-color; background: darken($ui-base-color, 8%);
background-size: cover;
background-attachment: fixed;
font-size: 13px; font-size: 13px;
line-height: 18px; line-height: 18px;
font-weight: 400; font-weight: 400;
color: $primary-text-color; color: $primary-text-color;
padding-bottom: 20px;
text-rendering: optimizelegibility; text-rendering: optimizelegibility;
font-feature-settings: "kern"; font-feature-settings: "kern";
text-size-adjust: none; text-size-adjust: none;
@ -35,16 +32,24 @@ body {
height: 100%; height: 100%;
padding: 0; padding: 0;
background: $ui-base-color; background: $ui-base-color;
&.with-modals--active {
overflow-y: hidden;
}
} }
&.about-body { &.lighter {
background: darken($ui-base-color, 8%); background: $ui-base-color;
padding-bottom: 0;
} }
&.tag-body { &.with-modals {
background: darken($ui-base-color, 8%); overflow-x: hidden;
padding-bottom: 0; overflow-y: scroll;
&--active {
overflow-y: hidden;
margin-right: 13px;
}
} }
&.player { &.player {
@ -52,7 +57,7 @@ body {
} }
&.embed { &.embed {
background: transparent; background: lighten($ui-base-color, 4%);
margin: 0; margin: 0;
padding-bottom: 0; padding-bottom: 0;

@ -946,6 +946,18 @@
background: lighten($ui-base-color, 4%); background: lighten($ui-base-color, 4%);
padding: 14px 10px; padding: 14px 10px;
&--flex {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
.status__content,
.detailed-status__meta {
flex: 100%;
}
}
.status__content { .status__content {
font-size: 19px; font-size: 19px;
line-height: 24px; line-height: 24px;
@ -1224,7 +1236,6 @@ a .account__avatar {
} }
.account__action-bar-dropdown { .account__action-bar-dropdown {
flex: 0 1 calc(50% - 140px);
padding: 10px; padding: 10px;
.icon-button { .icon-button {
@ -1256,9 +1267,9 @@ a .account__avatar {
.account__action-bar__tab { .account__action-bar__tab {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
flex: 0 1 80px; flex: 0 1 100%;
border-right: 1px solid lighten($ui-base-color, 8%); border-right: 1px solid lighten($ui-base-color, 8%);
padding: 10px 5px; padding: 10px 0;
& > span { & > span {
display: block; display: block;

@ -60,10 +60,6 @@
} }
} }
.media-standalone__body {
overflow: hidden;
}
.account-header { .account-header {
width: 400px; width: 400px;
margin: 0 auto; margin: 0 auto;
@ -118,3 +114,576 @@
margin-left: 8px; margin-left: 8px;
} }
} }
.public-layout {
@media screen and (max-width: $no-gap-breakpoint) {
padding-top: 48px;
}
.container {
max-width: 960px;
@media screen and (max-width: $no-gap-breakpoint) {
padding: 0;
}
}
.header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
height: 48px;
margin: 10px 0;
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
overflow: hidden;
@media screen and (max-width: $no-gap-breakpoint) {
position: fixed;
width: 100%;
top: 0;
left: 0;
margin: 0;
border-radius: 0;
box-shadow: none;
z-index: 110;
}
& > div {
flex: 1 1 33.3%;
min-height: 1px;
}
.nav-left {
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-wrap: nowrap;
}
.nav-center {
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
}
.nav-right {
display: flex;
align-items: stretch;
justify-content: flex-end;
flex-wrap: nowrap;
}
.brand {
display: block;
padding: 15px;
img {
display: block;
height: 18px;
width: auto;
position: relative;
bottom: -2px;
@media screen and (max-width: $no-gap-breakpoint) {
height: 20px;
}
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 12%);
}
}
.nav-link {
display: flex;
align-items: center;
padding: 0 1rem;
font-size: 12px;
font-weight: 500;
text-decoration: none;
color: $darker-text-color;
white-space: nowrap;
text-align: center;
&:hover,
&:focus,
&:active {
text-decoration: underline;
color: $primary-text-color;
}
}
.nav-button {
background: lighten($ui-base-color, 16%);
margin: 8px;
margin-left: 0;
border-radius: 4px;
&:hover,
&:focus,
&:active {
text-decoration: none;
background: lighten($ui-base-color, 20%);
}
}
}
$no-columns-breakpoint: 600px;
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr);
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-row: 1;
grid-column: 1;
}
.column-1 {
grid-row: 1;
grid-column: 2;
}
@media screen and (max-width: $no-columns-breakpoint) {
grid-template-columns: 100%;
grid-gap: 0;
.column-1 {
display: none;
}
}
}
.public-account-header {
overflow: hidden;
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__image {
border-radius: 4px 4px 0 0;
overflow: hidden;
height: 300px;
position: relative;
background: darken($ui-base-color, 12%);
&::after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15);
top: 0;
left: 0;
}
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 4px 4px 0 0;
}
@media screen and (max-width: 600px) {
height: 200px;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
&__image::after {
display: none;
}
&__image,
&__image img {
border-radius: 0;
}
}
&__bar {
position: relative;
margin-top: -80px;
display: flex;
justify-content: flex-start;
&::before {
content: "";
display: block;
background: lighten($ui-base-color, 4%);
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
border-radius: 0 0 4px 4px;
z-index: -1;
}
.avatar {
display: block;
width: 120px;
height: 120px;
padding-left: 20px - 4px;
flex: 0 0 auto;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 50%;
border: 4px solid lighten($ui-base-color, 4%);
background: darken($ui-base-color, 8%);
}
}
@media screen and (max-width: 600px) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
border-radius: 0 0 4px 4px;
padding: 5px;
&::before {
display: none;
}
.avatar {
width: 48px;
height: 48px;
padding: 7px 0;
padding-left: 10px;
img {
border: 0;
border-radius: 4px;
}
@media screen and (max-width: 360px) {
display: none;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
flex-wrap: wrap;
}
}
&__tabs {
flex: 1 1 auto;
margin-left: 20px;
&__name {
padding-top: 20px;
padding-bottom: 8px;
h1 {
font-size: 20px;
line-height: 18px * 1.5;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow: 1px 1px 1px $base-shadow-color;
small {
display: block;
font-size: 14px;
color: $primary-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media screen and (max-width: 600px) {
margin-left: 15px;
display: flex;
justify-content: space-between;
align-items: center;
&__name {
padding-top: 0;
padding-bottom: 0;
h1 {
font-size: 16px;
line-height: 24px;
text-shadow: none;
small {
color: $darker-text-color;
}
}
}
}
&__tabs {
display: flex;
justify-content: flex-start;
align-items: stretch;
height: 58px;
.details-counters {
display: flex;
flex-direction: row;
min-width: 300px;
}
@media screen and (max-width: $no-columns-breakpoint) {
.details-counters {
display: none;
}
}
.counter {
width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;
padding: 10px;
border-right: 1px solid lighten($ui-base-color, 4%);
cursor: default;
text-align: center;
position: relative;
a {
display: block;
}
&:last-child {
border-right: 0;
}
&::after {
display: block;
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 4px solid $ui-primary-color;
opacity: 0.5;
transition: all 400ms ease;
}
&.active {
&::after {
border-bottom: 4px solid $highlight-text-color;
opacity: 1;
}
}
&:hover {
&::after {
opacity: 1;
transition-duration: 100ms;
}
}
a {
text-decoration: none;
color: inherit;
}
.counter-label {
font-size: 12px;
display: block;
}
.counter-number {
font-weight: 500;
font-size: 18px;
margin-bottom: 5px;
color: $primary-text-color;
font-family: 'mastodon-font-display', sans-serif;
}
}
.spacer {
flex: 1 1 auto;
height: 1px;
}
&__buttons {
padding: 7px 8px;
}
}
}
&__extra {
display: none;
margin-top: 4px;
.public-account-bio {
border-radius: 0;
box-shadow: none;
background: transparent;
margin: 0 -5px;
.account__header__fields {
border-top: 1px solid lighten($ui-base-color, 12%);
}
.roles {
display: none;
}
}
&__links {
margin-top: -15px;
font-size: 14px;
color: $darker-text-color;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 15px;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
flex: 100%;
}
}
}
.account__section-headline {
border-radius: 4px 4px 0 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
}
.detailed-status__meta {
margin-top: 25px;
}
.public-account-bio {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
margin-bottom: 0;
border-radius: 0;
}
.account__header__fields {
margin: 0;
border-top: 0;
a {
color: lighten($ui-highlight-color, 8%);
}
}
.account__header__content {
padding: 20px;
padding-bottom: 0;
color: $primary-text-color;
}
&__extra,
.roles {
padding: 20px;
font-size: 14px;
color: $darker-text-color;
}
.roles {
padding-bottom: 0;
}
}
.static-icon-button {
color: $action-button-color;
font-size: 18px;
& > span {
font-size: 14px;
font-weight: 500;
}
}
.card-grid {
display: flex;
flex-wrap: wrap;
min-width: 100%;
margin: 0 -5px;
& > div {
box-sizing: border-box;
flex: 1 0 auto;
width: 300px;
padding: 0 5px;
margin-bottom: 10px;
max-width: 33.333%;
@media screen and (max-width: 900px) {
max-width: 50%;
}
@media screen and (max-width: 600px) {
max-width: 100%;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
margin: 0;
border-top: 1px solid lighten($ui-base-color, 8%);
& > div {
width: 100%;
padding: 0;
margin-bottom: 0;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
.card__bar {
background: $ui-base-color;
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 4%);
}
}
}
}
}
}

@ -1,39 +1,140 @@
.footer { .public-layout {
text-align: center; .footer {
margin-top: 30px; text-align: left;
padding-bottom: 60px; padding-top: 20px;
font-size: 12px; padding-bottom: 60px;
color: $darker-text-color; font-size: 12px;
color: lighten($ui-base-color, 34%);
.footer__domain {
font-weight: 500; @media screen and (max-width: $no-gap-breakpoint) {
padding-left: 20px;
a { padding-right: 20px;
color: inherit;
text-decoration: none;
} }
}
.powered-by, .grid {
.single-user-login { display: grid;
font-weight: 400; grid-gap: 10px;
grid-template-columns: 1fr 1fr 2fr 1fr 1fr;
.column-0 {
grid-column: 1;
grid-row: 1;
min-width: 0;
}
.column-1 {
grid-column: 2;
grid-row: 1;
min-width: 0;
}
.column-2 {
grid-column: 3;
grid-row: 1;
min-width: 0;
text-align: center;
h4 a {
color: lighten($ui-base-color, 34%);
}
}
.column-3 {
grid-column: 4;
grid-row: 1;
min-width: 0;
}
.column-4 {
grid-column: 5;
grid-row: 1;
min-width: 0;
}
@media screen and (max-width: 690px) {
grid-template-columns: 1fr 2fr 1fr;
.column-0,
.column-1 {
grid-column: 1;
}
.column-1 {
grid-row: 2;
}
.column-2 {
grid-column: 2;
}
a { .column-3,
color: inherit; .column-4 {
text-decoration: underline; grid-column: 3;
font-weight: 500; }
&:hover { .column-4 {
grid-row: 2;
}
}
@media screen and (max-width: 600px) {
.column-1 {
display: block;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
.column-0,
.column-1,
.column-3,
.column-4 {
display: none;
}
}
}
h4 {
text-transform: uppercase;
font-weight: 700;
margin-bottom: 8px;
color: $darker-text-color;
a {
color: inherit;
text-decoration: none; text-decoration: none;
} }
} }
img { ul a {
margin: 0 4px; text-decoration: none;
position: relative; color: lighten($ui-base-color, 34%);
bottom: -1px;
height: 18px; &:hover,
vertical-align: top; &:active,
&:focus {
text-decoration: underline;
}
}
.brand {
svg {
display: block;
height: 36px;
width: auto;
margin: 0 auto;
path {
fill: lighten($ui-base-color, 34%);
}
}
&:hover,
&:focus,
&:active {
svg path {
fill: lighten($ui-base-color, 38%);
}
}
} }
} }
} }

@ -1,111 +0,0 @@
.landing-strip,
.memoriam-strip {
background: rgba(darken($ui-base-color, 7%), 0.8);
color: $darker-text-color;
font-weight: 400;
padding: 14px;
border-radius: 4px;
margin-bottom: 20px;
display: flex;
align-items: center;
strong,
a {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: inherit;
text-decoration: underline;
}
.logo {
width: 30px;
height: 30px;
flex: 0 0 auto;
margin-right: 15px;
}
@media screen and (max-width: 740px) {
margin-bottom: 0;
}
}
.memoriam-strip {
background: rgba($base-shadow-color, 0.7);
}
.moved-strip {
padding: 14px;
border-radius: 4px;
background: rgba(darken($ui-base-color, 7%), 0.8);
color: $secondary-text-color;
font-weight: 400;
margin-bottom: 20px;
strong,
a {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: inherit;
text-decoration: underline;
&.mention {
text-decoration: none;
span {
text-decoration: none;
}
&:focus,
&:hover,
&:active {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
&__message {
margin-bottom: 15px;
.fa {
margin-right: 5px;
color: $darker-text-color;
}
}
&__card {
.detailed-status__display-avatar {
position: relative;
cursor: pointer;
}
.detailed-status__display-name {
margin-bottom: 0;
text-decoration: none;
span {
color: $highlight-text-color;
font-weight: 400;
}
}
}
}

@ -1,367 +1,145 @@
.activity-stream { .activity-stream {
clear: both;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
}
&--headless {
border-radius: 0;
margin: 0;
box-shadow: none;
.detailed-status,
.status {
border-radius: 0 !important;
}
}
div[data-component] { div[data-component] {
width: 100%; width: 100%;
} }
.entry { .entry {
background: $simple-background-color; background: $ui-base-color;
.detailed-status.light, .detailed-status,
.status.light, .status,
.more.light { .load-more {
border-bottom: 1px solid $ui-secondary-color;
animation: none; animation: none;
} }
&:last-child { &:last-child {
&, .detailed-status,
.detailed-status.light, .status {
.status.light {
border-bottom: 0; border-bottom: 0;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
} }
&:first-child { &:first-child {
&, .detailed-status,
.detailed-status.light, .status {
.status.light {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }
&:last-child { &:last-child {
&, .detailed-status,
.detailed-status.light, .status {
.status.light {
border-radius: 4px; border-radius: 4px;
} }
} }
} }
@media screen and (max-width: 740px) { @media screen and (max-width: 740px) {
&, .detailed-status,
.detailed-status.light, .status {
.status.light {
border-radius: 0 !important; border-radius: 0 !important;
} }
} }
} }
}
&.with-header { .button.logo-button {
.entry { flex: 0 auto;
&:first-child { font-size: 14px;
&, background: $ui-highlight-color;
.detailed-status.light, color: $primary-text-color;
.status.light { text-transform: none;
border-radius: 0; line-height: 36px;
} height: auto;
padding: 3px 15px;
&:last-child { border: 0;
&,
.detailed-status.light,
.status.light {
border-radius: 0 0 4px 4px;
}
}
}
}
}
.media-gallery__gifv__label {
bottom: 9px;
}
.status.light {
padding: 14px 14px 14px (48px + 14px * 2);
position: relative;
min-height: 48px;
cursor: default;
.status__header {
font-size: 15px;
.status__meta {
float: right;
font-size: 14px;
.status__relative-time {
color: $lighter-text-color;
}
}
}
.status__display-name {
display: block;
max-width: 100%;
padding-right: 25px;
color: $inverted-text-color;
}
.status__avatar {
position: absolute;
left: 14px;
top: 14px;
width: 48px;
height: 48px;
& > div {
width: 48px;
height: 48px;
}
img {
display: block;
border-radius: 4px;
}
}
.display-name {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
color: $inverted-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
span {
font-size: 14px;
color: $light-text-color;
}
}
.status__content {
color: $inverted-text-color;
a {
color: $highlight-text-color;
}
a.status__content__spoiler-link {
color: $primary-text-color;
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 8%);
}
}
}
}
.detailed-status.light {
padding: 14px;
background: $simple-background-color;
cursor: default;
.detailed-status__display-name {
display: block;
overflow: hidden;
margin-bottom: 15px;
& > div {
float: left;
margin-right: 10px;
}
.display-name {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
color: $inverted-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
span {
font-size: 14px;
color: $light-text-color;
}
}
}
.avatar {
width: 48px;
height: 48px;
img {
display: block;
border-radius: 4px;
}
}
.status__content {
color: $inverted-text-color;
a {
color: $highlight-text-color;
}
a.status__content__spoiler-link {
color: $primary-text-color;
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 8%);
}
}
}
.detailed-status__meta {
margin-top: 15px;
color: $light-text-color;
font-size: 14px;
line-height: 18px;
a {
color: inherit;
}
span > span {
font-weight: 500;
font-size: 12px;
margin-left: 6px;
display: inline-block;
}
}
.status-card {
border-color: lighten($ui-secondary-color, 4%);
color: $lighter-text-color;
&:hover {
background: lighten($ui-secondary-color, 4%);
}
}
.status-card__title,
.status-card__description {
color: $inverted-text-color;
}
.status-card__image {
background: $ui-secondary-color;
}
}
.media-spoiler {
background: $ui-base-color;
color: $darker-text-color;
}
.pre-header { svg {
padding: 14px 0; width: 20px;
padding-left: (48px + 14px * 2); height: auto;
padding-bottom: 0; vertical-align: middle;
margin-bottom: -4px; margin-right: 5px;
color: $light-text-color;
font-size: 14px;
position: relative;
.pre-header__icon { path:first-child {
position: absolute; fill: $primary-text-color;
left: (48px + 14px * 2 - 30px);
} }
.status__display-name.muted strong { path:last-child {
color: $light-text-color; fill: $ui-highlight-color;
} }
} }
.open-in-web-link { &:active,
text-decoration: none; &:focus,
&:hover {
background: lighten($ui-highlight-color, 10%);
&:hover { svg path:last-child {
text-decoration: underline; fill: lighten($ui-highlight-color, 10%);
} }
} }
.more { @media screen and (max-width: $no-gap-breakpoint) {
color: $darker-text-color; svg {
display: block; display: none;
padding: 14px;
text-align: center;
&:not(:hover) {
text-decoration: none;
} }
} }
} }
.embed { .embed,
.activity-stream { .public-layout {
box-shadow: none; .detailed-status {
padding: 15px;
} }
}
.entry { .status {
.detailed-status.light { padding: 15px 15px 15px (48px + 15px * 2);
display: flex; min-height: 48px + 2px;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
.detailed-status__display-name { &__avatar {
flex: 1; left: 15px;
margin: 0 5px 15px 0; top: 17px;
} }
.button.button-secondary.logo-button { &__content {
flex: 0 auto; padding-top: 5px;
font-size: 14px; }
background: $ui-highlight-color;
color: $primary-text-color;
border: 0;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
path:first-child {
fill: $primary-text-color;
}
path:last-child {
fill: $ui-highlight-color;
}
}
&:active, &__prepend {
&:focus, margin-left: 48px + 15px * 2;
&:hover { padding-top: 15px;
background: lighten($ui-highlight-color, 10%); }
svg path:last-child { &__prepend-icon-wrapper {
fill: lighten($ui-highlight-color, 10%); left: -32px;
}
}
} }
.status__content, .media-gallery,
.detailed-status__meta { &__action-bar,
flex: 100%; .video-player {
margin-top: 10px;
} }
} }
} }

@ -46,3 +46,5 @@ $cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
$media-modal-media-max-width: 100%; $media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image. // put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%; $media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;

@ -0,0 +1,161 @@
.hero-widget {
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__img {
width: 100%;
height: 167px;
position: relative;
overflow: hidden;
border-radius: 4px 4px 0 0;
background: $base-shadow-color;
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 4px 4px 0 0;
}
}
&__text {
background: $ui-base-color;
padding: 20px;
border-radius: 0 0 4px 4px;
font-size: 15px;
color: $darker-text-color;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: lighten($darker-text-color, 10%);
}
a {
color: $secondary-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
.moved-account-widget {
padding: 15px;
padding-bottom: 20px;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: $secondary-text-color;
font-weight: 400;
margin-bottom: 10px;
strong,
a {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: inherit;
text-decoration: underline;
&.mention {
text-decoration: none;
span {
text-decoration: none;
}
&:focus,
&:hover,
&:active {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
&__message {
margin-bottom: 15px;
.fa {
margin-right: 5px;
color: $darker-text-color;
}
}
&__card {
.detailed-status__display-avatar {
position: relative;
cursor: pointer;
}
.detailed-status__display-name {
margin-bottom: 0;
text-decoration: none;
span {
font-weight: 400;
}
}
}
}
.memoriam-widget {
padding: 20px;
border-radius: 4px;
background: $base-shadow-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
font-size: 14px;
color: $darker-text-color;
margin-bottom: 10px;
}
.moved-account-widget,
.memoriam-widget {
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
border-radius: 0;
}
}

@ -9,7 +9,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
{ {
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'sensitive' => 'as:sensitive', 'sensitive' => 'as:sensitive',
'movedTo' => 'as:movedTo', 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' },
'Hashtag' => 'as:Hashtag', 'Hashtag' => 'as:Hashtag',
'ostatus' => 'http://ostatus.org#', 'ostatus' => 'http://ostatus.org#',
'atomUri' => 'ostatus:atomUri', 'atomUri' => 'ostatus:atomUri',
@ -18,7 +18,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
'toot' => 'http://joinmastodon.org/ns#', 'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji', 'Emoji' => 'toot:Emoji',
'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' }, 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' },
'featured' => 'toot:featured', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' },
'schema' => 'http://schema.org#', 'schema' => 'http://schema.org#',
'PropertyValue' => 'schema:PropertyValue', 'PropertyValue' => 'schema:PropertyValue',
'value' => 'schema:value', 'value' => 'schema:value',

@ -70,6 +70,7 @@ class Account < ApplicationRecord
# Remote user validations # Remote user validations
validates :username, uniqueness: { scope: :domain, case_sensitive: true }, if: -> { !local? && will_save_change_to_username? } validates :username, uniqueness: { scope: :domain, case_sensitive: true }, if: -> { !local? && will_save_change_to_username? }
validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? }
# Local user validations # Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? } validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }

@ -5,11 +5,12 @@ module AccountHeader
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
LIMIT = 2.megabytes LIMIT = 2.megabytes
MAX_PIXELS = 750_000 # 1500x500px
class_methods do class_methods do
def header_styles(file) def header_styles(file)
styles = { original: { geometry: '700x335#', file_geometry_parser: FastGeometryParser } } styles = { original: { pixels: MAX_PIXELS, file_geometry_parser: FastGeometryParser } }
styles[:static] = { geometry: '700x335#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif' styles[:static] = { format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
styles styles
end end

@ -25,12 +25,13 @@ class MediaAttachment < ApplicationRecord
enum type: [:image, :gifv, :video, :audio, :unknown] enum type: [:image, :gifv, :video, :audio, :unknown]
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
IMAGE_STYLES = { IMAGE_STYLES = {
original: { original: {
@ -72,7 +73,25 @@ class MediaAttachment < ApplicationRecord
}, },
}.freeze }.freeze
LIMIT = 8.megabytes VIDEO_FORMAT = {
format: 'mp4',
convert_options: {
output: {
'movflags' => 'faststart',
'pix_fmt' => 'yuv420p',
'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
'vsync' => 'cfr',
'c:v' => 'h264',
'b:v' => '500K',
'maxrate' => '1300K',
'bufsize' => '1300K',
'crf' => 18,
},
},
}.freeze
IMAGE_LIMIT = 8.megabytes
VIDEO_LIMIT = 40.megabytes
belongs_to :account, inverse_of: :media_attachments, optional: true belongs_to :account, inverse_of: :media_attachments, optional: true
belongs_to :status, inverse_of: :media_attachments, optional: true belongs_to :status, inverse_of: :media_attachments, optional: true
@ -82,11 +101,10 @@ class MediaAttachment < ApplicationRecord
processors: ->(f) { file_processors f }, processors: ->(f) { file_processors f },
convert_options: { all: '-quality 90 -strip' } convert_options: { all: '-quality 90 -strip' }
include Remotable
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
validates_attachment_size :file, less_than: LIMIT validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video?
remotable_attachment :file, LIMIT validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video?
remotable_attachment :file, VIDEO_LIMIT
include Attachmentable include Attachmentable
@ -142,27 +160,17 @@ class MediaAttachment < ApplicationRecord
if f.instance.file_content_type == 'image/gif' if f.instance.file_content_type == 'image/gif'
{ {
small: IMAGE_STYLES[:small], small: IMAGE_STYLES[:small],
original: { original: VIDEO_FORMAT,
format: 'mp4',
convert_options: {
output: {
'movflags' => 'faststart',
'pix_fmt' => 'yuv420p',
'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
'vsync' => 'cfr',
'c:v' => 'h264',
'b:v' => '500K',
'maxrate' => '1300K',
'bufsize' => '1300K',
'crf' => 18,
},
},
},
} }
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
IMAGE_STYLES IMAGE_STYLES
elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type
AUDIO_STYLES AUDIO_STYLES
elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
{
small: VIDEO_STYLES[:small],
original: VIDEO_FORMAT,
}
else else
VIDEO_STYLES VIDEO_STYLES
end end

@ -7,14 +7,14 @@ class ActivityPub::FetchRemoteAccountService < BaseService
# Should be called when uri has already been checked for locality # Should be called when uri has already been checked for locality
# Does a WebFinger roundtrip on each call # Does a WebFinger roundtrip on each call
def call(uri, id: true, prefetched_body: nil) def call(uri, id: true, prefetched_body: nil, break_on_redirect: false)
@json = if prefetched_body.nil? @json = if prefetched_body.nil?
fetch_resource(uri, id) fetch_resource(uri, id)
else else
body_to_json(prefetched_body) body_to_json(prefetched_body)
end end
return unless supported_context? && expected_type? return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?)
@uri = @json['id'] @uri = @json['id']
@username = @json['preferredUsername'] @username = @json['preferredUsername']

@ -175,7 +175,7 @@ class ActivityPub::ProcessAccountService < BaseService
def moved_account def moved_account
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account) account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true) account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true)
account account
end end

@ -0,0 +1,15 @@
.public-account-bio
- unless account.fields.empty?
.account__header__fields
- account.fields.each do |field|
%dl
%dt.emojify{ title: field.name }= field.name
%dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true)
= account_badge(account)
- if account.note.present?
.account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
.public-account-bio__extra
= t 'accounts.joined', date: l(account.created_at, format: :month)

@ -1,28 +0,0 @@
- relationships ||= nil
- unless account.memorial? || account.moved?
- if user_signed_in?
- requested = relationships ? relationships.requested[account.id].present? : current_account.requested?(account)
- following = relationships ? relationships.following[account.id].present? : current_account.following?(account)
- if user_signed_in? && current_account.id != account.id && !requested
.controls
- if following
= link_to (account.local? ? account_unfollow_path(account) : remote_unfollow_path(acct: account.acct)), data: { method: :post }, class: 'icon-button' do
= fa_icon 'user-times'
= t('accounts.unfollow')
- else
= link_to (account.local? ? account_follow_path(account) : authorize_follow_path(acct: account.acct)), data: { method: :post }, class: 'icon-button' do
= fa_icon 'user-plus'
= t('accounts.follow')
- elsif user_signed_in? && current_account.id == account.id
.controls
= link_to settings_profile_url, class: 'icon-button' do
= fa_icon 'pencil'
= t('settings.edit_profile')
- elsif !user_signed_in?
.controls
.remote-follow
= link_to (account.local? ? account_remote_follow_path(account) : "web+mastodon://follow?uri=#{account.uri}"), class: 'icon-button' do
= fa_icon 'user-plus'
= t('accounts.remote_follow')

@ -1,8 +0,0 @@
.accounts-grid{ class: accounts.empty? ? 'empty' : '' }
- if accounts.empty?
= image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational'
= render partial: 'accounts/nothing_here'
- else
= render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in?
= paginate follows

@ -1,3 +0,0 @@
.accounts-grid.empty
= image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational'
%p.nothing-here= t('accounts.network_hidden')

@ -1,12 +0,0 @@
.account-grid-card
.account-grid-card__header{ style: "background-image: url(#{account.header.url(:original)})" }
= render 'accounts/follow_button', account: account, relationships: @relationships
.account-grid-card__avatar
.avatar= image_tag account.avatar.url(:original)
.name
= link_to TagManager.instance.url_for(account) do
%span.display_name.emojify= display_name(account, custom_emojify: true)
%span.username
@#{account.local? ? account.local_username_and_domain : account.acct}
= fa_icon('lock') if account.locked?
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(account)

@ -1,51 +1,43 @@
.card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .public-account-header
.card__illustration .public-account-header__image
= render 'accounts/follow_button', account: account = image_tag account.header.url, class: 'parallax'
.avatar= image_tag account.avatar.url(:original), class: 'u-photo' .public-account-header__bar
= link_to short_account_url(account), class: 'avatar' do
= image_tag account.avatar.url
.public-account-header__tabs
.public-account-header__tabs__name
%h1
= display_name(account)
%small
= acct(account)
= fa_icon('lock') if account.locked?
.public-account-header__tabs__tabs
.details-counters
.counter{ class: active_nav_class(short_account_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid' do
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.posts')
.card__bio .counter{ class: active_nav_class(account_following_index_url(account)) }
%h1.name = link_to account_following_index_url(account) do
%span.p-name.emojify= display_name(account, custom_emojify: true) %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
%small< %span.counter-label= t('accounts.following')
%span>< @#{account.local_username_and_domain}
= fa_icon('lock') if account.locked?
- if account.bot? .counter{ class: active_nav_class(account_followers_url(account)) }
.roles = link_to account_followers_url(account) do
.account-role.bot %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
= t 'accounts.roles.bot' %span.counter-label= t('accounts.followers')
- elsif Setting.show_staff_badge .spacer
- if account.user_admin? .public-account-header__tabs__tabs__buttons
.roles = account_action_button(account)
.account-role.admin
= t 'accounts.roles.admin'
- elsif account.user_moderator?
.roles
.account-role.moderator
= t 'accounts.roles.moderator'
.bio .public-account-header__extra
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) = render 'accounts/bio', account: account
- unless account.fields.empty? .public-account-header__extra__links
.account__header__fields
- account.fields.each do |field|
%dl
%dt.emojify{ title: field.name }= field.name
%dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true)
.details-counters
.counter{ class: active_nav_class(short_account_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid' do
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.posts')
.counter{ class: active_nav_class(account_following_index_url(account)) }
= link_to account_following_index_url(account) do = link_to account_following_index_url(account) do
%span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true %strong= number_to_human account.following_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.following') = t('accounts.following')
.counter{ class: active_nav_class(account_followers_url(account)) }
= link_to account_followers_url(account) do = link_to account_followers_url(account) do
%span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true %strong= number_to_human account.followers_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.followers') = t('accounts.followers')

@ -1,11 +1,11 @@
- moved_to_account = account.moved_to_account - moved_to_account = account.moved_to_account
.moved-strip .moved-account-widget
.moved-strip__message .moved-account-widget__message
= fa_icon 'suitcase' = fa_icon 'suitcase'
= t('accounts.moved_html', name: content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention'))
.moved-strip__card .moved-account-widget__card
= link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
.detailed-status__display-avatar .detailed-status__display-avatar
.account__avatar-overlay .account__avatar-overlay
@ -13,5 +13,6 @@
.account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
%span.display-name %span.display-name
%strong.emojify= display_name(moved_to_account, custom_emojify: true) %bdi
%strong.emojify= display_name(moved_to_account, custom_emojify: true)
%span @#{moved_to_account.acct} %span @#{moved_to_account.acct}

@ -1 +0,0 @@
%p.nothing-here= t('accounts.nothing_here')

@ -20,36 +20,39 @@
= opengraph 'og:type', 'profile' = opengraph 'og:type', 'profile'
= render 'og', account: @account, url: short_account_url(@account, only_path: false) = render 'og', account: @account, url: short_account_url(@account, only_path: false)
- if @account.memorial?
.memoriam-strip= t('in_memoriam_html') = render 'header', account: @account, with_bio: true
- elsif @account.moved?
= render partial: 'moved_strip', locals: { account: @account } .grid
- elsif show_landing_strip? .column-0
= render partial: 'shared/landing_strip', locals: { account: @account } .h-feed
%data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
.h-feed
%data.p-name{ value: "#{@account.username} on #{site_hostname}" }/ .account__section-headline
= active_link_to t('accounts.posts'), short_account_url(@account)
= render 'header', account: @account = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
= active_link_to t('accounts.media'), short_account_media_url(@account)
.activity-stream-tabs
= active_link_to t('accounts.posts'), short_account_url(@account) - if @statuses.empty?
= active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account) = nothing_here 'nothing-here--under-tabs'
= active_link_to t('accounts.media'), short_account_media_url(@account) - else
.activity-stream
- if @statuses.empty? - if params[:page].to_i.zero?
.accounts-grid = render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
= render 'nothing_here'
- else - if @newer_url
.activity-stream.with-header .entry= link_to_more @newer_url
- if params[:page].to_i.zero?
= render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } = render partial: 'stream_entries/status', collection: @statuses, as: :status
= render partial: 'stream_entries/status', collection: @statuses, as: :status - if @older_url
.entry= link_to_more @older_url
- if @newer_url || @older_url
.pagination .column-1
- if @older_url - if @account.memorial?
= link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'next' .memoriam-widget= t('in_memoriam_html')
- if @newer_url - elsif @account.moved?
= link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'prev' = render 'moved', account: @account
= render 'bio', account: @account
= render 'application/sidebar'

@ -0,0 +1,16 @@
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account)
.card.h-card
= link_to account_url, target: '_blank', rel: 'noopener' do
.card__img
= image_tag account.header.url, alt: ''
.card__bar
.avatar
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
.display-name
%bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true)
%span
= acct(account)
= fa_icon('lock') if account.locked?

@ -0,0 +1,6 @@
.hero-widget
.hero-widget__img
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
.hero-widget__text
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)

@ -10,7 +10,7 @@
- if @invite.present? && @invite.autofollow? - if @invite.present? && @invite.autofollow?
.fields-group{ style: 'margin-bottom: 30px' } .fields-group{ style: 'margin-bottom: 30px' }
%p.hint{ style: 'text-align: center' }= t('invites.invited_by') %p.hint{ style: 'text-align: center' }= t('invites.invited_by')
= render 'authorize_follows/card', account: @invite.user.account = render 'application/card', account: @invite.user.account
= f.simple_fields_for :account do |ff| = f.simple_fields_for :account do |ff|
.input-with-append .input-with-append

@ -1,23 +0,0 @@
.account-card
.account-card__header{ style: "background-image: url(#{account.header.url(:original)})" }
.detailed-status__display-name
%div
= image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
%span.display-name
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account)
= link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
%strong.emojify= display_name(account, custom_emojify: true)
%span @#{account.acct}
.counter
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.posts')
.counter
%span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.following')
.counter
%span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.followers')

@ -3,7 +3,7 @@
.form-container .form-container
.follow-prompt .follow-prompt
= render 'card', account: @account = render 'application/card', account: @account
- if current_account.following?(@account) - if current_account.following?(@account)
.flash-message .flash-message

@ -8,6 +8,6 @@
- else - else
%h2= t('authorize_follow.following') %h2= t('authorize_follow.following')
= render 'card', account: @account = render 'application/card', account: @account
= render 'post_follow_actions' = render 'post_follow_actions'

@ -8,6 +8,11 @@
= render 'accounts/header', account: @account = render 'accounts/header', account: @account
- if @account.user_hides_network? - if @account.user_hides_network?
= render 'accounts/follow_grid_hidden' .nothing-here= t('accounts.network_hidden')
- elsif @follows.empty?
= nothing_here
- else - else
= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) .card-grid
= render partial: 'application/card', collection: @follows.map(&:account), as: :account
= paginate @follows

@ -8,6 +8,11 @@
= render 'accounts/header', account: @account = render 'accounts/header', account: @account
- if @account.user_hides_network? - if @account.user_hides_network?
= render 'accounts/follow_grid_hidden' .nothing-here= t('accounts.network_hidden')
- elsif @follows.empty?
= nothing_here
- else - else
= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) .card-grid
= render partial: 'application/card', collection: @follows.map(&:target_account), as: :account
= paginate @follows

@ -1,14 +1,47 @@
- content_for :content do - content_for :content do
.container-alt= yield .public-layout
.footer .container
- if !user_signed_in? && single_user_mode? %nav.header
%span.single-user-login .nav-left
= link_to t('auth.login'), new_user_session_path = link_to root_url, class: 'brand' do
&mdash; = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
%span.footer__domain= link_to site_hostname, about_path .nav-center
- else .nav-right
%span.footer__domain= link_to site_hostname, root_path - if user_signed_in?
%span.powered-by = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
!= t('generic.powered_by', link: link_to('https://joinmastodon.org') { image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' }) - else
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
= link_to t('auth.register'), new_user_registration_path, class: 'webapp-btn nav-link nav-button'
.container= yield
.container
.footer
.grid
.column-0
%h4= t 'footer.resources'
%ul
%li= link_to t('about.terms'), terms_path
%li= link_to t('about.privacy_policy'), terms_path
.column-1
%h4= t 'footer.developers'
%ul
%li= link_to t('about.documentation'), 'https://github.com/tootsuite/documentation'
%li= link_to t('about.api'), 'https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md'
.column-2
%h4= link_to t('about.what_is_mastodon'), 'https://joinmastodon.org/'
= link_to root_url, class: 'brand' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
.column-3
%h4= site_hostname
%ul
%li= link_to t('about.about_this'), about_more_path
%li= "v#{Mastodon::Version.to_s}"
.column-4
%h4= t 'footer.more'
%ul
%li= link_to t('about.source_code'), Mastodon::Version.source_url
%li= link_to 'joinmastodon.org', 'https://joinmastodon.org'
= render template: 'layouts/application' = render template: 'layouts/application'

@ -6,7 +6,7 @@
.follow-prompt .follow-prompt
%h2= t('remote_follow.prompt') %h2= t('remote_follow.prompt')
= render partial: 'authorize_follows/card', locals: { account: @account } = render partial: 'application/card', locals: { account: @account }
= simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f| = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
= render 'shared/error_messages', object: @remote_follow = render 'shared/error_messages', object: @remote_follow

@ -5,6 +5,6 @@
.follow-prompt .follow-prompt
%h2= t('remote_unfollow.unfollowed') %h2= t('remote_unfollow.unfollowed')
= render 'card', account: @account = render 'application/card', account: @account
= render 'post_follow_actions' = render 'post_follow_actions'

@ -6,7 +6,7 @@
%p.hint= t('migrations.currently_redirecting') %p.hint= t('migrations.currently_redirecting')
.fields-group .fields-group
= render partial: 'authorize_follows/card', locals: { account: @migration.account } = render partial: 'application/card', locals: { account: @migration.account }
= render 'shared/error_messages', object: @migration = render 'shared/error_messages', object: @migration

@ -8,13 +8,12 @@
= f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
= f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 500 - @account.note.size).html_safe = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 500 - @account.note.size).html_safe
.card.compact{ style: "background-image: url(#{@account.header.url(:original)})", data: { original_src: @account.header.url(:original) } } = render 'application/card', account: @account
.avatar= image_tag @account.avatar.url(:original), data: { original_src: @account.avatar.url(:original) }
.fields-group .fields-group
= f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar') = f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(AccountAvatar::LIMIT))
= f.input :header, wrapper: :with_label, input_html: { accept: AccountHeader::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.header') = f.input :header, wrapper: :with_label, input_html: { accept: AccountHeader::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.header', dimensions: '1500x500', size: number_to_human_size(AccountHeader::LIMIT))
.fields-group .fields-group
= f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked')

@ -1,6 +0,0 @@
.landing-strip
= image_tag asset_pack_path('logo.svg'), class: 'logo'
%div
= t('landing_strip_html', name: content_tag(:span, display_name(account, custom_emojify: true), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path))
= t('landing_strip_signup_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')

@ -1,7 +0,0 @@
.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' }><
.spoiler-button
.icon-button.overlayed
%i.fa.fa-fw.fa-eye
.media-spoiler
%span= t('stream_entries.sensitive_content')
%span= t('stream_entries.click_to_show')

@ -1,16 +1,15 @@
.detailed-status.light .detailed-status.detailed-status--flex
= link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do
%div .detailed-status__display-avatar
.avatar = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'account__avatar u-photo'
= image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'u-photo'
%span.display-name %span.display-name
%strong.p-name.emojify= display_name(status.account, custom_emojify: true) %bdi
%span= acct(status.account) %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true)
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
- if !user_signed_in? || embedded_view? = account_action_button(status.account)
= link_to account_remote_follow_path(status.account), class: 'button button-secondary logo-button', target: '_new' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
= t('accounts.follow')
.status__content.emojify< .status__content.emojify<
- if status.spoiler_text? - if status.spoiler_text?
@ -30,6 +29,7 @@
.detailed-status__meta .detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 } %data.dt-published{ value: status.created_at.to_time.iso8601 }
= link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
· ·
@ -40,20 +40,20 @@
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
· ·
- if status.direct_visibility? - if status.direct_visibility?
%span< %span.detailed-status__link<
= fa_icon('envelope') = fa_icon('envelope')
- elsif status.private_visibility? - elsif status.private_visibility?
%span< %span.detailed-status__link<
= fa_icon('lock') = fa_icon('lock')
- else - else
%span< %span.detailed-status__link<
= fa_icon('retweet') = fa_icon('retweet')
%span= status.reblogs_count %span.detailed-status__reblogs= number_to_human status.reblogs_count, strip_insignificant_zeros: true
· ·
%span< %span.detailed-status__link<
= fa_icon('star') = fa_icon('star')
%span= status.favourites_count %span.detailed-status__favorites= number_to_human status.favourites_count, strip_insignificant_zeros: true
- if user_signed_in? - if user_signed_in?
· ·
= link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'open-in-web-link', target: '_blank' = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank'

@ -1,4 +0,0 @@
.media-item><
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : '', target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do
- unless media.image?
%video{ src: media.file.url(:original), autoplay: true, loop: true }/

@ -1,2 +0,0 @@
= link_to url, class: 'more light' do
= t('statuses.show_more')

@ -1,18 +1,19 @@
.status.light .status
.status__header .status__info
.status__meta = link_to TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do
= link_to TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %data.dt-published{ value: status.created_at.to_time.iso8601 }
%data.dt-published{ value: status.created_at.to_time.iso8601 }
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do = link_to TagManager.instance.url_for(status.account), class: 'status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do
.status__avatar .status__avatar
%div %div
= image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo' = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo account__avatar'
%span.display-name %span.display-name
%strong.p-name.emojify= display_name(status.account, custom_emojify: true) %bdi
%span= acct(status.account) %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true)
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
.status__content.emojify< .status__content.emojify<
- if status.spoiler_text? - if status.spoiler_text?
%p{ style: 'margin-bottom: 0' }< %p{ style: 'margin-bottom: 0' }<
@ -27,3 +28,16 @@
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true
- else - else
= react_component :media_gallery, height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } = react_component :media_gallery, height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
.status__action-bar
.status__action-bar-button.static-icon-button<
- if status.public_visibility? || status.unlisted_visibility?
= fa_icon 'retweet fw'
%span.detailed-status__reblogs= number_to_human status.reblogs_count, strip_insignificant_zeros: true
- elsif status.private_visibility?
= fa_icon 'lock fw'
- else
= fa_icon 'envelope fw'
.status__action-bar-button.static-icon-button<
= fa_icon 'star fw'
%span.detailed-status__favorites= number_to_human status.favourites_count, strip_insignificant_zeros: true

@ -16,24 +16,25 @@
- if status.reply? && include_threads - if status.reply? && include_threads
- if @next_ancestor - if @next_ancestor
.entry{ class: entry_classes } .entry{ class: entry_classes }
= render 'stream_entries/more', url: TagManager.instance.url_for(@next_ancestor) = link_to_more TagManager.instance.url_for(@next_ancestor)
= render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id } = render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }
.entry{ class: entry_classes } .entry{ class: entry_classes }
- if status.reblog? - if status.reblog?
.pre-header .status__prepend
.pre-header__icon .status__prepend-icon-wrapper
= fa_icon('retweet fw') %i.status__prepend-icon.fa.fa-fw.fa-retweet
%span %span
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do
%strong.emojify= display_name(status.account, custom_emojify: true) %bdi
%strong.emojify= display_name(status.account, custom_emojify: true)
= t('stream_entries.reblogged') = t('stream_entries.reblogged')
- elsif pinned - elsif pinned
.pre-header .status__prepend
.pre-header__icon .status__prepend-icon-wrapper
= fa_icon('thumb-tack fw') %i.status__prepend-icon.fa.fa-fw.fa-thumb-tack
%span %span
= t('stream_entries.pinned') = t('stream_entries.pinned')
@ -42,13 +43,13 @@
- if include_threads - if include_threads
- if @since_descendant_thread_id - if @since_descendant_thread_id
.entry{ class: entry_classes } .entry{ class: entry_classes }
= render 'stream_entries/more', url: short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1) = link_to_more short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1)
- @descendant_threads.each do |thread| - @descendant_threads.each do |thread|
= render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id } = render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }
- if thread[:next_status] - if thread[:next_status]
.entry{ class: entry_classes } .entry{ class: entry_classes }
= render 'stream_entries/more', url: TagManager.instance.url_for(thread[:next_status]) = link_to_more TagManager.instance.url_for(thread[:next_status])
- if @next_descendant_thread - if @next_descendant_thread
.entry{ class: entry_classes } .entry{ class: entry_classes }
= render 'stream_entries/more', url: short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1) = link_to_more short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1)

@ -1,3 +1,3 @@
- cache @stream_entry.activity do - cache @stream_entry.activity do
.activity-stream.activity-stream-headless .activity-stream.activity-stream--headless
= render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true = render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true

@ -17,8 +17,9 @@
= render 'stream_entries/og_description', activity: @stream_entry.activity = render 'stream_entries/og_description', activity: @stream_entry.activity
= render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
- if show_landing_strip? .grid
= render partial: 'shared/landing_strip', locals: { account: @stream_entry.account } .column-0
.activity-stream.activity-stream-headless.h-entry
.activity-stream.activity-stream-headless.h-entry = render partial: "stream_entries/#{@type}", locals: { @type.to_sym => @stream_entry.activity, include_threads: true }
= render partial: "stream_entries/#{@type}", locals: { @type.to_sym => @stream_entry.activity, include_threads: true } .column-1
= render 'application/sidebar'

@ -46,7 +46,6 @@ ar:
people_who_follow: الأشخاص الذين يتبعون %{name} people_who_follow: الأشخاص الذين يتبعون %{name}
posts: منشورات posts: منشورات
posts_with_replies: التبويقات و الردود posts_with_replies: التبويقات و الردود
remote_follow: إتبع عن بعد
reserved_username: إسم المستخدم محجوز reserved_username: إسم المستخدم محجوز
roles: roles:
admin: المدير admin: المدير
@ -516,7 +515,6 @@ ar:
unlocked_warning_title: إنّ حسابك غير مقفل unlocked_warning_title: إنّ حسابك غير مقفل
generic: generic:
changes_saved_msg: تم حفظ التعديلات بنجاح ! changes_saved_msg: تم حفظ التعديلات بنجاح !
powered_by: مدعوم بـ %{link}
save_changes: حفظ التغييرات save_changes: حفظ التغييرات
validation_errors: validation_errors:
one: لا يزال هناك خلل ما إلى حد الآن. يُرجى إعادة النظر في الخطأ أسفله one: لا يزال هناك خلل ما إلى حد الآن. يُرجى إعادة النظر في الخطأ أسفله
@ -551,8 +549,6 @@ ar:
expires_at: تنتهي مدة صلاحيتها في expires_at: تنتهي مدة صلاحيتها في
uses: يستخدِم uses: يستخدِم
title: دعوة أشخاص title: دعوة أشخاص
landing_strip_html: "<strong>%{name}</strong> هو أحد مُستخدِمي %{link_to_root_path}. بإمكانك متابعته أو التواصل معه إن كنت تملك حسابًا أيا كان على البيئة الموحَّدة فيديفرس."
landing_strip_signup_html: إن كنت لا تملك واحدا، يمكنك <a href="%{sign_up_path}">التسجيل مِن هنا</a>.
lists: lists:
errors: errors:
limit: لقد بلغت الحد الأقصى للقوائم limit: لقد بلغت الحد الأقصى للقوائم
@ -720,7 +716,6 @@ ar:
unlisted: غير مُدرَج unlisted: غير مُدرَج
unlisted_long: يُمكن لأيٍ كان رُؤيتَه و لكن لن يُعرَض على الخيوط العامة unlisted_long: يُمكن لأيٍ كان رُؤيتَه و لكن لن يُعرَض على الخيوط العامة
stream_entries: stream_entries:
click_to_show: إضغط للعرض
pinned: تبويق مثبّت pinned: تبويق مثبّت
reblogged: رقى reblogged: رقى
sensitive_content: محتوى حساس sensitive_content: محتوى حساس

@ -96,8 +96,6 @@ ast:
followers: followers:
domain: Dominiu domain: Dominiu
followers_count: Númberu de siguidores followers_count: Númberu de siguidores
generic:
powered_by: cola potencia de %{link}
invites: invites:
expires_in: expires_in:
'1800': 30 minutos '1800': 30 minutos
@ -183,7 +181,6 @@ ast:
other: "%{count} vídeos" other: "%{count} vídeos"
title: "%{name}: «%{quote}»" title: "%{name}: «%{quote}»"
stream_entries: stream_entries:
click_to_show: Primi p'amosar
sensitive_content: Conteníu sensible sensitive_content: Conteníu sensible
themes: themes:
default: Mastodon default: Mastodon

@ -22,7 +22,6 @@ bg:
people_followed_by: Хора, които %{name} следва people_followed_by: Хора, които %{name} следва
people_who_follow: Хора, които следват %{name} people_who_follow: Хора, които следват %{name}
posts: Публикации posts: Публикации
remote_follow: Последвай
unfollow: Не следвай unfollow: Не следвай
application_mailer: application_mailer:
settings: 'Промяна на предпочитанията за e-mail: %{link}' settings: 'Промяна на предпочитанията за e-mail: %{link}'
@ -64,7 +63,6 @@ bg:
storage: Съхранение на мултимедия storage: Съхранение на мултимедия
generic: generic:
changes_saved_msg: Успешно запазване на промените! changes_saved_msg: Успешно запазване на промените!
powered_by: поддържано от %{link}
save_changes: Запази промените save_changes: Запази промените
validation_errors: validation_errors:
one: Нещо все още не е наред! Моля, прегледай грешката по-долу one: Нещо все още не е наред! Моля, прегледай грешката по-долу
@ -76,8 +74,6 @@ bg:
blocking: Списък на блокираните blocking: Списък на блокираните
following: Списък на последователите following: Списък на последователите
upload: Качване upload: Качване
landing_strip_html: "<strong>%{name}</strong> е потребител от %{link_to_root_path}. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon."
landing_strip_signup_html: Ако нямаш акаунт, можеш да си <a href="%{sign_up_path}">създадеш ето тук</a>.
media_attachments: media_attachments:
validations: validations:
images_and_video: Не мога да прикача видеоклип към публикация, която вече съдържа изображения images_and_video: Не мога да прикача видеоклип към публикация, която вече съдържа изображения
@ -144,7 +140,6 @@ bg:
public: Публично public: Публично
unlisted: Публично, но не показвай в публичния канал unlisted: Публично, но не показвай в публичния канал
stream_entries: stream_entries:
click_to_show: Покажи
reblogged: споделено reblogged: споделено
sensitive_content: Деликатно съдържание sensitive_content: Деликатно съдържание
time: time:

@ -46,7 +46,6 @@ ca:
people_who_follow: Usuaris que segueixen %{name} people_who_follow: Usuaris que segueixen %{name}
posts: Toots posts: Toots
posts_with_replies: Toots i respostes posts_with_replies: Toots i respostes
remote_follow: Seguiment remot
reserved_username: El nom d'usuari està reservat reserved_username: El nom d'usuari està reservat
roles: roles:
admin: Administrador admin: Administrador
@ -504,7 +503,6 @@ ca:
unlocked_warning_title: El teu compte no està blocat unlocked_warning_title: El teu compte no està blocat
generic: generic:
changes_saved_msg: Els canvis s'han desat correctament! changes_saved_msg: Els canvis s'han desat correctament!
powered_by: amb tecnologia %{link}
save_changes: Desa els canvis save_changes: Desa els canvis
validation_errors: validation_errors:
one: Alguna cosa no va bé! Si us plau, revisa l'error one: Alguna cosa no va bé! Si us plau, revisa l'error
@ -540,8 +538,6 @@ ca:
expires_at: Caduca expires_at: Caduca
uses: Usos uses: Usos
title: Convida persones title: Convida persones
landing_strip_html: "<strong>%{name}</strong> és un usuari/a de %{link_to_root_path}. Pots seguir-lo/la o interactuar amb ell/a si tens un compte a qualsevol node del fediverse."
landing_strip_signup_html: Si no en tens, pots <a href="%{sign_up_path}">registrar-te aquí</a>.
lists: lists:
errors: errors:
limit: Has assolit la quantitat màxima de llistes limit: Has assolit la quantitat màxima de llistes
@ -710,7 +706,6 @@ ca:
unlisted: No llistat unlisted: No llistat
unlisted_long: Tothom ho pot veure, però no es mostra en la història federada unlisted_long: Tothom ho pot veure, però no es mostra en la història federada
stream_entries: stream_entries:
click_to_show: Clic per mostrar
pinned: Toot fixat pinned: Toot fixat
reblogged: ha impulsat reblogged: ha impulsat
sensitive_content: Contingut sensible sensitive_content: Contingut sensible

@ -46,7 +46,6 @@ co:
people_who_follow: Seguitanu %{name} people_who_follow: Seguitanu %{name}
posts: Statuti posts: Statuti
posts_with_replies: Statuti è risposte posts_with_replies: Statuti è risposte
remote_follow: Siguità daltrò
reserved_username: Stu cugnome hè riservatu reserved_username: Stu cugnome hè riservatu
roles: roles:
admin: Amministratore admin: Amministratore
@ -488,7 +487,6 @@ co:
unlocked_warning_title: U vostru contu hè pubblicu unlocked_warning_title: U vostru contu hè pubblicu
generic: generic:
changes_saved_msg: Cambiamenti salvati! changes_saved_msg: Cambiamenti salvati!
powered_by: mossu da %{link}
save_changes: Salvà e mudificazione save_changes: Salvà e mudificazione
validation_errors: validation_errors:
one: Qualcosa ùn và bè! Verificate u prublemu quì sottu one: Qualcosa ùn và bè! Verificate u prublemu quì sottu
@ -523,8 +521,6 @@ co:
expires_at: Spira expires_at: Spira
uses: Utiliza uses: Utiliza
title: Invità ghjente title: Invità ghjente
landing_strip_html: "<strong>%{name}</strong> hè nantà %{link_to_root_path}. Pudete seguitallu·a o cumunicà cù ellu·a cù un contu in qualche parte di u fediverse."
landing_strip_signup_html: Pudete ancu <a href="%{sign_up_path}">arrigistravi quì</a>.
lists: lists:
errors: errors:
limit: Ùn pudete più creà altre liste limit: Ùn pudete più creà altre liste
@ -691,7 +687,6 @@ co:
unlisted: Micca listatu unlisted: Micca listatu
unlisted_long: Tuttu u mondu pò vede, mà micca indè e linee pubbliche unlisted_long: Tuttu u mondu pò vede, mà micca indè e linee pubbliche
stream_entries: stream_entries:
click_to_show: Cliccà per vede
pinned: Statutu puntarulatu pinned: Statutu puntarulatu
reblogged: spartutu reblogged: spartutu
sensitive_content: Cuntenutu sensibile sensitive_content: Cuntenutu sensibile

@ -1,7 +1,7 @@
--- ---
cs: cs:
about: about:
about_hashtag_html: Toto jsou veřejné tooty označené jako <strong>#%{hashtag}</strong>. Pokud máte účet kdekoliv na fediverse, můžete s nimi interagovat. about_hashtag_html: Toto jsou veřejné tooty označené hashtagem<strong>#%{hashtag}</strong>. Pokud máte účet kdekoliv na fediverse, můžete s nimi interagovat.
about_mastodon_html: Mastodon je sociální síť založená na otevřených webových protokolech a svobodném, otevřeném softwaru. Je decentrovalizovaná jako e-mail. about_mastodon_html: Mastodon je sociální síť založená na otevřených webových protokolech a svobodném, otevřeném softwaru. Je decentrovalizovaná jako e-mail.
about_this: O této instanci about_this: O této instanci
administered_by: 'Server spravuje:' administered_by: 'Server spravuje:'
@ -18,14 +18,21 @@ cs:
features: features:
humane_approach_body: Mastodon, poučen z chyb jiných sociálních sítí, se snaží bojovat se zneužíváním sociálních sítí vytvářením etických možností. humane_approach_body: Mastodon, poučen z chyb jiných sociálních sítí, se snaží bojovat se zneužíváním sociálních sítí vytvářením etických možností.
humane_approach_title: Lidštější přístup humane_approach_title: Lidštější přístup
not_a_product_body: Mastodon není komerční síť. Žádné reklamy, žádné dolování dat, žádné hranice. Žádná centrální autorita.
not_a_product_title: Jste osoba, ne produkt not_a_product_title: Jste osoba, ne produkt
real_conversation_body: S 500 znaky k vaší dispozici a podporou pro varování o obsahu a médiích se můžete vyjadřovat tak, jak chcete.
real_conversation_title: Vytvořen pro opravdovou konverzaci
within_reach_body: Několik aplikací pro iOS, Android a jiné platformy vám díky jednoduchému API ekosystému dovolují držet krok s vašimi přáteli, ať už jste kdekoliv.
within_reach_title: Vždy v dosahu
generic_description: "%{domain} je jedním serverem v síti" generic_description: "%{domain} je jedním serverem v síti"
hosted_on: Mastodon hostovaný na %{domain}
learn_more: Zjistit více learn_more: Zjistit více
other_instances: Seznam instancí
source_code: Zdrojový kód source_code: Zdrojový kód
status_count_after: příspěvků status_count_after: příspěvků
status_count_before: Kdo je autorem status_count_before: Kteří napsali
user_count_after: uživatelů user_count_after: uživatelů
user_count_before: Domov pro user_count_before: Domov
what_is_mastodon: Co je Mastodon? what_is_mastodon: Co je Mastodon?
accounts: accounts:
follow: Sledovat follow: Sledovat
@ -35,10 +42,836 @@ cs:
moved_html: 'Účet %{name} byl přesunut na %{new_profile_link}:' moved_html: 'Účet %{name} byl přesunut na %{new_profile_link}:'
network_hidden: Tato informace není k dispozici network_hidden: Tato informace není k dispozici
nothing_here: Tady nic není! nothing_here: Tady nic není!
people_followed_by: Lidé, které %{name} sleduje
people_who_follow: Lidé, kteří sledují uživatele %{name}
posts: Tooty
posts_with_replies: Tooty s odpověďmi
reserved_username: Toto uživatelské jméno je rezervováno
roles:
admin: Administrátor
bot: Robot
moderator: Moderátor
unfollow: Přestat sledovat
admin:
account_moderation_notes:
create: Zanechat poznámku
created_msg: Poznámka moderátora byla úspěšně vytvořena!
delete: Smazat
destroyed_msg: Poznámka moderátora byla úspěšně zničena!
accounts:
are_you_sure: Jste si jistý/á?
avatar: Avatar
by_domain: Doména
change_email:
changed_msg: E-mail k tomuto účtu byl úspěšně změněn!
current_email: Současný e-mail
label: Změnit e-mail
new_email: Nový e-mail
submit: Změnit e-mail
title: Změnit e-mail pro uživatele %{username}
confirm: Potvrdit
confirmed: Potvrzeno
confirming: Potvrzující
demote: Degradovat
disable: Zablokovat
disable_two_factor_authentication: Zakázat 2FA
disabled: Blokováno
display_name: Zobrazované jméno
domain: Doména
edit: Upravit
email: E-mail
email_status: Stav e-mailu
enable: Povolit
enabled: Povoleno
feed_url: URL proudu
followers: Sledovatelé
followers_url: URL sledovatelů
follows: Sleduje
inbox_url: URL přijatých zpráv
ip: IP
location:
all: Vše
local: Místní
remote: Vzdálené
title: Umístění
login_status: Stav přihlášení
media_attachments: Mediální přílohy
memorialize: Změnit na "in memoriam"
moderation:
all: Vše
silenced: Utišen
suspended: Suspendován
title: Moderace
moderation_notes: Moderační poznámky
most_recent_activity: Nejnovější aktivita
most_recent_ip: Nejnovější IP
not_subscribed: Neodebírá
order:
alphabetic: Abecedně
most_recent: Nejnovější
title: Pořadí
outbox_url: URL odchozích zpráv
perform_full_suspension: Provést plnou suspenzaci
profile_url: URL profilu
promote: Povýšit
protocol: Protokol
public: Veřejný
push_subscription_expires: Odebírání PuSH expiruje
redownload: Obnovit avatar
remove_avatar: Odstranit avatar
resend_confirmation:
already_confirmed: Tento uživatel je již potvrzen
send: Znovu odeslat potvrzovací e-mail
success: Potvrzovací e-mail byl úspěšně odeslán!
reset: Resetovat
reset_password: Obnovit heslo
resubscribe: Znovu odebírat
role: Oprávnění
roles:
admin: Administrátor
moderator: Moderátor
staff: Personál
user: Uživatel
salmon_url: URL Salmon
search: Hledat
shared_inbox_url: URL sdílené schránky
show:
created_reports: Nahlášení vytvořené z tohoto účtu
report: nahlášení
targeted_reports: Nahlášení vytvořena o tomto účtu
silence: Utišit
statuses: Statusy
subscribe: Odebírat
title: Účty
unconfirmed_email: Nepotvrzený e-mail
undo_silenced: Zrušit utišení
undo_suspension: Zrušit suspenzaci
unsubscribe: Přestat odebírat
username: Uživatelské jméno
web: Web
action_logs:
actions:
assigned_to_self_report: "%{name} přidělil/a hlášení %{target} sobě"
change_email_user: "%{name} změnil/a e-mailovou adresu uživatele %{target}"
confirm_user: "%{name} potvrdil/a e-mailovou adresu uživatele %{target}"
create_custom_emoji: "%{name} nahrál/a nové emoji %{target}"
create_domain_block: "%{name} zablokoval/a doménu %{target}"
create_email_domain_block: "%{name} přidal/a e-mailovou doménu %{target} na černou listinu"
demote_user: "%{name} degradoval/a uživatele %{target}"
destroy_domain_block: "%{name} odblokoval/a doménu %{target}"
destroy_email_domain_block: "%{name} odebral/a e-mailovou doménu %{target} z černé listiny"
destroy_status: "%{name} odstranil/a status uživatele %{target}"
disable_2fa_user: "%{name} vypnul/a požadavek pro dvoufaktorovou autentikaci pro uživatele %{target}"
disable_custom_emoji: "%{name} zakázal/a emoji %{target}"
disable_user: "%{name} zakázal/a přihlašování pro uživatele %{target}"
enable_custom_emoji: "%{name} povolil/a emoji %{target}"
enable_user: "%{name} povolil/a přihlašování pro uživatele %{target}"
memorialize_account: '%{name} změnil/a účet %{target} na stránku "in memoriam"'
promote_user: "%{name} povýšil/a uživatele %{target}"
remove_avatar_user: "%{name} odstranil/a avatar uživatele %{target}"
reopen_report: "%{name} znovuotevřel/a nahlášení %{target}"
reset_password_user: "%{name} resetoval/a heslo uživatele %{target}"
resolve_report: "%{name} vyřešil/a nahlášení %{target}"
silence_account: "%{name} utišil/a účet uživatele %{target}"
suspend_account: "%{name} suspendoval/a účet uživatele %{target}"
unassigned_report: "%{name} odebral/a nahlášení %{target}"
unsilence_account: "%{name} odtišil/a účet uživatele %{target}"
unsuspend_account: "%{name} zrušil/a suspenzaci účtu uživatele %{target}"
update_custom_emoji: "%{name} aktualizoval/a emoji %{target}"
update_status: "%{name} aktualizoval/a status uživatele %{target}"
title: Záznam auditu
custom_emojis:
by_domain: Doména
copied_msg: Místní kopie emoji byla úspěšně vytvořena
copy: Kopírovat
copy_failed_msg: Nebylo možné vytvořit místní kopii tohoto emoji
created_msg: Emoji úspěšně vytvořeno!
delete: Smazat
destroyed_msg: Emoji úspěšně zničeno!
disable: Zakázat
disabled_msg: Emoji bylo úspěšně zakázáno
emoji: Emoji
enable: Povolit
enabled_msg: Emoji bylo úspěšně povoleno
image_hint: PNG až do 50KB
listed: Uvedené
new:
title: Přidat nové vlastní emoji
overwrite: Přepsat
shortcode: Zkratka
shortcode_hint: Alespoň 2 znaky, pouze alfanumerické znaky a podtržítka
title: Vlastní emoji
unlisted: Neuvedené
update_failed_msg: Nebylo možné aktualizovat toto emoji
updated_msg: Emoji úspěšně aktualizováno!
upload: Nahrát
dashboard:
backlog: opožděné úlohy
config: Konfigurace
feature_deletions: Smazání účtů
feature_invites: Pozvánky
feature_registrations: Registrace
feature_relay: Federovací most
features: Vlastnosti
hidden_service: Federace se skrytými službami
open_reports: otevřená hlášení
recent_users: Nedávní uživatelé
search: Fulltextové vyhledávání
single_user_mode: Režim jednoho uživatele
software: Software
space: Využití prostoru
title: Přehled
total_users: uživatelů celkem
trends: Trendy
week_interactions: interakcí tento týden
week_users_active: aktivních tento týden
week_users_new: uživatelů tento týden
domain_blocks:
add_new: Přidat nové
created_msg: Blokace domény se právě vyřizuje
destroyed_msg: Blokace domény byla zrušena
domain: Doména
new:
create: Vytvořit blokaci
hint: Blokace domény nezakáže vytváření účtových záznamů v databázi, ale bude na tyto účty zpětně a automaticky aplikovat specifické metody moderace.
severity:
desc_html: Funkce <strong>Utišit</strong> zneviditelní příspěvky z účtu komukoliv, kdo jej nesleduje. Funkce <strong>Suspendovat</strong> odstraní všechen obsah, média a profilová data účtu. Pro pouhé odmítnutí mediálních souborů použijte funkci <strong>Žádné</strong>.
noop: Žádné
silence: Utišit
suspend: Suspendovat
title: Nová doménová blokace
reject_media: Odmítat mediální soubory
reject_media_hint: Odstraní lokálně uložené soubory a odmítne jejich stažení v budoucnosti. Irelevantní pro suspenzace
severities:
noop: Žádné
silence: Utišit
suspend: Suspendovat
severity: Přísnost
show:
affected_accounts:
one: Jeden účet v databázi byl ovlivněn
other: "%{count} účtů v databázi byl ovlivněn"
retroactive:
silence: Odtišit všechny existující účty z této domény
suspend: Zrušit suspenzaci všech existujících účtů z této domény
title: Zrušit blokaci domény %{domain}
undo: Odvolat
title: Doménové blokace
undo: Odvolat
email_domain_blocks:
add_new: Přidat nový
created_msg: E-mailová doména úspěšně přidána na černou listinu
delete: Smazat
destroyed_msg: E-mailová doména úspěšně odstraněna z černé listiny
domain: Doména
new:
create: Přidat doménu
title: Nový e-mail pro zablokování
title: Černá listina e-mailů
instances:
account_count: Známé účty
domain_name: Doména
reset: Resetovat
search: Hledat
title: Známé instance
invites:
filter:
all: Vše
available: Dostupné
expired: Vypršelé
title: Filtrovat
title: Pozvánky
relays:
add_new: Přidat nový most
description_html: "<strong>Federovací most</strong> je přechodný server, který vyměňuje velká množství veřejných tootů mezi servery, které z něj odebírají a poblikují na něj. <strong>Může pomoci malým a středně velkým serverům objevovat obsah z fediverse</strong>, což by jinak vyžadovalo, aby místní uživatelé manuálně sledovali jiné lidi na vzdálených serverech."
enable_hint: Je-li tohle povoleno, začne váš server odebírat všechny veřejné tooty z tohoto mostu a odesílat na něj své vlastní veřejné tooty.
inbox_url: URL mostu
setup: Nastavit připojení k mostu
status: Stav
title: Mosty
report_notes:
created_msg: Poznámka o nahlášení úspěšně vytvořena!
destroyed_msg: Poznámka o nahlášení úspěšně smazána!
reports:
account:
note: poznámka
report: nahlášení
action_taken_by: Akci vykonal/a
are_you_sure: Jste si jistý/á?
assign_to_self: Přidělit ke mně
assigned: Přiřazený moderátor
comment:
none: Žádné
created_at: Nahlášené
id: ID
mark_as_resolved: Označit jako vyřešené
mark_as_unresolved: Označit jako nevyřešené
notes:
create: Přidat poznámku
create_and_resolve: Vyřešit s poznámkou
create_and_unresolve: Znovu otevřít s poznámkou
delete: Smazat
placeholder: Popište, jaké akce byly vykonány, nebo jakékoliv jiné související aktuality...
reopen: Znovu otevřít nahlášení
report: 'Nahlásit #%{id}'
report_contents: Obsah
reported_account: Nahlášený účet
reported_by: Nahlášeno uživatelem
resolved: Vyřešeno
resolved_msg: Nahlášení úspěšně vyřešeno!
silence_account: Utišit účet
status: Stav
suspend_account: Suspendovat účet
target: Cíl
title: Nahlášení
unassign: Odebrat
unresolved: Nevyřešeno
updated_at: Aktualizováno
view: Zobrazit
settings:
activity_api_enabled:
desc_html: Počty lokálně publikovaných statusů, aktivních uživatelů a nových registrací, v týdenních intervalech
title: Publikovat hromadné statistiky o uživatelské aktivitě
bootstrap_timeline_accounts:
desc_html: Je-li uživatelskch jmen více, oddělujte je čárkami. Lze zadat pouze místní a odemknuté účty. Je-li tohle prázdné, jsou výchozí hodnotou všichni místní administrátoři.
title: Výchozí sledování pro nové uživatele
contact_information:
email: Pracovní e-mail
username: Uživatelské jméno kontaktu
hero:
desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 600x100px. Pokud toto není nastavené, bude zobrazena miniatura instance
title: Hlavní obrázek
peers_api_enabled:
desc_html: Domény, na které tato instanve narazila ve fediverse
title: Zveřejnit seznam objevených instancí
preview_sensitive_media:
desc_html: Náhledy odkazů na jiných stránkách budou zobrazeny i pokud jsou media označena jako citlivá
title: Zobrazovat v náhledech OpenGraph i citlivá média
registrations:
closed_message:
desc_html: Zobrazí se na hlavní stránce, jsou-li registrace uzavřeny. Můžete použít i HTML značky
title: Zpráva o uzavřených registracích
deletion:
desc_html: Dovolit každému smazání svého účtu
title: Zpřístupnit smazání účtu
min_invite_role:
disabled: Nikdo
title: Povolit pozvánky od
open:
desc_html: Povolit každému vytvořit si účet
title: Zpřístupnit registraci
show_known_fediverse_at_about_page:
desc_html: Je-li toto zapnuto, zobrazí se v náhledu tooty ze všech známých serverů na fediverse. Jinak budou zobrazeny pouze místní tooty.
title: Zobrazit celou známou fediverse na náhledu časové osy
show_staff_badge:
desc_html: Zobrazit na stránce uživatele odznak člena personálu
title: Zobrazit odznak personálu
site_description:
desc_html: Úvodní odstavec na hlavní straně a v meta značkách. Můžete zde používat HTML značky, hlavně <code>&lt;a&gt;</code> a <code>&lt;em&gt;</code>.
title: Popis instance
site_description_extended:
desc_html: Dobré místo pro vaše pravidla, pokyny a jiné věci, které vaši instanci odlišují od ostatních. Lze použít HTML značky
title: Vlastní doplňujísí informace
site_terms:
desc_html: Můžete si napsat vlastní zásady soukromí, podmínky používání či jiné legality. Můžete použít HTML značky
title: Vlastní podmínky používání
site_title: Název instance
thumbnail:
desc_html: Používáno pro náhledy přes OpenGraph a API. Doporučuje se rozlišení 1200x630px
title: Miniatura instance
timeline_preview:
desc_html: Zobrazit na hlavní straně veřejnou časovou osu
title: Náhled časové osy
title: Nastavení stránky
statuses:
back_to_account: Zpět na stránku účtu
batch:
delete: Vymazat
nsfw_off: Označit, že není citlivý
nsfw_on: Označit jako citlivý
failed_to_execute: Nepodařilo se vykonat
media:
title: Média
no_media: Žádná média
title: Statusy účtů
with_media: S médii
subscriptions:
callback_url: Zpáteční URL
confirmed: Potvrzeno
expires_in: Vyprší v
last_delivery: Poslední doručení
title: WebSub
topic: Téma
title: Administrace
admin_mailer:
new_report:
body: "%{reporter} nahlásil/a uživatele %{target}"
body_remote: Někdo z %{domain} nahlásil uživatele %{target}
subject: Nové nahlášení pro %{instance} (#%{id})
application_mailer:
notification_preferences: Změnit volby e-mailu
salutation: "%{name},"
settings: 'Změnit volby e-mailu: %{link}'
view: 'Zobrazit:'
view_profile: Zobrazit profil
view_status: Zobrazit status
applications:
created: Aplikace úspěšně vytvořena
destroyed: Aplikace úspěšně smazána
invalid_url: Zadaná adresa URL je neplatná
regenerate_token: Znovu vygenerovat přístupový token
token_regenerated: Přístupový token byl úspěšně vygenerován
warning: Buďte s těmito daty velmi opatrní. Nikdy je s nikým nesdílejte!
your_token: Váš přístupový token
auth:
agreement_html: Registrací souhlasíte s následováním <a href="%{rules_path}">pravidel této instance</a> a <a href="%{terms_path}">našich podmínek používání</a>.
change_password: Heslo
confirm_email: Potvrdit e-mail
delete_account: Odstranit účet
delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádán/a o potvrzení.
didnt_get_confirmation: Neobdržel/a jste pokyny pro potvrzení?
forgot_password: Zapomněl/a jste heslo?
invalid_reset_password_token: Token na obnovu hesla je buď neplatný, nebo vypršel. Prosím vyžádejte si nový.
login: Přihlásit
logout: Odhlásit
migrate_account: Přesunout se na jiný účet
migrate_account_html: Chcete-li přesměrovat tento účet na jiný, můžete to <a href="%{path}">nastavit zde</a>.
or: nebo
or_log_in_with: Nebo se přihlaste pomocí
providers:
cas: CAS
saml: SAML
register: Registrovat
register_elsewhere: Registrovat na jiném serveru
resend_confirmation: Znovu odeslat pokyny pro potvrzení
reset_password: Obnovit heslo
security: Zabezpečení
set_new_password: Nastavit nové heslo
authorize_follow:
already_following: Tento účet již sledujete
error: Při hledání vzdáleného účtu bohužel nastala chyba
follow: Sledovat
follow_request: 'Poslal/a jste žádost o sledování uživateli:'
following: 'Podařilo se! Nyní sledujete uživatele:'
post_follow:
close: Nebo můžete toto okno klidně zavřít.
return: Zobrazit profil uživatele
web: Přejít na web
title: Sledovat uživatele %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count} hod"
about_x_months: "%{count} měsíců"
about_x_years: "%{count} let"
almost_x_years: "%{count} let"
half_a_minute: Právě teď
less_than_x_minutes: "%{count} min"
less_than_x_seconds: Právě teď
over_x_years: "%{count} let"
x_days: "%{count} dní"
x_minutes: "%{count} min"
x_months: "%{count} mesíců"
x_seconds: "%{count} s"
deletes:
bad_password_msg: Dobrý pokus, hackeři! Nesprávné heslo
confirm_password: Zadejte svoje současné heslo pro ověření vaší identity
description_html: Tímto <strong>trvale a nenávratně</strong> odstraníte obsah z vašeho účtu a deaktivuje ho. Vaše uživatelské jméno zůstane rezervované pro zabránění budoucím napodobováním.
proceed: Odstranit účet
success_msg: Váš účet byl úspěšně odstraněn
warning_html: Pouze vymazání obsahu z této konkrétní instance je zaručeno. Obsah, který byl široce sdílen, po sobě pravděpodobně zanechá stopy. U offline serverů a serverů, které vaše aktualizace již neodebírají, nebudou databáze aktualizovány.
warning_title: Dostupnost rozšířeného obsahu
errors:
'403': Nemáte povolení zobrazit tuto stránku.
'404': Stránka, kterou hledáte, neexistuje.
'410': Stránka, kterou hledáte, již neexistuje.
'422':
content: Bezpečnostní ověření selhalo. Neblokujete cookoes?
title: Bezpečnostní ověření selhalo
'429': Příliš mnoho požadavků
'500':
content: Omlouváme se, ale něco se pokazilo u nás.
title: Tato stránka není správná
noscript_html: Pro použití webové aplikace Mastodon prosím povolte JavaScript. Nebo zkuste jednu z <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">nativních aplikací</a> pro Mastodon pro vaši platformu.
exports:
archive_takeout:
date: Datum
download: Stáhnout svůj archiv
hint_html: Můžete si vyžádat archiv vašich <strong>tootů a nahraných médií</strong>. Exportovaná data budou ve formátu ActivityPub a budou čitelné kterýmkoliv kompatibilním softwarem. Archiv si můžete vyžádat každých 7 dní.
in_progress: Kompiluji váš archiv...
request: Vyžádat svůj archiv
size: Velikost
blocks: Blokujete
csv: CSV
follows: Sledujete
mutes: Ignorujete
storage: Úložisko médií
filters:
contexts:
home: Domovská časová osa
notifications: Oznámení
public: Veřejný časové osy
thread: Konverzace
edit:
title: Upravit filtr
errors:
invalid_context: Nebylo poskytnuto nic, nebo má neplatný kontext
invalid_irreversible: Nezvratné filtrování funguje pouze v souvislosti s domovskou osou či oznámeními
index:
delete: Smazat
title: Filtry
new:
title: Přidat nový filtr
followers:
domain: Doména
explanation_html: Chcete-li zaručit soukromí vašich statusů, musíte mít na vědomí, kdo vás sleduje. <strong>Vaše soukromé statusy jsou doručeny na všechny instance, kde máte sledovatele</strong>. Nejspíš si je budete chtít zkontrolovat a odstranit sledovatele na instancích, jejichž personálu či softwaru nedůvěřujete s respektováním vašeho soukromí.
followers_count: Počet sledovatelů
lock_link: Zamkněte svůj účet
purge: Odstranit ze sledovatelů
success:
one: V průběhu utišování sledovatelů z jedné domény...
other: V průběhu utišování sledovatelů z %{count} domén...
true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé statusy. %{lock_link}, abyste mohl/a zkontrolovat a odmítnout sledovatele.
unlocked_warning_title: Váš účet není zamknutý
generic:
changes_saved_msg: Změny byly úspěšně uloženy!
save_changes: Uložit změny
validation_errors:
one: Něco ještě není úplně v pořádku! Prosím zkontrolujte chybu níže
other: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže
imports:
preface: Můžete importovat data, která jste exportoval/a z jiné instance, jako například seznam lidí, které sledujete či blokujete.
success: Vaše data byla úspěšně nahrána a nyní budou zpracována v daný čas
types:
blocking: Seznam blokovaných
following: Seznam sledovaných
muting: Seznam ignorovaných
upload: Nahrát
in_memoriam_html: Navždy budeme vzpomínat.
invites:
delete: Deaktivovat
expired: Vypršelé
expires_in:
'1800': 30 minut
'21600': 6 hodin
'3600': 1 hodina
'43200': 12 hodin
'604800': 1 týden
'86400': 1 den
expires_in_prompt: Nikdy
generate: Vygenerovat
invited_by: 'Byl/a jste pozván/a uživatelem:'
max_uses:
one: 1 použití
other: "%{count} použití"
max_uses_prompt: Bez limitu
prompt: Vygenerujte a sdílejte s ostatními odkazy a umožněte jim přístup na tuto instanci
table:
expires_at: Vyprší
uses: Použití
title: Pozvěte lidi
lists:
errors:
limit: Dosáhl/a jste maximálního počtu seznamů
media_attachments:
validations:
images_and_video: Ke statusu, který již obsahuje obrázky, nelze připojit video
too_many: Nelze připojit více než 4 soubory
migrations:
acct: přezdívka@doména nového účtu
currently_redirecting: 'Váš profil má nastaveno přesměrování na:'
proceed: Uložit
updated_msg: Vaše nastavení migrace účtu bylo úspěšně aktualizováno!
moderation:
title: Moderace
notification_mailer:
digest:
action: Zobrazit všechna oznámení
body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
mention: "%{name} vás zmínil/a v:"
new_followers_summary:
one: Navíc jste získal/a jednoho nového sledovatele, zatímco jste byl/a pryč! Hurá!
other: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Hurá!
subject:
one: "Jedno nové oznámení od vaší poslední návštěvy \U0001F418"
other: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
title: Ve vaší absenci...
favourite:
body: 'Váš status si oblíbil/a %{name}:'
subject: "%{name} si oblíbil/a váš status"
title: Nové oblíbení
follow:
body: "%{name} vás nyní sleduje!"
subject: "%{name} vás nyní sleduje"
title: Nový sledovatel
follow_request:
action: Spravovat žádosti o sledování
body: "%{name} žádá o povolení vás následovat"
subject: 'Čekající sledovatel: %{name}'
title: Nová žádost o sledování
mention:
action: Odpovědět
body: 'Byl/a jste zmíněn/a uživatelem %{name} v:'
subject: Byl/a jste zmíněn/a uživatelem %{name}
title: Nová zmínka
reblog:
body: 'Váš status byl boostnutý uživatelem %{name}:'
subject: "%{name} boostnul/a váš status"
title: Nové boostnutí
number:
human:
decimal_units:
format: "%n %u"
units:
billion: mld
million: mil
quadrillion: bld
thousand: tis
trillion: T
pagination:
newer: Novější
next: Starší
older: Starší
prev: Před
truncate: "&hellip;"
preferences:
languages: Jazyky
other: Ostatní
publishing: Publikování
web: Web
remote_follow:
acct: Napište svou přezdívku@doménu, ze které chcete sledovat
missing_resource: Nemůžeme najít požadované přesměrovací URL pro váš účet
no_account_html: Ještě nemáte účet? Můžete se <a href='%{sign_up_path}' target='_blank'>registrovat zde</a>
proceed: Pokračujte pro sledování
prompt: 'Budete sledovat:'
remote_unfollow:
error: Chyba
title: Nadpis
unfollowed: Už nesledujete
sessions:
activity: Nejnovější aktivita
browser: Prohlížeč
browsers:
alipay: Alipay
blackberry: Blackberry
chrome: Chrome
edge: Microsoft Edge
electron: Electron
firefox: Firefox
generic: Neznámý prohlížeč
ie: Internet Explorer
micro_messenger: MicroMessenger
nokia: Nokia S40 Ovi Browser
opera: Opera
otter: Otter
phantom_js: PhantomJS
qq: QQ Browser
safari: Safari
uc_browser: UCBrowser
weibo: Weibo
current_session: Aktuální relace
description: "%{browser} na %{platform}"
explanation: Toto jsou webové prohlížeče aktuálně přihlášené na váš účet Mastodon.
ip: IP
platforms:
adobe_air: Adobe Air
android: Android
blackberry: Blackberry
chrome_os: Chrome OS
firefox_os: Firefox OS
ios: iOS
linux: Linux
mac: Mac
other: neznámé platformě
windows: Windows
windows_mobile: Windows Mobile
windows_phone: Windows Phone
revoke: Zamítnout
revoke_success: Relace úspěšně zamítnuta
title: Relace
settings:
authorized_apps: Autorizované aplikace
back: Zpět na Mastodon
delete: Smazání účtu
development: Vývoj
edit_profile: Upravit profil
export: Export dat
followers: Autorizovaní sledovatelé
import: Import
migrate: Přesunutí účtu
notifications: Oznámení
preferences: Předvolby
settings: Nastavení
two_factor_authentication: Dvoufaktorové ověřování
your_apps: Vaše aplikace
statuses:
attached:
description: 'Přiloženo: %{attached}'
image:
one: "%{count} obrázek"
other: "%{count} obrázků"
video:
one: "%{count} video"
other: "%{count} videí"
boosted_from_html: Boostnuto z %{acct_link}
content_warning: 'Varování o obsahu: %{warning}'
disallowed_hashtags:
one: 'obsahuje nepovolený hashtag: %{tags}'
other: 'obsahuje nepovolené hashtagy: %{tags}'
language_detection: Zjistit jazyk automaticky
open_in_web: Otevřít na webu
over_character_limit: limit %{max} znaků byl překročen
pin_errors:
limit: Už jste si připnul/a maximální počet tootů
ownership: Nelže připnout toot někoho jiného
private: Nelze připnout neveřejné tooty
reblog: Nelze připnout boostnutí
show_more: Zobrazit více
title: '%{name}: "%{quote}"'
visibilities:
private: Pouze pro sledovatele
private_long: Zobrazit pouze sledovatelům
public: Veřejné
public_long: Všichni mohou vidět
unlisted: Neuvedené
unlisted_long: Všichni mohou vidět, ale není zahrnut ve veřejných časových osách
stream_entries:
pinned: Připnutý toot
reblogged: boostnutý
sensitive_content: CItlivý obsah
terms:
body_html: |
<h2>Zásady soukromí</h2>
<h3 id="collect">Jaké informace sbíráme?</h3>
<ul>
<li><em>Základní informace o účtu</em>: Pokud se na tomto serveru zaregistrujete, můžeme vás požádat o zadání uživatelského jména, e-mailové adresy a hesla. Můžete také zadat dodatečné profilové informace, jako například zobrazované jméno a krátký životopis, a nahrát si profilovou fotografii a hlavičkový obrázek. Uživatelské i zobrazované jméno, životopis, profilová fotografie a hlavičkový obrázek jsou vždy uvedeny veřejně.</li>
<li><em>Příspěvky, sledovatelé a další veřejné informace</em>: Seznam lidí, které sledujete, je uveden veřejně, totéž platí i pro vaše sledovatele. Když sem nahrajete zprávu, bude uloženo datum a čas, společně s aplikací, ze které jste zprávu odeslali. Zprávy mohou obsahovat mediální přílohy, jako jsou obrázky a videa. Veřejné a nezobrazované příspěvky jsou dostupné veřejně. Pokud na vašem profilu uvedete příspěvek, je to také veřejně dostupná informace. Vaše příspěvky jsou doručeny vašim sledovatelům, což v některých případech znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Pokud příspěvky smažete, bude tohle taktéž doručeno vašim sledovatelům. Akce znovusdílení nebo oblíbení jiného příspěvku je vždy veřejná.</li>
<li><em>Příspěvky přímé a pouze pro sledovatele</em>: Všechny příspěvky jsou uloženy a zpracovány na serveru. Příspěvky pouze pro sledovatele jsou doručeny vašim sledovatelům a uživateům v nich zmíněných a přímé příspěvky jsou doručeny pouze uživatelům v nich zmíněných. V některých případech tohle znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Snažíme se omezit přístup k těmto příspěvkům pouze na autorizované uživatele, ovšem jiné servery tak nemusejí učinit. Proto je důležité posoudit servery, ke kterým vaši sledovatelé patří. V nastavení si můžete zapnout volbu pro manuální schvalování či odmítnutí nových sledovatelů. <em>Prosím mějte na paměti, že operátoři tohoto serveru a kteréhokoliv přijímacího serveru mohou tyto zprávy vidět</em> a příjemci mohou vytvořit jejich snímek, zkopírovat je, nebo je jinak sdílet. <em>Nesdílejte přes Mastodon jakékoliv nebezpečné informace.</em></li>
<li><em>IP adresy a další metadata</em>: Když se přihlásíte, zaznamenáváme IP adresu, ze které se přihlašujete, jakožto i název vašeho webového prohlížeče. Všechny vaše webové relace jsou v nastavení přístupné k vašemu posouzení a odvolání. Nejpozdější IP adresa použita je uložena maximálně do 12 měsíců. Můžeme také uchovávat serverové záznamy, které obsahují IP adresy každého požadavku odeslaného na náš server.</li>
</ul>
<hr class="spacer" />
<h3 id="use">Na co používáme vaše informace?</h3>
<p>Jakékoliv informace, které sbíráme, mohou být použity následujícími způsoby:</p>
<ul>
<li>K poskytnutí základních funkcí Mastodonu. Interagovat s obsahem od jiných lidí a přispívat svým vlastním obsahem můžete pouze, pokud jste přihlášeni. Můžete například sledovat jiné lidi a zobrazit si jejich kombinované příspěvky ve vaší vlastní personalizované časové ose.</li>
<li>Pro pomoc moderaci komunity, například porovnáním vaší IP adresy s dalšími známými adresami pro určení vyhýbání se zákazům či jiných přestupků.</li>
<li>E-mailová adresa, kterou nám poskytnete, může být použita pro zasílání informací, oznámení o interakcích jiných uživatelů s vaším obsahem nebo přijatých zprávách a k odpovědím na dotazy a/nebo další požadavky či otázky.</li>
</ul>
<hr class="spacer" />
<h3 id="protect">Jak vaše informace chráníme?</h3>
<p>Implenentujeme různá bezpečnostní opatření pro udržování bezpečnosti vašich osobních dat, když zadáváte, odesíláte, či přistupujete k vašim osobním datům. Mimo jiné je vaše relace v prohlížeči, jakož i provoz mezi vašimi aplikacemi a API, zabezpečena pomocí SSL, a vaše heslo je hashováno pomocí silného jednosměrného algoritmu. Pro větší zabezpečení vašeho účtu můžete povolit dvoufaktorovou autentikaci.</p>
<hr class="spacer" />
<h3 id="data-retention">Jaké jsou naše zásady o uchovávání údajů?</h3>
<p>Budeme se snažit:</p>
<ul>
<li>Uchovávat serverové záznamy obsahující IP adresy všech požadavků pro tento server, pokud se takové záznamy uchovávají, maximálně 90 dní.</li>
<li>Uchovávat IP adresy související s registrovanými uživateli maximálně 12 měsíců.</li>
</ul>
<p>Kdykoliv si můžete vyžádat a stáhnout archiv vašeho obsahu, včetně vašich příspěvků, mediálních příloh, profilové fotografie a hlavičkového obrázku.</p>
<p>Kdykoliv můžete nenávratně smazat váš účet.</p>
<hr class="spacer"/>
<h3 id="cookies">Používáme cookies?</h3>
<p>Ano. Cookies jsou malé soubory, které stránka nebo její poskytovatel uloží na pevný disk vašeho počítače (pokud to dovolíte). Tyto cookies umožňují stránce rozpoznat váš prohlížeč a, pokud máte registrovaný účet, přidružit ho s vaším registrovaným účtem.</p>
<p>POužíváme cookies pro pochopení a ukládání vašich předvoleb pro budoucí návštěvy.</p>
<hr class="spacer" />
<h3 id="disclose">Zveřejňujeme jakékoliv informace třetím stranám?</h3>
<p>Vaše osobně identifikovatelné informace neprodáváme, neobchodujeme s nimi, ani je nijak nepřenášíme vnějším stranám. Do tohoto se nepočítají důvěryhodné třetí strany, které nám pomáhají provozovat naši stránku, podnikat, nebo vás obsluhovat, pokud tyto strany souhlasí se zachováním důvěrnosti těchto informací. Můžeme také uvolnit vaše informace, pokud věříme, že je to nutné pro soulad se zákonem, prosazování našich zásad, nebo ochranu práv, majetku, či bezpečnost nás či ostatních.</p>
<p>Váš veřejný obsah může být stažen jinými servery na síti. Vaše příspěvky veřejné a pouze pro sledovatele budou doručeny na servery vašich sledovatelů a přímé zprávy budou doručeny na servery příjemců, pokud jsou tito sledovatelé nebo příjemci zaregistrováni na jiném serveru, než je tento.</p>
<p>Když autorizujete aplikaci, aby používala váš účet, může, v závislosti na rozsahu oprávnění, které jí udělíte, přistupovat k vašim veřejným profilovým informacím, seznamu lidí, které sledujete, vašim sledovatelům, vašim seznamům, všem vašim příspěvkům a příspěvkům, které jste si oblíbili. Aplikace nikdy nemohou získat vaši e-mailovou adresu či heslo.</p>
<hr class="spacer" />
<h3 id="children">Používání stránky dětmi</h3>
<p>Pokud se tento server nachází v EU nebo EHP: Naše stránka, produkty a služby jsou všechny směřovány na lidi, kterým je alespoň 16 let. Pokud je vám méně než 16, dle požadavků nařízení GDPR (<a href="https://cs.wikipedia.org/wiki/Obecn%C3%A9_na%C5%99%C3%ADzen%C3%AD_o_ochran%C4%9B_osobn%C3%ADch_%C3%BAdaj%C5%AF">Obecné nařízení o ochě sobních údajů</a>) </p>
<p>Pokud se tento server nachází v USA: Naše stránka, produkty a služby jsou všechny směřovány na lidi, kterým je alespoň 13 let. Pokud je vám méně než 13, dle požadavků zákona COPPA (<a href="https://cs.wikipedia.org/wiki/Children%27s_online_privacy_protection_act">Children's Online Privacy Protection Act</a>) tuto stránku nepoužívejte.</p>
<p>Právní požadavky mohou být jiné, pokud se tento server nachází v jiné jurisdikci.</p>
<hr class="spacer" />
<h3 id="changes">Změny v našich zásadách soukromí</h3>
<p>Rozhodneme-li se naše zásady soukromí změnit, zveřejníme tyto změny na této stránce.</p>
<p>Tento dokument je dostupný pod licencí CC-BY-SA. Byl naposledy aktualizován 7 března 2018.</p>
<p>Původně adaptováno ze <a href="https://github.com/discourse/discourse">zásad soukromí Discourse</a>.</p>
title: Podmínky používání a zásady soukromí %{instance}
themes:
contrast: Vysoký kontrast
default: Mastodon
mastodon-light: Mastodon (světlý)
time:
formats:
default: "%d. %b %Y, %H:%M"
two_factor_authentication:
code_hint: Pro potvrzení zadejte kód vygenerovaný vaší autentikační aplikací
description_html: Povolíte-li <strong>dvoufaktorové ověřování</strong>, budete při přihlášení potřebovat telefon, který vám vygeneruje přístupové tokeny, které musíte zadat.
disable: Zakázat
enable: Povolit
enabled: Dvoufaktorové ověřování je povoleno
enabled_success: Dvoufaktorové ověřování bylo úspěšně povoleno
generate_recovery_codes: Vygenerovat záložní kódy
instructions_html: "<strong>Naskenujte tento QR kód Google Authenticatorem nebo jinou TOTP aplikací na vašem telefonu</strong>. Od teď bude tato aplikace generovat tokeny, které budete muset zadat při přihlášení."
lost_recovery_codes: Záložní kódy vám dovolí dostat se k vašemu účtu, pokud ztratíte telefon. Ztratíte-li záložní kódy, můžete je zde znovu vygenerovat. Vaše staré záložní kódy budou zneplatněny.
manual_instructions: 'Nemůžete-li oskenovat QR kód a je potřebovat ho zadat ručně, zde je tajemství v prostém textu:'
recovery_codes: Záložní kódy pro obnovu
recovery_codes_regenerated: Záložní kódy byly úspěšně znovu vygenerované
recovery_instructions_html: Ztratíte-li někdy přístup k vašemu telefonu, můžete k získání přístupu k účtu použít jeden ze záložních kódů. <strong>Uchovávejte tyto kódy v bezpečí</strong>. Můžete si je například vytisknout a uložit je mezi jiné důležité dokumenty.
setup: Nastavit
wrong_code: Zadaný kód byl neplatný! Je serverový čas a čas na zařízení správný?
user_mailer: user_mailer:
backup_ready:
explanation: Vyžádal/a jste si úplnou zálohu svého účtu Mastodon. Nyní je připravena ke stažení!
subject: Váš archiv je připraven ke stažení
title: Stažení archivu
welcome: welcome:
edit_profile_action: Nastavit profil
edit_profile_step: Můžete si přizpůsobit svůj profil nahráním avataru a obrázku na hlavičce, změnou zobrazovaného jména a dalších. Chcete-li posoudit nové sledovatele předtím, než vás mohou sledovat, můžete svůj účet uzamknout.
explanation: Zde je pár tipů na začátek
final_action: Začněte přispívat
final_step: 'Začněte psát! I když nemáte sledovatele, mohou vaše zprávy vidět jiní lidé, například na místní časové ose a mezi hashtagy. Můžete se ostatním představit pomocí hashtagu #introductions.'
full_handle: Vaše celá adresa profilu
full_handle_hint: Tohle je, co byste řekl/a svým přátelům, aby vám mohli posílat zprávy nebo vás sledovat z jiné instance.
review_preferences_action: Změnit nastavení
review_preferences_step: Nezapomeňte si nastavit své volby, například jaké e-maily chcete přijímat či jak soukromé mají být vaše příspěvky ve výchozím stavu. Nemáte-li epilepsii, můžete si nastavit automatické přehrávání obrázků GIF.
subject: Vítejte na Mastodonu
tip_bridge_html: Pokud přicházíte z Twitteru, můžete najít vaše přátele na Mastodonu pomocí <a href="%{bridge_url}">mostové aplikace</a>. Funguje ovšem pouze, pokud ji oni někdy také použili!
tip_federated_timeline: Federovaná časová osa je náhled celé sítě Mastodon. Zahrnuje ovšem pouze lidi, které sledují vaši sousedé, takže není úplná.
tip_following: Administrátora/y serveru sledujete automaticky. Chcete-li najít další zajímavé lidi, podívejte se na místní a federované časové osy.
tip_local_timeline: Místní časová osa je náhled lidí na %{instance}. Toto jsou vaši nejbližší sousedé!
tip_mobile_webapp: Pokud vám váš mobilní prohlížeč nabídne přidat si Mastodon na vaši domovskou obrazovku, můžete dostávat oznámení. V mnoha ohledech to funguje jako nativní aplikace!
tips: Tipy tips: Tipy
title: Vítejte na palubě, %{name}! title: Vítejte na palubě, %{name}!
users: users:
invalid_email: E-mailová adresa je neplatná invalid_email: E-mailová adresa je neplatná
invalid_otp_token: Neplatný kód pro dvoufaktorovou autentikaci
otp_lost_help_html: Pokud jste ztratil/a přístup k oběma, můžete se spojit %{email}
seamless_external_login: Jste přihlášen/a přes externí službu, nastavení hesla a e-mailu proto nejsou dostupná.
signed_in_as: 'Přihlášen/a jako:' signed_in_as: 'Přihlášen/a jako:'

@ -46,7 +46,6 @@ da:
people_who_follow: Folk der følger %{name} people_who_follow: Folk der følger %{name}
posts: Dyt posts: Dyt
posts_with_replies: Toots og svar posts_with_replies: Toots og svar
remote_follow: Følg fra andre instanser
reserved_username: Brugernavnet er reserveret reserved_username: Brugernavnet er reserveret
roles: roles:
admin: Administrator admin: Administrator
@ -464,7 +463,6 @@ da:
unlocked_warning_title: Din konto er ikke låst unlocked_warning_title: Din konto er ikke låst
generic: generic:
changes_saved_msg: Ændringerne blev gemt! changes_saved_msg: Ændringerne blev gemt!
powered_by: drevet af %{link}
save_changes: Gem ændringer save_changes: Gem ændringer
imports: imports:
types: types:
@ -511,6 +509,9 @@ da:
action: Se alle notifikationer action: Se alle notifikationer
mention: "%{name} nævnte dig i:" mention: "%{name} nævnte dig i:"
title: Mens du var væk... title: Mens du var væk...
favourite:
body: 'Din status blev favoriseret af %{name}:'
subject: "%{name} favoriserede din status"
follow: follow:
body: "%{name} følger dig nu!" body: "%{name} følger dig nu!"
subject: "%{name} følger dig nu" subject: "%{name} følger dig nu"
@ -538,18 +539,22 @@ da:
preferences: preferences:
languages: Sprog languages: Sprog
other: Andet other: Andet
publishing: Offentligører
web: Web web: Web
remote_unfollow: remote_unfollow:
error: Fejl error: Fejl
title: Titel title: Titel
unfollowed: Følger ikke længere unfollowed: Følger ikke længere
sessions: sessions:
activity: Sidste aktivitet
browser: Browser
browsers: browsers:
blackberry: Blackberry OS blackberry: Blackberry OS
chrome: Google Chrome chrome: Google Chrome
firefox: Mozilla Firefox firefox: Mozilla Firefox
generic: Ukendt browser generic: Ukendt browser
ie: IE ie: IE
safari: Apple Safari
description: "%{browser} på %{platform}" description: "%{browser} på %{platform}"
ip: IP ip: IP
platforms: platforms:
@ -583,7 +588,9 @@ da:
content_warning: 'Advarsel om indhold: %{warning}' content_warning: 'Advarsel om indhold: %{warning}'
language_detection: Opfang automatisk sprog language_detection: Opfang automatisk sprog
pin_errors: pin_errors:
limit: Du har allerede fastgjort det maksimale antal trut
ownership: Dun kan ikke fastgøre en anden persons toot ownership: Dun kan ikke fastgøre en anden persons toot
private: Ikke offentlige trut kan ikke blive fastgjort
show_more: Vis mere show_more: Vis mere
title: '%{name}: "%{quote}"' title: '%{name}: "%{quote}"'
visibilities: visibilities:
@ -593,7 +600,6 @@ da:
public_long: Alle kan se public_long: Alle kan se
unlisted: Ikke listet unlisted: Ikke listet
stream_entries: stream_entries:
click_to_show: Tryk for at vise
pinned: Fastgjort toot pinned: Fastgjort toot
sensitive_content: Følsomt indhold sensitive_content: Følsomt indhold
themes: themes:

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save