Merge commit 'b9f59ebcc68e9da0a7158741a1a2ef3564e1321e' into merging-upstream

main
Ondřej Hruška 7 years ago
commit 83bda6c1a8
No known key found for this signature in database
GPG Key ID: 2C5FD5035250423D

@ -1,5 +1,6 @@
# Service dependencies # Service dependencies
# You may set REDIS_URL instead for more advanced options # You may set REDIS_URL instead for more advanced options
# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
REDIS_HOST=redis REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
# You may set DATABASE_URL instead for more advanced options # You may set DATABASE_URL instead for more advanced options

1
.gitignore vendored

@ -21,6 +21,7 @@ public/system
public/assets public/assets
public/packs public/packs
public/packs-test public/packs-test
public/500.html
.env .env
.env.production .env.production
node_modules/ node_modules/

@ -7,6 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \ RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.1.0
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
ARG LIBICONV_VERSION=1.15 ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@ -19,6 +21,7 @@ RUN apk -U upgrade \
build-base \ build-base \
icu-dev \ icu-dev \
libidn-dev \ libidn-dev \
libressl \
libtool \ libtool \
postgresql-dev \ postgresql-dev \
protobuf-dev \ protobuf-dev \
@ -32,16 +35,21 @@ RUN apk -U upgrade \
imagemagick \ imagemagick \
libidn \ libidn \
libpq \ libpq \
nodejs-npm \
nodejs \ nodejs \
nodejs-npm \
protobuf \ protobuf \
su-exec \ su-exec \
tini \ tini \
yarn \
&& update-ca-certificates \ && update-ca-certificates \
&& mkdir -p /tmp/src /opt \
&& wget -O yarn.tar.gz "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
&& echo "$YARN_DOWNLOAD_SHA256 *yarn.tar.gz" | sha256sum -c - \
&& tar -xzf yarn.tar.gz -C /tmp/src \
&& rm yarn.tar.gz \
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ && wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& mkdir -p /tmp/src \
&& tar -xzf libiconv.tar.gz -C /tmp/src \ && tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \ && rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \ && cd /tmp/src/libiconv-$LIBICONV_VERSION \
@ -56,7 +64,7 @@ COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \ RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \ && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn --ignore-optional --pure-lockfile && yarn --pure-lockfile
COPY . /mastodon COPY . /mastodon

@ -67,7 +67,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'statsd-instrument', '~> 2.1' gem 'statsd-instrument', '~> 2.1'
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017' gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0' gem 'webpacker', '~> 3.0'
gem 'webpush' gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1' gem 'json-ld-preloaded', '~> 2.2.1'
@ -102,9 +102,10 @@ group :development do
gem 'letter_opener', '~> 1.4' gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'brakeman', '~> 3.6', require: false gem 'brakeman', '~> 4.0', require: false
gem 'bundler-audit', '~> 0.5', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.53', require: false gem 'scss_lint', '~> 0.53', require: false
gem 'strong_migrations'
gem 'capistrano', '~> 3.8' gem 'capistrano', '~> 3.8'
gem 'capistrano-rails', '~> 1.2' gem 'capistrano-rails', '~> 1.2'

@ -74,7 +74,7 @@ GEM
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.3) bootsnap (1.1.3)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (3.7.2) brakeman (4.0.1)
browser (2.5.1) browser (2.5.1)
builder (3.2.3) builder (3.2.3)
bullet (5.6.1) bullet (5.6.1)
@ -335,6 +335,8 @@ GEM
rack-cors (0.4.1) rack-cors (0.4.1)
rack-protection (2.0.0) rack-protection (2.0.0)
rack rack
rack-proxy (0.6.2)
rack
rack-test (0.7.0) rack-test (0.7.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-timeout (0.4.2) rack-timeout (0.4.2)
@ -482,6 +484,8 @@ GEM
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
statsd-instrument (2.1.4) statsd-instrument (2.1.4)
strong_migrations (0.1.9)
activerecord (>= 3.2.0)
temple (0.8.0) temple (0.8.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
@ -508,9 +512,9 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (2.0) webpacker (3.0.1)
activesupport (>= 4.2) activesupport (>= 4.2)
multi_json (~> 1.2) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.2) webpush (0.3.2)
hkdf (~> 0.2) hkdf (~> 0.2)
@ -533,10 +537,10 @@ DEPENDENCIES
better_errors (~> 2.1) better_errors (~> 2.1)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap
brakeman (~> 3.6) brakeman (~> 4.0)
browser browser
bullet (~> 5.5) bullet (~> 5.5)
bundler-audit (~> 0.5) bundler-audit (~> 0.6)
capistrano (~> 3.8) capistrano (~> 3.8)
capistrano-rails (~> 1.2) capistrano-rails (~> 1.2)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
@ -614,11 +618,12 @@ DEPENDENCIES
simplecov (~> 0.14) simplecov (~> 0.14)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.2)
statsd-instrument (~> 2.1) statsd-instrument (~> 2.1)
strong_migrations
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2017) tzinfo-data (~> 1.2017)
uglifier (~> 3.2) uglifier (~> 3.2)
webmock (~> 3.0) webmock (~> 3.0)
webpacker (~> 2.0) webpacker (~> 3.0)
webpush webpush
RUBY VERSION RUBY VERSION

@ -1,4 +1,4 @@
web: PORT=3000 bundle exec puma -C config/puma.rb web: PORT=3000 bundle exec puma -C config/puma.rb
sidekiq: PORT=3000 bundle exec sidekiq sidekiq: PORT=3000 bundle exec sidekiq
stream: PORT=4000 yarn run start stream: PORT=4000 yarn run start
webpack: ./bin/webpack-dev-server --host 0.0.0.0 webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0

@ -3,7 +3,7 @@
module Admin module Admin
class CustomEmojisController < BaseController class CustomEmojisController < BaseController
def index def index
@custom_emojis = CustomEmoji.where(domain: nil) @custom_emojis = CustomEmoji.local
end end
def new def new

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json
def index
render json: CustomEmoji.local, each_serializer: REST::CustomEmojiSerializer
end
end

@ -17,12 +17,29 @@ class FollowerAccountsController < ApplicationController
private private
def page_url(page)
account_followers_url(@account, page: page) unless page.nil?
end
def collection_presenter def collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.followers_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
next: page_url(@follows.next_page),
prev: page_url(@follows.prev_page)
)
if params[:page].present?
page
else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account), id: account_followers_url(@account),
type: :ordered, type: :ordered,
size: @account.followers_count, size: @account.followers_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) } first: page
) )
end end
end
end end

@ -17,12 +17,29 @@ class FollowingAccountsController < ApplicationController
private private
def page_url(page)
account_following_index_url(@account, page: page) unless page.nil?
end
def collection_presenter def collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.following_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
part_of: account_following_index_url(@account),
next: page_url(@follows.next_page),
prev: page_url(@follows.prev_page)
)
if params[:page].present?
page
else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account), id: account_following_index_url(@account),
type: :ordered, type: :ordered,
size: @account.following_count, size: @account.following_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) } first: page
) )
end end
end
end end

@ -1,24 +0,0 @@
# frozen_string_literal: true
module EmojiHelper
def emojify(text)
return text if text.blank?
text.gsub(emoji_pattern) do |match|
emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
if emoji
emoji
else
match
end
end
end
def emoji_pattern
@emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
(#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
(?=[^[:alnum:]:]|$)/x
end
end

@ -1,4 +1,5 @@
import api from '../api'; import api from '../api';
import { emojiIndex } from 'emoji-mart';
import { import {
updateTimeline, updateTimeline,
@ -213,19 +214,33 @@ export function clearComposeSuggestions() {
export function fetchComposeSuggestions(token) { export function fetchComposeSuggestions(token) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (token[0] === ':') {
const results = emojiIndex.search(token.replace(':', ''), { maxResults: 3 });
dispatch(readyComposeSuggestionsEmojis(token, results));
return;
}
api(getState).get('/api/v1/accounts/search', { api(getState).get('/api/v1/accounts/search', {
params: { params: {
q: token, q: token.slice(1),
resolve: false, resolve: false,
limit: 4, limit: 4,
}, },
}).then(response => { }).then(response => {
dispatch(readyComposeSuggestions(token, response.data)); dispatch(readyComposeSuggestionsAccounts(token, response.data));
}); });
}; };
}; };
export function readyComposeSuggestions(token, accounts) { export function readyComposeSuggestionsEmojis(token, emojis) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
emojis,
};
};
export function readyComposeSuggestionsAccounts(token, accounts) {
return { return {
type: COMPOSE_SUGGESTIONS_READY, type: COMPOSE_SUGGESTIONS_READY,
token, token,
@ -233,13 +248,21 @@ export function readyComposeSuggestions(token, accounts) {
}; };
}; };
export function selectComposeSuggestion(position, token, accountId) { export function selectComposeSuggestion(position, token, suggestion) {
return (dispatch, getState) => { return (dispatch, getState) => {
const completion = getState().getIn(['accounts', accountId, 'acct']); let completion, startPosition;
if (typeof suggestion === 'object' && suggestion.id) {
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
startPosition = position;
}
dispatch({ dispatch({
type: COMPOSE_SUGGESTION_SELECT, type: COMPOSE_SUGGESTION_SELECT,
position, position: startPosition,
token, token,
completion, completion,
}); });

@ -5,8 +5,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState => const convertState = rawState =>
fromJS(rawState, (k, v) => fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => Iterable.isIndexed(v) ? v.toList() : v.toMap());
Number.isNaN(x * 1) ? x : x * 1));
export function hydrateStore(rawState) { export function hydrateStore(rawState) {
const state = convertState(rawState); const state = convertState(rawState);

@ -23,7 +23,7 @@ export default class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { unicodeMapping } from '../emojione_light';
const assetHost = process.env.CDN_HOST || '';
export default class AutosuggestEmoji extends React.PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const [ filename ] = unicodeMapping[emoji.native];
url = `${assetHost}/emoji/${filename}.svg`;
}
return (
<div className='autosuggest-emoji'>
<img
className='emojione'
src={url}
alt={emoji.native || emoji.colons}
/>
{emoji.colons}
</div>
);
}
}

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isRtl } from '../rtl'; import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize'; import Textarea from 'react-textarea-autosize';
import classNames from 'classnames';
const textAtCursorMatchesToken = (str, caretPosition) => { const textAtCursorMatchesToken = (str, caretPosition) => {
let word; let word;
@ -18,11 +20,11 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition); word = str.slice(left, right + caretPosition);
} }
if (!word || word.trim().length < 2 || word[0] !== '@') { if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
return [null, null]; return [null, null];
} }
word = word.trim().toLowerCase().slice(1); word = word.trim().toLowerCase();
if (word.length > 0) { if (word.length > 0) {
return [left + 1, word]; return [left + 1, word];
@ -128,7 +130,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
onSuggestionClick = (e) => { onSuggestionClick = (e) => {
const suggestion = Number(e.currentTarget.getAttribute('data-index')); const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus(); this.textarea.focus();
@ -151,9 +153,28 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
} }
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
}
return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
}
render () { render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
const { suggestionsHidden, selectedSuggestion } = this.state; const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' }; const style = { direction: 'ltr' };
if (isRtl(value)) { if (isRtl(value)) {
@ -164,6 +185,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<label> <label>
<span style={{ display: 'none' }}>{placeholder}</span> <span style={{ display: 'none' }}>{placeholder}</span>
<Textarea <Textarea
inputRef={this.setTextarea} inputRef={this.setTextarea}
className='autosuggest-textarea__textarea' className='autosuggest-textarea__textarea'
@ -181,18 +203,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
</label> </label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map((suggestion, i) => ( {suggestions.map(this.renderSuggestion)}
<div
role='button'
tabIndex='0'
key={suggestion}
data-index={suggestion}
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
onMouseDown={this.onSuggestionClick}
>
<AutosuggestAccountContainer id={suggestion} />
</div>
))}
</div> </div>
</div> </div>
); );

@ -1,53 +1,58 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button';
import { Overlay } from 'react-overlays';
import { Motion, spring } from 'react-motion';
import detectPassiveEvents from 'detect-passive-events';
export default class DropdownMenu extends React.PureComponent { const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
class DropdownMenu extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
}; };
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired,
direction: PropTypes.string, style: PropTypes.object,
status: ImmutablePropTypes.map, placement: PropTypes.string,
ariaLabel: PropTypes.string, arrowOffsetLeft: PropTypes.string,
disabled: PropTypes.bool, arrowOffsetTop: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
ariaLabel: 'Menu', style: {},
isModalOpen: false, placement: 'bottom',
isUserTouching: () => false,
}; };
state = { handleDocumentClick = e => {
direction: 'left', if (this.node && !this.node.contains(e.target)) {
expanded: false, this.props.onClose();
}; }
}
setRef = (c) => { componentDidMount () {
this.dropdown = c; document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
} }
handleClick = (e) => { componentWillUnmount () {
const i = Number(e.currentTarget.getAttribute('data-index')); document.removeEventListener('click', this.handleDocumentClick, false);
const { action, to } = this.props.items[i]; document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
if (this.props.isModalOpen) { setRef = c => {
this.props.onModalClose(); this.node = c;
} }
// Don't call e.preventDefault() when the item uses 'href' property. handleClick = e => {
// ex. "Edit profile" on the account action bar const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') { if (typeof action === 'function') {
e.preventDefault(); e.preventDefault();
@ -56,91 +61,150 @@ export default class DropdownMenu extends React.PureComponent {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(to); this.context.router.history.push(to);
} }
}
this.dropdown.hide(); renderItem (option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
} }
handleShow = () => { const { text, href = '#' } = option;
if (this.props.isUserTouching()) {
this.props.onModalOpen({ return (
status: this.props.status, <li className='dropdown-menu__item' key={`${text}-${i}`}>
actions: this.props.items, <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}>
onClick: this.handleClick, {text}
}); </a>
} else { </li>
this.setState({ expanded: true }); );
} }
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
} }
handleHide = () => this.setState({ expanded: false }) }
export default class Dropdown extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
ariaLabel: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
};
static defaultProps = {
ariaLabel: 'Menu',
};
state = {
expanded: false,
};
handleClick = () => {
if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
const { status, items } = this.props;
this.props.onModalOpen({
status,
actions: items,
onClick: this.handleItemClick,
});
return;
}
handleToggle = (e) => {
if (e.key === 'Enter') {
if (this.props.isUserTouching()) {
this.handleShow();
} else {
this.setState({ expanded: !this.state.expanded }); this.setState({ expanded: !this.state.expanded });
} }
} else if (e.key === 'Escape') {
handleClose = () => {
if (this.props.onModalClose) {
this.props.onModalClose();
}
this.setState({ expanded: false }); this.setState({ expanded: false });
} }
handleKeyDown = e => {
switch(e.key) {
case 'Enter':
this.handleClick();
break;
case 'Escape':
this.handleClose();
break;
}
} }
renderItem = (item, i) => { handleItemClick = e => {
if (item === null) { const i = Number(e.currentTarget.getAttribute('data-index'));
return <li key={`sep-${i}`} className='dropdown__sep' />; const { action, to } = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
} }
const { text, href = '#' } = item; setTargetRef = c => {
this.target = c;
}
return ( findTarget = () => {
<li className='dropdown__content-list-item' key={`${text}-${i}`}> return this.target;
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
{text}
</a>
</li>
);
} }
render () { render () {
const { icon, items, size, direction, ariaLabel, disabled } = this.props; const { icon, items, size, ariaLabel, disabled } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const isUserTouching = this.props.isUserTouching();
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
if (disabled) {
return ( return (
<div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}> <div onKeyDown={this.handleKeyDown}>
<i className={iconClassname} aria-hidden /> <IconButton
icon={icon}
title={ariaLabel}
active={expanded}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={expanded} placement='bottom' target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} />
</Overlay>
</div> </div>
); );
} }
const dropdownItems = expanded && (
<ul role='group' className='dropdown__content-list' onClick={this.handleHide}>
{items.map(this.renderItem)}
</ul>
);
// No need to render the actual dropdown if we use the modal. If we
// don't render anything <Dropdow /> breaks, so we just put an empty div.
const dropdownContent = !isUserTouching ? (
<DropdownContent className={directionClass} >
{dropdownItems}
</DropdownContent>
) : <div />;
return (
<Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>
{dropdownContent}
</Dropdown>
);
}
} }

@ -1,10 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import { is } from 'immutable';
export default class IntersectionObserverArticle extends ImmutablePureComponent { // Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
export default class IntersectionObserverArticle extends React.Component {
static propTypes = { static propTypes = {
intersectionObserverWrapper: PropTypes.object.isRequired, intersectionObserverWrapper: PropTypes.object.isRequired,
@ -22,18 +27,15 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
shouldComponentUpdate (nextProps, nextState) { shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) { const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
// that either "isIntersecting" or "isHidden" matter, and then they're if (!!isUnrendered !== !!willBeUnrendered) {
// the only things that matter (and updated ARIA attributes). // If we're going from rendered to unrendered (or vice versa) then update
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true; return true;
} }
// Otherwise, diff based on "updateOnProps" and "updateOnStates" // Otherwise, diff based on props
return super.shouldComponentUpdate(nextProps, nextState); const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
} }
componentDidMount () { componentDidMount () {

@ -4,9 +4,12 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { is } from 'immutable';
import IconButton from './icon_button'; import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile'; import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import sizeMe from 'react-sizeme';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -20,6 +23,7 @@ class Item extends React.PureComponent {
static propTypes = { static propTypes = {
attachment: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
standalone: PropTypes.bool,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
@ -28,6 +32,9 @@ class Item extends React.PureComponent {
static defaultProps = { static defaultProps = {
autoPlayGif: false, autoPlayGif: false,
standalone: false,
index: 0,
size: 1,
}; };
handleMouseEnter = (e) => { handleMouseEnter = (e) => {
@ -60,7 +67,7 @@ class Item extends React.PureComponent {
} }
render () { render () {
const { attachment, index, size } = this.props; const { attachment, index, size, standalone } = this.props;
let width = 50; let width = 50;
let height = 100; let height = 100;
@ -139,7 +146,7 @@ class Item extends React.PureComponent {
const autoPlay = !isIOS() && this.props.autoPlayGif; const autoPlay = !isIOS() && this.props.autoPlayGif;
thumbnail = ( thumbnail = (
<div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video <video
className='media-gallery__item-gifv-thumbnail' className='media-gallery__item-gifv-thumbnail'
role='application' role='application'
@ -158,7 +165,7 @@ class Item extends React.PureComponent {
} }
return ( return (
<div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
{thumbnail} {thumbnail}
</div> </div>
); );
@ -167,11 +174,14 @@ class Item extends React.PureComponent {
} }
@injectIntl @injectIntl
@sizeMe({})
export default class MediaGallery extends React.PureComponent { export default class MediaGallery extends React.PureComponent {
static propTypes = { static propTypes = {
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
standalone: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
size: PropTypes.object,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -180,6 +190,7 @@ export default class MediaGallery extends React.PureComponent {
static defaultProps = { static defaultProps = {
autoPlayGif: false, autoPlayGif: false,
standalone: false,
}; };
state = { state = {
@ -187,7 +198,7 @@ export default class MediaGallery extends React.PureComponent {
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.sensitive !== this.props.sensitive) { if (!is(nextProps.media, this.props.media)) {
this.setState({ visible: !nextProps.sensitive }); this.setState({ visible: !nextProps.sensitive });
} }
} }
@ -201,10 +212,19 @@ export default class MediaGallery extends React.PureComponent {
} }
render () { render () {
const { media, intl, sensitive } = this.props; const { media, intl, sensitive, height, standalone, size } = this.props;
let children; let children;
const standaloneEligible = standalone && size.width && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
const style = {};
if (standaloneEligible) {
style.height = size.width / media.getIn([0, 'meta', 'small', 'aspect']);
} else {
style.height = height;
}
if (!this.state.visible) { if (!this.state.visible) {
let warning; let warning;
@ -215,19 +235,24 @@ export default class MediaGallery extends React.PureComponent {
} }
children = ( children = (
<button className='media-spoiler' onClick={this.handleOpen}> <button className='media-spoiler' onClick={this.handleOpen} style={style}>
<span className='media-spoiler__warning'>{warning}</span> <span className='media-spoiler__warning'>{warning}</span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</button> </button>
); );
} else { } else {
const size = media.take(4).size; const size = media.take(4).size;
if (standaloneEligible) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
} }
}
return ( return (
<div className='media-gallery' style={{ height: `${this.props.height}px` }}> <div className='media-gallery' style={style}>
<div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}> <div className={classNames('spoiler-button', { 'spoiler-button--visible': this.state.visible })}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
</div> </div>

@ -37,7 +37,7 @@ export default class Status extends ImmutablePureComponent {
onBlock: PropTypes.func, onBlock: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onHeightChange: PropTypes.func, onHeightChange: PropTypes.func,
me: PropTypes.number, me: PropTypes.string,
boostModal: PropTypes.bool, boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool, autoPlayGif: PropTypes.bool,
muted: PropTypes.bool, muted: PropTypes.bool,
@ -73,7 +73,7 @@ export default class Status extends ImmutablePureComponent {
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (this.context.router && e.button === 0) { if (this.context.router && e.button === 0) {
const id = Number(e.currentTarget.getAttribute('data-id')); const id = e.currentTarget.getAttribute('data-id');
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/accounts/${id}`); this.context.router.history.push(`/accounts/${id}`);
} }

@ -49,7 +49,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
me: PropTypes.number, me: PropTypes.string,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };

@ -3,48 +3,70 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping)); const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || '';
const emojify = (str, customEmojis = {}) => { const emojify = (str, customEmojis = {}) => {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) let rtn = '';
// and replacing valid unicode strings for (;;) {
// that _aren't_ within tags with an <img> version. let match, i = 0, tag;
// The goal is to be the same as an emojione.regUnicode replacement, but faster. while (i < str.length && (tag = '<&'.indexOf(str[i])) === -1 && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
let i = -1; i += str.codePointAt(i) < 65536 ? 1 : 2;
let insideTag = false;
let insideShortname = false;
let shortnameStartIndex = -1;
let match;
while (++i < str.length) {
const char = str.charAt(i);
if (insideShortname && char === ':') {
const shortname = str.substring(shortnameStartIndex, i + 1);
if (shortname in customEmojis) {
const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`;
str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
} else {
i--;
} }
insideShortname = false; if (i === str.length)
} else if (insideTag && char === '>') { break;
insideTag = false; else if (tag >= 0) {
} else if (char === '<') { const tagend = str.indexOf('>;'[tag], i + 1) + 1;
insideTag = true; if (!tagend)
insideShortname = false; break;
} else if (!insideTag && char === ':') { rtn += str.slice(0, tagend);
insideShortname = true; str = str.slice(tagend);
shortnameStartIndex = i; } else if (str[i] === ':') {
} else if (!insideTag && (match = trie.search(str.substring(i)))) { try {
const unicodeStr = match; // if replacing :shortname: succeed, exit this block with "continue"
if (unicodeStr in unicodeMapping) { const closeColon = str.indexOf(':', i + 1) + 1;
const [filename, shortCode] = unicodeMapping[unicodeStr]; if (!closeColon) throw null; // no pair of ':'
const alt = unicodeStr; const lt = str.indexOf('<', i + 1);
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; if (!(lt === -1 || lt >= closeColon)) throw null; // tag appeared before closing ':'
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); const shortname = str.slice(i, closeColon);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string if (shortname in customEmojis) {
rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`;
str = str.slice(closeColon);
continue;
} }
} catch (e) {}
// replacing :shortname: failed
rtn += str.slice(0, i + 1);
str = str.slice(i + 1);
} else {
const [filename, shortCode] = unicodeMapping[match];
rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="${assetHost}/emoji/${filename}.svg" />`;
str = str.slice(i + match.length);
} }
} }
return str; return rtn + str;
}; };
export default emojify; export default emojify;
export const buildCustomEmojis = customEmojis => {
const emojis = [];
customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode');
const url = emoji.get('url');
const name = shortcode.replace(':', '');
emojis.push({
id: name,
name,
short_names: [name],
text: '',
emoticons: [],
keywords: [name],
imageUrl: url,
custom: true,
});
});
return emojis;
};

File diff suppressed because one or more lines are too long

@ -1,13 +1,38 @@
// @preval // @preval
// Force tree shaking on emojione by exposing just a subset of its functionality // http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
const emojione = require('emojione'); const emojis = require('./emoji_map.json');
const { emojiIndex } = require('emoji-mart');
const mappedUnicode = emojione.mapUnicodeToShort();
const excluded = ['®', '©', '™']; const excluded = ['®', '©', '™'];
const skins = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};
Object.keys(emojiIndex.emojis).forEach(key => {
shortcodeMap[emojiIndex.emojis[key].native] = emojiIndex.emojis[key].id;
});
const stripModifiers = unicode => {
skins.forEach(tone => {
unicode = unicode.replace(tone, '');
});
return unicode;
};
Object.keys(emojis).forEach(key => {
if (excluded.includes(key)) {
delete emojis[key];
return;
}
const normalizedKey = stripModifiers(key);
let shortcode = shortcodeMap[normalizedKey];
if (!shortcode) {
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
}
emojis[key] = [emojis[key], shortcode];
});
module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap) module.exports.unicodeMapping = emojis;
.filter(c => !excluded.includes(c))
.map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
.map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] }))
.reduce((x, y) => Object.assign(x, y), { });

@ -26,7 +26,7 @@ export default class ActionBar extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
onFollow: PropTypes.func, onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,

@ -80,7 +80,7 @@ export default class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired, autoPlayGif: PropTypes.bool.isRequired,

@ -16,9 +16,9 @@ import { ScrollContainer } from 'react-router-scroll';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, Number(props.params.accountId)), medias: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'next']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']), autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
}); });
@ -35,20 +35,20 @@ export default class AccountGallery extends ImmutablePureComponent {
}; };
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
} }
} }
handleScrollToBottom = () => { handleScrollToBottom = () => {
if (this.props.hasMore) { if (this.props.hasMore) {
this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId))); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
} }
} }

@ -10,7 +10,7 @@ export default class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,

@ -26,7 +26,7 @@ const makeMapStateToProps = () => {
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({ const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, Number(accountId)), account: getAccount(state, accountId),
me: state.getIn(['meta', 'me']), me: state.getIn(['meta', 'me']),
unfollowModal: state.getIn(['meta', 'unfollow_modal']), unfollowModal: state.getIn(['meta', 'unfollow_modal']),
}); });

@ -13,9 +13,9 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()), statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
me: state.getIn(['meta', 'me']), me: state.getIn(['meta', 'me']),
}); });
@ -28,24 +28,24 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(refreshAccountTimeline(Number(this.props.params.accountId))); this.props.dispatch(refreshAccountTimeline(this.props.params.accountId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(refreshAccountTimeline(Number(nextProps.params.accountId))); this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId));
} }
} }
handleScrollToBottom = () => { handleScrollToBottom = () => {
if (!this.props.isLoading && this.props.hasMore) { if (!this.props.isLoading && this.props.hasMore) {
this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId))); this.props.dispatch(expandAccountTimeline(this.props.params.accountId));
} }
} }

@ -13,7 +13,7 @@ import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import ComposeAdvancedOptionsContainer from '../../../../glitch/components/compose/advanced_options/container'; import ComposeAdvancedOptionsContainer from '../../../../glitch/components/compose/advanced_options/container';
import SensitiveButtonContainer from '../containers/sensitive_button_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container';
import EmojiPickerDropdown from './emoji_picker_dropdown'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import UploadFormContainer from '../containers/upload_form_container'; import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container'; import WarningContainer from '../containers/warning_container';
import { isMobile } from '../../../is_mobile'; import { isMobile } from '../../../is_mobile';
@ -46,7 +46,7 @@ export default class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool, is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool, is_uploading: PropTypes.bool,
me: PropTypes.number, me: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired,
@ -150,7 +150,7 @@ export default class ComposeForm extends ImmutablePureComponent {
handleEmojiPick = (data) => { handleEmojiPick = (data) => {
const position = this.autosuggestTextarea.textarea.selectionStart; const position = this.autosuggestTextarea.textarea.selectionStart;
const emojiChar = data.unicode.split('-').map(code => String.fromCodePoint(parseInt(code, 16))).join(''); const emojiChar = data.native;
this._restoreCaret = position + emojiChar.length + 1; this._restoreCaret = position + emojiChar.length + 1;
this.props.onPickEmoji(position, data); this.props.onPickEmoji(position, data);
} }

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; import { Picker, Emoji } from 'emoji-mart';
import { Overlay } from 'react-overlays';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
const messages = defineMessages({ const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
people: { id: 'emoji_button.people', defaultMessage: 'People' }, people: { id: 'emoji_button.people', defaultMessage: 'People' },
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
@ -17,48 +24,250 @@ const messages = defineMessages({
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
}); });
const settings = { const assetHost = process.env.CDN_HOST || '';
imageType: 'png', const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`;
sprites: false, const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
imagePathPNG: '/emoji/',
};
let EmojiPicker; // load asynchronously class ModifierPickerMenu extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
handleClick = (e) => {
const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1;
this.props.onSelect(modifier);
}
componentWillReceiveProps (nextProps) {
if (nextProps.active) {
this.attachListeners();
} else {
this.removeListeners();
}
}
componentWillUnmount () {
this.removeListeners();
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
attachListeners () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
removeListeners () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render () {
const { active } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div>
);
}
}
class ModifierPicker extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
modifier: PropTypes.number,
onChange: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
};
handleClick = () => {
if (this.props.active) {
this.props.onClose();
} else {
this.props.onOpen();
}
}
handleSelect = modifier => {
this.props.onChange(modifier);
this.props.onClose();
}
render () {
const { active, modifier } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
}
}
@injectIntl
class EmojiPickerMenu extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
style: {},
placement: 'bottom',
};
state = {
modifierOpen: false,
modifier: 1,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
getI18n = () => {
const { intl } = this.props;
return {
search: intl.formatMessage(messages.emoji_search),
notfound: intl.formatMessage(messages.emoji_not_found),
categories: {
search: intl.formatMessage(messages.search_results),
recent: intl.formatMessage(messages.recent),
people: intl.formatMessage(messages.people),
nature: intl.formatMessage(messages.nature),
foods: intl.formatMessage(messages.food),
activity: intl.formatMessage(messages.activity),
places: intl.formatMessage(messages.travel),
objects: intl.formatMessage(messages.objects),
symbols: intl.formatMessage(messages.symbols),
flags: intl.formatMessage(messages.flags),
custom: intl.formatMessage(messages.custom),
},
};
}
handleClick = emoji => {
if (!emoji.native) {
emoji.native = emoji.colons;
}
this.props.onClose();
this.props.onPick(emoji);
}
handleModifierOpen = () => {
this.setState({ modifierOpen: true });
}
handleModifierClose = () => {
this.setState({ modifierOpen: false });
}
handleModifierChange = modifier => {
if (modifier !== this.state.modifier) {
this.setState({ modifier });
}
}
render () {
const { style, intl } = this.props;
const title = intl.formatMessage(messages.emoji);
const { modifierOpen, modifier } = this.state;
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<Picker
perLine={8}
emojiSize={22}
sheetSize={32}
color=''
emoji=''
set='twitter'
title={title}
i18n={this.getI18n()}
onClick={this.handleClick}
skin={modifier}
backgroundImageFn={backgroundImageFn}
/>
<ModifierPicker
active={modifierOpen}
modifier={modifier}
onOpen={this.handleModifierOpen}
onClose={this.handleModifierClose}
onChange={this.handleModifierChange}
/>
</div>
);
}
}
@injectIntl @injectIntl
export default class EmojiPickerDropdown extends React.PureComponent { export default class EmojiPickerDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
custom_emojis: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired,
}; };
state = { state = {
active: false, active: false,
loading: false,
}; };
setRef = (c) => { setRef = (c) => {
this.dropdown = c; this.dropdown = c;
} }
handleChange = (data) => {
this.dropdown.hide();
this.props.onPickEmoji(data);
}
onShowDropdown = () => { onShowDropdown = () => {
this.setState({ active: true }); this.setState({ active: true });
if (!EmojiPicker) {
this.setState({ loading: true });
EmojiPickerAsync().then(TheEmojiPicker => {
EmojiPicker = TheEmojiPicker.default;
this.setState({ loading: false });
}).catch(() => {
// TODO: show the user an error?
this.setState({ loading: false });
});
}
} }
onHideDropdown = () => { onHideDropdown = () => {
@ -66,7 +275,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
} }
onToggle = (e) => { onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === 'Enter')) { if (!e.key || e.key === 'Enter') {
if (this.state.active) { if (this.state.active) {
this.onHideDropdown(); this.onHideDropdown();
} else { } else {
@ -75,70 +284,43 @@ export default class EmojiPickerDropdown extends React.PureComponent {
} }
} }
onEmojiPickerKeyDown = (e) => { handleKeyDown = e => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
this.onHideDropdown(); this.onHideDropdown();
} }
} }
render () { setTargetRef = c => {
const { intl } = this.props; this.target = c;
}
const categories = { findTarget = () => {
people: { return this.target;
title: intl.formatMessage(messages.people), }
emoji: 'smile',
},
nature: {
title: intl.formatMessage(messages.nature),
emoji: 'hamster',
},
food: {
title: intl.formatMessage(messages.food),
emoji: 'pizza',
},
activity: {
title: intl.formatMessage(messages.activity),
emoji: 'soccer',
},
travel: {
title: intl.formatMessage(messages.travel),
emoji: 'earth_americas',
},
objects: {
title: intl.formatMessage(messages.objects),
emoji: 'bulb',
},
symbols: {
title: intl.formatMessage(messages.symbols),
emoji: 'clock9',
},
flags: {
title: intl.formatMessage(messages.flags),
emoji: 'flag_gb',
},
};
const { active, loading } = this.state; render () {
const { intl, onPickEmoji } = this.props;
const title = intl.formatMessage(messages.emoji); const title = intl.formatMessage(messages.emoji);
const { active } = this.state;
return ( return (
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}> <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
<DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} > <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
<img <img
className={`emojione ${active && loading ? 'pulse-loading' : ''}`} className='emojione'
alt='🙂' alt='🙂'
src='/emoji/1f602.svg' src={`${assetHost}/emoji/1f602.svg`}
/> />
</DropdownTrigger> </div>
<DropdownContent className='dropdown__left'> <Overlay show={active} placement='bottom' target={this.findTarget}>
{ <EmojiPickerMenu
this.state.active && !this.state.loading && custom_emojis={this.props.custom_emojis}
(<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />) onClose={this.onHideDropdown}
} onPick={onPickEmoji}
</DropdownContent> />
</Dropdown> </Overlay>
</div>
); );
} }

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import detectPassiveEvents from 'detect-passive-events';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@ -89,12 +90,12 @@ export default class PrivacyDropdown extends React.PureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('click', this.onGlobalClick); window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false);
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick); window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick); window.removeEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false);
} }
setRef = (c) => { setRef = (c) => {

@ -21,7 +21,7 @@ export default class UploadForm extends React.PureComponent {
}; };
onRemoveFile = (e) => { onRemoveFile = (e) => {
const id = Number(e.currentTarget.parentElement.getAttribute('data-id')); const id = e.currentTarget.parentElement.getAttribute('data-id');
this.props.onRemoveFile(id); this.props.onRemoveFile(id);
} }

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
const mapStateToProps = state => ({
custom_emojis: state.get('custom_emojis'),
});
export default connect(mapStateToProps)(EmojiPickerDropdown);

@ -1,51 +1,23 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Warning from '../components/warning'; import Warning from '../components/warning';
import { createSelector } from 'reselect';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { OrderedSet } from 'immutable';
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig)); const mapStateToProps = state => ({
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
return OrderedSet(mentionedUsernamesWithDomains !== null ? mentionedUsernamesWithDomains.map(item => item.split('@')[2]) : []);
});
const mapStateToProps = state => {
const mentionedUsernames = getMentionedUsernames(state);
const mentionedUsernamesWithDomains = getMentionedDomains(state);
return {
needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
mentionedDomains: mentionedUsernamesWithDomains,
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']), needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']),
}; });
};
const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => { const WarningWrapper = ({ needsLockWarning }) => {
if (needsLockWarning) { if (needsLockWarning) {
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
} else if (needsLeakWarning) {
return (
<Warning
message={<FormattedMessage
id='compose_form.privacy_disclaimer'
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.'
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.size }}
/>}
/>
);
} }
return null; return null;
}; };
WarningWrapper.propTypes = { WarningWrapper.propTypes = {
needsLeakWarning: PropTypes.bool,
needsLockWarning: PropTypes.bool, needsLockWarning: PropTypes.bool,
mentionedDomains: ImmutablePropTypes.orderedSet.isRequired,
}; };
export default connect(mapStateToProps)(WarningWrapper); export default connect(mapStateToProps)(WarningWrapper);

@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]), accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -24,12 +24,12 @@ export default class Favourites extends ImmutablePureComponent {
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchFavourites(Number(this.props.params.statusId))); this.props.dispatch(fetchFavourites(this.props.params.statusId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId))); this.props.dispatch(fetchFavourites(nextProps.params.statusId));
} }
} }

@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']), accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'next']), hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -32,14 +32,14 @@ export default class Followers extends ImmutablePureComponent {
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowers(Number(this.props.params.accountId))); this.props.dispatch(fetchFollowers(this.props.params.accountId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId))); this.props.dispatch(fetchFollowers(nextProps.params.accountId));
} }
} }
@ -47,13 +47,13 @@ export default class Followers extends ImmutablePureComponent {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); this.props.dispatch(expandFollowers(this.props.params.accountId));
} }
} }
handleLoadMore = (e) => { handleLoadMore = (e) => {
e.preventDefault(); e.preventDefault();
this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); this.props.dispatch(expandFollowers(this.props.params.accountId));
} }
render () { render () {

@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']), accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', Number(props.params.accountId), 'next']), hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -32,14 +32,14 @@ export default class Following extends ImmutablePureComponent {
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowing(Number(this.props.params.accountId))); this.props.dispatch(fetchFollowing(this.props.params.accountId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId))); this.props.dispatch(fetchFollowing(nextProps.params.accountId));
} }
} }
@ -47,13 +47,13 @@ export default class Following extends ImmutablePureComponent {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); this.props.dispatch(expandFollowing(this.props.params.accountId));
} }
} }
handleLoadMore = (e) => { handleLoadMore = (e) => {
e.preventDefault(); e.preventDefault();
this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); this.props.dispatch(expandFollowing(this.props.params.accountId));
} }
render () { render () {

@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]), accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -24,12 +24,12 @@ export default class Reblogs extends ImmutablePureComponent {
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchReblogs(Number(this.props.params.statusId))); this.props.dispatch(fetchReblogs(this.props.params.statusId));
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchReblogs(Number(nextProps.params.statusId))); this.props.dispatch(fetchReblogs(nextProps.params.statusId));
} }
} }

@ -36,7 +36,7 @@ export default class ActionBar extends React.PureComponent {
onReport: PropTypes.func, onReport: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
me: PropTypes.number.isRequired, me: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };

@ -38,10 +38,10 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, Number(props.params.statusId)), status: getStatus(state, props.params.statusId),
settings: state.get('local_settings'), settings: state.get('local_settings'),
ancestorsIds: state.getIn(['contexts', 'ancestors', Number(props.params.statusId)]), ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
descendantsIds: state.getIn(['contexts', 'descendants', Number(props.params.statusId)]), descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
me: state.getIn(['meta', 'me']), me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal']), boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']), deleteModal: state.getIn(['meta', 'delete_modal']),
@ -66,7 +66,7 @@ export default class Status extends ImmutablePureComponent {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list, ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list,
me: PropTypes.number, me: PropTypes.string,
boostModal: PropTypes.bool, boostModal: PropTypes.bool,
deleteModal: PropTypes.bool, deleteModal: PropTypes.bool,
autoPlayGif: PropTypes.bool, autoPlayGif: PropTypes.bool,
@ -74,12 +74,12 @@ export default class Status extends ImmutablePureComponent {
}; };
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchStatus(Number(this.props.params.statusId))); this.props.dispatch(fetchStatus(this.props.params.statusId));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchStatus(Number(nextProps.params.statusId))); this.props.dispatch(fetchStatus(nextProps.params.statusId));
} }
} }

@ -1,32 +1,35 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar'; import Avatar from '../../../components/avatar';
import RelativeTimestamp from '../../../components/relative_timestamp'; import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import classNames from 'classnames';
export default class ActionsModal extends ImmutablePureComponent { export default class ActionsModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map,
actions: PropTypes.array, actions: PropTypes.array,
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
renderAction = (action, i) => { renderAction = (action, i) => {
if (action === null) { if (action === null) {
return <li key={`sep-${i}`} className='dropdown__sep' />; return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
} }
const { icon = null, text, meta = null, active = false, href = '#' } = action; const { icon = null, text, meta = null, active = false, href = '#' } = action;
return ( return (
<li key={`${text}-${i}`}> <li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={active && 'active'}> <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />} {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />}
<div> <div>
<div>{text}</div> <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div> <div>{meta}</div>
</div> </div>
</a> </a>

@ -3,17 +3,28 @@ import PropTypes from 'prop-types';
import Column from '../../../components/column'; import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header'; import ColumnHeader from '../../../components/column_header';
import ImmutablePureComponent from 'react-immutable-pure-component';
const ColumnLoading = ({ title = '', icon = ' ' }) => ( export default class ColumnLoading extends ImmutablePureComponent {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
icon: PropTypes.string,
};
static defaultProps = {
title: '',
icon: '',
};
render() {
let { title, icon } = this.props;
return (
<Column> <Column>
<ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
<div className='scrollable' /> <div className='scrollable' />
</Column> </Column>
); );
}
ColumnLoading.propTypes = {
title: PropTypes.node,
icon: PropTypes.string,
};
export default ColumnLoading; }

@ -57,7 +57,7 @@ export default class UI extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object.isRequired, router: PropTypes.object.isRequired,
} };
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@ -193,14 +193,18 @@ export default class UI extends React.PureComponent {
document.removeEventListener('dragend', this.handleDragEnd); document.removeEventListener('dragend', this.handleDragEnd);
} }
setRef = (c) => { setRef = c => {
this.node = c; this.node = c;
} }
setColumnsAreaRef = (c) => { setColumnsAreaRef = c => {
this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance(); this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
} }
setOverlayRef = c => {
this.overlay = c;
}
render () { render () {
const { width, draggingOver } = this.state; const { width, draggingOver } = this.state;
const { children, layout, isWide, navbarUnder } = this.props; const { children, layout, isWide, navbarUnder } = this.props;

@ -1,7 +1,3 @@
export function EmojiPicker () {
return import(/* webpackChunkName: "emojione_picker" */'emojione-picker');
}
export function Compose () { export function Compose () {
return import(/* webpackChunkName: "features/compose" */'../../compose'); return import(/* webpackChunkName: "features/compose" */'../../compose');
} }

@ -1,4 +1,6 @@
const LAYOUT_BREAKPOINT = 1024; import detectPassiveEvents from 'detect-passive-events';
const LAYOUT_BREAKPOINT = 630;
export function isMobile(width, columns) { export function isMobile(width, columns) {
switch (columns) { switch (columns) {
@ -12,11 +14,16 @@ export function isMobile(width, columns) {
}; };
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let userTouching = false; let userTouching = false;
let listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
window.addEventListener('touchstart', () => { function touchListener() {
userTouching = true; userTouching = true;
}, { once: true }); window.removeEventListener('touchstart', touchListener, listenerOptions);
}
window.addEventListener('touchstart', touchListener, listenerOptions);
export function isUserTouching() { export function isUserTouching() {
return userTouching; return userTouching;

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.", "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.",
"compose_form.lock_disclaimer.lock": "مقفل", "compose_form.lock_disclaimer.lock": "مقفل",
"compose_form.placeholder": "فيمَ تفكّر؟", "compose_form.placeholder": "فيمَ تفكّر؟",
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "بوّق", "compose_form.publish": "بوّق",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس", "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "الأنشطة", "emoji_button.activity": "الأنشطة",
"emoji_button.custom": "Custom",
"emoji_button.flags": "الأعلام", "emoji_button.flags": "الأعلام",
"emoji_button.food": "الطعام والشراب", "emoji_button.food": "الطعام والشراب",
"emoji_button.label": "أدرج إيموجي", "emoji_button.label": "أدرج إيموجي",
"emoji_button.nature": "الطبيعة", "emoji_button.nature": "الطبيعة",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "أشياء", "emoji_button.objects": "أشياء",
"emoji_button.people": "الناس", "emoji_button.people": "الناس",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "ابحث...", "emoji_button.search": "ابحث...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "رموز", "emoji_button.symbols": "رموز",
"emoji_button.travel": "أماكن و أسفار", "emoji_button.travel": "أماكن و أسفار",
"empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.", "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Какво си мислиш?", "compose_form.placeholder": "Какво си мислиш?",
"compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?",
"compose_form.publish": "Раздумай", "compose_form.publish": "Раздумай",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Отбележи съдържанието като деликатно", "compose_form.sensitive": "Отбележи съдържанието като деликатно",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.", "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
"compose_form.lock_disclaimer.lock": "bloquejat", "compose_form.lock_disclaimer.lock": "bloquejat",
"compose_form.placeholder": "En què estàs pensant?", "compose_form.placeholder": "En què estàs pensant?",
"compose_form.privacy_disclaimer": "El teu missatge serà lliurat als usuaris esmentats en els dominis {domains}. Confies en {domainsCount, plural, one {that server} other {those servers}}? Els missatges privats només funcionen en instàncies Mastodon. Si {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, res indicarà que el teu missatge no es públic i pot ser impulsat (boosted) o ser visible per destinataris no desitjats.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar multimèdia com a sensible", "compose_form.sensitive": "Marcar multimèdia com a sensible",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activitat", "emoji_button.activity": "Activitat",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Menjar i Beure", "emoji_button.food": "Menjar i Beure",
"emoji_button.label": "Inserir emoji", "emoji_button.label": "Inserir emoji",
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objectes", "emoji_button.objects": "Objectes",
"emoji_button.people": "Gent", "emoji_button.people": "Gent",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Cercar...", "emoji_button.search": "Cercar...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Símbols", "emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i Llocs", "emoji_button.travel": "Viatges i Llocs",
"empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!", "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.", "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
"compose_form.lock_disclaimer.lock": "gesperrt", "compose_form.lock_disclaimer.lock": "gesperrt",
"compose_form.placeholder": "Worüber möchtest du schreiben?", "compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Profile auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
"compose_form.publish": "Tröt", "compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Medien als heikel markieren", "compose_form.sensitive": "Medien als heikel markieren",
@ -67,13 +66,17 @@
"embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, in dem du den folgenden Code einfügst.", "embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, in dem du den folgenden Code einfügst.",
"embed.preview": "So wird es aussehen:", "embed.preview": "So wird es aussehen:",
"emoji_button.activity": "Aktivitäten", "emoji_button.activity": "Aktivitäten",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flaggen", "emoji_button.flags": "Flaggen",
"emoji_button.food": "Essen und Trinken", "emoji_button.food": "Essen und Trinken",
"emoji_button.label": "Emoji einfügen", "emoji_button.label": "Emoji einfügen",
"emoji_button.nature": "Natur", "emoji_button.nature": "Natur",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Dinge", "emoji_button.objects": "Dinge",
"emoji_button.people": "Leute", "emoji_button.people": "Leute",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Suche…", "emoji_button.search": "Suche…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reise und Orte", "emoji_button.travel": "Reise und Orte",
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",

@ -516,6 +516,22 @@
"defaultMessage": "Search...", "defaultMessage": "Search...",
"id": "emoji_button.search" "id": "emoji_button.search"
}, },
{
"defaultMessage": "No emojos!! (╯°□°)╯︵ ┻━┻",
"id": "emoji_button.not_found"
},
{
"defaultMessage": "Custom",
"id": "emoji_button.custom"
},
{
"defaultMessage": "Frequently used",
"id": "emoji_button.recent"
},
{
"defaultMessage": "Search results",
"id": "emoji_button.search_results"
},
{ {
"defaultMessage": "People", "defaultMessage": "People",
"id": "emoji_button.people" "id": "emoji_button.people"
@ -682,10 +698,6 @@
{ {
"defaultMessage": "locked", "defaultMessage": "locked",
"id": "compose_form.lock_disclaimer.lock" "id": "compose_form.lock_disclaimer.lock"
},
{
"defaultMessage": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
"id": "compose_form.privacy_disclaimer"
} }
], ],
"path": "app/javascript/mastodon/features/compose/containers/warning_container.json" "path": "app/javascript/mastodon/features/compose/containers/warning_container.json"
@ -1331,15 +1343,6 @@
], ],
"path": "app/javascript/mastodon/features/ui/components/upload_area.json" "path": "app/javascript/mastodon/features/ui/components/upload_area.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Close",
"id": "lightbox.close"
}
],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json"
},
{ {
"descriptors": [ "descriptors": [
{ {

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "What is on your mind?", "compose_form.placeholder": "What is on your mind?",
"compose_form.privacy_disclaimer": "Your post will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is not a public post, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Mark media as sensitive", "compose_form.sensitive": "Mark media as sensitive",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Pri kio vi pensas?", "compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.",
"compose_form.publish": "Hup", "compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marki ke la enhavo estas tikla", "compose_form.sensitive": "Marki ke la enhavo estas tikla",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.", "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
"compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.lock_disclaimer.lock": "bloqueado",
"compose_form.placeholder": "¿En qué estás pensando?", "compose_form.placeholder": "¿En qué estás pensando?",
"compose_form.privacy_disclaimer": "Tu toot privado será enviado a usuario/s mencionados de {domains}. ¿Confías en {domainsCount, plural, one {ese servidor} other {esos servidores}}? La privacidad del toot funcionará solamente en instancias de Mastodon. Si {domains} {domainsCount, plural, one {no es una instancia de Mastodon} other {no son instancias de Mastodon}}, no habrá indicación de que tu toot es privado, y puede hacerse visible a remitentes inesperados.",
"compose_form.publish": "Tootear", "compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar contenido como sensible", "compose_form.sensitive": "Marcar contenido como sensible",
@ -67,13 +66,17 @@
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
"embed.preview": "Así es como se verá:", "embed.preview": "Así es como se verá:",
"emoji_button.activity": "Actividad", "emoji_button.activity": "Actividad",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Marcas", "emoji_button.flags": "Marcas",
"emoji_button.food": "Comida y bebida", "emoji_button.food": "Comida y bebida",
"emoji_button.label": "Insertar emoji", "emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Naturaleza", "emoji_button.nature": "Naturaleza",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objetos", "emoji_button.objects": "Objetos",
"emoji_button.people": "Gente", "emoji_button.people": "Gente",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Buscar…", "emoji_button.search": "Buscar…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares", "emoji_button.travel": "Viajes y lugares",
"empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!", "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.", "compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.",
"compose_form.lock_disclaimer.lock": "قفل", "compose_form.lock_disclaimer.lock": "قفل",
"compose_form.placeholder": "تازه چه خبر؟", "compose_form.placeholder": "تازه چه خبر؟",
"compose_form.privacy_disclaimer": "نوشتهٔ خصوصی شما به کاربران نام‌برده‌شده در {domains} فرستاده می‌شود. آیا به {domainsCount, plural, one {آن سرور} other {آن سرورها}} اعتماد دارید؟ تنظیمات حریم خصوصی نوشته‌ها تنها در سرورهای ماستدون کار می‌کند. اگر {domains} {domainsCount, plural, one {یک سرور ماستدون نباشد} other {سرورهای ماستدون نباشند}}، اشاره‌ای به خصوصی‌بودن نوشتهٔ شما نخواهد شد و شاید نوشتهٔ شما هم‌رسان شود یا برای کاربرانی که نمی‌خواهید نمایش یابد.",
"compose_form.publish": "بوق", "compose_form.publish": "بوق",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "تصاویر حساس هستند", "compose_form.sensitive": "تصاویر حساس هستند",
@ -67,13 +66,17 @@
"embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.", "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
"embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:", "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
"emoji_button.activity": "فعالیت", "emoji_button.activity": "فعالیت",
"emoji_button.custom": "Custom",
"emoji_button.flags": "پرچم‌ها", "emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی", "emoji_button.food": "غذا و نوشیدنی",
"emoji_button.label": "افزودن شکلک", "emoji_button.label": "افزودن شکلک",
"emoji_button.nature": "طبیعت", "emoji_button.nature": "طبیعت",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "اشیا", "emoji_button.objects": "اشیا",
"emoji_button.people": "مردم", "emoji_button.people": "مردم",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "جستجو...", "emoji_button.search": "جستجو...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "نمادها", "emoji_button.symbols": "نمادها",
"emoji_button.travel": "سفر و مکان", "emoji_button.travel": "سفر و مکان",
"empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!", "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Mitä sinulla on mielessä?", "compose_form.placeholder": "Mitä sinulla on mielessä?",
"compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Merkitse media herkäksi", "compose_form.sensitive": "Merkitse media herkäksi",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Votre compte nest pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.", "compose_form.lock_disclaimer": "Votre compte nest pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
"compose_form.lock_disclaimer.lock": "verrouillé", "compose_form.lock_disclaimer.lock": "verrouillé",
"compose_form.placeholder": "Quavez-vous en tête?", "compose_form.placeholder": "Quavez-vous en tête?",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {nest pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il ny aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible dune autre manière à dautres personnes imprévues.",
"compose_form.publish": "Pouet ", "compose_form.publish": "Pouet ",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marquer le média comme sensible", "compose_form.sensitive": "Marquer le média comme sensible",
@ -67,13 +66,17 @@
"embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.", "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
"embed.preview": "Il apparaîtra comme cela:", "embed.preview": "Il apparaîtra comme cela:",
"emoji_button.activity": "Activités", "emoji_button.activity": "Activités",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Drapeaux", "emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger", "emoji_button.food": "Boire et manger",
"emoji_button.label": "Insérer un emoji", "emoji_button.label": "Insérer un emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objets", "emoji_button.objects": "Objets",
"emoji_button.people": "Personnages", "emoji_button.people": "Personnages",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Recherche…", "emoji_button.search": "Recherche…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symboles", "emoji_button.symbols": "Symboles",
"emoji_button.travel": "Lieux et voyages", "emoji_button.travel": "Lieux et voyages",
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir!", "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.", "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.",
"compose_form.lock_disclaimer.lock": "נעול", "compose_form.lock_disclaimer.lock": "נעול",
"compose_form.placeholder": "מה עובר לך בראש?", "compose_form.placeholder": "מה עובר לך בראש?",
"compose_form.privacy_disclaimer": "הודעתך הפרטית תשלח למשתמשים על {domains}. האם ניתן לסמוך על {domainsCount, plural, one {שרת זה} other {שרתים אלו}}? פרטיות ההודעה קיימת רק על שרתי מסטודון. אם {domains} {domainsCount, plural, one {הוא לא שרת מסטודון} other {הם לא שרתי מסטודון}}, לא יהיה שום סימן שההודעה פרטית, והוא עשוי להיות מקודם או להחשף למשתמשים שלא ברשימת היעד.",
"compose_form.publish": "ללחוש", "compose_form.publish": "ללחוש",
"compose_form.publish_loud": "לחצרץ!", "compose_form.publish_loud": "לחצרץ!",
"compose_form.sensitive": "סימון תוכן כרגיש", "compose_form.sensitive": "סימון תוכן כרגיש",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "פעילות", "emoji_button.activity": "פעילות",
"emoji_button.custom": "Custom",
"emoji_button.flags": "דגלים", "emoji_button.flags": "דגלים",
"emoji_button.food": "אוכל ושתיה", "emoji_button.food": "אוכל ושתיה",
"emoji_button.label": "הוספת אמוג'י", "emoji_button.label": "הוספת אמוג'י",
"emoji_button.nature": "טבע", "emoji_button.nature": "טבע",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "חפצים", "emoji_button.objects": "חפצים",
"emoji_button.people": "אנשים", "emoji_button.people": "אנשים",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "חיפוש...", "emoji_button.search": "חיפוש...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "סמלים", "emoji_button.symbols": "סמלים",
"emoji_button.travel": "טיולים ואתרים", "emoji_button.travel": "טיולים ואתרים",
"empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!", "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.", "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.",
"compose_form.lock_disclaimer.lock": "zaključan", "compose_form.lock_disclaimer.lock": "zaključan",
"compose_form.placeholder": "Što ti je na umu?", "compose_form.placeholder": "Što ti je na umu?",
"compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Označi media sadržaj kao osjetljiv", "compose_form.sensitive": "Označi media sadržaj kao osjetljiv",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivnost", "emoji_button.activity": "Aktivnost",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Zastave", "emoji_button.flags": "Zastave",
"emoji_button.food": "Hrana & Piće", "emoji_button.food": "Hrana & Piće",
"emoji_button.label": "Umetni smajlije", "emoji_button.label": "Umetni smajlije",
"emoji_button.nature": "Priroda", "emoji_button.nature": "Priroda",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objekti", "emoji_button.objects": "Objekti",
"emoji_button.people": "Ljudi", "emoji_button.people": "Ljudi",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Traži...", "emoji_button.search": "Traži...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Putovanja & Mjesta", "emoji_button.travel": "Putovanja & Mjesta",
"empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Mire gondolsz?", "compose_form.placeholder": "Mire gondolsz?",
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "Tülk!", "compose_form.publish": "Tülk!",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Tartalom érzékenynek jelölése", "compose_form.sensitive": "Tartalom érzékenynek jelölése",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.",
"compose_form.lock_disclaimer.lock": "dikunci", "compose_form.lock_disclaimer.lock": "dikunci",
"compose_form.placeholder": "Apa yang ada di pikiran anda?", "compose_form.placeholder": "Apa yang ada di pikiran anda?",
"compose_form.privacy_disclaimer": "Status pribadi anda akan dikirim ke pengguna yang disebut dalam {domains}. Apa anda mempercayai {domainsCount, plural, one {server tersebut} other {server tersebut}}? Privasi postingan hanya bekerja dalam server Mastodon. Jika {domains} {domainsCount, plural, one {bukan server Mastodon} other {bukan server Mastodon}}, akan ada indikasi bahwa postingan anda adalah postingan pribadi, dan dapat di-boost atau dapat dilihat oleh orang lain.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Tandai media sensitif", "compose_form.sensitive": "Tandai media sensitif",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivitas", "emoji_button.activity": "Aktivitas",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Bendera", "emoji_button.flags": "Bendera",
"emoji_button.food": "Makanan & Minuman", "emoji_button.food": "Makanan & Minuman",
"emoji_button.label": "Tambahkan emoji", "emoji_button.label": "Tambahkan emoji",
"emoji_button.nature": "Alam", "emoji_button.nature": "Alam",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Benda-benda", "emoji_button.objects": "Benda-benda",
"emoji_button.people": "Orang", "emoji_button.people": "Orang",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Cari...", "emoji_button.search": "Cari...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Simbol", "emoji_button.symbols": "Simbol",
"emoji_button.travel": "Tempat Wisata", "emoji_button.travel": "Tempat Wisata",
"empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Quo esas en tua spirito?", "compose_form.placeholder": "Quo esas en tua spirito?",
"compose_form.privacy_disclaimer": "Tua privata mesajo livresos a mencionata uzeri en {domains}. Ka tu fidas {domainsCount, plural, one {ta servero} other {ta serveri}}? Privateso di mesaji funcionas nur en instaluri di Mastodon. Se {domains} {domainsCount, plural, one {ne esas instaluro di Mastodon} other {ne esas instaluri di Mastodon}}, esos nula indiko, ke tua mesajo esas privata, ed ol povos repetesar od altre divenar videbla da nedezirinda recevanti.",
"compose_form.publish": "Siflar", "compose_form.publish": "Siflar",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Markizar kontenajo kom trubliva", "compose_form.sensitive": "Markizar kontenajo kom trubliva",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insertar emoji", "emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!", "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "A cosa stai pensando?", "compose_form.placeholder": "A cosa stai pensando?",
"compose_form.privacy_disclaimer": "Il tuo status privato verrà condiviso con gli utenti menzionati su {domains}. Ti fidi di {domainsCount, plural, one {quel server} other {quei server}}? Le impostazioni sulla privacy valgono solo su server Mastodon. Se {domains} {domainsCount, plural, one {non è un server Mastodon} other {non sono server Mastodon}}, non ci saranno indicazioni sulla privacy del tuo status, e potrebbe essere condiviso o reso visibile a destinatari indesiderati.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Segnala file come sensibile", "compose_form.sensitive": "Segnala file come sensibile",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Inserisci emoji", "emoji_button.label": "Inserisci emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。", "compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.lock_disclaimer.lock": "非公開", "compose_form.lock_disclaimer.lock": "非公開",
"compose_form.placeholder": "今なにしてる?", "compose_form.placeholder": "今なにしてる?",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先ユーザーが所属する{domains}に送信されます。{domainsCount, plural, one {このサーバー} other {これらのサーバー}}は信頼できますか 投稿のプライバシー保護はMastodonサーバー内でのみ有効です。{domains}がMastodonインスタンスでない場合、あなたの投稿がプライベートなものとして扱われず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.publish": "トゥート", "compose_form.publish": "トゥート",
"compose_form.publish_loud": "{publish}", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "メディアを閲覧注意としてマークする", "compose_form.sensitive": "メディアを閲覧注意としてマークする",
@ -67,13 +66,17 @@
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。", "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
"embed.preview": "表示例:", "embed.preview": "表示例:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.custom": "Custom",
"emoji_button.flags": "国旗", "emoji_button.flags": "国旗",
"emoji_button.food": "食べ物", "emoji_button.food": "食べ物",
"emoji_button.label": "絵文字を追加", "emoji_button.label": "絵文字を追加",
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物", "emoji_button.objects": "物",
"emoji_button.people": "人々", "emoji_button.people": "人々",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "検索...", "emoji_button.search": "検索...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "記号", "emoji_button.symbols": "記号",
"emoji_button.travel": "旅行と場所", "emoji_button.travel": "旅行と場所",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!", "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.", "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
"compose_form.lock_disclaimer.lock": "비공개", "compose_form.lock_disclaimer.lock": "비공개",
"compose_form.placeholder": "지금 무엇을 하고 있나요?", "compose_form.placeholder": "지금 무엇을 하고 있나요?",
"compose_form.privacy_disclaimer": "이 계정의 비공개 포스트는 멘션된 사용자가 소속된 {domains}으로 전송됩니다. {domainsCount, plural, one {이 서버를} other {이 서버들을}} 신뢰할 수 있습니까? 포스팅의 프라이버시 보호는 Mastodon 서버에서만 유효합니다. {domains}가 Mastodon 인스턴스가 아닐 경우, 이 투고가 사적인 것으로 취급되지 않은 채 부스트 되거나 원하지 않는 사용자에게 보여질 가능성이 있습니다.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "이 미디어를 민감한 미디어로 취급", "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "활동", "emoji_button.activity": "활동",
"emoji_button.custom": "Custom",
"emoji_button.flags": "국기", "emoji_button.flags": "국기",
"emoji_button.food": "음식", "emoji_button.food": "음식",
"emoji_button.label": "emoji를 추가", "emoji_button.label": "emoji를 추가",
"emoji_button.nature": "자연", "emoji_button.nature": "자연",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "물건", "emoji_button.objects": "물건",
"emoji_button.people": "사람들", "emoji_button.people": "사람들",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "검색...", "emoji_button.search": "검색...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "기호", "emoji_button.symbols": "기호",
"emoji_button.travel": "여행과 장소", "emoji_button.travel": "여행과 장소",
"empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!", "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",

@ -33,9 +33,8 @@
"column.home": "Start", "column.home": "Start",
"column.mutes": "Genegeerde gebruikers", "column.mutes": "Genegeerde gebruikers",
"column.notifications": "Meldingen", "column.notifications": "Meldingen",
"column.pins": "Pinned toot",
"column.public": "Globale tijdlijn",
"column.pins": "Vastgezette toots", "column.pins": "Vastgezette toots",
"column.public": "Globale tijdlijn",
"column_back_button.label": "terug", "column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen", "column_header.hide_settings": "Instellingen verbergen",
"column_header.moveLeft_settings": "Kolom naar links verplaatsen", "column_header.moveLeft_settings": "Kolom naar links verplaatsen",
@ -48,7 +47,6 @@
"compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.", "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.",
"compose_form.lock_disclaimer.lock": "besloten", "compose_form.lock_disclaimer.lock": "besloten",
"compose_form.placeholder": "Wat wil je kwijt?", "compose_form.placeholder": "Wat wil je kwijt?",
"compose_form.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {die server} other {die servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Wanneer {domains} {domainsCount, plural, one {geen Mastodon-server is} other {geen Mastodon-servers zijn}}, dan wordt er niet aangegeven dat de toot privé is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Media als gevoelig markeren (nsfw)", "compose_form.sensitive": "Media als gevoelig markeren (nsfw)",
@ -68,13 +66,17 @@
"embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.", "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
"embed.preview": "Zo komt het eruit te zien:", "embed.preview": "Zo komt het eruit te zien:",
"emoji_button.activity": "Activiteiten", "emoji_button.activity": "Activiteiten",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Vlaggen", "emoji_button.flags": "Vlaggen",
"emoji_button.food": "Eten en drinken", "emoji_button.food": "Eten en drinken",
"emoji_button.label": "Emoji toevoegen", "emoji_button.label": "Emoji toevoegen",
"emoji_button.nature": "Natuur", "emoji_button.nature": "Natuur",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Voorwerpen", "emoji_button.objects": "Voorwerpen",
"emoji_button.people": "Mensen", "emoji_button.people": "Mensen",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Zoeken...", "emoji_button.search": "Zoeken...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbolen", "emoji_button.symbols": "Symbolen",
"emoji_button.travel": "Reizen en plekken", "emoji_button.travel": "Reizen en plekken",
"empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!", "empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
@ -87,7 +89,6 @@
"follow_request.authorize": "Goedkeuren", "follow_request.authorize": "Goedkeuren",
"follow_request.reject": "Afkeuren", "follow_request.reject": "Afkeuren",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
"getting_started.donate": "Doneren",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Beginnen", "getting_started.heading": "Beginnen",
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.", "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
@ -112,10 +113,9 @@
"navigation_bar.info": "Uitgebreide informatie", "navigation_bar.info": "Uitgebreide informatie",
"navigation_bar.logout": "Afmelden", "navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers", "navigation_bar.mutes": "Genegeerde gebruikers",
"navigation_bar.pins": "Pinned toots", "navigation_bar.pins": "Vastgezette toots",
"navigation_bar.preferences": "Instellingen", "navigation_bar.preferences": "Instellingen",
"navigation_bar.public_timeline": "Globale tijdlijn", "navigation_bar.public_timeline": "Globale tijdlijn",
"navigation_bar.pins": "Vastgezette toots",
"notification.favourite": "{name} markeerde jouw toot als favoriet", "notification.favourite": "{name} markeerde jouw toot als favoriet",
"notification.follow": "{name} volgt jou nu", "notification.follow": "{name} volgt jou nu",
"notification.mention": "{name} vermeldde jou", "notification.mention": "{name} vermeldde jou",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.", "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
"compose_form.lock_disclaimer.lock": "låst", "compose_form.lock_disclaimer.lock": "låst",
"compose_form.placeholder": "Hva har du på hjertet?", "compose_form.placeholder": "Hva har du på hjertet?",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ikke er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
"compose_form.publish": "Tut", "compose_form.publish": "Tut",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Merk media som følsomt", "compose_form.sensitive": "Merk media som følsomt",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivitet", "emoji_button.activity": "Aktivitet",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flagg", "emoji_button.flags": "Flagg",
"emoji_button.food": "Mat og drikke", "emoji_button.food": "Mat og drikke",
"emoji_button.label": "Sett inn emoji", "emoji_button.label": "Sett inn emoji",
"emoji_button.nature": "Natur", "emoji_button.nature": "Natur",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objekter", "emoji_button.objects": "Objekter",
"emoji_button.people": "Mennesker", "emoji_button.people": "Mennesker",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Søk...", "emoji_button.search": "Søk...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symboler", "emoji_button.symbols": "Symboler",
"emoji_button.travel": "Reise & steder", "emoji_button.travel": "Reise & steder",
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!", "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.", "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
"compose_form.lock_disclaimer.lock": "clavat", "compose_form.lock_disclaimer.lock": "clavat",
"compose_form.placeholder": "A de qué pensatz?", "compose_form.placeholder": "A de qué pensatz?",
"compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz daqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas dindicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
"compose_form.publish": "Tut", "compose_form.publish": "Tut",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar lo mèdia coma sensible", "compose_form.sensitive": "Marcar lo mèdia coma sensible",
@ -67,13 +66,17 @@
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.", "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Semblarà aquò:", "embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats", "emoji_button.activity": "Activitats",
"emoji_button.custom": "Personalizats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
"emoji_button.label": "Inserir un emoji", "emoji_button.label": "Inserir un emoji",
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.not_found": "Cap emoji!(╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objèctes", "emoji_button.objects": "Objèctes",
"emoji_button.people": "Gents", "emoji_button.people": "Gents",
"emoji_button.recent": "Sovent utilizats",
"emoji_button.search": "Cercar…", "emoji_button.search": "Cercar…",
"emoji_button.search_results": "Resultat de recèrca",
"emoji_button.symbols": "Simbòls", "emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs", "emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir!", "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.", "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
"compose_form.lock_disclaimer.lock": "zablokowane", "compose_form.lock_disclaimer.lock": "zablokowane",
"compose_form.placeholder": "Co Ci chodzi po głowie?", "compose_form.placeholder": "Co Ci chodzi po głowie?",
"compose_form.privacy_disclaimer": "Twój wpis zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność wpisów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, wpis może być widoczny dla niewłaściwych osób.",
"compose_form.publish": "Wyślij", "compose_form.publish": "Wyślij",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Oznacz treści jako wrażliwe", "compose_form.sensitive": "Oznacz treści jako wrażliwe",
@ -67,13 +66,17 @@
"embed.instructions": "Osadź ten status na swojej stronie wklejając poniższy kod.", "embed.instructions": "Osadź ten status na swojej stronie wklejając poniższy kod.",
"embed.preview": "Tak będzie to wyglądać:", "embed.preview": "Tak będzie to wyglądać:",
"emoji_button.activity": "Aktywność", "emoji_button.activity": "Aktywność",
"emoji_button.custom": "Niestandardowe",
"emoji_button.flags": "Flagi", "emoji_button.flags": "Flagi",
"emoji_button.food": "Żywność i napoje", "emoji_button.food": "Żywność i napoje",
"emoji_button.label": "Wstaw emoji", "emoji_button.label": "Wstaw emoji",
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.not_found": "Brak emoji!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objekty", "emoji_button.objects": "Objekty",
"emoji_button.people": "Ludzie", "emoji_button.people": "Ludzie",
"emoji_button.recent": "Najczęściej używane",
"emoji_button.search": "Szukaj…", "emoji_button.search": "Szukaj…",
"emoji_button.search_results": "Wyniki wyszukiwania",
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Podróże i miejsca", "emoji_button.travel": "Podróże i miejsca",
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",

@ -35,7 +35,6 @@
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.pins": "Postagens fixadas", "column.pins": "Postagens fixadas",
"column.public": "Global", "column.public": "Global",
"column.pins": "Postagens fixadas",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Esconder configurações", "column_header.hide_settings": "Esconder configurações",
"column_header.moveLeft_settings": "Mover coluna para a esquerda", "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@ -48,7 +47,6 @@
"compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.", "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
"compose_form.lock_disclaimer.lock": "trancado", "compose_form.lock_disclaimer.lock": "trancado",
"compose_form.placeholder": "No que você está pensando?", "compose_form.placeholder": "No que você está pensando?",
"compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários de {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com destinatários indesejados.",
"compose_form.publish": "Publicar", "compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar mídia como conteúdo sensível", "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
@ -68,13 +66,17 @@
"embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:", "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
"embed.preview": "Aqui está uma previsão de como ficará:", "embed.preview": "Aqui está uma previsão de como ficará:",
"emoji_button.activity": "Atividades", "emoji_button.activity": "Atividades",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Bandeiras", "emoji_button.flags": "Bandeiras",
"emoji_button.food": "Comidas & Bebidas", "emoji_button.food": "Comidas & Bebidas",
"emoji_button.label": "Inserir Emoji", "emoji_button.label": "Inserir Emoji",
"emoji_button.nature": "Natureza", "emoji_button.nature": "Natureza",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objetos", "emoji_button.objects": "Objetos",
"emoji_button.people": "Pessoas", "emoji_button.people": "Pessoas",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Buscar...", "emoji_button.search": "Buscar...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares", "emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!", "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
@ -114,9 +116,6 @@
"navigation_bar.pins": "Postagens fixadas", "navigation_bar.pins": "Postagens fixadas",
"navigation_bar.preferences": "Preferências", "navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global", "navigation_bar.public_timeline": "Global",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global",
"navigation_bar.pins": "Postagens fixadas",
"notification.favourite": "{name} adicionou a sua postagem aos favoritos", "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
"notification.follow": "{name} te seguiu", "notification.follow": "{name} te seguiu",
"notification.mention": "{name} te mencionou", "notification.mention": "{name} te mencionou",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Em que estás a pensar?", "compose_form.placeholder": "Em que estás a pensar?",
"compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
"compose_form.publish": "Publicar", "compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar media como conteúdo sensível", "compose_form.sensitive": "Marcar media como conteúdo sensível",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Inserir Emoji", "emoji_button.label": "Inserir Emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "Ainda não existem conteúdo local para mostrar!", "empty_column.community": "Ainda não existem conteúdo local para mostrar!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.",
"compose_form.lock_disclaimer.lock": "закрыт", "compose_form.lock_disclaimer.lock": "закрыт",
"compose_form.placeholder": "О чем Вы думаете?", "compose_form.placeholder": "О чем Вы думаете?",
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
"compose_form.publish": "Трубить", "compose_form.publish": "Трубить",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Отметить как чувствительный контент", "compose_form.sensitive": "Отметить как чувствительный контент",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Занятия", "emoji_button.activity": "Занятия",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Флаги", "emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки", "emoji_button.food": "Еда и напитки",
"emoji_button.label": "Вставить эмодзи", "emoji_button.label": "Вставить эмодзи",
"emoji_button.nature": "Природа", "emoji_button.nature": "Природа",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предметы", "emoji_button.objects": "Предметы",
"emoji_button.people": "Люди", "emoji_button.people": "Люди",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Найти...", "emoji_button.search": "Найти...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Символы", "emoji_button.symbols": "Символы",
"emoji_button.travel": "Путешествия", "emoji_button.travel": "Путешествия",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "What is on your mind?", "compose_form.placeholder": "What is on your mind?",
"compose_form.privacy_disclaimer": "Your post will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is not a public post, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Mark media as sensitive", "compose_form.sensitive": "Mark media as sensitive",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji", "emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objects",
"emoji_button.people": "People", "emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...", "emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Hesabınız {locked} değil. Sadece takipçilerle paylaştığınız gönderileri görebilmek için sizi herhangi bir kullanıcı takip edebilir.", "compose_form.lock_disclaimer": "Hesabınız {locked} değil. Sadece takipçilerle paylaştığınız gönderileri görebilmek için sizi herhangi bir kullanıcı takip edebilir.",
"compose_form.lock_disclaimer.lock": "kilitli", "compose_form.lock_disclaimer.lock": "kilitli",
"compose_form.placeholder": "Ne düşünüyorsun?", "compose_form.placeholder": "Ne düşünüyorsun?",
"compose_form.privacy_disclaimer": "Gönderiniz {domains}teki bahsettiğiniz kullanıcılara iletilecektir.{domainsCount, plural, one {bu sunucuya} other {bu sunuculara}} güveniyor musunuz? Gönderi gizliliği sadece Mastodon sunucularında çalışır. Eğer {domains} {domainsCount, plural, one {bir Mastodon sunucusu değilse} other {Mastodon sunucuları değilse}}, gönderinizin herkese açık bir gönderi olmadığına ilişkin bir gösterge bulunmayacaktır. Bu yüzden gönderiniz boost edilebilir veya istenmeyen alıcılara görünebilir.",
"compose_form.publish": "Toot", "compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Görseli hassas olarak işaretle", "compose_form.sensitive": "Görseli hassas olarak işaretle",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivite", "emoji_button.activity": "Aktivite",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Bayraklar", "emoji_button.flags": "Bayraklar",
"emoji_button.food": "Yiyecek ve İçecek", "emoji_button.food": "Yiyecek ve İçecek",
"emoji_button.label": "Emoji ekle", "emoji_button.label": "Emoji ekle",
"emoji_button.nature": "Doğa", "emoji_button.nature": "Doğa",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Nesneler", "emoji_button.objects": "Nesneler",
"emoji_button.people": "İnsanlar", "emoji_button.people": "İnsanlar",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Emoji ara...", "emoji_button.search": "Emoji ara...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Semboller", "emoji_button.symbols": "Semboller",
"emoji_button.travel": "Seyahat ve Yerler", "emoji_button.travel": "Seyahat ve Yerler",
"empty_column.community": "Yerel zaman tüneliniz boş. Daha fazla eğlence için herkese açık bir gönderi paylaşın.", "empty_column.community": "Yerel zaman tüneliniz boş. Daha fazla eğlence için herkese açık bir gönderi paylaşın.",

@ -47,7 +47,6 @@
"compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.", "compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.",
"compose_form.lock_disclaimer.lock": "приватний", "compose_form.lock_disclaimer.lock": "приватний",
"compose_form.placeholder": "Що у Вас на думці?", "compose_form.placeholder": "Що у Вас на думці?",
"compose_form.privacy_disclaimer": "Ваш приватний допис буде доставлено до згаданих користувачів на доменах {domains}. Ви довіряєте {domainsCount, plural, one {цьому серверу} other {цим серверам}}? Приватність постів працює тільки на інстанціях Mastodon. Якщо {domains} {domainsCount, plural, one {не є інстанцією Mastodon} other {не є інстанціями Mastodon}}, приватність поста не буде активована, та він може бути передмухнутий або іншим чином показаний не позначеним Вами користувачам.",
"compose_form.publish": "Дмухнути", "compose_form.publish": "Дмухнути",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Відмітити як непристойний зміст", "compose_form.sensitive": "Відмітити як непристойний зміст",
@ -67,13 +66,17 @@
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Заняття", "emoji_button.activity": "Заняття",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Прапори", "emoji_button.flags": "Прапори",
"emoji_button.food": "Їжа та напої", "emoji_button.food": "Їжа та напої",
"emoji_button.label": "Вставити емодзі", "emoji_button.label": "Вставити емодзі",
"emoji_button.nature": "Природа", "emoji_button.nature": "Природа",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предмети", "emoji_button.objects": "Предмети",
"emoji_button.people": "Люди", "emoji_button.people": "Люди",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Знайти...", "emoji_button.search": "Знайти...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Символи", "emoji_button.symbols": "Символи",
"emoji_button.travel": "Подорожі", "emoji_button.travel": "Подорожі",
"empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!", "empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!",

@ -33,21 +33,20 @@
"column.home": "主页", "column.home": "主页",
"column.mutes": "被静音的用户", "column.mutes": "被静音的用户",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "Pinned toot", "column.pins": "置顶嘟文",
"column.public": "跨站公共时间轴", "column.public": "跨站公共时间轴",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "隐藏设置", "column_header.hide_settings": "隐藏设置",
"column_header.moveLeft_settings": "将栏左移", "column_header.moveLeft_settings": "将栏左移",
"column_header.moveRight_settings": "将栏右移", "column_header.moveRight_settings": "将栏右移",
"column_header.pin": "置顶", "column_header.pin": "固定",
"column_header.show_settings": "显示设置", "column_header.show_settings": "显示设置",
"column_header.unpin": "撤顶", "column_header.unpin": "取下",
"column_subheading.navigation": "导航", "column_subheading.navigation": "导航",
"column_subheading.settings": "设置", "column_subheading.settings": "设置",
"compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.", "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer.lock": "被保护", "compose_form.lock_disclaimer.lock": "被保护",
"compose_form.placeholder": "在想啥?", "compose_form.placeholder": "在想啥?",
"compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
"compose_form.publish": "嘟嘟", "compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "将媒体文件标示为“敏感内容”", "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
@ -67,13 +66,17 @@
"embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。", "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
"embed.preview": "到时大概长这样:", "embed.preview": "到时大概长这样:",
"emoji_button.activity": "活动", "emoji_button.activity": "活动",
"emoji_button.custom": "Custom",
"emoji_button.flags": "旗帜", "emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料", "emoji_button.food": "食物和饮料",
"emoji_button.label": "加入表情符号", "emoji_button.label": "加入表情符号",
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物体", "emoji_button.objects": "物体",
"emoji_button.people": "人物", "emoji_button.people": "人物",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "搜索…", "emoji_button.search": "搜索…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "符号", "emoji_button.symbols": "符号",
"emoji_button.travel": "旅途和地点", "emoji_button.travel": "旅途和地点",
"empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!", "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
@ -196,9 +199,9 @@
"upload_form.undo": "还原", "upload_form.undo": "还原",
"upload_progress.label": "上传中……", "upload_progress.label": "上传中……",
"video.close": "关闭影片", "video.close": "关闭影片",
"video.exit_fullscreen": "退出全荧幕", "video.exit_fullscreen": "退出全",
"video.expand": "展开影片", "video.expand": "展开影片",
"video.fullscreen": "全荧幕", "video.fullscreen": "全",
"video.hide": "隐藏影片", "video.hide": "隐藏影片",
"video.mute": "静音", "video.mute": "静音",
"video.pause": "暂停", "video.pause": "暂停",

@ -33,21 +33,20 @@
"column.home": "主頁", "column.home": "主頁",
"column.mutes": "靜音名單", "column.mutes": "靜音名單",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "Pinned toot", "column.pins": "置頂文章",
"column.public": "跨站時間軸", "column.public": "跨站時間軸",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "隱藏設定", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移", "column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "將欄右移", "column_header.moveRight_settings": "將欄右移",
"column_header.pin": "置頂", "column_header.pin": "固定",
"column_header.show_settings": "顯示設定", "column_header.show_settings": "顯示設定",
"column_header.unpin": "撤頂", "column_header.unpin": "取下",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。", "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
"compose_form.lock_disclaimer.lock": "公共", "compose_form.lock_disclaimer.lock": "公共",
"compose_form.placeholder": "你在想甚麼?", "compose_form.placeholder": "你在想甚麼?",
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。",
"compose_form.publish": "發文", "compose_form.publish": "發文",
"compose_form.publish_loud": "{publish}", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」", "compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
@ -67,13 +66,17 @@
"embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。", "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
"embed.preview": "看上去會是這樣:", "embed.preview": "看上去會是這樣:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.custom": "Custom",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "飲飲食食", "emoji_button.food": "飲飲食食",
"emoji_button.label": "加入表情符號", "emoji_button.label": "加入表情符號",
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物品", "emoji_button.objects": "物品",
"emoji_button.people": "人物", "emoji_button.people": "人物",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "搜尋…", "emoji_button.search": "搜尋…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "符號", "emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊景物", "emoji_button.travel": "旅遊景物",
"empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!", "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!",

@ -39,15 +39,14 @@
"column_header.hide_settings": "隱藏設定", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移", "column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "將欄右移", "column_header.moveRight_settings": "將欄右移",
"column_header.pin": "置頂", "column_header.pin": "固定",
"column_header.show_settings": "顯示設定", "column_header.show_settings": "顯示設定",
"column_header.unpin": "撤頂", "column_header.unpin": "取下",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。", "compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。",
"compose_form.lock_disclaimer.lock": "上鎖", "compose_form.lock_disclaimer.lock": "上鎖",
"compose_form.placeholder": "在想些什麼?", "compose_form.placeholder": "在想些什麼?",
"compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。",
"compose_form.publish": "貼掉", "compose_form.publish": "貼掉",
"compose_form.publish_loud": "{publish}", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將此媒體標為敏感", "compose_form.sensitive": "將此媒體標為敏感",
@ -67,13 +66,17 @@
"embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。", "embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。",
"embed.preview": "看上去會變成這樣:", "embed.preview": "看上去會變成這樣:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.custom": "Custom",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "食物與飲料", "emoji_button.food": "食物與飲料",
"emoji_button.label": "插入表情符號", "emoji_button.label": "插入表情符號",
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物件", "emoji_button.objects": "物件",
"emoji_button.people": "人", "emoji_button.people": "人",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "搜尋…", "emoji_button.search": "搜尋…",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "符號", "emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊與地點", "emoji_button.travel": "旅遊與地點",
"empty_column.community": "本地時間軸是空的。公開寫點什麼吧!", "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",

@ -110,7 +110,7 @@ export default function accounts(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS: case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS: case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS: case MUTES_EXPAND_SUCCESS:
return normalizeAccounts(state, action.accounts); return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
case SEARCH_FETCH_SUCCESS: case SEARCH_FETCH_SUCCESS:

@ -106,7 +106,7 @@ export default function accountsCounters(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS: case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS: case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS: case MUTES_EXPAND_SUCCESS:
return normalizeAccounts(state, action.accounts); return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
case SEARCH_FETCH_SUCCESS: case SEARCH_FETCH_SUCCESS:

@ -128,7 +128,7 @@ const insertSuggestion = (state, position, token, completion) => {
}; };
const insertEmoji = (state, position, emojiData) => { const insertEmoji = (state, position, emojiData) => {
const emoji = emojiData.unicode.split('-').map(code => String.fromCodePoint(parseInt(code, 16))).join(''); const emoji = emojiData.native;
return state.withMutations(map => { return state.withMutations(map => {
map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`); map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
@ -262,7 +262,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_SUGGESTIONS_CLEAR: case COMPOSE_SUGGESTIONS_CLEAR:
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
case COMPOSE_SUGGESTIONS_READY: case COMPOSE_SUGGESTIONS_READY:
return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token); return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
case COMPOSE_SUGGESTION_SELECT: case COMPOSE_SUGGESTION_SELECT:
return insertSuggestion(state, action.position, action.token, action.completion); return insertSuggestion(state, action.position, action.token, action.completion);
case TIMELINE_DELETE: case TIMELINE_DELETE:

@ -0,0 +1,16 @@
import { List as ImmutableList } from 'immutable';
import { STORE_HYDRATE } from '../actions/store';
import { emojiIndex } from 'emoji-mart';
import { buildCustomEmojis } from '../emoji';
const initialState = ImmutableList();
export default function custom_emojis(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
emojiIndex.search('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) });
return action.state.get('custom_emojis');
default:
return state;
}
};

@ -22,6 +22,7 @@ import search from './search';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import notifications from './notifications'; import notifications from './notifications';
import height_cache from './height_cache'; import height_cache from './height_cache';
import custom_emojis from './custom_emojis';
const reducers = { const reducers = {
timelines, timelines,
@ -47,6 +48,7 @@ const reducers = {
media_attachments, media_attachments,
notifications, notifications,
height_cache, height_cache,
custom_emojis,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

@ -124,6 +124,7 @@
box-sizing: border-box; box-sizing: border-box;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
word-wrap: break-word;
} }
.header-wrapper { .header-wrapper {

@ -13,6 +13,7 @@
@import 'accounts'; @import 'accounts';
@import 'stream_entries'; @import 'stream_entries';
@import 'components'; @import 'components';
@import 'emoji_picker';
@import 'about'; @import 'about';
@import 'tables'; @import 'tables';
@import 'admin'; @import 'admin';

@ -62,6 +62,26 @@ body {
height: 100%; height: 100%;
padding: 0; padding: 0;
} }
&.error {
text-align: center;
color: $ui-primary-color;
padding: 20px;
.dialog img {
display: block;
margin: 0 auto;
max-width: 470px;
width: 100%;
height: auto;
}
.dialog h1 {
font-size: 20px;
line-height: 28px;
font-weight: 400;
}
}
} }
button { button {

@ -6,7 +6,7 @@
font-weight: 500; font-weight: 500;
margin-bottom: 20px; margin-bottom: 20px;
padding: 0 10px; padding: 0 10px;
overflow-wrap: break-word; word-wrap: break-word;
@media screen and (max-width: 740px) { @media screen and (max-width: 740px) {
text-align: center; text-align: center;

@ -222,12 +222,16 @@
} }
} }
.dropdown-menu {
position: absolute;
}
.dropdown--active .icon-button { .dropdown--active .icon-button {
color: $ui-highlight-color; color: $ui-highlight-color;
} }
.dropdown--active::after { .dropdown--active::after {
@media screen and (min-width: 1025px) { @media screen and (min-width: 631px) {
content: ""; content: "";
display: block; display: block;
position: absolute; position: absolute;
@ -395,17 +399,11 @@
.compose-form__autosuggest-wrapper { .compose-form__autosuggest-wrapper {
position: relative; position: relative;
.emoji-picker__dropdown { .emoji-picker-dropdown {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 5px; top: 5px;
&.dropdown--active::after {
border-color: transparent transparent $base-border-color;
bottom: -1px;
right: 8px;
}
::-webkit-scrollbar-track:hover, ::-webkit-scrollbar-track:hover,
::-webkit-scrollbar-track:active { ::-webkit-scrollbar-track:active {
background-color: rgba($base-overlay-background, 0.3); background-color: rgba($base-overlay-background, 0.3);
@ -444,6 +442,7 @@
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;
vertical-align: middle; vertical-align: middle;
object-fit: contain;
margin: -.2ex .15em .2ex; margin: -.2ex .15em .2ex;
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -809,8 +808,8 @@
.status__action-bar-dropdown { .status__action-bar-dropdown {
float: left; float: left;
height: 18px; height: 23.15px;
width: 18px; width: 23.15px;
// Dropdown style override for centering on the icon // Dropdown style override for centering on the icon
.dropdown--active { .dropdown--active {
@ -836,26 +835,6 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
.dropdown {
display: block;
width: 18px;
height: 18px;
}
.dropdown--active {
.dropdown__content.dropdown__left {
left: 20px;
right: initial;
}
&::after {
bottom: initial;
margin-left: 7px;
margin-top: -7px;
right: initial;
}
}
} }
.detailed-status { .detailed-status {
@ -1131,7 +1110,7 @@
} }
.account__action-bar-dropdown { .account__action-bar-dropdown {
flex: 1 1 auto; flex: 0 1 calc(50% - 140px);
padding: 10px; padding: 10px;
.dropdown--active { .dropdown--active {
@ -1158,7 +1137,7 @@
.account__action-bar__tab { .account__action-bar__tab {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
width: 80px; flex: 0 1 80px;
border-left: 1px solid lighten($ui-base-color, 8%); border-left: 1px solid lighten($ui-base-color, 8%);
padding: 10px 5px; padding: 10px 5px;
@ -1445,10 +1424,80 @@
position: absolute; position: absolute;
} }
.dropdown__sep { .dropdown-menu__separator {
border-bottom: 1px solid darken($ui-secondary-color, 8%); border-bottom: 1px solid darken($ui-secondary-color, 8%);
margin: 5px 7px 6px; margin: 5px 7px 6px;
padding-top: 1px; height: 0;
}
.dropdown-menu {
background: $ui-secondary-color;
padding: 4px 0;
border-radius: 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
ul {
list-style: none;
}
}
.dropdown-menu__arrow {
position: absolute;
width: 0;
height: 0;
border: 0 solid transparent;
&.left {
right: -5px;
margin-top: -5px;
border-width: 5px 0 5px 5px;
border-left-color: $ui-secondary-color;
}
&.top {
bottom: -5px;
margin-left: -13px;
border-width: 5px 7px 0;
border-top-color: $ui-secondary-color;
}
&.bottom {
top: -5px;
margin-left: -13px;
border-width: 0 7px 5px;
border-bottom-color: $ui-secondary-color;
}
&.right {
left: -5px;
margin-top: -5px;
border-width: 5px 5px 5px 0;
border-right-color: $ui-secondary-color;
}
}
.dropdown-menu__item {
a {
font-size: 13px;
line-height: 18px;
display: block;
padding: 4px 14px;
box-sizing: border-box;
text-decoration: none;
background: $ui-secondary-color;
color: $ui-base-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:focus,
&:hover,
&:active {
background: $ui-highlight-color;
color: $ui-secondary-color;
outline: 0;
}
}
} }
.dropdown--active .dropdown__content { .dropdown--active .dropdown__content {
@ -1633,7 +1682,7 @@
} }
:root { // Overrides .wide stylings for mobile view :root { // Overrides .wide stylings for mobile view
@include single-column('screen and (max-width: 1024px)', $parent: null) { @include single-column('screen and (max-width: 630px)', $parent: null) {
.column, .column,
.drawer { .drawer {
flex: auto; flex: auto;
@ -1654,7 +1703,7 @@
} }
} }
@include multi-columns('screen and (min-width: 1025px)', $parent: null) { @include multi-columns('screen and (min-width: 631px)', $parent: null) {
.columns-area { .columns-area {
padding: 0; padding: 0;
} }
@ -1766,7 +1815,7 @@
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
@include multi-columns('screen and (min-width: 1025px)') { @include multi-columns('screen and (min-width: 631px)') {
background: lighten($ui-base-color, 14%); background: lighten($ui-base-color, 14%);
transition: all 100ms linear; transition: all 100ms linear;
} }
@ -1786,7 +1835,7 @@
} }
} }
@include multi-columns('screen and (min-width: 1025px)', $parent: null) { @include multi-columns('screen and (min-width: 631px)', $parent: null) {
.tabs-bar { .tabs-bar {
display: none; display: none;
} }
@ -2043,15 +2092,18 @@
} }
.autosuggest-textarea__suggestions { .autosuggest-textarea__suggestions {
box-sizing: border-box;
display: none; display: none;
position: absolute; position: absolute;
top: 100%; top: 100%;
width: 100%; width: 100%;
z-index: 99; z-index: 99;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color; background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
color: $ui-base-color; color: $ui-base-color;
font-size: 14px; font-size: 14px;
padding: 6px;
&.autosuggest-textarea__suggestions--visible { &.autosuggest-textarea__suggestions--visible {
display: block; display: block;
@ -2061,34 +2113,36 @@
.autosuggest-textarea__suggestions__item { .autosuggest-textarea__suggestions__item {
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
border-radius: 4px;
&:hover { &:hover,
background: darken($ui-secondary-color, 10%); &:focus,
} &:active,
&.selected { &.selected {
background: $ui-highlight-color; background: darken($ui-secondary-color, 10%);
color: $base-border-color;
} }
} }
.autosuggest-account { .autosuggest-account,
overflow: hidden; .autosuggest-emoji {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
line-height: 18px;
font-size: 14px;
} }
.autosuggest-account-icon { .autosuggest-account-icon,
float: left; .autosuggest-emoji img {
margin-right: 5px; display: block;
margin-right: 8px;
width: 16px;
height: 16px;
} }
.autosuggest-status { .autosuggest-account .display-name__account {
overflow: hidden; color: lighten($ui-base-color, 36%);
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
}
} }
.character-counter__wrapper { .character-counter__wrapper {
@ -2837,197 +2891,61 @@ button.icon-button.active i.fa-retweet {
animation-direction: alternate; animation-direction: alternate;
} }
.emoji-dialog { .emoji-picker-dropdown__menu {
width: 245px;
height: 270px;
background: $simple-background-color; background: $simple-background-color;
box-sizing: border-box; position: absolute;
border-radius: 4px; box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
overflow: hidden;
position: relative;
box-shadow: 0 0 8px rgba($base-shadow-color, 0.2);
.emojione {
margin: 0;
width: 100%;
height: auto;
}
.emoji-dialog-header {
padding: 0 10px;
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
display: inline-block;
box-sizing: border-box;
padding: 10px 5px;
cursor: pointer;
border-bottom: 2px solid transparent;
.emoji {
width: 18px;
height: 18px;
}
img,
svg {
width: 18px;
height: 18px;
filter: grayscale(100%);
}
&:hover {
img,
svg {
filter: grayscale(0);
}
}
&.active {
border-bottom-color: $ui-highlight-color;
img,
svg {
filter: grayscale(0);
}
}
}
}
.emoji-row {
box-sizing: border-box;
overflow-y: hidden;
padding-left: 10px;
.emoji {
display: inline-block;
padding: 2.5px;
border-radius: 4px; border-radius: 4px;
} margin-top: 5px;
}
.emoji-category-header {
box-sizing: border-box;
overflow-y: hidden;
padding: 10px 8px 10px 16px;
display: table;
> * {
display: table-cell;
vertical-align: middle;
}
}
.emoji-category-title {
font-size: 12px;
text-transform: uppercase;
font-weight: 500;
color: darken($ui-secondary-color, 18%);
cursor: default;
}
.emoji-category-heading-decoration { .emoji-mart-scroll {
text-align: right; transition: opacity 200ms ease;
} }
.modifiers { &.selecting .emoji-mart-scroll {
list-style: none; opacity: 0.5;
padding: 0;
margin: 0;
vertical-align: middle;
white-space: nowrap;
margin-top: 4px;
li {
display: inline-block;
padding: 0 2px;
&:last-of-type {
padding-right: 0;
}
} }
}
.modifier { .emoji-picker-dropdown__modifiers {
display: inline-block; position: absolute;
border-radius: 10px; top: 60px;
width: 15px; right: 11px;
height: 15px;
position: relative;
cursor: pointer; cursor: pointer;
}
&.active::after { .emoji-picker-dropdown__modifiers__menu {
content: "";
display: block;
position: absolute; position: absolute;
width: 7px; z-index: 4;
height: 7px; top: -4px;
border-radius: 10px; left: -8px;
border: 2px solid $base-border-color; background: $simple-background-color;
top: 2px;
left: 2px;
}
}
}
.emoji-search-wrapper {
padding: 10px;
border-bottom: 1px solid lighten($ui-secondary-color, 4%);
}
.emoji-search {
font-size: 14px;
font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%;
background: rgba($ui-secondary-color, 0.3);
color: darken($ui-secondary-color, 18%);
border: 1px solid $ui-secondary-color;
border-radius: 4px; border-radius: 4px;
} box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
overflow: hidden;
.emoji-categories-wrapper {
position: absolute;
top: 42px;
bottom: 0;
left: 0;
right: 0;
}
.emoji-search-wrapper + .emoji-categories-wrapper {
top: 93px;
}
.emoji-row .emoji {
img,
svg {
transition: transform 60ms ease-in-out;
}
&:hover { button {
background: lighten($ui-secondary-color, 3%); display: block;
cursor: pointer;
border: 0;
padding: 4px 8px;
background: transparent;
img, &:hover,
svg { &:focus,
transform: translateZ(0) scale(1.2); &:active {
} background: rgba($ui-secondary-color, 0.4);
} }
} }
.emoji { .emoji-mart-emoji {
width: 22px;
height: 22px; height: 22px;
cursor: pointer;
&:focus {
outline: 0;
} }
}
.emoji-mart-emoji {
span {
background-repeat: no-repeat;
} }
} }
@ -3314,8 +3232,6 @@ button.icon-button.active i.fa-retweet {
} }
.search__input { .search__input {
padding-right: 30px;
color: $ui-secondary-color;
outline: 0; outline: 0;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
@ -3851,6 +3767,10 @@ button.icon-button.active i.fa-retweet {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
} }
.dropdown-menu__separator {
border-bottom-color: $ui-secondary-color;
}
} }
.boost-modal__container { .boost-modal__container {
@ -3929,6 +3849,10 @@ button.icon-button.active i.fa-retweet {
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
.actions-modal__item-label {
font-weight: 500;
}
ul { ul {
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
@ -3941,11 +3865,20 @@ button.icon-button.active i.fa-retweet {
a { a {
color: $ui-base-color; color: $ui-base-color;
display: flex; display: flex;
padding: 10px; padding: 12px 16px;
font-size: 15px;
align-items: center; align-items: center;
text-decoration: none; text-decoration: none;
&.active { &,
button {
transition: none;
}
&.active,
&:hover,
&:active,
&:focus {
&, &,
button { button {
background: $ui-highlight-color; background: $ui-highlight-color;
@ -4102,6 +4035,12 @@ button.icon-button.active i.fa-retweet {
display: block; display: block;
float: left; float: left;
position: relative; position: relative;
&.standalone {
.media-gallery__item-gifv-thumbnail {
transform: none;
}
}
} }
.media-gallery__item-thumbnail { .media-gallery__item-thumbnail {
@ -4109,6 +4048,7 @@ button.icon-button.active i.fa-retweet {
text-decoration: none; text-decoration: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
line-height: 0;
display: flex; display: flex;
img { img {
@ -4417,12 +4357,14 @@ button.icon-button.active i.fa-retweet {
.account-gallery__container { .account-gallery__container {
margin: -2px; margin: -2px;
padding: 4px; padding: 4px;
display: flex;
flex-wrap: wrap;
} }
.account-gallery__item { .account-gallery__item {
float: left; flex: 1 1 auto;
width: 96px; width: calc(100% / 3 - 4px);
height: 96px; height: 95px;
margin: 2px; margin: 2px;
a { a {
@ -4433,6 +4375,14 @@ button.icon-button.active i.fa-retweet {
background-size: cover; background-size: cover;
background-position: center; background-position: center;
position: relative; position: relative;
color: inherit;
text-decoration: none;
&:hover,
&:active,
&:focus {
outline: 0;
}
} }
} }
@ -4502,7 +4452,7 @@ noscript {
100% { opacity: 1; } 100% { opacity: 1; }
} }
@media screen and (max-width: 1024px) and (max-height: 400px) { @media screen and (max-width: 630px) and (max-height: 400px) {
$duration: 400ms; $duration: 400ms;
$delay: 100ms; $delay: 100ms;

@ -0,0 +1,199 @@
.emoji-mart {
&,
* {
box-sizing: border-box;
line-height: 1.15;
}
font-size: 13px;
display: inline-block;
color: $ui-base-color;
.emoji-mart-emoji {
padding: 6px;
}
}
.emoji-mart-bar {
border: 0 solid darken($ui-secondary-color, 8%);
&:first-child {
border-bottom-width: 1px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
background: $ui-secondary-color;
}
&:last-child {
border-top-width: 1px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
display: none;
}
}
.emoji-mart-anchors {
display: flex;
justify-content: space-between;
padding: 0 6px;
color: $ui-primary-color;
line-height: 0;
}
.emoji-mart-anchor {
position: relative;
flex: 1;
text-align: center;
padding: 12px 4px;
overflow: hidden;
transition: color .1s ease-out;
cursor: pointer;
&:hover {
color: darken($ui-primary-color, 4%);
}
}
.emoji-mart-anchor-selected {
color: darken($ui-highlight-color, 3%);
&:hover {
color: darken($ui-highlight-color, 3%);
}
.emoji-mart-anchor-bar {
bottom: 0;
}
}
.emoji-mart-anchor-bar {
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
height: 3px;
background-color: darken($ui-highlight-color, 3%);
}
.emoji-mart-anchors {
i {
display: inline-block;
width: 100%;
max-width: 22px;
}
svg {
fill: currentColor;
max-height: 18px;
}
}
.emoji-mart-scroll {
overflow-y: scroll;
height: 270px;
max-height: 35vh;
padding: 0 6px 6px;
background: $simple-background-color;
will-change: transform;
}
.emoji-mart-search {
padding: 10px;
padding-right: 45px;
background: $simple-background-color;
input {
font-size: 14px;
font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%;
background: rgba($ui-secondary-color, 0.3);
color: $ui-primary-color;
border: 1px solid $ui-secondary-color;
border-radius: 4px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
}
}
.emoji-mart-category .emoji-mart-emoji {
cursor: pointer;
span {
z-index: 1;
position: relative;
text-align: center;
}
&:hover::before {
z-index: 0;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba($ui-secondary-color, 0.7);
border-radius: 100%;
}
}
.emoji-mart-category-label {
z-index: 2;
position: relative;
position: -webkit-sticky;
position: sticky;
top: 0;
span {
display: block;
width: 100%;
font-weight: 500;
padding: 5px 6px;
background: $simple-background-color;
}
}
.emoji-mart-emoji {
position: relative;
display: inline-block;
font-size: 0;
span {
width: 22px;
height: 22px;
}
}
.emoji-mart-no-results {
font-size: 14px;
text-align: center;
padding-top: 70px;
color: $ui-primary-color;
.emoji-mart-category-label {
display: none;
}
.emoji-mart-no-results-label {
margin-top: .2em;
}
.emoji-mart-emoji:hover::before {
content: none;
}
}
.emoji-mart-preview {
display: none;
}

@ -245,7 +245,7 @@ body.rtl {
margin-left: 30px; margin-left: 30px;
} }
@media screen and (min-width: 1025px) { @media screen and (min-width: 631px) {
.column, .column,
.drawer { .drawer {
padding-left: 5px; padding-left: 5px;

@ -25,6 +25,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
def fetch_remote_original_status def fetch_remote_original_status
if object_uri.start_with?('http') if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
ActivityPub::FetchRemoteStatusService.new.call(object_uri) ActivityPub::FetchRemoteStatusService.new.call(object_uri)
elsif @object['url'].present? elsif @object['url'].present?
::FetchRemoteStatusService.new.call(@object['url']) ::FetchRemoteStatusService.new.call(@object['url'])

@ -68,6 +68,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def process_hashtag(tag, status) def process_hashtag(tag, status)
return if tag['name'].blank?
hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag)
@ -75,6 +77,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def process_mention(tag, status) def process_mention(tag, status)
return if tag['href'].blank?
account = account_from_uri(tag['href']) account = account_from_uri(tag['href'])
account = FetchRemoteAccountService.new.call(tag['href']) if account.nil? account = FetchRemoteAccountService.new.call(tag['href']) if account.nil?
return if account.nil? return if account.nil?
@ -82,6 +86,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def process_emoji(tag, _status) def process_emoji(tag, _status)
return if tag['name'].blank? || tag['href'].blank?
shortcode = tag['name'].delete(':') shortcode = tag['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
@ -96,7 +102,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return unless @object['attachment'].is_a?(Array) return unless @object['attachment'].is_a?(Array)
@object['attachment'].each do |attachment| @object['attachment'].each do |attachment|
next if unsupported_media_type?(attachment['mediaType']) next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s href = Addressable::URI.parse(attachment['url']).normalize.to_s
media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href) media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href)
@ -106,6 +112,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
media_attachment.file_remote_url = href media_attachment.file_remote_url = href
media_attachment.save media_attachment.save
end end
rescue Addressable::URI::InvalidURIError => e
Rails.logger.debug e
end end
def resolve_thread(status) def resolve_thread(status)
@ -115,8 +123,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def conversation_from_uri(uri) def conversation_from_uri(uri)
return nil if uri.nil? return nil if uri.nil?
return Conversation.find_by(id: TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if TagManager.instance.local_id?(uri) return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) Conversation.find_by(uri: uri) || Conversation.create(uri: uri)
end end
def visibility_from_audience def visibility_from_audience

@ -98,8 +98,8 @@ class ActivityPub::TagManager
else else
StatusFinder.new(uri).status StatusFinder.new(uri).status
end end
elsif ::TagManager.instance.local_id?(uri) elsif OStatus::TagManager.instance.local_id?(uri)
klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) klass.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
else else
klass.find_by(uri: uri.split('#').first) klass.find_by(uri: uri.split('#').first)
end end

@ -1,40 +0,0 @@
# frozen_string_literal: true
require 'singleton'
class Emoji
include Singleton
def initialize
data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
@map = {}
data.each do |_, emoji|
keys = [emoji['shortname']] + emoji['aliases']
unicode = codepoint_to_unicode(emoji['unicode'])
keys.each do |key|
@map[key] = unicode
end
end
end
def unicode(shortcode)
@map[shortcode]
end
def names
@map.keys
end
private
def codepoint_to_unicode(codepoint)
if codepoint.include?('-')
codepoint.split('-').map(&:hex).pack('U*')
else
[codepoint.hex].pack('U')
end
end
end

@ -22,7 +22,7 @@ class Formatter
unless status.local? unless status.local?
html = reformat(raw_content) html = reformat(raw_content)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
return html return html.html_safe # rubocop:disable Rails/OutputSafety
end end
linkable_accounts = status.mentions.map(&:account) linkable_accounts = status.mentions.map(&:account)
@ -39,7 +39,7 @@ class Formatter
end end
def reformat(html) def reformat(html)
sanitize(html, Sanitize::Config::MASTODON_STRICT).html_safe # rubocop:disable Rails/OutputSafety sanitize(html, Sanitize::Config::MASTODON_STRICT)
end end
def plaintext(status) def plaintext(status)
@ -63,6 +63,12 @@ class Formatter
Sanitize.fragment(html, config) Sanitize.fragment(html, config)
end end
def format_spoiler(status)
html = encode(status.spoiler_text)
html = encode_custom_emojis(html, status.emojis)
html.html_safe # rubocop:disable Rails/OutputSafety
end
private private
def encode(html) def encode(html)

@ -11,30 +11,30 @@ class OStatus::Activity::Base
end end
def verb def verb
raw = @xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content
TagManager::VERBS.key(raw) OStatus::TagManager::VERBS.key(raw)
rescue rescue
:post :post
end end
def type def type
raw = @xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content
TagManager::TYPES.key(raw) OStatus::TagManager::TYPES.key(raw)
rescue rescue
:activity :activity
end end
def id def id
@xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content
end end
def url def url
link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' }
link.nil? ? nil : link['href'] link.nil? ? nil : link['href']
end end
def activitypub_uri def activitypub_uri
link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) }
link.nil? ? nil : link['href'] link.nil? ? nil : link['href']
end end
@ -45,8 +45,8 @@ class OStatus::Activity::Base
private private
def find_status(uri) def find_status(uri)
if TagManager.instance.local_id?(uri) if OStatus::TagManager.instance.local_id?(uri)
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status') local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')
return Status.find_by(id: local_id) return Status.find_by(id: local_id)
elsif ActivityPub::TagManager.instance.local_uri?(uri) elsif ActivityPub::TagManager.instance.local_uri?(uri)
local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri) local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri)

@ -14,14 +14,22 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
return result if result.first.present? return result if result.first.present?
end end
Rails.logger.debug "Creating remote status #{id}" RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
# Return early if status already exists in db # Return early if status already exists in db
status = find_status(id) @status = find_status(id)
return [@status, false] unless @status.nil?
@status = process_status
end
end
return [status, false] unless status.nil? [@status, true]
end
def process_status
Rails.logger.debug "Creating remote status #{id}"
cached_reblog = reblog cached_reblog = reblog
status = nil
ApplicationRecord.transaction do ApplicationRecord.transaction do
status = Status.create!( status = Status.create!(
@ -55,7 +63,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
DistributionWorker.perform_async(status.id) DistributionWorker.perform_async(status.id)
[status, true] status
end end
def perform_via_activitypub def perform_via_activitypub
@ -63,42 +71,42 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end end
def content def content
@xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content
end end
def content_language def content_language
@xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en' @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en'
end end
def content_warning def content_warning
@xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || '' @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || ''
end end
def visibility_scope def visibility_scope
@xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public
end end
def published def published
@xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content
end end
def thread? def thread?
!@xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil? !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil?
end end
def thread def thread
thr = @xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS) thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS)
[thr['ref'], thr['href']] [thr['ref'], thr['href']]
end end
private private
def find_or_create_conversation def find_or_create_conversation
uri = @xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content
return if uri.nil? return if uri.nil?
if TagManager.instance.local_id?(uri) if OStatus::TagManager.instance.local_id?(uri)
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
return Conversation.find_by(id: local_id) return Conversation.find_by(id: local_id)
end end
@ -108,8 +116,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
def save_mentions(parent) def save_mentions(parent)
processed_account_ids = [] processed_account_ids = []
@xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link| @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type'] next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type']
mentioned_account = account_from_href(link['href']) mentioned_account = account_from_href(link['href'])
@ -123,14 +131,14 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end end
def save_hashtags(parent) def save_hashtags(parent)
tags = @xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
ProcessHashtagsService.new.call(parent, tags) ProcessHashtagsService.new.call(parent, tags)
end end
def save_media(parent) def save_media(parent)
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
@xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link| @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
next unless link['href'] next unless link['href']
media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href']) media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
@ -156,7 +164,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
return if do_not_download return if do_not_download
@xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link| @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
next unless link['href'] && link['name'] next unless link['href'] && link['name']
shortcode = link['name'].delete(':') shortcode = link['name'].delete(':')
@ -179,4 +187,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href)
end end
end end
def lock_options
{ redis: Redis.current, key: "create:#{id}" }
end
end end

@ -10,7 +10,7 @@ class OStatus::Activity::Share < OStatus::Activity::Creation
end end
def object def object
@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS) @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS)
end end
private private

@ -15,10 +15,10 @@ class OStatus::AtomSerializer
def author(account) def author(account)
author = Ox::Element.new('author') author = Ox::Element.new('author')
uri = TagManager.instance.uri_for(account) uri = OStatus::TagManager.instance.uri_for(account)
append_element(author, 'id', uri) append_element(author, 'id', uri)
append_element(author, 'activity:object-type', TagManager::TYPES[:person]) append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person])
append_element(author, 'uri', uri) append_element(author, 'uri', uri)
append_element(author, 'name', account.username) append_element(author, 'name', account.username)
append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
@ -65,15 +65,15 @@ class OStatus::AtomSerializer
add_namespaces(entry) if root add_namespaces(entry) if root
append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status)) append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status))
append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'published', stream_entry.created_at.iso8601)
append_element(entry, 'updated', stream_entry.updated_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601)
append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
entry << author(stream_entry.account) if root entry << author(stream_entry.account) if root
append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type])
append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb])
entry << object(stream_entry.target) if stream_entry.targeted? entry << object(stream_entry.target) if stream_entry.targeted?
@ -88,7 +88,7 @@ class OStatus::AtomSerializer
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status)) append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
entry entry
@ -97,20 +97,20 @@ class OStatus::AtomSerializer
def object(status) def object(status)
object = Ox::Element.new('activity:object') object = Ox::Element.new('activity:object')
append_element(object, 'id', TagManager.instance.uri_for(status)) append_element(object, 'id', OStatus::TagManager.instance.uri_for(status))
append_element(object, 'published', status.created_at.iso8601) append_element(object, 'published', status.created_at.iso8601)
append_element(object, 'updated', status.updated_at.iso8601) append_element(object, 'updated', status.updated_at.iso8601)
append_element(object, 'title', status.title) append_element(object, 'title', status.title)
object << author(status.account) object << author(status.account)
append_element(object, 'activity:object-type', TagManager::TYPES[status.object_type]) append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type])
append_element(object, 'activity:verb', TagManager::VERBS[status.verb]) append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb])
serialize_status_attributes(object, status) serialize_status_attributes(object, status)
append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status)) append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status))
append_element(object, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) unless status.thread.nil? append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) unless status.thread.nil?
append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil? append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil?
object object
@ -122,14 +122,14 @@ class OStatus::AtomSerializer
description = "#{follow.account.acct} started following #{follow.target_account.acct}" description = "#{follow.account.acct} started following #{follow.target_account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
append_element(entry, 'content', description, type: :html) append_element(entry, 'content', description, type: :html)
entry << author(follow.account) entry << author(follow.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:follow]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow])
object = author(follow.target_account) object = author(follow.target_account)
object.value = 'activity:object' object.value = 'activity:object'
@ -142,13 +142,13 @@ class OStatus::AtomSerializer
entry = Ox::Element.new('entry') entry = Ox::Element.new('entry')
add_namespaces(entry) add_namespaces(entry)
append_element(entry, 'id', TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest'))
append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}") append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}")
entry << author(follow_request.account) entry << author(follow_request.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:request_friend]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
object = author(follow_request.target_account) object = author(follow_request.target_account)
object.value = 'activity:object' object.value = 'activity:object'
@ -161,19 +161,19 @@ class OStatus::AtomSerializer
entry = Ox::Element.new('entry') entry = Ox::Element.new('entry')
add_namespaces(entry) add_namespaces(entry)
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}") append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}")
entry << author(follow_request.target_account) entry << author(follow_request.target_account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:authorize]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize])
object = Ox::Element.new('activity:object') object = Ox::Element.new('activity:object')
object << author(follow_request.account) object << author(follow_request.account)
append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
inner_object = author(follow_request.target_account) inner_object = author(follow_request.target_account)
inner_object.value = 'activity:object' inner_object.value = 'activity:object'
@ -187,19 +187,19 @@ class OStatus::AtomSerializer
entry = Ox::Element.new('entry') entry = Ox::Element.new('entry')
add_namespaces(entry) add_namespaces(entry)
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}") append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}")
entry << author(follow_request.target_account) entry << author(follow_request.target_account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:reject]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject])
object = Ox::Element.new('activity:object') object = Ox::Element.new('activity:object')
object << author(follow_request.account) object << author(follow_request.account)
append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
inner_object = author(follow_request.target_account) inner_object = author(follow_request.target_account)
inner_object.value = 'activity:object' inner_object.value = 'activity:object'
@ -215,14 +215,14 @@ class OStatus::AtomSerializer
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
append_element(entry, 'content', description, type: :html) append_element(entry, 'content', description, type: :html)
entry << author(follow.account) entry << author(follow.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:unfollow]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow])
object = author(follow.target_account) object = author(follow.target_account)
object.value = 'activity:object' object.value = 'activity:object'
@ -237,13 +237,13 @@ class OStatus::AtomSerializer
description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
entry << author(block.account) entry << author(block.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:block]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block])
object = author(block.target_account) object = author(block.target_account)
object.value = 'activity:object' object.value = 'activity:object'
@ -258,13 +258,13 @@ class OStatus::AtomSerializer
description = "#{block.account.acct} no longer blocks #{block.target_account.acct}" description = "#{block.account.acct} no longer blocks #{block.target_account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
entry << author(block.account) entry << author(block.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:unblock]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock])
object = author(block.target_account) object = author(block.target_account)
object.value = 'activity:object' object.value = 'activity:object'
@ -279,18 +279,18 @@ class OStatus::AtomSerializer
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
append_element(entry, 'content', description, type: :html) append_element(entry, 'content', description, type: :html)
entry << author(favourite.account) entry << author(favourite.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:favorite]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite])
entry << object(favourite.status) entry << object(favourite.status)
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
entry entry
end end
@ -301,18 +301,18 @@ class OStatus::AtomSerializer
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite'))
append_element(entry, 'title', description) append_element(entry, 'title', description)
append_element(entry, 'content', description, type: :html) append_element(entry, 'content', description, type: :html)
entry << author(favourite.account) entry << author(favourite.account)
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
append_element(entry, 'activity:verb', TagManager::VERBS[:unfavorite]) append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite])
entry << object(favourite.status) entry << object(favourite.status)
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
entry entry
end end
@ -332,17 +332,17 @@ class OStatus::AtomSerializer
def conversation_uri(conversation) def conversation_uri(conversation)
return conversation.uri if conversation.uri? return conversation.uri if conversation.uri?
TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation') OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation')
end end
def add_namespaces(parent) def add_namespaces(parent)
parent['xmlns'] = TagManager::XMLNS parent['xmlns'] = OStatus::TagManager::XMLNS
parent['xmlns:thr'] = TagManager::THR_XMLNS parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS
parent['xmlns:activity'] = TagManager::AS_XMLNS parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS
parent['xmlns:poco'] = TagManager::POCO_XMLNS parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS
parent['xmlns:media'] = TagManager::MEDIA_XMLNS parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS
parent['xmlns:ostatus'] = TagManager::OS_XMLNS parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS
parent['xmlns:mastodon'] = TagManager::MTDN_XMLNS parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS
end end
def serialize_status_attributes(entry, status) def serialize_status_attributes(entry, status)
@ -352,10 +352,10 @@ class OStatus::AtomSerializer
append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language) append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language)
status.mentions.each do |mentioned| status.mentions.each do |mentioned|
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account)) append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account))
end end
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility? append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility?
status.tags.each do |tag| status.tags.each do |tag|
append_element(entry, 'category', nil, term: tag.name) append_element(entry, 'category', nil, term: tag.name)

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

Loading…
Cancel
Save