diff options
author | Thibaut Girka <thib@sitedethib.com> | 2018-05-09 17:43:30 +0200 |
---|---|---|
committer | Thibaut Girka <thib@sitedethib.com> | 2018-05-10 00:03:28 +0200 |
commit | 1c9c0167b7ea7dd03ef233a085adc0a6ef1b2527 (patch) | |
tree | ccfcb53c4ddb61d7ef3158d29403ff7cd6516e55 /app | |
parent | 143878d9dadd03347c54c9261b9bc754a1d0f5bc (diff) | |
parent | ac788ad47e32a3cf84a46ac87f84f376185cdad4 (diff) |
Merge branch 'master' into glitch-soc/master
Conflicts: app/models/account.rb app/views/accounts/_header.html.haml
Diffstat (limited to 'app')
77 files changed, 973 insertions, 402 deletions
diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 34dfb458e..8d3477e66 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,6 +3,7 @@ module Admin class ConfirmationsController < BaseController before_action :set_user + before_action :check_confirmation, only: [:resend] def create authorize @user, :confirm? @@ -11,10 +12,28 @@ module Admin redirect_to admin_accounts_path end + def resend + authorize @user, :confirm? + + @user.resend_confirmation_instructions + + log_action :confirm, @user + + flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') + redirect_to admin_accounts_path + end + private def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end + + def check_confirmation + if @user.confirmed? + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end + end end end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 522f68c98..d3c2f5e9e 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -3,7 +3,6 @@ module Admin class ReportedStatusesController < BaseController before_action :set_report - before_action :set_status, only: [:update, :destroy] def create authorize :status, :update? @@ -14,20 +13,6 @@ module Admin redirect_to admin_report_path(@report) end - def update - authorize @status, :update? - @status.update!(status_params) - log_action :update, @status - redirect_to admin_report_path(@report) - end - - def destroy - authorize @status, :destroy? - RemovalWorker.perform_async(@status.id) - log_action :destroy, @status - render json: @status - end - private def status_params @@ -51,9 +36,5 @@ module Admin def set_report @report = Report.find(params[:report_id]) end - - def set_status - @status = @report.statuses.find(params[:id]) - end end end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index d5787acfb..382bfc4a2 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -5,7 +5,6 @@ module Admin helper_method :current_params before_action :set_account - before_action :set_status, only: [:update, :destroy] PER_PAGE = 20 @@ -26,40 +25,18 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) end - def update - authorize @status, :update? - @status.update!(status_params) - log_action :update, @status - redirect_to admin_account_statuses_path(@account.id, current_params) - end - - def destroy - authorize @status, :destroy? - RemovalWorker.perform_async(@status.id) - log_action :destroy, @status - render json: @status - end - private - def status_params - params.require(:status).permit(:sensitive) - end - def form_status_batch_params params.require(:form_status_batch).permit(:action, status_ids: []) end - def set_status - @status = @account.statuses.find(params[:id]) - end - def set_account @account = Account.find(params[:account_id]) end @@ -72,5 +49,15 @@ module Admin page: page > 1 && page, }.select { |_, value| value.present? } end + + def action_from_button + if params[:nsfw_on] + 'nsfw_on' + elsif params[:nsfw_off] + 'nsfw_off' + elsif params[:delete] + 'delete' + end + end end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index a3c4008e6..259d07be8 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def user_settings_params diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index bba6a6f48..54f8be667 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -39,7 +39,7 @@ class Api::V1::Statuses::PinsController < Api::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) end def distribute_remove_activity! @@ -49,6 +49,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) end end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 2b8330f2e..f9b003dcd 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -24,7 +24,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def set_account diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f78e5fbc3..f6d86a18e 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -6,6 +6,7 @@ module SettingsHelper ar: 'العربية', bg: 'Български', ca: 'Català', + co: 'Corsu', de: 'Deutsch', el: 'Ελληνικά', eo: 'Esperanto', diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index c6f12ecd4..707c8e26c 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -4,8 +4,8 @@ module StreamEntriesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def display_name(account) - account.display_name.presence || account.username + def display_name(account, **options) + Formatter.instance.format_display_name(account, options) end def account_description(account) diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 5f1274fab..c015d3a99 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,20 +1,29 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; +import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + export function normalizeAccount(account) { account = { ...account }; + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); - account.note_emojified = emojify(account.note); + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, name_emojified: emojify(escapeTextContentForBrowser(pair.name)), - value_emojified: emojify(pair.value), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), })); } @@ -42,11 +51,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.hidden = normalOldStatus.get('hidden'); } else { const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 7aa070f56..393268811 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -8,6 +8,7 @@ import { importFetchedStatuses, } from './importer'; import { defineMessages } from 'react-intl'; +import { unescapeHTML } from '../utils/html'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -31,13 +32,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; -const unescapeHTML = (html) => { - const wrapper = document.createElement('div'); - html = html.replace(/<br \/>|<br>|\n/g, ' '); - wrapper.innerHTML = html; - return wrapper.textContent; -}; - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 982d34718..0a6e7c627 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -43,6 +43,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus(); this.setState({ mounted: true }); } @@ -55,6 +56,46 @@ class DropdownMenu extends React.PureComponent { this.node = c; } + setFocusRef = c => { + this.focusedItem = c; + } + + handleKeyDown = e => { + const items = Array.from(this.node.getElementsByTagName('a')); + const index = items.indexOf(e.currentTarget); + let element; + + switch(e.key) { + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = items[index+1]; + if (element) { + element.focus(); + } + break; + case 'ArrowUp': + element = items[index-1]; + if (element) { + element.focus(); + } + break; + case 'Home': + element = items[0]; + if (element) { + element.focus(); + } + break; + case 'End': + element = items[items.length-1]; + if (element) { + element.focus(); + } + break; + } + } + handleClick = e => { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; @@ -79,7 +120,7 @@ class DropdownMenu extends React.PureComponent { return ( <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}> + <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}> {text} </a> </li> @@ -156,9 +197,6 @@ export default class Dropdown extends React.PureComponent { handleKeyDown = e => { switch(e.key) { - case 'Enter': - this.handleClick(e); - break; case 'Escape': this.handleClose(); break; diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 7cdd63910..fd6858d05 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -35,7 +35,6 @@ export default class ScrollableList extends PureComponent { state = { fullscreen: null, - mouseOver: false, }; intersectionObserverWrapper = new IntersectionObserverWrapper(); @@ -72,7 +71,7 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && this.node.scrollTop > 0 || (this.state.mouseOver && !prevProps.isLoading)) { + if (someItemInserted && this.node.scrollTop > 0) { return this.node.scrollHeight - this.node.scrollTop; } else { return null; @@ -140,14 +139,6 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } - handleMouseEnter = () => { - this.setState({ mouseOver: true }); - } - - handleMouseLeave = () => { - this.setState({ mouseOver: false }); - } - render () { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; @@ -158,7 +149,7 @@ export default class ScrollableList extends PureComponent { if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = ( - <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> <div role='feed' className='item-list'> {prepend} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 402d558c4..953d98c20 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -206,7 +206,7 @@ export default class Status extends ImmutablePureComponent { ); } else { media = ( - <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > + <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />} </Bundle> ); diff --git a/app/javascript/mastodon/containers/card_container.js b/app/javascript/mastodon/containers/card_container.js deleted file mode 100644 index 11b9f88d4..000000000 --- a/app/javascript/mastodon/containers/card_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Card from '../features/status/components/card'; -import { fromJS } from 'immutable'; - -export default class CardContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string, - card: PropTypes.array.isRequired, - }; - - render () { - const { card, ...props } = this.props; - return <Card card={fromJS(card)} {...props} />; - } - -} diff --git a/app/javascript/mastodon/containers/cards_container.js b/app/javascript/mastodon/containers/cards_container.js new file mode 100644 index 000000000..894bf4ef9 --- /dev/null +++ b/app/javascript/mastodon/containers/cards_container.js @@ -0,0 +1,59 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import Card from '../features/status/components/card'; +import ModalRoot from '../components/modal_root'; +import MediaModal from '../features/ui/components/media_modal'; +import { fromJS } from 'immutable'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export default class CardsContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string, + cards: PropTypes.object.isRequired, + }; + + state = { + media: null, + }; + + handleOpenCard = (media) => { + document.body.classList.add('card-standalone__body'); + this.setState({ media }); + } + + handleCloseCard = () => { + document.body.classList.remove('card-standalone__body'); + this.setState({ media: null }); + } + + render () { + const { locale, cards } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Fragment> + {[].map.call(cards, container => { + const { card, ...props } = JSON.parse(container.getAttribute('data-props')); + + return ReactDOM.createPortal( + <Card card={fromJS(card)} onOpenMedia={this.handleOpenCard} {...props} />, + container, + ); + })} + <ModalRoot onClose={this.handleCloseCard}> + {this.state.media && ( + <MediaModal media={this.state.media} index={0} onClose={this.handleCloseCard} /> + )} + </ModalRoot> + </Fragment> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index 8719bb5c9..a1a4bd024 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; import configureStore from '../store/configureStore'; @@ -8,6 +9,7 @@ import { getLocale } from '../locales'; import PublicTimeline from '../features/standalone/public_timeline'; import CommunityTimeline from '../features/standalone/community_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline'; +import ModalContainer from '../features/ui/containers/modal_container'; import initialState from '../initial_state'; const { localeData, messages } = getLocale(); @@ -47,7 +49,13 @@ export default class TimelineContainer extends React.PureComponent { return ( <IntlProvider locale={locale} messages={messages}> <Provider store={store}> - {timeline} + <Fragment> + {timeline} + {ReactDOM.createPortal( + <ModalContainer />, + document.getElementById('modal-container'), + )} + </Fragment> </Provider> </IntlProvider> ); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index bbf886dca..7358053da 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -131,6 +131,7 @@ export default class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); + const badge = account.get('bot') ? (<div className='roles'><div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div></div>) : null; return ( <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}> @@ -139,19 +140,20 @@ export default class Header extends ImmutablePureComponent { <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} /> <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> + + {badge} + <div className='account__header__content' dangerouslySetInnerHTML={content} /> {fields.size > 0 && ( - <table className='account__header__fields'> - <tbody> - {fields.map((pair, i) => ( - <tr key={i}> - <th dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} /> - <td dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> - </tr> - ))} - </tbody> - </table> + <div className='account__header__fields'> + {fields.map((pair, i) => ( + <dl key={i}> + <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> + <dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} /> + </dl> + ))} + </div> )} {info} diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 6b22ba84a..a772c1c95 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -42,22 +42,65 @@ class PrivacyDropdownMenu extends React.PureComponent { } } - handleClick = e => { - if (e.key === 'Escape') { - this.props.onClose(); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); + handleKeyDown = e => { + const { items } = this.props; + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => { + return (item.value === value); + }); + let element; + switch(e.key) { + case 'Escape': this.props.onClose(); - this.props.onChange(value); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1]; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1]; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'Home': + element = this.node.firstChild; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'End': + element = this.node.lastChild; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; } } + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + } + componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus(); this.setState({ mounted: true }); } @@ -70,6 +113,10 @@ class PrivacyDropdownMenu extends React.PureComponent { this.node = c; } + setFocusRef = c => { + this.focusedItem = c; + } + render () { const { mounted } = this.state; const { style, items, value } = this.props; @@ -80,9 +127,9 @@ class PrivacyDropdownMenu extends React.PureComponent { // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays - <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> + <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}> {items.map(item => ( - <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}> + <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div className='privacy-dropdown__option__icon'> <i className={`fa fa-fw fa-${item.icon}`} /> </div> @@ -147,9 +194,6 @@ export default class PrivacyDropdown extends React.PureComponent { handleKeyDown = e => { switch(e.key) { - case 'Enter': - this.handleToggle(e); - break; case 'Escape': this.handleClose(); break; diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 67f0e7981..19aae0332 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -43,11 +43,19 @@ export default class Compose extends React.PureComponent { }; componentDidMount () { - this.props.dispatch(mountCompose()); + const { isSearchPage } = this.props; + + if (!isSearchPage) { + this.props.dispatch(mountCompose()); + } } componentWillUnmount () { - this.props.dispatch(unmountCompose()); + const { isSearchPage } = this.props; + + if (!isSearchPage) { + this.props.dispatch(unmountCompose()); + } } onFocus = () => { diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 4185cba32..a334318ce 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -40,6 +40,17 @@ export default class ModalRoot extends React.PureComponent { onClose: PropTypes.func.isRequired, }; + getSnapshotBeforeUpdate () { + const visible = !!this.props.type; + return { + overflowY: visible ? 'hidden' : null, + }; + } + + componentDidUpdate (prevProps, prevState, { overflowY }) { + document.body.style.overflowY = overflowY; + } + renderLoading = modalId => () => { return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; } diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index 8a55c553c..8616f0315 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -30,7 +30,7 @@ const makeMapStateToProps = () => { account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), forward: state.getIn(['reports', 'new', 'forward']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), + statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -64,12 +64,12 @@ export default class ReportModal extends ImmutablePureComponent { } componentDidMount () { - this.props.dispatch(expandAccountTimeline(this.props.account.get('id'))); + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'))); + this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); } } diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 4efacda65..e5b1edc4a 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -21,6 +21,8 @@ const makeGetStatusIds = () => createSelector([ } return statusIds.filter(id => { + if (id === null) return true; + const statusForId = statuses.get(id); let showStatus = true; diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 947348f70..d531d8fd5 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -258,7 +258,7 @@ "status.pin": "تدبيس على الملف الشخصي", "status.pinned": "تبويق مثبَّت", "status.reblog": "رَقِّي", - "status.reblog_private": "Boost to original audience", + "status.reblog_private": "القيام بالترقية إلى الجمهور الأصلي", "status.reblogged_by": "{name} رقى", "status.reply": "ردّ", "status.replyAll": "رُد على الخيط", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json new file mode 100644 index 000000000..2d7427c55 --- /dev/null +++ b/app/javascript/mastodon/locales/co.json @@ -0,0 +1,296 @@ +{ + "account.block": "Bluccà @{name}", + "account.block_domain": "Piattà tuttu da {domain}", + "account.blocked": "Bluccatu", + "account.direct": "Missaghju direttu @{name}", + "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Duminiu piattatu", + "account.edit_profile": "Mudificà u prufile", + "account.follow": "Siguità", + "account.followers": "Abbunati", + "account.follows": "Abbunamenti", + "account.follows_you": "Vi seguita", + "account.hide_reblogs": "Piattà spartere da @{name}", + "account.media": "Media", + "account.mention": "Mintuvà @{name}", + "account.moved_to": "{name} hè partutu nant'à:", + "account.mute": "Piattà @{name}", + "account.mute_notifications": "Piattà nutificazione da @{name}", + "account.muted": "Piattatu", + "account.posts": "Statuti", + "account.posts_with_replies": "Statuti è risposte", + "account.report": "Palisà @{name}", + "account.requested": "In attesa d'apprubazione. Cliccate per annullà a dumanda", + "account.share": "Sparte u prufile di @{name}", + "account.show_reblogs": "Vede spartere da @{name}", + "account.unblock": "Sbluccà @{name}", + "account.unblock_domain": "Ùn piattà più {domain}", + "account.unfollow": "Ùn siguità più", + "account.unmute": "Ùn piattà più @{name}", + "account.unmute_notifications": "Ùn piattà più nutificazione da @{name}", + "account.view_full_profile": "View full profile", + "alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.", + "alert.unexpected.title": "Uups!", + "boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta", + "bundle_column_error.body": "C'hè statu un prublemu caricandu st'elementu.", + "bundle_column_error.retry": "Pruvà torna", + "bundle_column_error.title": "Errore di cunnessione", + "bundle_modal_error.close": "Chjudà", + "bundle_modal_error.message": "C'hè statu un prublemu caricandu st'elementu.", + "bundle_modal_error.retry": "Pruvà torna", + "column.blocks": "Utilizatori bluccati", + "column.community": "Linea pubblica lucale", + "column.direct": "Missaghji diretti", + "column.domain_blocks": "Duminii piattati", + "column.favourites": "Favuriti", + "column.follow_requests": "Dumande d'abbunamentu", + "column.home": "Accolta", + "column.lists": "Liste", + "column.mutes": "Utilizatori piattati", + "column.notifications": "Nutificazione", + "column.pins": "Statuti puntarulati", + "column.public": "Linea pubblica glubale", + "column_back_button.label": "Ritornu", + "column_header.hide_settings": "Piattà i parametri", + "column_header.moveLeft_settings": "Spiazzà à manca", + "column_header.moveRight_settings": "Spiazzà à diritta", + "column_header.pin": "Puntarulà", + "column_header.show_settings": "Mustrà i parametri", + "column_header.unpin": "Spuntarulà", + "column_subheading.navigation": "Navigazione", + "column_subheading.settings": "Parametri", + "compose_form.direct_message_warning": "Solu l'utilizatori mintuvati puderenu vede stu statutu.", + "compose_form.hashtag_warning": "Stu statutu ùn hè \"Micca listatu\" è ùn sarà micca listatu indè e circate da hashtag. Per esse vistu in quesse, u statutu deve esse \"Pubblicu\".", + "compose_form.lock_disclaimer": "U vostru contu ùn hè micca {locked}. Tuttu u mondu pò seguitavi è vede i vostri statuti privati.", + "compose_form.lock_disclaimer.lock": "privatu", + "compose_form.placeholder": "À chè pensate?", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.marked": "Media indicatu cum'è sensibile", + "compose_form.sensitive.unmarked": "Media micca indicatu cum'è sensibile", + "compose_form.spoiler.marked": "Testu piattatu daret'à un'avertimentu", + "compose_form.spoiler.unmarked": "Testu micca piattatu", + "compose_form.spoiler_placeholder": "Scrive u vostr'avertimentu quì", + "confirmation_modal.cancel": "Annullà", + "confirmations.block.confirm": "Bluccà", + "confirmations.block.message": "Site sicuru·a che vulete bluccà @{name}?", + "confirmations.delete.confirm": "Toglie", + "confirmations.delete.message": "Site sicuru·a che vulete supprime stu statutu?", + "confirmations.delete_list.confirm": "Toglie", + "confirmations.delete_list.message": "Site sicuru·a che vulete supprime sta lista?", + "confirmations.domain_block.confirm": "Piattà tuttu u duminiu?", + "confirmations.domain_block.message": "Site sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà.", + "confirmations.mute.confirm": "Piattà", + "confirmations.mute.message": "Site sicuru·a che vulete piattà @{name}?", + "confirmations.unfollow.confirm": "Disabbunassi", + "confirmations.unfollow.message": "Site sicuru·a ch'ùn vulete più siguità @{name}?", + "embed.instructions": "Integrà stu statutu à u vostru situ cù u codice quì sottu.", + "embed.preview": "Assumiglierà à qualcosa cusì:", + "emoji_button.activity": "Attività", + "emoji_button.custom": "Persunalizati", + "emoji_button.flags": "Bandere", + "emoji_button.food": "Manghjusca è Bienda", + "emoji_button.label": "Mette un'emoji", + "emoji_button.nature": "Natura", + "emoji_button.not_found": "Ùn c'hè nunda! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Oggetti", + "emoji_button.people": "Parsunaghji", + "emoji_button.recent": "Assai utilizati", + "emoji_button.search": "Cercà...", + "emoji_button.search_results": "Risultati di a cerca", + "emoji_button.symbols": "Simbuli", + "emoji_button.travel": "Lochi è Viaghju", + "empty_column.community": "Ùn c'hè nunda indè a linea lucale. Scrivete puru qualcosa!", + "empty_column.direct": "Ùn avete ancu nisun missaghju direttu. S'è voi mandate o ricevete unu, u vidarete quì.", + "empty_column.hashtag": "Ùn c'hè ancu nunda quì.", + "empty_column.home": "A vostr'accolta hè viota! Pudete andà nant'à {public} o pruvà a ricerca per truvà parsone da siguità.", + "empty_column.home.public_timeline": "a linea pubblica", + "empty_column.list": "Ùn c'hè ancu nunda quì. Quandu membri di sta lista manderanu novi statuti, i vidarete quì.", + "empty_column.notifications": "Ùn avete ancu nisuna nutificazione. Interact with others to start the conversation.", + "empty_column.public": "Ùn c'hè nunda quì! Scrivete qualcosa in pubblicu o seguitate utilizatori d'altre istanze per empie a linea pubblica.", + "follow_request.authorize": "Auturizà", + "follow_request.reject": "Righjittà", + "getting_started.appsshort": "Applicazione", + "getting_started.faq": "FAQ", + "getting_started.heading": "Per principià", + "getting_started.open_source_notice": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un bug, nant'à GitHub: {github}", + "getting_started.userguide": "Guida d'utilizazione", + "home.column_settings.advanced": "Avanzati", + "home.column_settings.basic": "Bàsichi", + "home.column_settings.filter_regex": "Filtrà cù spressione regulare (regex)", + "home.column_settings.show_reblogs": "Vede e spartere", + "home.column_settings.show_replies": "Vede e risposte", + "home.settings": "Parametri di a colonna", + "keyboard_shortcuts.back": "rivultà", + "keyboard_shortcuts.boost": "sparte", + "keyboard_shortcuts.column": "fucalizà un statutu indè una colonna", + "keyboard_shortcuts.compose": "fucalizà nant'à l'area di ridazzione", + "keyboard_shortcuts.description": "Descrizzione", + "keyboard_shortcuts.down": "falà indè a lista", + "keyboard_shortcuts.enter": "apre u statutu", + "keyboard_shortcuts.favourite": "aghjunghje à i favuriti", + "keyboard_shortcuts.heading": "Accorte cù a tastera", + "keyboard_shortcuts.hotkey": "Accorta", + "keyboard_shortcuts.legend": "vede a legenda", + "keyboard_shortcuts.mention": "mintuvà l'autore", + "keyboard_shortcuts.reply": "risponde", + "keyboard_shortcuts.search": "fucalizà nant'à l'area di circata", + "keyboard_shortcuts.toggle_hidden": "vede/piattà u testu daretu à l'avertimentu CW", + "keyboard_shortcuts.toot": "scrive un novu statutu", + "keyboard_shortcuts.unfocus": "ùn fucalizà più l'area di testu", + "keyboard_shortcuts.up": "cullà indè a lista", + "lightbox.close": "Chjudà", + "lightbox.next": "Siguente", + "lightbox.previous": "Pricidente", + "lists.account.add": "Aghjunghje à a lista", + "lists.account.remove": "Toglie di a lista", + "lists.delete": "Supprime a lista", + "lists.edit": "Mudificà a lista", + "lists.new.create": "Aghjustà una lista", + "lists.new.title_placeholder": "Titulu di a lista", + "lists.search": "Circà indè i vostr'abbunamenti", + "lists.subheading": "E vo liste", + "loading_indicator.label": "Caricamentu...", + "media_gallery.toggle_visible": "Cambià a visibilità", + "missing_indicator.label": "Micca trovu", + "missing_indicator.sublabel": "Ùn era micca pussivule di truvà sta risorsa", + "mute_modal.hide_notifications": "Piattà nutificazione da st'utilizatore?", + "navigation_bar.blocks": "Utilizatori bluccati", + "navigation_bar.community_timeline": "Linea pubblica lucale", + "navigation_bar.direct": "Missaghji diretti", + "navigation_bar.domain_blocks": "Duminii piattati", + "navigation_bar.edit_profile": "Mudificà u prufile", + "navigation_bar.favourites": "Favuriti", + "navigation_bar.follow_requests": "Dumande d'abbunamentu", + "navigation_bar.info": "À prupositu di l'istanza", + "navigation_bar.keyboard_shortcuts": "Accorte cù a tastera", + "navigation_bar.lists": "Liste", + "navigation_bar.logout": "Scunnettassi", + "navigation_bar.mutes": "Utilizatori piattati", + "navigation_bar.pins": "Statuti puntarulati", + "navigation_bar.preferences": "Preferenze", + "navigation_bar.public_timeline": "Linea pubblica glubale", + "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti", + "notification.follow": "{name} v'hà seguitatu", + "notification.mention": "{name} v'hà mintuvatu", + "notification.reblog": "{name} hà spartutu u vostru statutu", + "notifications.clear": "Purgà e nutificazione", + "notifications.clear_confirmation": "Site sicuru·a che vulete toglie tutte ste nutificazione?", + "notifications.column_settings.alert": "Nutificazione nant'à l'urdinatore", + "notifications.column_settings.favourite": "Favuriti:", + "notifications.column_settings.follow": "Abbunati novi:", + "notifications.column_settings.mention": "Minzione:", + "notifications.column_settings.push": "Nutificazione Push", + "notifications.column_settings.push_meta": "Quess'apparechju", + "notifications.column_settings.reblog": "Spartere:", + "notifications.column_settings.show": "Mustrà indè a colonna", + "notifications.column_settings.sound": "Sunà", + "onboarding.done": "Fatta", + "onboarding.next": "Siguente", + "onboarding.page_five.public_timelines": "A linea pubblica lucale mostra statuti pubblichi da tuttu u mondu nant'à {domain}. A linea pubblica glubale mostra ancu quelli di a ghjente seguitata da l'utilizatori di {domain}. Quesse sò una bona manera d'incuntrà nove parsone.", + "onboarding.page_four.home": "A linea d'accolta mostra i statuti di i vostr'abbunamenti.", + "onboarding.page_four.notifications": "A colonna di nutificazione mostra l'interazzione ch'altre parsone anu cù u vostru contu.", + "onboarding.page_one.federation": "Mastodon ghjè una rete di servori independenti, chjamati istanze, uniti indè una sola rete suciale.", + "onboarding.page_one.full_handle": "U vostru identificatore cumplettu", + "onboarding.page_one.handle_hint": "Quessu ghjè cio chì direte à i vostri amichi per circavi.", + "onboarding.page_one.welcome": "Benvenuti/a nant'à Mastodon!", + "onboarding.page_six.admin": "L'amministratore di a vostr'istanza hè {admin}.", + "onboarding.page_six.almost_done": "Quasgi finitu...", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "Ci sò {apps} dispunibule per iOS, Android è altre piattaforme.", + "onboarding.page_six.github": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un prublemu, nant'à {github}.", + "onboarding.page_six.guidelines": "regule di a cumunità", + "onboarding.page_six.read_guidelines": "Ùn vi scurdate di leghje e {guidelines} di {domain}", + "onboarding.page_six.various_app": "applicazione pè u telefuninu", + "onboarding.page_three.profile": "Pudete mudificà u prufile per cambia u ritrattu, a descrizzione è u nome affissatu. Ci sò ancu alcun'altre preferenze.", + "onboarding.page_three.search": "Fate usu di l'area di ricerca per truvà altre persone è vede hashtag cum'è {illustration} o {introductions}. Per vede qualcunu ch'ùn hè micca nant'à st'istanza, cercate u so identificatore complettu (pare un'email).", + "onboarding.page_two.compose": "I statuti è missaghji si scrivenu indè l'area di ridazzione. Pudete caricà imagine, cambià i parametri di pubblicazione, è mette avertimenti di cuntenuti cù i buttoni quì sottu.", + "onboarding.skip": "Passà", + "privacy.change": "Mudificà a cunfidenzialità di u statutu", + "privacy.direct.long": "Mandà solu à quelli chì so mintuvati", + "privacy.direct.short": "Direttu", + "privacy.private.long": "Mustrà solu à l'abbunati", + "privacy.private.short": "Privatu", + "privacy.public.long": "Mustrà à tuttu u mondu nant'a linea pubblica", + "privacy.public.short": "Pubblicu", + "privacy.unlisted.long": "Ùn mette micca nant'a linea pubblica (ma tutt'u mondu pò vede u statutu nant'à u vostru prufile)", + "privacy.unlisted.short": "Micca listatu", + "regeneration_indicator.label": "Caricamentu…", + "regeneration_indicator.sublabel": "Priparazione di a vostra pagina d'accolta", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "avà", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Annullà", + "report.forward": "Trasferisce à {target}", + "report.forward_hint": "U contu hè nant'à un'altru servore. Vulete ancu mandà una copia anonima di u signalamentu quallà?", + "report.hint": "U signalamentu sarà mandatu à i muderatori di l'istanza. Pudete spiegà perchè avete palisatu stu contu quì sottu:", + "report.placeholder": "Altri cummenti", + "report.submit": "Mandà", + "report.target": "Signalamentu", + "search.placeholder": "Circà", + "search_popout.search_format": "Ricerca avanzata", + "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.hashtag": "hashtag", + "search_popout.tips.status": "statutu", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "utilizatore", + "search_results.accounts": "Ghjente", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Statuti", + "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}", + "standalone.public_title": "Una vista di...", + "status.block": "Bluccà @{name}", + "status.cancel_reblog_private": "Ùn sparte più", + "status.cannot_reblog": "Stu statutu ùn pò micca esse spartutu", + "status.delete": "Toglie", + "status.direct": "Mandà un missaghju @{name}", + "status.embed": "Integrà", + "status.favourite": "Aghjunghje à i favuriti", + "status.load_more": "Vede di più", + "status.media_hidden": "Media piattata", + "status.mention": "Mintuvà @{name}", + "status.more": "Più", + "status.mute": "Piattà @{name}", + "status.mute_conversation": "Piattà a cunversazione", + "status.open": "Apre stu statutu", + "status.pin": "Puntarulà à u prufile", + "status.pinned": "Statutu puntarulatu", + "status.reblog": "Sparte", + "status.reblog_private": "Sparte à l'audienza uriginale", + "status.reblogged_by": "{name} hà spartutu", + "status.reply": "Risponde", + "status.replyAll": "Risponde à tutti", + "status.report": "Palisà @{name}", + "status.sensitive_toggle": "Cliccate per vede", + "status.sensitive_warning": "Cuntinutu sensibile", + "status.share": "Sparte", + "status.show_less": "Ripiegà", + "status.show_less_all": "Ripiegà tuttu", + "status.show_more": "Slibrà", + "status.show_more_all": "Slibrà tuttu", + "status.unmute_conversation": "Ùn piattà più a cunversazione", + "status.unpin": "Spuntarulà da u prufile", + "tabs_bar.federated_timeline": "Glubale", + "tabs_bar.home": "Accolta", + "tabs_bar.local_timeline": "Lucale", + "tabs_bar.notifications": "Nutificazione", + "tabs_bar.search": "Cercà", + "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", + "upload_area.title": "Drag & drop per caricà un fugliale", + "upload_button.label": "Aghjunghje un media", + "upload_form.description": "Discrive per i malvistosi", + "upload_form.focus": "Riquatrà", + "upload_form.undo": "Annullà", + "upload_progress.label": "Caricamentu...", + "video.close": "Chjudà a video", + "video.exit_fullscreen": "Caccià u pienu screnu", + "video.expand": "Ingrandà a video", + "video.fullscreen": "Pienu screnu", + "video.hide": "Piattà a video", + "video.mute": "Surdina", + "video.pause": "Pausa", + "video.play": "Lettura", + "video.unmute": "Caccià a surdina" +} diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index a7e1c408f..5b36348eb 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -2,9 +2,9 @@ "account.block": "Απόκλεισε τον/την @{name}", "account.block_domain": "Απόκρυψε τα πάντα από τον/την", "account.blocked": "Αποκλεισμένος/η", - "account.direct": "Απευθείας μήνυμα προς @{name}", + "account.direct": "Άμεσο μήνυμα προς @{name}", "account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορει να μην αντανακλούν το προφίλ του χρήστη επαρκως.", - "account.domain_blocked": "Domain hidden", + "account.domain_blocked": "Κρυμμένος τομέας", "account.edit_profile": "Επεξεργάσου το προφίλ", "account.follow": "Ακολούθησε", "account.followers": "Ακόλουθοι", @@ -23,15 +23,15 @@ "account.requested": "Εκκρεμεί έγκριση. Κάνε κλικ για να ακυρώσεις το αίτημα ακολούθησης", "account.share": "Μοιράσου το προφίλ του/της @{name}", "account.show_reblogs": "Δείξε τις προωθήσεις του/της @{name}", - "account.unblock": "Unblock @{name}", + "account.unblock": "Ξεμπλόκαρε τον/την @{name}", "account.unblock_domain": "Αποκάλυψε το {domain}", - "account.unfollow": "Unfollow", - "account.unmute": "Unmute @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", + "account.unfollow": "Διακοπή παρακολούθησης", + "account.unmute": "Διακοπή αποσιώπησης του/της @{name}", + "account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}", "account.view_full_profile": "Δες το πλήρες προφίλ", "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.", "alert.unexpected.title": "Εεπ!", - "boost_modal.combo": "You can press {combo} to skip this next time", + "boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις αυτό την επόμενη φορά", "bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.", "bundle_column_error.retry": "Δοκίμασε ξανά", "bundle_column_error.title": "Σφάλμα δικτύου", @@ -41,7 +41,7 @@ "column.blocks": "Αποκλεισμένοι χρήστες", "column.community": "Τοπική ροή", "column.direct": "Απευθείας μηνύματα", - "column.domain_blocks": "Hidden domains", + "column.domain_blocks": "Κρυμμένοι τομείς", "column.favourites": "Αγαπημένα", "column.follow_requests": "Αιτήματα παρακολούθησης", "column.home": "Αρχική", @@ -60,7 +60,7 @@ "column_subheading.navigation": "Πλοήγηση", "column_subheading.settings": "Ρυθμίσεις", "compose_form.direct_message_warning": "Αυτό το τουτ θα εμφανίζεται μόνο σε όλους τους αναφερόμενους χρήστες.", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Αυτό το τουτ δεν θα εμφανίζεται κάτω από καμία ταμπέλα καθώς είναι αφανές. Μόνο τα δημόσια τουτ μπορούν να αναζητηθούν ανά ταμπέλα.", "compose_form.lock_disclaimer": "Ο λογαριασμός σου δεν είναι {locked}. Οποιοσδήποτε μπορεί να σε ακολουθήσει για να δει τις δημοσιεύσεις σας προς τους ακολούθους σας.", "compose_form.lock_disclaimer.lock": "κλειδωμένος", "compose_form.placeholder": "Τι σκέφτεσαι;", @@ -78,65 +78,65 @@ "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή την κατάσταση;", "confirmations.delete_list.confirm": "Διέγραψε", "confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", - "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", - "getting_started.appsshort": "Apps", + "confirmations.domain_block.confirm": "Απόκρυψη ολόκληρου του τομέα", + "confirmations.domain_block.message": "Σίγουρα θες να μπλοκάρεις ολόκληρο το {domain}; Συνήθως μερικά εστιασμένα μπλοκ ή αποσιωπήσεις επαρκούν και προτιμούνται.", + "confirmations.mute.confirm": "Αποσιώπηση", + "confirmations.mute.message": "Σίγουρα θες να αποσιωπήσεις τον/την {name};", + "confirmations.unfollow.confirm": "Διακοπή παρακολούθησης", + "confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};", + "embed.instructions": "Ενσωματώστε αυτή την κατάσταση στην ιστοσελίδα σας αντιγράφοντας τον παρακάτω κώδικα.", + "embed.preview": "Ορίστε πως θα φαίνεται:", + "emoji_button.activity": "Δραστηριότητα", + "emoji_button.custom": "Προσαρμοσμένα", + "emoji_button.flags": "Σημαίες", + "emoji_button.food": "Φαγητά & Ποτά", + "emoji_button.label": "Εισάγετε emoji", + "emoji_button.nature": "Φύση", + "emoji_button.not_found": "Ουδέν emojo!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Αντικείμενα", + "emoji_button.people": "Άνθρωποι", + "emoji_button.recent": "Δημοφιλή", + "emoji_button.search": "Αναζήτηση…", + "emoji_button.search_results": "Αποτελέσματα αναζήτησης", + "emoji_button.symbols": "Σύμβολα", + "emoji_button.travel": "Ταξίδια & Τοποθεσίες", + "empty_column.community": "Η τοπική ροή είναι κενή. Γράψε κάτι δημόσιο παραμύθι ν' αρχινίσει!", + "empty_column.direct": "Δεν έχεις απευθείας μηνύματα ακόμα. Όταν στείλεις ή λάβεις κανένα, θα εμφανιστεί εδώ.", + "empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ταμπέλα.", + "empty_column.home": "Η τοπική σου ροή είναι κενή! Πήγαινε στο {public} ή κάνε αναζήτηση για να ξεκινήσεις και να γνωρίσεις άλλους χρήστες.", + "empty_column.home.public_timeline": "η δημόσια ροή", + "empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμα. Όταν τα μέλη της δημοσιεύσουν νέες καταστάσεις, θα εμφανιστούν εδώ", + "empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Αλληλεπίδρασε με άλλους χρήστες για να ξεκινήσεις την κουβέντα.", + "empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο, ή ακολούθησε χειροκίνητα χρήστες από άλλα instances για να το γεμίσεις", + "follow_request.authorize": "Ενέκρινε", + "follow_request.reject": "Απέρριψε", + "getting_started.appsshort": "Εφαρμογές", "getting_started.faq": "FAQ", - "getting_started.heading": "Getting started", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", - "getting_started.userguide": "User Guide", - "home.column_settings.advanced": "Advanced", - "home.column_settings.basic": "Basic", - "home.column_settings.filter_regex": "Filter out by regular expressions", - "home.column_settings.show_reblogs": "Show boosts", - "home.column_settings.show_replies": "Show replies", - "home.settings": "Column settings", - "keyboard_shortcuts.back": "to navigate back", - "keyboard_shortcuts.boost": "to boost", - "keyboard_shortcuts.column": "to focus a status in one of the columns", - "keyboard_shortcuts.compose": "to focus the compose textarea", + "getting_started.heading": "Ξεκινώντας", + "getting_started.open_source_notice": "Το Mastodon είναι ελεύθερο λογισμικό. Μπορείς να συνεισφέρεις ή να αναφέρεις ζητήματα στο GitHub στο {github}.", + "getting_started.userguide": "Οδηγός Χρηστών", + "home.column_settings.advanced": "Προχωρημένα", + "home.column_settings.basic": "Βασικά", + "home.column_settings.filter_regex": "Φιλτράρετε μέσω regular expressions", + "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", + "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", + "home.settings": "Ρυθμίσεις στηλών", + "keyboard_shortcuts.back": "για επιστροφή πίσω", + "keyboard_shortcuts.boost": "για προώθηση", + "keyboard_shortcuts.column": "για εστίαση μιας κατάστασης σε μια από τις στήλες", + "keyboard_shortcuts.compose": "για εστίαση στην περιοχή κειμένου συγγραφής", "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.down": "για κίνηση προς τα κάτω στη λίστα", "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourite": "για σημείωση αγαπημένου", "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.hotkey": "Συντόμευση", + "keyboard_shortcuts.legend": "για να εμφανίσεις αυτόν τον οδηγό", + "keyboard_shortcuts.mention": "για να αναφέρεις το συγγραφέα", + "keyboard_shortcuts.reply": "για απάντηση", + "keyboard_shortcuts.search": "για εστίαση αναζήτησης", + "keyboard_shortcuts.toggle_hidden": "για εμφάνιση/απόκρυψη κειμένου πίσω από την προειδοποίηση", + "keyboard_shortcuts.toot": "για δημιουργία ολοκαίνουριου τουτ", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Close", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 37587c14c..1ab6d2aa0 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -2,7 +2,7 @@ "account.block": "Bloki @{name}", "account.block_domain": "Kaŝi ĉion de {domain}", "account.blocked": "Blokita", - "account.direct": "Direct message @{name}", + "account.direct": "Rekte mesaĝi @{name}", "account.disclaimer_full": "Subaj informoj povas reflekti la profilon de la uzanto nekomplete.", "account.domain_blocked": "Domajno kaŝita", "account.edit_profile": "Redakti profilon", @@ -29,8 +29,8 @@ "account.unmute": "Malsilentigi @{name}", "account.unmute_notifications": "Malsilentigi sciigojn de @{name}", "account.view_full_profile": "Vidi plenan profilon", - "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.message": "Neatendita eraro okazis.", + "alert.unexpected.title": "Ups!", "boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje", "bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.", "bundle_column_error.retry": "Bonvolu reprovi", @@ -40,8 +40,8 @@ "bundle_modal_error.retry": "Bonvolu reprovi", "column.blocks": "Blokitaj uzantoj", "column.community": "Loka tempolinio", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", + "column.direct": "Rektaj mesaĝoj", + "column.domain_blocks": "Kaŝitaj domajnoj", "column.favourites": "Stelumoj", "column.follow_requests": "Petoj de sekvado", "column.home": "Hejmo", @@ -59,7 +59,7 @@ "column_header.unpin": "Depingli", "column_subheading.navigation": "Navigado", "column_subheading.settings": "Agordado", - "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", + "compose_form.direct_message_warning": "Tiu mesaĝo videblos nur por ĉiuj menciitaj uzantoj.", "compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.", "compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn, kiuj estas nur por sekvantoj.", "compose_form.lock_disclaimer.lock": "ŝlosita", @@ -101,7 +101,7 @@ "emoji_button.symbols": "Simboloj", "emoji_button.travel": "Vojaĝoj kaj lokoj", "empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!", - "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": "Vi ankoraŭ ne havas rektan mesaĝon. Kiam vi sendos aŭ ricevos iun, ĝi aperos ĉi tie.", "empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.", "empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.", "empty_column.home.public_timeline": "la publikan tempolinion", @@ -135,7 +135,7 @@ "keyboard_shortcuts.mention": "por mencii la aŭtoron", "keyboard_shortcuts.reply": "por respondi", "keyboard_shortcuts.search": "por fokusigi la serĉilon", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_hidden": "por montri/kaŝi tekston malantaŭ enhava averto", "keyboard_shortcuts.toot": "por komenci tute novan mesaĝon", "keyboard_shortcuts.unfocus": "por malfokusigi la tekstujon aŭ la serĉilon", "keyboard_shortcuts.up": "por iri supren en la listo", @@ -157,8 +157,8 @@ "mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn el ĉi tiu uzanto?", "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.community_timeline": "Loka tempolinio", - "navigation_bar.direct": "Direct messages", - "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.direct": "Rektaj mesaĝoj", + "navigation_bar.domain_blocks": "Kaŝitaj domajnoj", "navigation_bar.edit_profile": "Redakti profilon", "navigation_bar.favourites": "Stelumoj", "navigation_bar.follow_requests": "Petoj de sekvado", @@ -242,10 +242,10 @@ "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}", "standalone.public_title": "Enrigardo…", "status.block": "Bloki @{name}", - "status.cancel_reblog_private": "Unboost", + "status.cancel_reblog_private": "Eksdiskonigi", "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas", "status.delete": "Forigi", - "status.direct": "Direct message @{name}", + "status.direct": "Rekte mesaĝi @{name}", "status.embed": "Enkorpigi", "status.favourite": "Stelumi", "status.load_more": "Ŝargi pli", @@ -258,7 +258,7 @@ "status.pin": "Alpingli profile", "status.pinned": "Alpinglita mesaĝo", "status.reblog": "Diskonigi", - "status.reblog_private": "Boost to original audience", + "status.reblog_private": "Diskonigi al la originala atentaro", "status.reblogged_by": "{name} diskonigis", "status.reply": "Respondi", "status.replyAll": "Respondi al la fadeno", @@ -276,7 +276,7 @@ "tabs_bar.home": "Hejmo", "tabs_bar.local_timeline": "Loka tempolinio", "tabs_bar.notifications": "Sciigoj", - "tabs_bar.search": "Search", + "tabs_bar.search": "Serĉi", "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", "upload_area.title": "Altreni kaj lasi por alŝuti", "upload_button.label": "Aldoni aŭdovidaĵon", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index a4af97dda..16bf4033c 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -13,7 +13,7 @@ "account.hide_reblogs": "Masquer les partages de @{name}", "account.media": "Média", "account.mention": "Mentionner", - "account.moved_to": "{name} a déménagé vers :", + "account.moved_to": "{name} a déménagé vers :", "account.mute": "Masquer @{name}", "account.mute_notifications": "Ignorer les notifications de @{name}", "account.muted": "Silencé", @@ -30,7 +30,7 @@ "account.unmute_notifications": "Réactiver les notifications de @{name}", "account.view_full_profile": "Afficher le profil complet", "alert.unexpected.message": "Une erreur non-attendue s'est produite.", - "alert.unexpected.title": "Oups !", + "alert.unexpected.title": "Oups !", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.", "bundle_column_error.retry": "Réessayer", @@ -77,7 +77,7 @@ "confirmations.delete.confirm": "Supprimer", "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?", "confirmations.delete_list.confirm": "Supprimer", - "confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste ?", + "confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste ?", "confirmations.domain_block.confirm": "Masquer le domaine entier", "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.", "confirmations.mute.confirm": "Masquer", @@ -85,14 +85,14 @@ "confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?", "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.", - "embed.preview": "Il apparaîtra comme cela :", + "embed.preview": "Il apparaîtra comme cela :", "emoji_button.activity": "Activités", "emoji_button.custom": "Personnalisés", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Nourriture & Boisson", "emoji_button.label": "Insérer un émoji", "emoji_button.nature": "Nature", - "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objets", "emoji_button.people": "Personnages", "emoji_button.recent": "Fréquemment utilisés", @@ -154,7 +154,7 @@ "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", "missing_indicator.sublabel": "Ressource introuvable", - "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?", + "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?", "navigation_bar.blocks": "Comptes bloqués", "navigation_bar.community_timeline": "Fil public local", "navigation_bar.direct": "Messages directs", @@ -177,9 +177,9 @@ "notifications.clear": "Nettoyer les notifications", "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.column_settings.alert": "Notifications locales", - "notifications.column_settings.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :", - "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :", + "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.push": "Notifications push", "notifications.column_settings.push_meta": "Cet appareil", "notifications.column_settings.reblog": "Partages :", @@ -216,7 +216,7 @@ "privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", "regeneration_indicator.label": "Chargement…", - "regeneration_indicator.sublabel": "Le flux de votre page principale est en cours de préparation !", + "regeneration_indicator.sublabel": "Le flux de votre page principale est en cours de préparation !", "relative_time.days": "{number} j", "relative_time.hours": "{number} h", "relative_time.just_now": "à l’instant", @@ -224,8 +224,8 @@ "relative_time.seconds": "{number} s", "reply_indicator.cancel": "Annuler", "report.forward": "Transférer à {target}", - "report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?", - "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :", + "report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?", + "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", "report.target": "Signalement", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index d4836e9fe..c4cb996cf 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -110,7 +110,7 @@ "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public", "follow_request.authorize": "Autorizar", "follow_request.reject": "Regetar", - "getting_started.appsshort": "Apps", + "getting_started.appsshort": "Aplicacions", "getting_started.faq": "FAQ", "getting_started.heading": "Per començar", "getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via {github} sus GitHub.", @@ -158,7 +158,7 @@ "navigation_bar.blocks": "Personas blocadas", "navigation_bar.community_timeline": "Flux public local", "navigation_bar.direct": "Messatges dirèctes", - "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.domain_blocks": "Domenis amagats", "navigation_bar.edit_profile": "Modificar lo perfil", "navigation_bar.favourites": "Favorits", "navigation_bar.follow_requests": "Demandas d’abonament", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index e5e826c96..d69648ccf 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -231,7 +231,7 @@ "report.target": "Nahlásenie {target}", "search.placeholder": "Hľadaj", "search_popout.search_format": "Pokročilé vyhľadávanie", - "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 statusov ktoré si napísal/a, ktoré si obľúbil/a, povýšil/a, alebo aj tých, v ktorých si bol/a spomenutý/á, a potom všetky zadaniu odpovedajúce prezívky, mená a haštagy.", "search_popout.tips.hashtag": "haštag", "search_popout.tips.status": "status", "search_popout.tips.text": "Jednoduchý text vráti zhodujúce sa mená, prezývky a hashtagy", diff --git a/app/javascript/mastodon/locales/whitelist_co.json b/app/javascript/mastodon/locales/whitelist_co.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_co.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index dd675d78f..b09d78b0f 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -134,7 +134,7 @@ export default function timelines(state = initialState, action) { initialTimeline, map => map.update( 'items', - items => items.first() ? items : items.unshift(null) + items => items.first() ? items.unshift(null) : items ) ); default: diff --git a/app/javascript/mastodon/utils/html.js b/app/javascript/mastodon/utils/html.js new file mode 100644 index 000000000..0b646ce58 --- /dev/null +++ b/app/javascript/mastodon/utils/html.js @@ -0,0 +1,6 @@ +export const unescapeHTML = (html) => { + const wrapper = document.createElement('div'); + html = html.replace(/<br \/>|<br>|\n/g, ' '); + wrapper.innerHTML = html; + return wrapper.textContent; +}; diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index 6442eda38..54459de3e 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -1,3 +1,5 @@ +import EXIF from 'exif-js'; + const MAX_IMAGE_DIMENSION = 1280; const getImageUrl = inputFile => new Promise((resolve, reject) => { @@ -28,6 +30,84 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }).catch(reject); }); +const getOrientation = (img, type = 'image/png') => new Promise(resolve => { + if (type !== 'image/jpeg') { + resolve(1); + return; + } + + EXIF.getData(img, () => { + const orientation = EXIF.getTag(img, 'Orientation'); + resolve(orientation); + }); +}); + +const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => { + const canvas = document.createElement('canvas'); + [canvas.width, canvas.height] = orientation < 5 ? [width, height] : [height, width]; + + const context = canvas.getContext('2d'); + + switch (orientation) { + case 2: + context.translate(width, 0); + break; + case 3: + context.translate(width, height); + break; + case 4: + context.translate(0, height); + break; + case 5: + context.rotate(0.5 * Math.PI); + context.translate(1, -1); + break; + case 6: + context.rotate(0.5 * Math.PI); + context.translate(0, -height); + break; + case 7: + context.rotate(0.5, Math.PI); + context.translate(width, -height); + break; + case 8: + context.rotate(-0.5, Math.PI); + context.translate(-width, 0); + break; + } + + context.drawImage(img, 0, 0, width, height); + + canvas.toBlob(resolve, type); +}); + +const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => { + const { width, height } = img; + + let newWidth, newHeight; + + if (width > height) { + newHeight = height * MAX_IMAGE_DIMENSION / width; + newWidth = MAX_IMAGE_DIMENSION; + } else if (height > width) { + newWidth = width * MAX_IMAGE_DIMENSION / height; + newHeight = MAX_IMAGE_DIMENSION; + } else { + newWidth = MAX_IMAGE_DIMENSION; + newHeight = MAX_IMAGE_DIMENSION; + } + + getOrientation(img, type) + .then(orientation => processImage(img, { + width: newWidth, + height: newHeight, + orientation, + type, + })) + .then(resolve) + .catch(reject); +}); + export default inputFile => new Promise((resolve, reject) => { if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { resolve(inputFile); @@ -35,32 +115,13 @@ export default inputFile => new Promise((resolve, reject) => { } loadImage(inputFile).then(img => { - const canvas = document.createElement('canvas'); - const { width, height } = img; - - let newWidth, newHeight; - - if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) { + if (img.width < MAX_IMAGE_DIMENSION && img.height < MAX_IMAGE_DIMENSION) { resolve(inputFile); return; } - if (width > height) { - newHeight = height * MAX_IMAGE_DIMENSION / width; - newWidth = MAX_IMAGE_DIMENSION; - } else if (height > width) { - newWidth = width * MAX_IMAGE_DIMENSION / height; - newHeight = MAX_IMAGE_DIMENSION; - } else { - newWidth = MAX_IMAGE_DIMENSION; - newHeight = MAX_IMAGE_DIMENSION; - } - - canvas.width = newWidth; - canvas.height = newHeight; - - canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight); - - canvas.toBlob(resolve, inputFile.type); + resizeImage(img, inputFile.type) + .then(resolve) + .catch(() => resolve(inputFile)); }).catch(reject); }); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 3377c2329..38aaf895f 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -7,7 +7,6 @@ function main() { const { getLocale } = require('../mastodon/locales'); const { localeData } = getLocale(); const VideoContainer = require('../mastodon/containers/video_container').default; - const CardContainer = require('../mastodon/containers/card_container').default; const React = require('react'); const ReactDOM = require('react-dom'); @@ -57,10 +56,16 @@ function main() { ReactDOM.render(<VideoContainer locale={locale} {...props} />, content); }); - [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(<CardContainer locale={locale} {...props} />, content); - }); + const cards = document.querySelectorAll('[data-component="Card"]'); + + if (cards.length > 0) { + import(/* webpackChunkName: "containers/cards_container" */ '../mastodon/containers/cards_container').then(({ default: CardsContainer }) => { + const content = document.createElement('div'); + + ReactDOM.render(<CardsContainer locale={locale} cards={cards} />, content); + document.body.appendChild(content); + }).catch(error => console.error(error)); + } const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index c2d0de4b9..b063ca52d 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -565,36 +565,41 @@ } .account__header__fields { - border-collapse: collapse; padding: 0; margin: 15px -15px -15px; border: 0 none; border-top: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 4%); + font-size: 14px; + line-height: 20px; - th, - td { - padding: 15px; - padding-left: 15px; - border: 0 none; + dl { + display: flex; border-bottom: 1px solid lighten($ui-base-color, 4%); - vertical-align: middle; } - th { - padding-left: 15px; - font-weight: 500; + dt, + dd { + box-sizing: border-box; + padding: 14px; text-align: center; - width: 94px; + max-height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + dt { + font-weight: 500; + width: 120px; + flex: 0 0 auto; color: $secondary-text-color; background: rgba(darken($ui-base-color, 8%), 0.5); } - td { + dd { + flex: 1 1 auto; color: $darker-text-color; - text-align: center; - width: 100%; - padding-left: 0; } a { @@ -608,12 +613,7 @@ } } - tr { - &:last-child { - th, - td { - border-bottom: 0; - } - } + dl:last-child { + border-bottom: 0; } } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index a6cc8b62b..1948a2a23 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -336,7 +336,8 @@ } } -.simple_form.new_report_note { +.simple_form.new_report_note, +.simple_form.new_account_moderation_note { max-width: 100%; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a982585c3..70ef96aa7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4033,7 +4033,7 @@ a.status-card { .report-modal__statuses { flex: 1 1 auto; min-height: 20vh; - max-height: 40vh; + max-height: 80vh; overflow-y: auto; overflow-x: hidden; @@ -5159,38 +5159,45 @@ noscript { } } +.account__header .roles { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} + .account__header .account__header__fields { font-size: 14px; line-height: 20px; overflow: hidden; - border-collapse: collapse; margin: 20px -10px -20px; border-bottom: 0; - tr { + dl { border-top: 1px solid lighten($ui-base-color, 8%); - text-align: center; + display: flex; } - th, - td { + dt, + dd { + box-sizing: border-box; padding: 14px 20px; - vertical-align: middle; - max-height: 40px; + text-align: center; + max-height: 48px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } - th { + dt { color: $darker-text-color; background: darken($ui-base-color, 4%); - max-width: 120px; + width: 120px; + flex: 0 0 auto; font-weight: 500; } - td { - flex: auto; + dd { + flex: 1 1 auto; color: $primary-text-color; background: $ui-base-color; } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 9d5ab66a4..c40b38a5a 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -60,6 +60,7 @@ } } +.card-standalone__body, .media-gallery-standalone__body { overflow: hidden; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f97890187..de16784a8 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -87,6 +87,10 @@ code { align-items: flex-start; } + &.file .label_input { + flex-wrap: nowrap; + } + &.select .label_input { align-items: initial; } diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 908ea9639..95d1cf9f3 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -86,6 +86,8 @@ class ActivityPub::TagManager end def local_uri?(uri) + return false if uri.nil? + uri = Addressable::URI.parse(uri) host = uri.normalized_host host = "#{host}:#{uri.port}" if uri.port @@ -99,6 +101,8 @@ class ActivityPub::TagManager end def uri_to_resource(uri, klass) + return if uri.nil? + if local_uri?(uri) case klass.name when 'Account' diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 050c651ee..e1ab05cc0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -67,9 +67,17 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end - def format_field(account, str) + def format_display_name(account, **options) + html = encode(account.display_name.presence || account.username) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety + end + + def format_field(account, str, **options) return reformat(str).html_safe unless account.local? # rubocop:disable Rails/OutputSafety - encode_and_link_urls(str, me: true).html_safe # rubocop:disable Rails/OutputSafety + html = encode_and_link_urls(str, me: true) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety end def linkify(text) diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1e7f47029..dbccc8330 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -46,7 +46,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base visibility: visibility_scope, conversation: find_or_create_conversation, thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id) + media_attachment_ids: media_attachments.map(&:id), + sensitive: sensitive? ) save_mentions(status) @@ -105,6 +106,11 @@ class OStatus::Activity::Creation < OStatus::Activity::Base private + def sensitive? + # OStatus-specific convention (not standard) + @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } + end + def find_or_create_conversation uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content return if uri.nil? diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 7c66f2066..698f2ee22 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -368,6 +368,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) end + append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any? append_element(entry, 'mastodon:scope', status.visibility) status.emojis.each do |emoji| diff --git a/app/lib/request.rb b/app/lib/request.rb index 00f94dacf..fc7d398e0 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -51,7 +51,7 @@ class Request end def headers - (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET) + (@account ? @headers.merge('Signature' => signature) : @headers).reverse_merge('Accept-Encoding' => 'gzip').without(REQUEST_TARGET) end private @@ -100,7 +100,7 @@ class Request end def http_client - @http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2) + @http_client ||= HTTP.use(:auto_inflate).timeout(:per_operation, timeout).follow(max_hops: 2) end def use_proxy? diff --git a/app/models/account.rb b/app/models/account.rb index c1ce1e99e..06c190446 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # moved_to_account_id :bigint(8) # featured_collection_url :string # fields :jsonb +# actor_type :string # class Account < ApplicationRecord @@ -76,6 +77,7 @@ class Account < ApplicationRecord validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy @@ -151,6 +153,16 @@ class Account < ApplicationRecord moved_to_account_id.present? end + def bot? + %w(Application Service).include? actor_type + end + + alias bot bot? + + def bot=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end @@ -201,9 +213,11 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] - attributes.each_value do |attr| - next if attr[:name].blank? - fields << attr + if attributes.is_a?(Hash) + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end end self[:fields] = fields @@ -272,8 +286,8 @@ class Account < ApplicationRecord def initialize(account, attr) @account = account - @name = attr['name'] - @value = attr['value'] + @name = attr['name'].strip[0, 255] + @value = attr['value'].strip[0, 255] @errors = {} end @@ -398,7 +412,7 @@ class Account < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text(note, domain) + @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end before_create :generate_keys @@ -441,4 +455,8 @@ class Account < ApplicationRecord self.domain = TagManager.instance.normalize_domain(domain) end + + def emojifiable_text + [note, display_name, fields.map(&:value)].join(' ') + end end diff --git a/app/models/user.rb b/app/models/user.rb index 24beb77b2..82d165079 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord include Settings::Extend include Omniauthable - ACTIVE_DURATION = 14.days + ACTIVE_DURATION = 7.days devise :two_factor_authenticatable, otp_secret_encryption_key: Rails.configuration.x.otp_secret diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index fcf3bdf17..41c9aa44e 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -37,7 +37,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end def type - 'Person' + object.bot? ? 'Service' : 'Person' end def following diff --git a/app/serializers/activitypub/block_serializer.rb b/app/serializers/activitypub/block_serializer.rb index b3bd9f868..624ce2fce 100644 --- a/app/serializers/activitypub/block_serializer.rb +++ b/app/serializers/activitypub/block_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::BlockSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join end def type diff --git a/app/serializers/activitypub/follow_serializer.rb b/app/serializers/activitypub/follow_serializer.rb index 24dfe96f8..bb204ee8f 100644 --- a/app/serializers/activitypub/follow_serializer.rb +++ b/app/serializers/activitypub/follow_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - ActivityPub::TagManager.instance.uri_for(object) + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join end def type diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 863238eb7..6adcd7039 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -3,11 +3,12 @@ class REST::AccountSerializer < ActiveModel::Serializer include RoutingHelper - attributes :id, :username, :acct, :display_name, :locked, :created_at, + attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer class FieldSerializer < ActiveModel::Serializer attributes :name, :value diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 930fbad1f..b6c00a9e7 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -34,6 +34,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService end def trustworthy_attribution?(uri, attributed_to) + return false if uri.nil? || attributed_to.nil? Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero? end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f67ebb443..721c9c928 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -71,6 +71,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} + @account.actor_type = actor_type end def set_fetchable_attributes! @@ -95,6 +96,14 @@ class ActivityPub::ProcessAccountService < BaseService ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id) end + def actor_type + if @json['type'].is_a?(Array) + @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) } + else + @json['type'] + end + end + def image_url(key) value = first_of_value(@json[key]) diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index eb93329e9..79cdca297 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -45,5 +45,8 @@ class ActivityPub::ProcessCollectionService < BaseService def verify_account! @account = ActivityPub::LinkedDataSignature.new(@json).verify_account! + rescue JSON::LD::JsonLdError => e + Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" + nil end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 510b80c82..cb82a79ed 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -37,7 +37,7 @@ class FanOutOnWriteService < BaseService def deliver_to_followers(status) Rails.logger.debug "Delivering status #{status.id} to followers" - status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers| + status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |followers| FeedInsertWorker.push_bulk(followers) do |follower| [status.id, follower.id, :home] end @@ -47,7 +47,7 @@ class FanOutOnWriteService < BaseService def deliver_to_lists(status) Rails.logger.debug "Delivering status #{status.id} to lists" - status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists| + status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| [status.id, list.id, :list] end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 77d4aa538..f9b1b2f0c 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -27,7 +27,7 @@ class FetchLinkCardService < BaseService end attach_card if @card&.persisted? - rescue HTTP::Error, Addressable::URI::InvalidURIError => e + rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching link #{@url}: #{e}" nil end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index fe03c044c..6eb233f9d 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -31,12 +31,12 @@ class PostStatusService < BaseService sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]), spoiler_text: options[:spoiler_text] || '', visibility: options[:visibility] || account.user&.setting_default_privacy, - language: LanguageDetector.instance.detect(text, account), + language: language_from_option(options[:language]) || LanguageDetector.instance.detect(text, account), application: options[:application]) end - process_mentions_service.call(status) process_hashtags_service.call(status) + process_mentions_service.call(status) LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? DistributionWorker.perform_async(status.id) @@ -68,6 +68,10 @@ class PostStatusService < BaseService media end + def language_from_option(str) + ISO_639.find(str)&.alpha2 + end + def process_mentions_service ProcessMentionsService.new end diff --git a/app/views/about/_administration.html.haml b/app/views/about/_administration.html.haml index ec5834f9c..02286d68b 100644 --- a/app/views/about/_administration.html.haml +++ b/app/views/about/_administration.html.haml @@ -6,7 +6,7 @@ .account__avatar{ style: "background-image: url(#{@instance_presenter.contact_account.avatar.url})" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account) + %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account, custom_emojify: true) %span.display-name__account @#{@instance_presenter.contact_account.acct} - else .account__display-name diff --git a/app/views/about/_contact.html.haml b/app/views/about/_contact.html.haml index cf21ad5a3..3215d50b5 100644 --- a/app/views/about/_contact.html.haml +++ b/app/views/about/_contact.html.haml @@ -12,7 +12,7 @@ .avatar= image_tag contact.contact_account.avatar.url .name = link_to TagManager.instance.url_for(contact.contact_account) do - %span.display_name.emojify= display_name(contact.contact_account) + %span.display_name.emojify= display_name(contact.contact_account, custom_emojify: true) %span.username @#{contact.contact_account.acct} - else .owner diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index e264c8574..e6d4cd10e 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -141,3 +141,5 @@ %p = link_to t('about.source_code'), @instance_presenter.source_url = " (#{@instance_presenter.version_number})" + +#modal-container diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 95acbd581..a59ed128e 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -5,7 +5,7 @@ .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do - %span.display_name.emojify= display_name(account) + %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? diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index af79922c2..652213526 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -6,11 +6,16 @@ .card__bio %h1.name - %span.p-name.emojify= display_name(account) + %span.p-name.emojify= display_name(account, custom_emojify: true) %small< %span>< @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - if Setting.show_staff_badge + + - if account.bot? + .roles + .account-role.bot + = t 'accounts.roles.bot' + - elsif Setting.show_staff_badge - if account.user_admin? .roles .account-role.admin @@ -21,19 +26,19 @@ = t 'accounts.roles.moderator' .bio .account__header__content.p-note.emojify!=processed_bio[:text] + - if !account.fields.empty? - %table.account__header__fields - %tbody - - account.fields.each do |field| - %tr - %th.emojify= field.name - %td.emojify= Formatter.instance.format_field(account, field.value) + .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) - elsif processed_bio[:metadata].length > 0 - %table.account__header__fields< + .account__header__fields - processed_bio[:metadata].each do |i| - %tr - %th.emojify>!=i[0] - %td.emojify>!=i[1] + %dl + %dt.emojify{ title: i[0] }= i[0] + %dd.emojify{ title: i[1] }= i[1] .details-counters .counter{ class: active_nav_class(short_account_url(account)) } diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml index 6a14a5dd3..ae18c6dc7 100644 --- a/app/views/accounts/_moved_strip.html.haml +++ b/app/views/accounts/_moved_strip.html.haml @@ -3,7 +3,7 @@ .moved-strip .moved-strip__message = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:strong, display_name(account), 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(: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 = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do @@ -13,5 +13,5 @@ .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } %span.display-name - %strong.emojify= display_name(moved_to_account) + %strong.emojify= display_name(moved_to_account, custom_emojify: true) %span @#{moved_to_account.acct} diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml index 6761a4319..432fb79a6 100644 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml @@ -1,10 +1,7 @@ -%tr - %td +.speech-bubble + .speech-bubble__bubble = simple_format(h(account_moderation_note.content)) - %td - = account_moderation_note.account.acct - %td - %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } - = l account_moderation_note.created_at - %td - = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) + .speech-bubble__owner + = admin_account_link_to account_moderation_note.account + %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at + = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7312618ee..ed8190af5 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -2,7 +2,7 @@ = @account.acct .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.username') @@ -36,14 +36,20 @@ %th= t('admin.accounts.email') %td = @account.user_email - - if @account.user_confirmed? - = fa_icon('check') = table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user) - if @account.user_unconfirmed_email.present? %th= t('admin.accounts.unconfirmed_email') %td = @account.user_unconfirmed_email %tr + %th= t('admin.accounts.email_status') + %td + - if @account.user&.confirmed? + = t('admin.accounts.confirmed') + - else + = t('admin.accounts.confirming') + = table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) + %tr %th= t('admin.accounts.login_status') %td - if @account.user&.disabled? @@ -73,17 +79,17 @@ %tr %th= t('admin.accounts.follows') - %td= @account.following_count + %td= number_to_human @account.following_count %tr %th= t('admin.accounts.followers') - %td= @account.followers_count + %td= number_to_human @account.followers_count %tr %th= t('admin.accounts.statuses') - %td= link_to @account.statuses_count, admin_account_statuses_path(@account.id) + %td= link_to number_to_human(@account.statuses_count), admin_account_statuses_path(@account.id) %tr %th= t('admin.accounts.media_attachments') %td - = link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true }) + = link_to number_to_human(@account.media_attachments.count), admin_account_statuses_path(@account.id, { media: true }) = surround '(', ')' do = number_to_human_size @account.media_attachments.sum('file_file_size') %tr @@ -120,11 +126,12 @@ = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) - if !@account.local? && @account.hub_url.present? - %hr + %hr.spacer/ + %h3 OStatus .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.feed_url') @@ -148,11 +155,12 @@ = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) - if !@account.local? && @account.inbox_url.present? - %hr + %hr.spacer/ + %h3 ActivityPub .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.inbox_url') @@ -167,24 +175,15 @@ %th= t('admin.accounts.followers_url') %td= link_to @account.followers_url, @account.followers_url -%hr -%h3= t('admin.accounts.moderation_notes') +%hr.spacer/ + += render @moderation_notes = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f| = render 'shared/error_messages', object: @account_moderation_note - = f.input :content + = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6 = f.hidden_field :target_account_id .actions - = f.button :button, t('admin.account_moderation_notes.create'), type: :submit - -.table-wrapper - %table.table - %thead - %tr - %th - %th= t('admin.account_moderation_notes.account') - %th= t('admin.account_moderation_notes.created_at') - %tbody - = render @moderation_notes + = f.button :button, t('admin.account_moderation_notes.create'), type: :submit diff --git a/app/views/admin/reports/_account.html.haml b/app/views/admin/reports/_account.html.haml index 22b7a0861..9ac161c9c 100644 --- a/app/views/admin/reports/_account.html.haml +++ b/app/views/admin/reports/_account.html.haml @@ -15,5 +15,5 @@ .account__avatar{ style: "background-image: url(#{account.avatar.url}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(account) + %strong.display-name__html.emojify= display_name(account, custom_emojify: true) %span.display-name__account @#{account.acct} diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 137609539..9057e6048 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -7,7 +7,7 @@ %p>< %strong= Formatter.instance.format_spoiler(status) - = Formatter.instance.format(status) + = Formatter.instance.format(status, custom_emojify: true) - unless status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 9747a92cf..704ce1dbb 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -1,10 +1,7 @@ - content_for :page_title do = t('admin.statuses.title') - -.back-link - = link_to admin_account_path(@account.id) do - %i.fa.fa-chevron-left.fa-fw - = t('admin.statuses.back_to_account') + \- + = "@#{@account.acct}" .filters .filter-subset @@ -12,33 +9,26 @@ %ul %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected' %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected' + .back-link{ style: 'flex: 1 1 auto; text-align: right' } + = link_to admin_account_path(@account.id) do + %i.fa.fa-chevron-left.fa-fw + = t('admin.statuses.back_to_account') + +%hr.spacer/ + += form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| + = hidden_field_tag :page, params[:page] + = hidden_field_tag :media, params[:media] -- if @statuses.empty? - .accounts-grid - = render 'accounts/nothing_here' -- else - = form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| - = hidden_field_tag :page, params[:page] - = hidden_field_tag :media, params[:media] - .batch-form-box - .batch-checkbox-all + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all = check_box_tag :batch_checkbox_all, nil, false - = f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]} - = f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button' - .media-spoiler-toggle-buttons - .media-spoiler-show-button.button= t('admin.statuses.media.show') - .media-spoiler-hide-button.button= t('admin.statuses.media.hide') - - @statuses.each do |status| - .account-status{ data: { id: status.id } } - .batch-checkbox - = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id - .activity-stream.activity-stream-headless - .entry= render 'stream_entries/simple_status', status: status - .account-status__actions - - unless status.media_attachments.empty? - = link_to admin_account_status_path(@account.id, status, current_params.merge(status: { sensitive: !status.sensitive })), method: :patch, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do - = fa_icon status.sensitive? ? 'eye' : 'eye-slash' - = link_to admin_account_status_path(@account.id, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do - = fa_icon 'trash' + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } = paginate @statuses diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index 2b07c923b..1af3193ae 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -2,9 +2,12 @@ = t('auth.login') = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp') + %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp') + + = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true .actions = f.button :button, t('auth.login'), type: :submit -.form-footer= render 'auth/shared/links' + - if Setting.site_contact_email.present? + %p.hint.subtle-hint= t('users.otp_lost_help_html', email: mail_to(Setting.site_contact_email, nil)) diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index e81e292ba..9abcfd37e 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -6,7 +6,7 @@ %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) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index e81e292ba..9abcfd37e 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -6,7 +6,7 @@ %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) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 89d768d3f..30cd26914 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -10,15 +10,15 @@ %td %tr %th= t('exports.follows') - %td= @export.total_follows + %td= number_to_human @export.total_follows %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %tr %th= t('exports.blocks') - %td= @export.total_blocks + %td= number_to_human @export.total_blocks %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %tr %th= t('exports.mutes') - %td= @export.total_mutes + %td= number_to_human @export.total_mutes %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv) %p.muted-hint= t('exports.archive_takeout.hint_html') diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 5f63466d9..d65a7f36f 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -20,6 +20,9 @@ = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') .fields-group + = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + + .fields-group .input.with_block_label %label= t('simple_form.labels.defaults.fields') %span.hint= t('simple_form.hints.defaults.fields') diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index ae26fc1ff..78f5ed4bc 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -2,7 +2,7 @@ = image_tag asset_pack_path('logo.svg'), class: 'logo' %div - = t('landing_strip_html', name: content_tag(:span, display_name(account), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) + = 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)) - if open_registrations? = t('landing_strip_signup_html', sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index afc66d148..c0f1e4f0f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -4,7 +4,7 @@ .avatar = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) - if embedded_view? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index cc2b6abe8..990e45094 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -10,7 +10,7 @@ %div = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) .status__content.p-name.emojify< diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 9764bc74d..b87ca2177 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -28,7 +28,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong.emojify= display_name(status.account) + %strong.emojify= display_name(status.account, custom_emojify: true) = t('stream_entries.reblogged') - elsif pinned .pre-header diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 000aa0c4d..c2202a9eb 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -33,3 +33,5 @@ %p= t 'about.about_mastodon_html' = render 'features' + +#modal-container |