Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master

shrike
Jenkins 2018-03-09 00:17:17 +00:00
commit 447d7e6127
206 changed files with 2401 additions and 737 deletions

View File

@ -13,11 +13,29 @@ DB_PORT=5432
DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Optional ElasticSearch configuration
# ES_ENABLED=true
# ES_HOST=localhost
# ES_PORT=9200
# Optimizations
LD_PRELOAD=/data/lib/libjemalloc.so
# ImageMagick optimizations
MAGICK_TEMPORARY_PATH=/app/tmp
MAGICK_MEMORY_LIMIT=128MiB
MAGICK_MAP_LIMIT=64MiB
MAGICK_TIME_LIMIT=15
MAGICK_AREA_LIMIT=16MP
MAGICK_WIDTH_LIMIT=8KP
MAGICK_HEIGHT_LIMIT=8KP
# Federation
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
LOCAL_DOMAIN=${APP_NAME}.nanoapp.io
LOCAL_HTTPS=false
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
@ -31,7 +49,6 @@ LOCAL_HTTPS=false
# Application secrets
# Generate each with the `rake secret` task (`nanobox run bundle exec rake secret`)
PAPERCLIP_SECRET=$PAPERCLIP_SECRET
SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET
@ -131,9 +148,79 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
STREAMING_CLUSTER_NUM=1
# STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_METHOD=simple_tls
# LDAP_BASE=
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
# The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# Fallback Suffix for email address generation (nil by default)
# PAM_DEFAULT_SUFFIX=pam
# Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
# PAM_CONTROLLED_SERVICE=rpam
# Global OAuth settings (optional) :
# If you have only one strategy, you may want to enable this
# OAUTH_REDIRECT_AT_SIGN_IN=true
# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
# CAS_HOST=sso.myserver.com/
# CAS_PORT=443
# CAS_SSL=true
# CAS_VALIDATE_URL=
# CAS_CALLBACK_URL=
# CAS_LOGOUT_URL=
# CAS_LOGIN_URL=
# CAS_UID_FIELD='user'
# CAS_CA_PATH=
# CAS_DISABLE_SSL_VERIFICATION=false
# CAS_UID_KEY='user'
# CAS_NAME_KEY='name'
# CAS_EMAIL_KEY='email'
# CAS_NICKNAME_KEY='nickname'
# CAS_FIRST_NAME_KEY='firstname'
# CAS_LAST_NAME_KEY='lastname'
# CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone'
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=
# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
# SAML_NAME_IDENTIFIER_FORMAT=
# SAML_CERT=
# SAML_PRIVATE_KEY=
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=

View File

@ -207,7 +207,9 @@ STREAMING_CLUSTER_NUM=1
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=

View File

@ -1,4 +1,3 @@
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4

View File

@ -1,7 +1,7 @@
FROM ruby:2.5.0-alpine3.7
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"
description="Your self-hosted, globally interconnected microblogging community"
ARG UID=991
ARG GID=991
@ -9,8 +9,8 @@ ARG GID=991
ENV RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.3.2
ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
ARG YARN_VERSION=1.5.1
ARG YARN_DOWNLOAD_SHA256=cd31657232cf48d57fdbff55f38bfa058d2fb4950450bd34af72dac796af4de1
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@ -38,7 +38,6 @@ RUN apk -U upgrade \
libidn \
libpq \
nodejs \
nodejs-npm \
protobuf \
su-exec \
tini \
@ -73,9 +72,13 @@ RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-in
&& yarn --pure-lockfile \
&& yarn cache clean
RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon
RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \
&& mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \
&& chown -R mastodon:mastodon /mastodon/public
COPY --chown=mastodon:mastodon . /mastodon
COPY . /mastodon
RUN chown -R mastodon:mastodon /mastodon
VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs

View File

@ -28,15 +28,15 @@ gem 'bootsnap'
gem 'browser'
gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'chewy', '~> 0.10', git: 'https://github.com/toptal/chewy.git'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.4'
gem 'devise-two-factor', '~> 3.0'
gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' }
gem 'net-ldap', '~> 0.10', install_if: -> { ENV['LDAP_ENABLED'] == 'true' }
gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' }
gem 'omniauth-saml', '~> 1.10', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
gem 'net-ldap', '~> 0.10'
gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.2'
gem 'doorkeeper', '~> 4.2'

View File

@ -1,12 +1,3 @@
GIT
remote: https://github.com/toptal/chewy.git
revision: a7d21eb4b0bd7415533ef134bb6d31b2df309701
specs:
chewy (0.10.1)
activesupport (>= 4.0)
elasticsearch (>= 2.0.0)
elasticsearch-dsl
GEM
remote: https://rubygems.org/
specs:
@ -118,6 +109,10 @@ GEM
case_transform (0.2)
activesupport
charlock_holmes (0.7.5)
chewy (5.0.0)
activesupport (>= 4.0)
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.8)
cld3 (3.2.2)
ffi (>= 1.1.0, < 1.10.0)
@ -634,7 +629,7 @@ DEPENDENCIES
capistrano-yarn (~> 2.0)
capybara (~> 2.15)
charlock_holmes (~> 0.7.5)
chewy (~> 0.10)!
chewy (~> 5.0)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
devise (~> 4.4)

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
class ActivityPub::CollectionsController < Api::BaseController
include SignatureVerification
before_action :set_account
before_action :set_size
before_action :set_statuses
def show
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses
@statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
end
def set_size
case params[:id]
when 'featured'
@account.pinned_statuses.count
else
raise ActiveRecord::NotFound
end
end
def scope_for_collection
case params[:id]
when 'featured'
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
scope.merge!(@account.pinned_statuses)
end
else
raise ActiveRecord::NotFound
end
end
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_collection_url(@account, params[:id]),
type: :ordered,
size: @size,
items: @statuses
)
end
end

View File

@ -9,7 +9,7 @@ class ActivityPub::OutboxesController < Api::BaseController
@statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private

View File

@ -11,12 +11,18 @@ class Api::V1::Statuses::PinsController < Api::BaseController
def create
StatusPin.create!(account: current_account, status: @status)
distribute_add_activity!
render json: @status, serializer: REST::StatusSerializer
end
def destroy
pin = StatusPin.find_by(account: current_account, status: @status)
pin&.destroy!
if pin
pin.destroy!
distribute_remove_activity!
end
render json: @status, serializer: REST::StatusSerializer
end
@ -25,4 +31,24 @@ class Api::V1::Statuses::PinsController < Api::BaseController
def set_status
@status = Status.find(params[:status_id])
end
def distribute_add_activity!
json = ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::AddSerializer,
adapter: ActivityPub::Adapter
).as_json
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
end
def distribute_remove_activity!
json = ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::RemoveSerializer,
adapter: ActivityPub::Adapter
).as_json
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
end
end

View File

@ -13,10 +13,9 @@ class Auth::SessionsController < Devise::SessionsController
def new
Devise.omniauth_configs.each do |provider, config|
if config.strategy.redirect_at_sign_in
return redirect_to(omniauth_authorize_path(resource_name, provider))
end
return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
end
super
end
@ -60,6 +59,14 @@ class Auth::SessionsController < Devise::SessionsController
end
end
def after_sign_out_path_for(_resource_or_scope)
Devise.omniauth_configs.each_value do |config|
return root_path if config.strategy.redirect_at_sign_in
end
super
end
def two_factor_enabled?
find_user.try(:otp_required_for_login?)
end

View File

@ -17,11 +17,7 @@ module Localized
end
def default_locale
request_locale || env_locale || I18n.default_locale
end
def env_locale
ENV['DEFAULT_LOCALE']
request_locale || I18n.default_locale
end
def request_locale
@ -29,12 +25,10 @@ module Localized
end
def preferred_locale
http_accept_language.preferred_language_from([env_locale]) ||
http_accept_language.preferred_language_from(I18n.available_locales)
http_accept_language.preferred_language_from(I18n.available_locales)
end
def compatible_locale
http_accept_language.compatible_language_from([env_locale]) ||
http_accept_language.compatible_language_from(I18n.available_locales)
http_accept_language.compatible_language_from(I18n.available_locales)
end
end

View File

@ -35,7 +35,8 @@ class HomeController < ApplicationController
end
end
redirect_to(default_redirect_path)
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
end
def set_pack

View File

@ -2,7 +2,7 @@
module InstanceHelper
def site_title
Setting.site_title.presence || site_hostname
Setting.site_title
end
def site_hostname

View File

@ -8,6 +8,27 @@ module StreamEntriesHelper
account.display_name.presence || account.username
end
def account_description(account)
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
t('accounts.posts'),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
t('accounts.following'),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
t('accounts.followers'),
].join(' '),
].join(', ')
[prepend_str, account.note].join(' · ')
end
def stream_link_target
embedded_view? ? '_blank' : nil
end

View File

@ -1,6 +1,7 @@
import api from '../api';
import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
import {
@ -27,6 +28,9 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
@ -92,8 +96,9 @@ export function mentionCompose(account, router) {
export function submitCompose() {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
if (!status || !status.length) {
if ((!status || !status.length) && media.size === 0) {
return;
}
@ -102,7 +107,7 @@ export function submitCompose() {
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
visibility: getState().getIn(['compose', 'privacy']),
@ -111,6 +116,7 @@ export function submitCompose() {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
dispatch(insertIntoTagHistory(response.data.tags));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
@ -273,12 +279,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
dispatch(readyComposeSuggestionsEmojis(token, results));
};
const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
dispatch(updateSuggestionTags(token));
};
export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
if (token[0] === ':') {
switch (token[0]) {
case ':':
fetchComposeSuggestionsEmojis(dispatch, getState, token);
} else {
break;
case '#':
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
default:
fetchComposeSuggestionsAccounts(dispatch, getState, token);
break;
}
};
};
@ -308,6 +324,9 @@ export function selectComposeSuggestion(position, token, suggestion) {
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else if (suggestion[0] === '#') {
completion = suggestion;
startPosition = position - 1;
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
startPosition = position;
@ -322,6 +341,48 @@ export function selectComposeSuggestion(position, token, suggestion) {
};
};
export function updateSuggestionTags(token) {
return {
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
token,
};
}
export function updateTagHistory(tags) {
return {
type: COMPOSE_TAG_HISTORY_UPDATE,
tags,
};
}
export function hydrateCompose() {
return (dispatch, getState) => {
const me = getState().getIn(['meta', 'me']);
const history = tagHistory.get(me);
if (history !== null) {
dispatch(updateTagHistory(history));
}
};
}
function insertIntoTagHistory(tags) {
return (dispatch, getState) => {
const state = getState();
const oldHistory = state.getIn(['compose', 'tagHistory']);
const me = state.getIn(['meta', 'me']);
const names = tags.map(({ name }) => name);
const intersectedOldHistory = oldHistory.filter(name => !names.includes(name));
names.push(...intersectedOldHistory.toJS());
const newHistory = names.slice(0, 1000);
tagHistory.set(me, newHistory);
dispatch(updateTagHistory(newHistory));
};
}
export function mountCompose() {
return {
type: COMPOSE_MOUNT,

View File

@ -0,0 +1,10 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, placement) {
return { type: DROPDOWN_MENU_OPEN, id, placement };
}
export function closeDropdownMenu(id) {
return { type: DROPDOWN_MENU_CLOSE, id };
}

View File

@ -1,4 +1,5 @@
import { Iterable, fromJS } from 'immutable';
import { hydrateCompose } from './compose';
export const STORE_HYDRATE = 'STORE_HYDRATE';
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
@ -8,10 +9,14 @@ const convertState = rawState =>
Iterable.isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) {
const state = convertState(rawState);
return dispatch => {
const state = convertState(rawState);
return {
type: STORE_HYDRATE,
state,
dispatch({
type: STORE_HYDRATE,
state,
});
dispatch(hydrateCompose());
};
};

View File

@ -117,13 +117,14 @@ export function refreshTimeline(timelineId, path, params = {}) {
};
};
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function refreshTimelineFail(timeline, error, skipLoading) {
return {

View File

@ -3,6 +3,7 @@ import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import includes from 'array-includes';
import assign from 'object-assign';
import values from 'object.values';
import isNaN from 'is-nan';
if (!Array.prototype.includes) {
@ -13,6 +14,10 @@ if (!Object.assign) {
Object.assign = assign;
}
if (!Object.values) {
values.shim();
}
if (!Number.isNaN) {
Number.isNaN = isNaN;
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
@ -8,10 +9,29 @@ export default class AttachmentList extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
compact: PropTypes.bool,
};
render () {
const { media } = this.props;
const { media, compact } = this.props;
if (compact) {
return (
<div className='attachment-list compact'>
<ul className='attachment-list__list'>
{media.map(attachment => {
const displayUrl = attachment.get('remote_url') || attachment.get('url');
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
</li>
);
})}
</ul>
</div>
);
}
return (
<div className='attachment-list'>
@ -20,11 +40,15 @@ export default class AttachmentList extends ImmutablePureComponent {
</div>
<ul className='attachment-list__list'>
{media.map(attachment => (
<li key={attachment.get('id')}>
<a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a>
</li>
))}
{media.map(attachment => {
const displayUrl = attachment.get('remote_url') || attachment.get('url');
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
</li>
);
})}
</ul>
</div>
);

View File

@ -20,7 +20,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
return [null, null];
}
@ -170,6 +170,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;

View File

@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
let id = 0;
class DropdownMenu extends React.PureComponent {
@ -29,6 +30,10 @@ class DropdownMenu extends React.PureComponent {
placement: 'bottom',
};
state = {
mounted: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
@ -38,6 +43,7 @@ class DropdownMenu extends React.PureComponent {
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
this.setState({ mounted: true });
}
componentWillUnmount () {
@ -82,11 +88,15 @@ class DropdownMenu extends React.PureComponent {
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { mounted } = this.state;
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}>
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
@ -115,8 +125,10 @@ export default class Dropdown extends React.PureComponent {
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
};
static defaultProps = {
@ -124,37 +136,28 @@ export default class Dropdown extends React.PureComponent {
};
state = {
expanded: false,
id: id++,
};
handleClick = () => {
if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
const { status, items } = this.props;
handleClick = ({ target }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onModalOpen({
status,
actions: items,
onClick: this.handleItemClick,
});
return;
this.props.onOpen(this.state.id, this.handleItemClick, placement);
}
this.setState({ expanded: !this.state.expanded });
}
handleClose = () => {
if (this.props.onModalClose) {
this.props.onModalClose();
}
this.setState({ expanded: false });
this.props.onClose(this.state.id);
}
handleKeyDown = e => {
switch(e.key) {
case 'Enter':
this.handleClick();
this.handleClick(e);
break;
case 'Escape':
this.handleClose();
@ -186,22 +189,22 @@ export default class Dropdown extends React.PureComponent {
}
render () {
const { icon, items, size, title, disabled } = this.props;
const { expanded } = this.state;
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props;
const open = this.state.id === openDropdownId;
return (
<div onKeyDown={this.handleKeyDown}>
<IconButton
icon={icon}
title={title}
active={expanded}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={expanded} placement='bottom' target={this.findTarget}>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} />
</Overlay>
</div>

View File

@ -11,6 +11,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
};
handleLoadedData = () => {
@ -31,6 +32,12 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
this.video = c;
}
handleClick = e => {
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
}
render () {
const { src, muted, controls, alt } = this.props;
@ -46,6 +53,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
muted={muted}
controls={controls}
loop={!controls}
onClick={this.handleClick}
/>
</div>
);

View File

@ -167,6 +167,14 @@ class Item extends React.PureComponent {
vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true);
}
if (originalWidth > originalHeight) {
imageStyle.height = '100%';
imageStyle.width = 'auto';
} else {
imageStyle.height = 'auto';
imageStyle.width = '100%';
}
imageStyle.top = vShift;
imageStyle.left = hShift;
} else {

View File

@ -12,9 +12,15 @@ export default class Permalink extends React.PureComponent {
href: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
children: PropTypes.node,
onInterceptClick: PropTypes.func,
};
handleClick = (e) => {
handleClick = e => {
if (this.props.onInterceptClick && this.props.onInterceptClick()) {
e.preventDefault();
return;
}
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(this.props.to);
@ -22,7 +28,7 @@ export default class Permalink extends React.PureComponent {
}
render () {
const { href, children, className, ...other } = this.props;
const { href, children, className, onInterceptClick, ...other } = this.props;
return (
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>

View File

@ -17,7 +17,7 @@ export default class ScrollableList extends PureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func,
onLoadMore: PropTypes.func.isRequired,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
@ -45,9 +45,11 @@ export default class ScrollableList extends PureComponent {
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
this.props.onLoadMore();
}
if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
@ -138,7 +140,7 @@ export default class ScrollableList extends PureComponent {
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
this.props.onLoadMore();
}
_recentlyMoved () {

View File

@ -7,6 +7,7 @@ import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components';
@ -138,7 +139,7 @@ export default class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend;
const { hidden } = this.props;
const { hidden, featured } = this.props;
const { isExpanded } = this.state;
let { status, account, ...other } = this.props;
@ -156,7 +157,14 @@ export default class Status extends ImmutablePureComponent {
);
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
@ -170,9 +178,14 @@ export default class Status extends ImmutablePureComponent {
status = status.get('reblog');
}
if (status.get('media_attachments').size > 0 && !this.props.muted) {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
if (status.get('media_attachments').size > 0) {
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
media = (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);

View File

@ -24,7 +24,12 @@ export default class StatusContent extends React.PureComponent {
};
_updateStatusLinks () {
const node = this.node;
const node = this.node;
if (!node) {
return;
}
const links = node.querySelectorAll('a');
for (var i = 0; i < links.length; ++i) {
@ -115,6 +120,10 @@ export default class StatusContent extends React.PureComponent {
render () {
const { status } = this.props;
if (status.get('content').length === 0) {
return null;
}
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const content = { __html: status.get('contentHtml') };

View File

@ -11,7 +11,8 @@ export default class StatusList extends ImmutablePureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
onScrollToBottom: PropTypes.func,
featuredStatusIds: ImmutablePropTypes.list,
onLoadMore: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
const { statusIds, ...other } = this.props;
const { statusIds, featuredStatusIds, ...other } = this.props;
const { isLoading, isPartial } = other;
if (isPartial) {
@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent {
);
}
const scrollableContent = (isLoading || statusIds.size > 0) ? (
statusIds.map((statusId) => (
let scrollableContent = (isLoading || statusIds.size > 0) ? (
statusIds.map(statusId => (
<StatusContainer
key={statusId}
id={statusId}
@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent {
))
) : null;
if (scrollableContent && featuredStatusIds) {
scrollableContent = featuredStatusIds.map(statusId => (
<StatusContainer
key={`f-${statusId}`}
id={statusId}
featured
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
)).concat(scrollableContent);
}
return (
<ScrollableList {...other} ref={this.setRef}>
{scrollableContent}

View File

@ -1,3 +1,4 @@
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
import { openModal, closeModal } from '../actions/modal';
import { connect } from 'react-redux';
import DropdownMenu from '../components/dropdown_menu';
@ -5,12 +6,22 @@ import { isUserTouching } from '../is_mobile';
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
});
const mapDispatchToProps = dispatch => ({
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement) {
dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items,
onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement));
},
onClose(id) {
dispatch(closeModal());
dispatch(closeDropdownMenu(id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View File

@ -13,6 +13,7 @@ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});
class Avatar extends ImmutablePureComponent {
@ -69,6 +70,7 @@ export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@ -80,11 +82,20 @@ export default class Header extends ImmutablePureComponent {
}
let info = '';
let mutingInfo = '';
let actionBtn = '';
let lockedIcon = '';
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info = <span className='account--follows-info'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>;
}
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>;
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>;
}
if (me !== account.get('id')) {
@ -100,6 +111,12 @@ export default class Header extends ImmutablePureComponent {
<IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />
</div>
);
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
</div>
);
}
}
@ -124,6 +141,7 @@ export default class Header extends ImmutablePureComponent {
<div className='account__header__content' dangerouslySetInnerHTML={content} />
{info}
{mutingInfo}
{actionBtn}
</div>
</div>

View File

@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink';
import { displaySensitiveMedia } from '../../../initial_state';
export default class MediaItem extends ImmutablePureComponent {
@ -9,8 +10,22 @@ export default class MediaItem extends ImmutablePureComponent {
media: ImmutablePropTypes.map.isRequired,
};
state = {
visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia,
};
handleClick = () => {
if (!this.state.visible) {
this.setState({ visible: true });
return true;
}
return false;
}
render () {
const { media } = this.props;
const { visible } = this.state;
const status = media.get('status');
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
@ -18,21 +33,28 @@ export default class MediaItem extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100;
const style = {};
let content;
let label, icon;
if (media.get('type') === 'gifv') {
content = <span className='media-gallery__gifv__label'>GIF</span>;
label = <span className='media-gallery__gifv__label'>GIF</span>;
}
if (!status.get('sensitive')) {
if (visible) {
style.backgroundImage = `url(${media.get('preview_url')})`;
style.backgroundPosition = `${x}% ${y}%`;
} else {
icon = (
<span className='account-gallery__item__icons'>
<i className='fa fa-eye-slash' />
</span>
);
}
return (
<div className='account-gallery__item'>
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style}>
{content}
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}>
{icon}
{label}
</Permalink>
</div>
);

View File

@ -21,6 +21,7 @@ export default class Header extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
};
static contextTypes = {
@ -68,7 +69,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account } = this.props;
const { account, hideTabs } = this.props;
if (account === null) {
return <MissingIndicator />;
@ -81,6 +82,7 @@ export default class Header extends ImmutablePureComponent {
<InnerHeader
account={account}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
/>
<ActionBar
@ -94,11 +96,13 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain={this.handleUnblockDomain}
/>
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
{!hideTabs && (
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
)}
</div>
);
}

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { fetchAccount } from '../../actions/accounts';
import { refreshAccountTimeline, expandAccountTimeline } from '../../actions/timelines';
import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
@ -17,6 +17,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
return {
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
featuredStatusIds: state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
};
@ -29,31 +30,36 @@ export default class AccountTimeline extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
};
componentWillMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(refreshAccountTimeline(this.props.params.accountId, this.props.withReplies));
const { params: { accountId }, withReplies } = this.props;
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
}
componentWillReceiveProps (nextProps) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
}
}
handleScrollToBottom = () => {
handleLoadMore = () => {
if (!this.props.isLoading && this.props.hasMore) {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
}
}
render () {
const { statusIds, isLoading, hasMore } = this.props;
const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) {
return (
@ -71,9 +77,10 @@ export default class AccountTimeline extends ImmutablePureComponent {
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
scrollKey='account_timeline'
statusIds={statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
hasMore={hasMore}
onScrollToBottom={this.handleScrollToBottom}
onLoadMore={this.handleLoadMore}
/>
</Column>
);

View File

@ -50,6 +50,7 @@ export default class ComposeForm extends ImmutablePureComponent {
onPaste: PropTypes.func.isRequired,
onPickEmoji: PropTypes.func.isRequired,
showSearch: PropTypes.bool,
anyMedia: PropTypes.bool,
};
static defaultProps = {
@ -142,10 +143,10 @@ export default class ComposeForm extends ImmutablePureComponent {
}
render () {
const { intl, onPaste, showSearch } = this.props;
const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent {
</div>
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
</div>
</div>
);

View File

@ -23,6 +23,7 @@ const mapStateToProps = state => ({
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
});
const mapDispatchToProps = (dispatch) => ({

View File

@ -62,7 +62,7 @@ export default class Favourites extends ImmutablePureComponent {
this.column = c;
}
handleScrollToBottom = debounce(() => {
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
}, 300, { leading: true })
@ -89,7 +89,7 @@ export default class Favourites extends ImmutablePureComponent {
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onScrollToBottom={this.handleScrollToBottom}
onLoadMore={this.handleLoadMore}
/>
</Column>
);

View File

@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
<ScrollContainer scrollKey='followers'>
<div className='scrollable' onScroll={this.handleScroll}>
<div className='followers'>
<HeaderContainer accountId={this.props.params.accountId} />
<HeaderContainer accountId={this.props.params.accountId} hideTabs />
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
{loadMore}
</div>

View File

@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
<ScrollContainer scrollKey='following'>
<div className='scrollable' onScroll={this.handleScroll}>
<div className='following'>
<HeaderContainer accountId={this.props.params.accountId} />
<HeaderContainer accountId={this.props.params.accountId} hideTabs />
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
{loadMore}
</div>

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
export default class ClearColumnButton extends React.Component {
export default class ClearColumnButton extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,

View File

@ -50,8 +50,14 @@ export default class Notifications extends React.PureComponent {
trackScroll: true,
};
handleScrollToBottom = debounce(() => {
componentWillUnmount () {
this.handleLoadMore.cancel();
this.handleScrollToTop.cancel();
this.handleScroll.cancel();
this.props.dispatch(scrollTopNotifications(false));
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandNotifications());
}, 300, { leading: true });
@ -136,7 +142,7 @@ export default class Notifications extends React.PureComponent {
isLoading={isLoading}
hasMore={hasMore}
emptyMessage={emptyMessage}
onScrollToBottom={this.handleScrollToBottom}
onLoadMore={this.handleLoadMore}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}

View File

@ -2,6 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
import noop from 'lodash/noop';
import StatusContent from '../../../components/status_content';
import { MediaGallery, Video } from '../../ui/util/async-components';
import Bundle from '../../ui/components/bundle';
export default class StatusCheckBox extends React.PureComponent {
@ -14,18 +18,48 @@ export default class StatusCheckBox extends React.PureComponent {
render () {
const { status, checked, onToggle, disabled } = this.props;
const content = { __html: status.get('contentHtml') };
let media = null;
if (status.get('reblog')) {
return null;
}
if (status.get('media_attachments').size > 0) {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={video.get('preview_url')}
src={video.get('url')}
width={239}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={noop}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
</Bundle>
);
}
}
return (
<div className='status-check-box'>
<div
className='status__content'
dangerouslySetInnerHTML={content}
/>
<div className='status-check-box__status'>
<StatusContent status={status} />
{media}
</div>
<div className='status-check-box-toggle'>
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const emptyComponent = () => null;
const noop = () => { };
class Bundle extends React.Component {
class Bundle extends React.PureComponent {
static propTypes = {
fetchComponent: PropTypes.func.isRequired,

View File

@ -13,7 +13,7 @@ const messages = defineMessages({
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
});
class BundleColumnError extends React.Component {
class BundleColumnError extends React.PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,

View File

@ -10,7 +10,7 @@ const messages = defineMessages({
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
});
class BundleModalError extends React.Component {
class BundleModalError extends React.PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,

View File

@ -103,8 +103,8 @@ export default class FocalPointModal extends ImmutablePureComponent {
const height = media.getIn(['meta', 'original', 'height']) || null;
return (
<div className='modal-root__modal media-modal'>
<div className={classNames('media-modal__content focal-point', { dragging })} ref={this.setRef}>
<div className='modal-root__modal video-modal'>
<div className={classNames('focal-point', { dragging })} ref={this.setRef}>
<ImageLoader
previewSrc={media.get('preview_url')}
src={media.get('url')}

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ZoomableImage from './zoomable_image';
export default class ImageLoader extends React.PureComponent {
@ -10,6 +11,7 @@ export default class ImageLoader extends React.PureComponent {
previewSrc: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
}
static defaultProps = {
@ -24,6 +26,7 @@ export default class ImageLoader extends React.PureComponent {
}
removers = [];
canvas = null;
get canvasContext() {
if (!this.canvas) {
@ -43,6 +46,10 @@ export default class ImageLoader extends React.PureComponent {
}
}
componentWillUnmount () {
this.removeEventListeners();
}
loadImage (props) {
this.removeEventListeners();
this.setState({ loading: true, error: false });
@ -118,7 +125,7 @@ export default class ImageLoader extends React.PureComponent {
}
render () {
const { alt, src, width, height } = this.props;
const { alt, src, width, height, onClick } = this.props;
const { loading } = this.state;
const className = classNames('image-loader', {
@ -128,22 +135,19 @@ export default class ImageLoader extends React.PureComponent {
return (
<div className={className}>
<canvas
className='image-loader__preview-canvas'
width={width}
height={height}
ref={this.setCanvasRef}
style={{ opacity: loading ? 1 : 0 }}
/>
{!loading && (
<img
alt={alt}
className='image-loader__img'
src={src}
{loading ? (
<canvas
className='image-loader__preview-canvas'
ref={this.setCanvasRef}
width={width}
height={height}
/>
) : (
<ZoomableImage
alt={alt}
src={src}
onClick={onClick}
/>
)}
</div>
);

View File

@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ExtendedVideoPlayer from '../../../components/extended_video_player';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -26,6 +27,7 @@ export default class MediaModal extends ImmutablePureComponent {
state = {
index: null,
navigationHidden: false,
};
handleSwipe = (index) => {
@ -68,14 +70,21 @@ export default class MediaModal extends ImmutablePureComponent {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
render () {
const { media, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const index = this.getIndex();
let pagination = [];
const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
if (media.size > 1) {
pagination = media.map((item, i) => {
@ -92,9 +101,30 @@ export default class MediaModal extends ImmutablePureComponent {
const height = image.getIn(['meta', 'original', 'height']) || null;
if (image.get('type') === 'image') {
return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('url')} />;
return (
<ImageLoader
previewSrc={image.get('preview_url')}
src={image.get('url')}
width={width}
height={height}
alt={image.get('description')}
key={image.get('url')}
onClick={this.toggleNavigation}
/>
);
} else if (image.get('type') === 'gifv') {
return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} alt={image.get('description')} />;
return (
<ExtendedVideoPlayer
src={image.get('url')}
muted
controls={false}
width={width}
height={height}
key={image.get('preview_url')}
alt={image.get('description')}
onClick={this.toggleNavigation}
/>
);
}
return null;
@ -104,21 +134,43 @@ export default class MediaModal extends ImmutablePureComponent {
alignItems: 'center', // center vertically
};
const navigationClassName = classNames('media-modal__navigation', {
'media-modal__navigation--hidden': navigationHidden,
});
return (
<div className='modal-root__modal media-modal'>
{leftNav}
<div className='media-modal__content'>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
<ReactSwipeableViews containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index}>
{content}
</ReactSwipeableViews>
<div
className='media-modal__closer'
role='presentation'
onClick={onClose}
>
<div className='media-modal__content'>
<ReactSwipeableViews
style={{
// you can't use 100vh, because the viewport height is taller
// than the visible part of the document in some mobile
// browsers when it's address bar is visible.
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
height: `${document.body.clientHeight}px`,
}}
containerStyle={containerStyle}
onChangeIndex={this.handleSwipe}
onSwitching={this.handleSwitching}
index={index}
>
{content}
</ReactSwipeableViews>
</div>
</div>
<div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
{leftNav}
{rightNav}
<ul className='media-modal__pagination'>
{pagination}
</ul>
</div>
<ul className='media-modal__pagination'>
{pagination}
</ul>
{rightNav}
</div>
);
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
@ -24,14 +24,12 @@ export function getLink (index) {
}
@injectIntl
export default class TabsBar extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired,
}
@withRouter
export default class TabsBar extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
}
setRef = ref => {
@ -59,7 +57,7 @@ export default class TabsBar extends React.Component {
const listener = debounce(() => {
nextTab.removeEventListener('transitionend', listener);
this.context.router.history.push(to);
this.props.history.push(to);
}, 50);
nextTab.addEventListener('transitionend', listener);

View File

@ -16,7 +16,7 @@ export default class VideoModal extends ImmutablePureComponent {
const { media, time, onClose } = this.props;
return (
<div className='modal-root__modal media-modal'>
<div className='modal-root__modal video-modal'>
<div>
<Video
preview={media.get('preview_url')}

View File

@ -0,0 +1,151 @@
import React from 'react';
import PropTypes from 'prop-types';
const MIN_SCALE = 1;
const MAX_SCALE = 4;
const getMidpoint = (p1, p2) => ({
x: (p1.clientX + p2.clientX) / 2,
y: (p1.clientY + p2.clientY) / 2,
});
const getDistance = (p1, p2) =>
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
export default class ZoomableImage extends React.PureComponent {
static propTypes = {
alt: PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
}
static defaultProps = {
alt: '',
width: null,
height: null,
};
state = {
scale: MIN_SCALE,
}
removers = [];
container = null;
image = null;
lastTouchEndTime = 0;
lastDistance = 0;
componentDidMount () {
let handler = this.handleTouchStart;
this.container.addEventListener('touchstart', handler);
this.removers.push(() => this.container.removeEventListener('touchstart', handler));
handler = this.handleTouchMove;
// on Chrome 56+, touch event listeners will default to passive
// https://www.chromestatus.com/features/5093566007214080
this.container.addEventListener('touchmove', handler, { passive: false });
this.removers.push(() => this.container.removeEventListener('touchend', handler));
}
componentWillUnmount () {
this.removeEventListeners();
}
removeEventListeners () {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
handleTouchStart = e => {
if (e.touches.length !== 2) return;
this.lastDistance = getDistance(...e.touches);
}
handleTouchMove = e => {
const { scrollTop, scrollHeight, clientHeight } = this.container;
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
// prevent propagating event to MediaModal
e.stopPropagation();
return;
}
if (e.touches.length !== 2) return;
e.preventDefault();
e.stopPropagation();
const distance = getDistance(...e.touches);
const midpoint = getMidpoint(...e.touches);
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint);
this.lastMidpoint = midpoint;
this.lastDistance = distance;
}
zoom(nextScale, midpoint) {
const { scale } = this.state;
const { scrollLeft, scrollTop } = this.container;
// math memo:
// x = (scrollLeft + midpoint.x) / scrollWidth
// x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
// scrollWidth = clientWidth * scale
// scrollWidth' = clientWidth * nextScale
// Solve x = x' for nextScrollLeft
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
this.setState({ scale: nextScale }, () => {
this.container.scrollLeft = nextScrollLeft;
this.container.scrollTop = nextScrollTop;
});
}
handleClick = e => {
// don't propagate event to MediaModal
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
}
setContainerRef = c => {
this.container = c;
}
setImageRef = c => {
this.image = c;
}
render () {
const { alt, src } = this.props;
const { scale } = this.state;
const overflow = scale === 1 ? 'hidden' : 'scroll';
return (
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
src={src}
style={{
transform: `scale(${scale})`,
transformOrigin: '0 0',
}}
onClick={this.handleClick}
/>
</div>
);
}
}

View File

@ -56,10 +56,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({
onScrollToBottom: debounce(() => {
dispatch(scrollTopTimeline(timelineId, false));
loadMore();
}, 300, { leading: true }),
onLoadMore: debounce(loadMore, 300, { leading: true }),
onScrollToTop: debounce(() => {
dispatch(scrollTopTimeline(timelineId, true));

View File

@ -1,3 +1,4 @@
import classNames from 'classnames';
import React from 'react';
import NotificationsContainer from './containers/notifications_container';
import PropTypes from 'prop-types';
@ -55,6 +56,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']) !== '',
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
});
const keyMap = {
@ -84,10 +86,93 @@ const keyMap = {
goToMuted: 'g m',
};
class SwitchingColumnsArea extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
};
state = {
mobile: isMobile(window.innerWidth),
};
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.node.handleChildrenContentChange();
}
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
this.setState({ mobile: isMobile(window.innerWidth) });
}, 500, {
trailing: true,
});
setRef = c => {
this.node = c.getWrappedInstance().getWrappedInstance();
}
render () {
const { children } = this.props;
const { mobile } = this.state;
return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
<WrappedSwitch>
<Redirect from='/' to='/getting-started' exact />
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch>
</ColumnsAreaContainer>
);
}
}
@connect(mapStateToProps)
@injectIntl
@withRouter
export default class UI extends React.Component {
export default class UI extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
@ -100,10 +185,10 @@ export default class UI extends React.Component {
hasComposingText: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
};
state = {
width: window.innerWidth,
draggingOver: false,
};
@ -118,14 +203,10 @@ export default class UI extends React.Component {
}
}
handleResize = debounce(() => {
handleLayoutChange = () => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
this.setState({ width: window.innerWidth });
}, 500, {
trailing: true,
});
}
handleDragEnter = (e) => {
e.preventDefault();
@ -193,7 +274,6 @@ export default class UI extends React.Component {
componentWillMount () {
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
document.addEventListener('drop', this.handleDrop, false);
@ -214,28 +294,8 @@ export default class UI extends React.Component {
};
}
shouldComponentUpdate (nextProps) {
if (nextProps.isComposing !== this.props.isComposing) {
// Avoid expensive update just to toggle a class
this.node.classList.toggle('is-composing', nextProps.isComposing);
return false;
}
// Why isn't this working?!?
// return super.shouldComponentUpdate(nextProps, nextState);
return true;
}
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.columnsAreaNode.handleChildrenContentChange();
}
}
componentWillUnmount () {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);
document.removeEventListener('drop', this.handleDrop);
@ -247,10 +307,6 @@ export default class UI extends React.Component {
this.node = c;
}
setColumnsAreaRef = c => {
this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
}
handleHotkeyNew = e => {
e.preventDefault();
@ -350,8 +406,8 @@ export default class UI extends React.Component {
}
render () {
const { width, draggingOver } = this.state;
const { children } = this.props;
const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@ -374,43 +430,12 @@ export default class UI extends React.Component {
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}>
<div className='ui' ref={this.setRef}>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<TabsBar />
<ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}>
<WrappedSwitch>
<Redirect from='/' to='/getting-started' exact />
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch>
</ColumnsAreaContainer>
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
{children}
</SwitchingColumnsArea>
<NotificationsContainer />
<LoadingBarContainer className='loading-bar' />

View File

@ -14,6 +14,7 @@ function loadPolyfills() {
const needsBasePolyfills = !(
window.Intl &&
Object.assign &&
Object.values &&
Number.isNaN &&
window.Symbol &&
Array.prototype.includes

View File

@ -1,7 +1,9 @@
{
"account.block": "حظر @{name}",
"account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
"account.blocked": "محظور",
"account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "تعديل الملف الشخصي",
"account.follow": "تابِع",
"account.followers": "المتابعون",
@ -13,8 +15,9 @@
"account.moved_to": "{name} إنتقل إلى :",
"account.mute": "أكتم @{name}",
"account.mute_notifications": "كتم إخطارات @{name}",
"account.muted": "مكتوم",
"account.posts": "التبويقات",
"account.posts_with_replies": "Toots with replies",
"account.posts_with_replies": "تبويقات تحتوي على رُدود",
"account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة",
"account.share": "مشاركة @{name}'s profile",
@ -208,21 +211,22 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "إلغاء",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.forward": "التحويل إلى {target}",
"report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.placeholder": "تعليقات إضافية",
"report.submit": "إرسال",
"report.target": "إبلاغ",
"search.placeholder": "ابحث",
"search_popout.search_format": "نمط البحث المتقدم",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "وسم",
"search_popout.tips.status": "حالة",
"search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
"search_popout.tips.user": "مستخدِم",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.accounts": "أشخاص",
"search_results.hashtags": "الوُسوم",
"search_results.statuses": "التبويقات",
"search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"standalone.public_title": "نظرة على ...",
"status.block": "Block @{name}",
@ -238,6 +242,7 @@
"status.mute_conversation": "كتم المحادثة",
"status.open": "وسع هذه المشاركة",
"status.pin": "تدبيس على الملف الشخصي",
"status.pinned": "تبويق مثبَّت",
"status.reblog": "رَقِّي",
"status.reblogged_by": "{name} رقى",
"status.reply": "ردّ",
@ -250,7 +255,6 @@
"status.show_more": "أظهر المزيد",
"status.unmute_conversation": "فك الكتم عن المحادثة",
"status.unpin": "فك التدبيس من الملف الشخصي",
"tabs_bar.compose": "تحرير",
"tabs_bar.federated_timeline": "الموحَّد",
"tabs_bar.home": "الرئيسية",
"tabs_bar.local_timeline": "المحلي",
@ -259,7 +263,7 @@
"upload_area.title": "إسحب ثم أفلت للرفع",
"upload_button.label": "إضافة وسائط",
"upload_form.description": "وصف للمعاقين بصريا",
"upload_form.focus": "Crop",
"upload_form.focus": "قص",
"upload_form.undo": "إلغاء",
"upload_progress.label": "يرفع...",
"video.close": "إغلاق الفيديو",

View File

@ -1,7 +1,9 @@
{
"account.block": "Блокирай",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Редактирай профила си",
"account.follow": "Последвай",
"account.followers": "Последователи",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Публикации",
"account.posts_with_replies": "Toots with replies",
"account.report": "Report @{name}",
@ -216,6 +219,7 @@
"report.target": "Reporting",
"search.placeholder": "Търсене",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Споделяне",
"status.reblogged_by": "{name} сподели",
"status.reply": "Отговор",
@ -250,7 +255,6 @@
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Съставяне",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Начало",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloca @{name}",
"account.block_domain": "Amaga-ho tot de {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Edita el perfil",
"account.follow": "Segueix",
"account.followers": "Seguidors",
@ -13,8 +15,9 @@
"account.moved_to": "{name} s'ha mogut a:",
"account.mute": "Silencia @{name}",
"account.mute_notifications": "Notificacions desactivades de @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
"account.posts_with_replies": "Toots with replies",
"account.posts_with_replies": "Toots amb respostes",
"account.report": "Informe @{name}",
"account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
"account.share": "Comparteix el perfil de @{name}",
@ -208,20 +211,21 @@
"relative_time.minutes": "fa {number} minuts",
"relative_time.seconds": "fa {number} segons",
"reply_indicator.cancel": "Cancel·lar",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Reenvia a {target}",
"report.forward_hint": "Aquest compte és d'un altre servidor. Enviar-hi també una copia anònima del informe?",
"report.hint": "El informe s'enviarà als moderadors de la teva instància. Pots explicar perquè vols informar d'aquest compte aquí:",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Enviar",
"report.target": "Informes",
"search.placeholder": "Cercar",
"search_popout.search_format": "Format de cerca avançada",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status",
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
"search_popout.tips.user": "usuari",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.accounts": "Gent",
"search_results.hashtags": "Etiquetes",
"search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
"standalone.public_title": "Una mirada a l'interior ...",
@ -238,6 +242,7 @@
"status.mute_conversation": "Silenciar conversació",
"status.open": "Ampliar aquest estat",
"status.pin": "Fixat en el perfil",
"status.pinned": "Pinned toot",
"status.reblog": "Impuls",
"status.reblogged_by": "{name} ha retootejat",
"status.reply": "Respondre",
@ -250,7 +255,6 @@
"status.show_more": "Mostra més",
"status.unmute_conversation": "Activar conversació",
"status.unpin": "Deslliga del perfil",
"tabs_bar.compose": "Compondre",
"tabs_bar.federated_timeline": "Federada",
"tabs_bar.home": "Inici",
"tabs_bar.local_timeline": "Local",
@ -259,7 +263,7 @@
"upload_area.title": "Arrossega i deixa anar per carregar",
"upload_button.label": "Afegir multimèdia",
"upload_form.description": "Descriure els problemes visuals",
"upload_form.focus": "Crop",
"upload_form.focus": "Retallar",
"upload_form.undo": "Desfer",
"upload_progress.label": "Pujant...",
"video.close": "Tancar el vídeo",

View File

@ -1,7 +1,9 @@
{
"account.block": "@{name} blocken",
"account.block_domain": "Alles von {domain} verstecken",
"account.blocked": "Blocked",
"account.disclaimer_full": "Das Profil wird möglicherweise unvollständig wiedergegeben.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Profil bearbeiten",
"account.follow": "Folgen",
"account.followers": "Folgende",
@ -13,6 +15,7 @@
"account.moved_to": "{name} ist umgezogen auf:",
"account.mute": "@{name} stummschalten",
"account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
"account.muted": "Muted",
"account.posts": "Beiträge",
"account.posts_with_replies": "Toots with replies",
"account.report": "@{name} melden",
@ -216,6 +219,7 @@
"report.target": "{target} melden",
"search.placeholder": "Suche",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Thread stummschalten",
"status.open": "Diesen Beitrag öffnen",
"status.pin": "Im Profil anheften",
"status.pinned": "Pinned toot",
"status.reblog": "Teilen",
"status.reblogged_by": "{name} teilte",
"status.reply": "Antworten",
@ -250,7 +255,6 @@
"status.show_more": "Mehr anzeigen",
"status.unmute_conversation": "Stummschaltung von Thread aufheben",
"status.unpin": "Vom Profil lösen",
"tabs_bar.compose": "Schreiben",
"tabs_bar.federated_timeline": "Föderation",
"tabs_bar.home": "Startseite",
"tabs_bar.local_timeline": "Lokal",

View File

@ -274,6 +274,10 @@
},
{
"descriptors": [
{
"defaultMessage": "Pinned toot",
"id": "status.pinned"
},
{
"defaultMessage": "{name} boosted",
"id": "status.reblogged_by"
@ -469,9 +473,25 @@
"defaultMessage": "Awaiting approval. Click to cancel follow request",
"id": "account.requested"
},
{
"defaultMessage": "Unblock @{name}",
"id": "account.unblock"
},
{
"defaultMessage": "Follows you",
"id": "account.follows_you"
},
{
"defaultMessage": "Blocked",
"id": "account.blocked"
},
{
"defaultMessage": "Muted",
"id": "account.muted"
},
{
"defaultMessage": "Domain hidden",
"id": "account.domain_blocked"
}
],
"path": "app/javascript/mastodon/features/account/components/header.json"
@ -683,6 +703,14 @@
"defaultMessage": "Search",
"id": "search.placeholder"
},
{
"defaultMessage": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"id": "search_popout.tips.full_text"
},
{
"defaultMessage": "Simple text returns matching display names, usernames and hashtags",
"id": "search_popout.tips.text"
},
{
"defaultMessage": "Advanced search format",
"id": "search_popout.search_format"
@ -698,10 +726,6 @@
{
"defaultMessage": "status",
"id": "search_popout.tips.status"
},
{
"defaultMessage": "Simple text returns matching display names, usernames and hashtags",
"id": "search_popout.tips.text"
}
],
"path": "app/javascript/mastodon/features/compose/components/search.json"
@ -1589,6 +1613,10 @@
},
{
"descriptors": [
{
"defaultMessage": "Close",
"id": "lightbox.close"
},
{
"defaultMessage": "Additional comments",
"id": "report.placeholder"
@ -1618,10 +1646,6 @@
},
{
"descriptors": [
{
"defaultMessage": "Compose",
"id": "tabs_bar.compose"
},
{
"defaultMessage": "Home",
"id": "tabs_bar.home"

View File

@ -1,7 +1,9 @@
{
"account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Edit profile",
"account.follow": "Follow",
"account.followers": "Followers",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
"account.posts_with_replies": "Toots with replies",
"account.report": "Report @{name}",
@ -223,6 +226,7 @@
"report.target": "Reporting {target}",
"search.placeholder": "Search",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -245,6 +249,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boosted",
"status.reply": "Reply",
@ -257,7 +262,6 @@
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloki @{name}",
"account.block_domain": "Kaŝi ĉion de {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Subaj informoj povas reflekti la profilon de la uzanto nekomplete.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Redakti profilon",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
@ -13,8 +15,9 @@
"account.moved_to": "{name} moviĝis al:",
"account.mute": "Silentigi @{name}",
"account.mute_notifications": "Silentigi sciigojn el @{name}",
"account.posts": "Hupoj",
"account.posts_with_replies": "Toots with replies",
"account.muted": "Muted",
"account.posts": "Mesaĝoj",
"account.posts_with_replies": "Mesaĝoj kun respondoj",
"account.report": "Signali @{name}",
"account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
"account.share": "Diskonigi la profilon de @{name}",
@ -147,7 +150,7 @@
"navigation_bar.edit_profile": "Redakti profilon",
"navigation_bar.favourites": "Stelumoj",
"navigation_bar.follow_requests": "Petoj de sekvado",
"navigation_bar.info": "Pri ĉiu tiu nodo",
"navigation_bar.info": "Pri ĉi tiu nodo",
"navigation_bar.keyboard_shortcuts": "Klavaraj mallongigoj",
"navigation_bar.lists": "Listoj",
"navigation_bar.logout": "Elsaluti",
@ -208,21 +211,22 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Nuligi",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Plusendi al {target}",
"report.forward_hint": "La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien?",
"report.hint": "La signalo estos sendita al la kontrolantoj de via nodo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:",
"report.placeholder": "Pliaj komentoj",
"report.submit": "Sendi",
"report.target": "Signali {target}",
"search.placeholder": "Serĉi",
"search_popout.search_format": "Detala serĉo",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "kradvorto",
"search_popout.tips.status": "mesaĝoj",
"search_popout.tips.text": "Simpla teksto montras la kongruajn afiŝitajn nomojn, uzantnomojn kaj kradvortojn",
"search_popout.tips.user": "uzanto",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.accounts": "Homoj",
"search_results.hashtags": "Kradvortoj",
"search_results.statuses": "Mesaĝoj",
"search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
"standalone.public_title": "Enrigardo…",
"status.block": "Bloki @{name}",
@ -238,6 +242,7 @@
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Grandigi ĉi tiun mesaĝon",
"status.pin": "Alpingli en la profilo",
"status.pinned": "Pinned toot",
"status.reblog": "Diskonigi",
"status.reblogged_by": "{name} diskonigis",
"status.reply": "Respondi",
@ -250,7 +255,6 @@
"status.show_more": "Grandigi",
"status.unmute_conversation": "Malsilentigi konversacion",
"status.unpin": "Depingli de profilo",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.federated_timeline": "Fratara tempolinio",
"tabs_bar.home": "Hejmo",
"tabs_bar.local_timeline": "Loka tempolinio",
@ -259,7 +263,7 @@
"upload_area.title": "Altreni kaj lasi por alŝuti",
"upload_button.label": "Aldoni aŭdovidaĵon",
"upload_form.description": "Priskribi por misvidantaj homoj",
"upload_form.focus": "Crop",
"upload_form.focus": "Stuci",
"upload_form.undo": "Malfari",
"upload_progress.label": "Alŝutado…",
"video.close": "Fermi videon",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloquear",
"account.block_domain": "Ocultar todo de {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -13,6 +15,7 @@
"account.moved_to": "{name} se ha mudado a:",
"account.mute": "Silenciar a @{name}",
"account.mute_notifications": "Silenciar notificaciones de @{name}",
"account.muted": "Muted",
"account.posts": "Publicaciones",
"account.posts_with_replies": "Toots with replies",
"account.report": "Reportar a @{name}",
@ -216,6 +219,7 @@
"report.target": "Reportando",
"search.placeholder": "Buscar",
"search_popout.search_format": "Formato de búsqueda avanzada",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status",
"search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
@ -238,6 +242,7 @@
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado",
"status.pin": "Fijar",
"status.pinned": "Pinned toot",
"status.reblog": "Retootear",
"status.reblogged_by": "Retooteado por {name}",
"status.reply": "Responder",
@ -250,7 +255,6 @@
"status.show_more": "Mostrar más",
"status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar",
"tabs_bar.compose": "Redactar",
"tabs_bar.federated_timeline": "Federado",
"tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "مسدودسازی @{name}",
"account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "ویرایش نمایه",
"account.follow": "پی بگیرید",
"account.followers": "پیگیران",
@ -13,6 +15,7 @@
"account.moved_to": "{name} منتقل شده است به:",
"account.mute": "بی‌صدا کردن @{name}",
"account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",
"account.muted": "Muted",
"account.posts": "نوشته‌ها",
"account.posts_with_replies": "Toots with replies",
"account.report": "گزارش @{name}",
@ -216,6 +219,7 @@
"report.target": "گزارش‌دادن",
"search.placeholder": "جستجو",
"search_popout.search_format": "راهنمای جستجوی پیشرفته",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "هشتگ",
"search_popout.tips.status": "نوشته",
"search_popout.tips.text": "جستجوی متنی ساده برای نام‌ها، نام‌های کاربری، و هشتگ‌ها",
@ -238,6 +242,7 @@
"status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن",
"status.pin": "نوشتهٔ ثابت نمایه",
"status.pinned": "Pinned toot",
"status.reblog": "بازبوقیدن",
"status.reblogged_by": "{name} بازبوقید",
"status.reply": "پاسخ",
@ -250,7 +255,6 @@
"status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو",
"status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
"tabs_bar.compose": "بنویسید",
"tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه",
"tabs_bar.local_timeline": "محلی",

View File

@ -1,7 +1,9 @@
{
"account.block": "Estä @{name}",
"account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Alla olevat käyttäjän profiilitiedot saattavat olla epätäydellisiä.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Muokkaa",
"account.follow": "Seuraa",
"account.followers": "Seuraajia",
@ -13,6 +15,7 @@
"account.moved_to": "{name} on muuttanut instanssiin:",
"account.mute": "Mykistä @{name}",
"account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}",
"account.muted": "Muted",
"account.posts": "Töötit",
"account.posts_with_replies": "Toots with replies",
"account.report": "Report @{name}",
@ -216,6 +219,7 @@
"report.target": "Reporting",
"search.placeholder": "Hae",
"search_popout.search_format": "Tarkennettu haku",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtagi",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Pelkkä tekstihaku palauttaa hakua vastaavat nimimerkit, käyttäjänimet ja hastagit",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mykistä keskustelu",
"status.open": "Laajenna statuspäivitys",
"status.pin": "Kiinnitä profiiliin",
"status.pinned": "Pinned toot",
"status.reblog": "Buustaa",
"status.reblogged_by": "{name} buustasi",
"status.reply": "Vastaa",
@ -250,7 +255,6 @@
"status.show_more": "Näytä lisää",
"status.unmute_conversation": "Poista mykistys keskustelulta",
"status.unpin": "Irrota profiilista",
"tabs_bar.compose": "Luo",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Koti",
"tabs_bar.local_timeline": "Paikallinen",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloquer @{name}",
"account.block_domain": "Tout masquer venant de {domain}",
"account.blocked": "Bloqué",
"account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Modifier le profil",
"account.follow": "Suivre",
"account.followers": "Abonné⋅e⋅s",
@ -13,8 +15,9 @@
"account.moved_to": "{name} a déménagé vers :",
"account.mute": "Masquer @{name}",
"account.mute_notifications": "Ignorer les notifications de @{name}",
"account.posts": "Statuts",
"account.posts_with_replies": "Toots with replies",
"account.muted": "Silencé",
"account.posts": "Pouets",
"account.posts_with_replies": "Pouets avec réponses",
"account.report": "Signaler",
"account.requested": "Invitation envoyée",
"account.share": "Partager le profil de @{name}",
@ -208,21 +211,22 @@
"relative_time.minutes": "{number} min",
"relative_time.seconds": "{number} s",
"reply_indicator.cancel": "Annuler",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Transférer à {target}",
"report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?",
"report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :",
"report.placeholder": "Commentaires additionnels",
"report.submit": "Envoyer",
"report.target": "Signalement",
"search.placeholder": "Rechercher",
"search_popout.search_format": "Recherche avancée",
"search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les noms d'utilisateurs, les noms affichés, et les hashtags correspondant.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "statuts",
"search_popout.tips.text": "Un texte simple renvoie les noms affichés, les noms dutilisateur⋅ice et les hashtags correspondants",
"search_popout.tips.user": "utilisateur⋅ice",
"search_results.accounts": "People",
"search_results.accounts": "Personnes",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses": "Pouets",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"standalone.public_title": "Jeter un coup dœil…",
"status.block": "Block @{name}",
@ -238,6 +242,7 @@
"status.mute_conversation": "Masquer la conversation",
"status.open": "Déplier ce statut",
"status.pin": "Épingler sur le profil",
"status.pinned": "Pouet épinglé",
"status.reblog": "Partager",
"status.reblogged_by": "{name} a partagé:",
"status.reply": "Répondre",
@ -250,7 +255,6 @@
"status.show_more": "Déplier",
"status.unmute_conversation": "Ne plus masquer la conversation",
"status.unpin": "Retirer du profil",
"tabs_bar.compose": "Composer",
"tabs_bar.federated_timeline": "Fil public global",
"tabs_bar.home": "Accueil",
"tabs_bar.local_timeline": "Fil public local",
@ -259,7 +263,7 @@
"upload_area.title": "Glissez et déposez pour envoyer",
"upload_button.label": "Joindre un média",
"upload_form.description": "Décrire pour les malvoyants",
"upload_form.focus": "Crop",
"upload_form.focus": "Recadrer",
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"video.close": "Fermer la vidéo",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloquear @{name}",
"account.block_domain": "Ocultar calquer contido de {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidoras",
@ -13,6 +15,7 @@
"account.moved_to": "{name} marchou a:",
"account.mute": "Acalar @{name}",
"account.mute_notifications": "Acalar as notificacións de @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
"account.posts_with_replies": "Toots with replies",
"account.report": "Informar sobre @{name}",
@ -216,6 +219,7 @@
"report.target": "Informar {target}",
"search.placeholder": "Buscar",
"search_popout.search_format": "Formato de busca avanzada",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "estado",
"search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
@ -238,6 +242,7 @@
"status.mute_conversation": "Acalar conversa",
"status.open": "Expandir este estado",
"status.pin": "Fixar no perfil",
"status.pinned": "Pinned toot",
"status.reblog": "Promover",
"status.reblogged_by": "{name} promoveu",
"status.reply": "Resposta",
@ -250,7 +255,6 @@
"status.show_more": "Mostrar máis",
"status.unmute_conversation": "Non acalar a conversa",
"status.unpin": "Despegar do perfil",
"tabs_bar.compose": "Compoñer",
"tabs_bar.federated_timeline": "Federado",
"tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "חסימת @{name}",
"account.block_domain": "להסתיר הכל מהקהילה {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "עריכת פרופיל",
"account.follow": "מעקב",
"account.followers": "עוקבים",
@ -13,6 +15,7 @@
"account.moved_to": "החשבון {name} הועבר אל:",
"account.mute": "להשתיק את @{name}",
"account.mute_notifications": "להסתיר התראות מאת @{name}",
"account.muted": "Muted",
"account.posts": "הודעות",
"account.posts_with_replies": "Toots with replies",
"account.report": "לדווח על @{name}",
@ -216,6 +219,7 @@
"report.target": "דיווח",
"search.placeholder": "חיפוש",
"search_popout.search_format": "מבנה חיפוש מתקדם",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "האשתג",
"search_popout.tips.status": "status",
"search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים",
@ -238,6 +242,7 @@
"status.mute_conversation": "השתקת שיחה",
"status.open": "הרחבת הודעה",
"status.pin": "לקבע באודות",
"status.pinned": "Pinned toot",
"status.reblog": "הדהוד",
"status.reblogged_by": "הודהד על ידי {name}",
"status.reply": "תגובה",
@ -250,7 +255,6 @@
"status.show_more": "הראה יותר",
"status.unmute_conversation": "הסרת השתקת שיחה",
"status.unpin": "לשחרר מקיבוע באודות",
"tabs_bar.compose": "חיבור",
"tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
"tabs_bar.home": "בבית",
"tabs_bar.local_timeline": "ציר זמן מקומי",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokiraj @{name}",
"account.block_domain": "Sakrij sve sa {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Uredi profil",
"account.follow": "Slijedi",
"account.followers": "Sljedbenici",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Utišaj @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Postovi",
"account.posts_with_replies": "Toots with replies",
"account.report": "Prijavi @{name}",
@ -216,6 +219,7 @@
"report.target": "Prijavljivanje",
"search.placeholder": "Traži",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Utišaj razgovor",
"status.open": "Proširi ovaj status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Podigni",
"status.reblogged_by": "{name} je podigao",
"status.reply": "Odgovori",
@ -250,7 +255,6 @@
"status.show_more": "Pokaži više",
"status.unmute_conversation": "Poništi utišavanje razgovora",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Sastavi",
"tabs_bar.federated_timeline": "Federalni",
"tabs_bar.home": "Dom",
"tabs_bar.local_timeline": "Lokalno",

View File

@ -1,7 +1,9 @@
{
"account.block": "@{name} letiltása",
"account.block_domain": "Minden elrejtése innen: {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Az alul található információk hiányosan mutathatják be a felhasználót.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Profil szerkesztése",
"account.follow": "Követés",
"account.followers": "Követők",
@ -13,6 +15,7 @@
"account.moved_to": "{name} átköltözött:",
"account.mute": "@{name} némítása",
"account.mute_notifications": "@{name} értesítések némítása",
"account.muted": "Muted",
"account.posts": "Státuszok",
"account.posts_with_replies": "Toots with replies",
"account.report": "@{name} jelentése",
@ -216,6 +219,7 @@
"report.target": "Reporting",
"search.placeholder": "Keresés",
"search_popout.search_format": "Fejlett keresés",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Beszélgetés némítása",
"status.open": "Státusz kinagyítása",
"status.pin": "Kitűzés a profilra",
"status.pinned": "Pinned toot",
"status.reblog": "Reblog",
"status.reblogged_by": "{name} reblogolta",
"status.reply": "Válasz",
@ -250,7 +255,6 @@
"status.show_more": "Többet",
"status.unmute_conversation": "Beszélgetés némításának elvonása",
"status.unpin": "Kitűzés eltávolítása a profilról",
"tabs_bar.compose": "Összeállítás",
"tabs_bar.federated_timeline": "Federált",
"tabs_bar.home": "Kezdőlap",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Արգելափակել @{name}֊ին",
"account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Ներքոհիշյալը կարող է ոչ ամբողջությամբ արտացոլել օգտատիրոջ էջի տվյալները։",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Խմբագրել անձնական էջը",
"account.follow": "Հետեւել",
"account.followers": "Հետեւվողներ",
@ -13,6 +15,7 @@
"account.moved_to": "{name}֊ը տեղափոխվել է՝",
"account.mute": "Լռեցնել @{name}֊ին",
"account.mute_notifications": "Անջատել ծանուցումները @{name}֊ից",
"account.muted": "Muted",
"account.posts": "Գրառումներ",
"account.posts_with_replies": "Toots with replies",
"account.report": "Բողոքել @{name}֊ից",
@ -216,6 +219,7 @@
"report.target": "Բողոքել {target}֊ի մասին",
"search.placeholder": "Փնտրել",
"search_popout.search_format": "Փնտրելու առաջադեմ ձեւ",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "պիտակ",
"search_popout.tips.status": "թութ",
"search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ",
@ -238,6 +242,7 @@
"status.mute_conversation": "Լռեցնել խոսակցությունը",
"status.open": "Ընդարձակել այս թութը",
"status.pin": "Ամրացնել անձնական էջում",
"status.pinned": "Pinned toot",
"status.reblog": "Տարածել",
"status.reblogged_by": "{name} տարածել է",
"status.reply": "Պատասխանել",
@ -250,7 +255,6 @@
"status.show_more": "Ավելին",
"status.unmute_conversation": "Ապալռեցնել խոսակցությունը",
"status.unpin": "Հանել անձնական էջից",
"tabs_bar.compose": "Շարադրել",
"tabs_bar.federated_timeline": "Դաշնային",
"tabs_bar.home": "Հիմնական",
"tabs_bar.local_timeline": "Տեղական",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokir @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Ubah profil",
"account.follow": "Ikuti",
"account.followers": "Pengikut",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Bisukan @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Postingan",
"account.posts_with_replies": "Toots with replies",
"account.report": "Laporkan @{name}",
@ -216,6 +219,7 @@
"report.target": "Melaporkan",
"search.placeholder": "Pencarian",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Tampilkan status ini",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Boost",
"status.reblogged_by": "di-boost {name}",
"status.reply": "Balas",
@ -250,7 +255,6 @@
"status.show_more": "Tampilkan semua",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Tulis",
"tabs_bar.federated_timeline": "Gabungan",
"tabs_bar.home": "Beranda",
"tabs_bar.local_timeline": "Lokal",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokusar @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Modifikar profilo",
"account.follow": "Sequar",
"account.followers": "Sequanti",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Celar @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Mesaji",
"account.posts_with_replies": "Toots with replies",
"account.report": "Denuncar @{name}",
@ -216,6 +219,7 @@
"report.target": "Denuncante",
"search.placeholder": "Serchez",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Detaligar ca mesajo",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Repetar",
"status.reblogged_by": "{name} repetita",
"status.reply": "Respondar",
@ -250,7 +255,6 @@
"status.show_more": "Montrar plue",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Kompozar",
"tabs_bar.federated_timeline": "Federata",
"tabs_bar.home": "Hemo",
"tabs_bar.local_timeline": "Lokala",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blocca @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Modifica profilo",
"account.follow": "Segui",
"account.followers": "Seguaci",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Silenzia @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Posts",
"account.posts_with_replies": "Toots with replies",
"account.report": "Segnala @{name}",
@ -216,6 +219,7 @@
"report.target": "Invio la segnalazione",
"search.placeholder": "Cerca",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Espandi questo post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Condividi",
"status.reblogged_by": "{name} ha condiviso",
"status.reply": "Rispondi",
@ -250,7 +255,6 @@
"status.show_more": "Mostra di più",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Scrivi",
"tabs_bar.federated_timeline": "Federazione",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Locale",

View File

@ -1,7 +1,9 @@
{
"account.block": "@{name}さんをブロック",
"account.block_domain": "{domain}全体を非表示",
"account.blocked": "ブロック済み",
"account.disclaimer_full": "以下の情報は不正確な可能性があります。",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー",
"account.followers": "フォロワー",
@ -13,8 +15,9 @@
"account.moved_to": "{name}さんは引っ越しました:",
"account.mute": "@{name}さんをミュート",
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
"account.muted": "ミュート済み",
"account.posts": "投稿",
"account.posts_with_replies": "トゥートと返信",
"account.posts_with_replies": "投稿と返信",
"account.report": "@{name}さんを通報",
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
"account.share": "@{name}さんのプロフィールを共有する",
@ -223,6 +226,7 @@
"report.target": "{target}さんを通報する",
"search.placeholder": "検索",
"search_popout.search_format": "高度な検索フォーマット",
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。",
"search_popout.tips.hashtag": "ハッシュタグ",
"search_popout.tips.status": "トゥート",
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
@ -245,8 +249,9 @@
"status.mute_conversation": "会話をミュート",
"status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示",
"status.pinned": "固定されたトゥート",
"status.reblog": "ブースト",
"status.reblogged_by": "{name}さんにブーストされました",
"status.reblogged_by": "{name}さんがブースト",
"status.reply": "返信",
"status.replyAll": "全員に返信",
"status.report": "@{name}さんを通報",
@ -257,12 +262,11 @@
"status.show_more": "もっと見る",
"status.unmute_conversation": "会話のミュートを解除",
"status.unpin": "プロフィールの固定表示を解除",
"tabs_bar.compose": "投稿",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.home": "ホーム",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.notifications": "通知",
"ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。",
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加",
"upload_form.description": "視覚障害者のための説明",

View File

@ -1,7 +1,9 @@
{
"account.block": "@{name}을 차단",
"account.block_domain": "{domain} 전체를 숨김",
"account.blocked": "Blocked",
"account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "프로필 편집",
"account.follow": "팔로우",
"account.followers": "팔로워",
@ -13,6 +15,7 @@
"account.moved_to": "{name}는 계정을 이동했습니다:",
"account.mute": "@{name} 뮤트",
"account.mute_notifications": "@{name}의 알림을 뮤트",
"account.muted": "Muted",
"account.posts": "게시물",
"account.posts_with_replies": "Toots with replies",
"account.report": "@{name} 신고",
@ -216,6 +219,7 @@
"report.target": "문제가 된 사용자",
"search.placeholder": "검색",
"search_popout.search_format": "고급 검색 방법",
"search_popout.tips.full_text": "단순한 텍스트 검색은 당신이 작성했거나, 관심글로 지정했거나, 부스트했거나, 멘션을 받은 게시글, 그리고 유저네임, 디스플레이네임, 해시태그를 반환합니다.",
"search_popout.tips.hashtag": "해시태그",
"search_popout.tips.status": "툿",
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
@ -238,6 +242,7 @@
"status.mute_conversation": "이 대화를 뮤트",
"status.open": "상세 정보 표시",
"status.pin": "고정",
"status.pinned": "Pinned toot",
"status.reblog": "부스트",
"status.reblogged_by": "{name}님이 부스트 했습니다",
"status.reply": "답장",
@ -250,7 +255,6 @@
"status.show_more": "더 보기",
"status.unmute_conversation": "이 대화의 뮤트 해제하기",
"status.unpin": "고정 해제",
"tabs_bar.compose": "포스트",
"tabs_bar.federated_timeline": "연합",
"tabs_bar.home": "홈",
"tabs_bar.local_timeline": "로컬",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokkeer @{name}",
"account.block_domain": "Negeer alles van {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "De informatie hieronder kan mogelijk een incompleet beeld geven van dit gebruikersprofiel.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Profiel bewerken",
"account.follow": "Volgen",
"account.followers": "Volgers",
@ -13,8 +15,9 @@
"account.moved_to": "{name} is verhuisd naar:",
"account.mute": "Negeer @{name}",
"account.mute_notifications": "Negeer meldingen van @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
"account.posts_with_replies": "Toots with replies",
"account.posts_with_replies": "Toots met reacties",
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
"account.share": "Profiel van @{name} delen",
@ -208,19 +211,20 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Annuleren",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Doorsturen naar {target}",
"report.forward_hint": "Het account bevindt zich op een andere server. Stuur daar eveneens een geanonimiseerde kopie van de gerapporteerde toot(s) naartoe?",
"report.hint": "De gerapporteerde toot(s) worden naar de moderatoren van jouw server gestuurd. Je kunt hieronder een uitleg geven waarom je dit account rapporteert:",
"report.placeholder": "Extra opmerkingen",
"report.submit": "Verzenden",
"report.target": "Rapporteer {target}",
"search.placeholder": "Zoeken",
"search_popout.search_format": "Geavanceerd zoeken",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "toot",
"search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
"search_popout.tips.user": "gebruiker",
"search_results.accounts": "People",
"search_results.accounts": "Gebruikers",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
@ -238,6 +242,7 @@
"status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen",
"status.pin": "Aan profielpagina vastmaken",
"status.pinned": "Pinned toot",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boostte",
"status.reply": "Reageren",
@ -250,7 +255,6 @@
"status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren",
"status.unpin": "Van profielpagina losmaken",
"tabs_bar.compose": "Schrijven",
"tabs_bar.federated_timeline": "Globaal",
"tabs_bar.home": "Start",
"tabs_bar.local_timeline": "Lokaal",
@ -259,7 +263,7 @@
"upload_area.title": "Hierin slepen om te uploaden",
"upload_button.label": "Media toevoegen",
"upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
"upload_form.focus": "Crop",
"upload_form.focus": "Bijsnijden",
"upload_form.undo": "Ongedaan maken",
"upload_progress.label": "Uploaden...",
"video.close": "Video sluiten",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokkér @{name}",
"account.block_domain": "Skjul alt fra {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Rediger profil",
"account.follow": "Følg",
"account.followers": "Følgere",
@ -13,6 +15,7 @@
"account.moved_to": "{name} har flyttet til:",
"account.mute": "Demp @{name}",
"account.mute_notifications": "Ignorer varsler fra @{name}",
"account.muted": "Muted",
"account.posts": "Innlegg",
"account.posts_with_replies": "Toots with replies",
"account.report": "Rapportér @{name}",
@ -216,6 +219,7 @@
"report.target": "Rapporterer",
"search.placeholder": "Søk",
"search_popout.search_format": "Avansert søkeformat",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "emneknagg",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Enkel tekst returnerer matchende visningsnavn, brukernavn og emneknagger",
@ -238,6 +242,7 @@
"status.mute_conversation": "Demp samtale",
"status.open": "Utvid denne statusen",
"status.pin": "Fest på profilen",
"status.pinned": "Pinned toot",
"status.reblog": "Fremhev",
"status.reblogged_by": "Fremhevd av {name}",
"status.reply": "Svar",
@ -250,7 +255,6 @@
"status.show_more": "Vis mer",
"status.unmute_conversation": "Ikke demp samtale",
"status.unpin": "Angre festing på profilen",
"tabs_bar.compose": "Komponer",
"tabs_bar.federated_timeline": "Felles",
"tabs_bar.home": "Hjem",
"tabs_bar.local_timeline": "Lokal",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blocar @{name}",
"account.block_domain": "Tot amagar del domeni {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incomplètas.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Modificar lo perfil",
"account.follow": "Sègre",
"account.followers": "Seguidors",
@ -13,6 +15,7 @@
"account.moved_to": "{name} a mudat los catons a:",
"account.mute": "Rescondre @{name}",
"account.mute_notifications": "Rescondre las notificacions de @{name}",
"account.muted": "Muted",
"account.posts": "Estatuts",
"account.posts_with_replies": "Toots with replies",
"account.report": "Senhalar @{name}",
@ -216,6 +219,7 @@
"report.target": "Senhalar {target}",
"search.placeholder": "Recercar",
"search_popout.search_format": "Format recèrca avançada",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "estatut",
"search_popout.tips.text": "Lo tèxt brut tòrna escais, noms dutilizaire e etiquetas correspondents",
@ -238,6 +242,7 @@
"status.mute_conversation": "Rescondre la conversacion",
"status.open": "Desplegar aqueste estatut",
"status.pin": "Penjar al perfil",
"status.pinned": "Pinned toot",
"status.reblog": "Partejar",
"status.reblogged_by": "{name} a partejat",
"status.reply": "Respondre",
@ -250,7 +255,6 @@
"status.show_more": "Desplegar",
"status.unmute_conversation": "Tornar mostrar la conversacion",
"status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh",
"tabs_bar.local_timeline": "Flux public local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokuj @{name}",
"account.block_domain": "Blokuj wszystko z {domain}",
"account.blocked": "Zablokowany",
"account.disclaimer_full": "Poniższe informacje mogą nie odwzorowywać bezbłędnie profilu użytkownika.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Edytuj profil",
"account.follow": "Śledź",
"account.followers": "Śledzący",
@ -13,6 +15,7 @@
"account.moved_to": "{name} przeniósł się do:",
"account.mute": "Wycisz @{name}",
"account.mute_notifications": "Wycisz powiadomienia o @{name}",
"account.muted": "Wyciszony",
"account.posts": "Wpisy",
"account.posts_with_replies": "Wpisy z odpowiedziami",
"account.report": "Zgłoś @{name}",
@ -223,6 +226,7 @@
"report.target": "Zgłaszanie {target}",
"search.placeholder": "Szukaj",
"search_popout.search_format": "Zaawansowane wyszukiwanie",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "wpis",
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
@ -245,6 +249,7 @@
"status.mute_conversation": "Wycisz konwersację",
"status.open": "Rozszerz ten wpis",
"status.pin": "Przypnij do profilu",
"status.pinned": "Przypięty wpis",
"status.reblog": "Podbij",
"status.reblogged_by": "{name} podbił",
"status.reply": "Odpowiedz",
@ -257,7 +262,6 @@
"status.show_more": "Pokaż więcej",
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",
"status.unpin": "Odepnij z profilu",
"tabs_bar.compose": "Napisz",
"tabs_bar.federated_timeline": "Globalne",
"tabs_bar.home": "Strona główna",
"tabs_bar.local_timeline": "Lokalne",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloquear @{name}",
"account.block_domain": "Esconder tudo de {domain}",
"account.blocked": "Bloqueado",
"account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de maneira incompleta.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -13,8 +15,9 @@
"account.moved_to": "{name} se mudou para:",
"account.mute": "Silenciar @{name}",
"account.mute_notifications": "Silenciar notificações de @{name}",
"account.posts": "Posts",
"account.posts_with_replies": "Toots with replies",
"account.muted": "Silenciado",
"account.posts": "Toots",
"account.posts_with_replies": "Toots e respostas",
"account.report": "Denunciar @{name}",
"account.requested": "Aguardando aprovação. Clique para cancelar a solicitação",
"account.share": "Compartilhar perfil de @{name}",
@ -208,19 +211,20 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancelar",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Encaminhar para {target}",
"report.forward_hint": "Essa conta pertence à um outro servidor. Encaminhar uma cópia da denúncia com seus dados tornados anônimos para esse servidor?",
"report.hint": "A sua denúncia será enviada aos moderadores da instância. Você pode adicionar uma explicação de porque você está denunciando essa conta abaixo:",
"report.placeholder": "Comentários adicionais",
"report.submit": "Enviar",
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
"search_popout.search_format": "Formato de busca avançado",
"search_popout.tips.full_text": "Texto simples retorna status que você escreveu, favoritou, compartilhou ou em que tenha sido mencionado; também retorna nomes de exibição, usuários e hashtags correspondentes.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Texto simples retorna nomes de exibição, usuários e hashtags correspondentes",
"search_popout.tips.user": "usuário",
"search_results.accounts": "People",
"search_results.accounts": "Pessoas",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
@ -238,6 +242,7 @@
"status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir",
"status.pin": "Fixar no perfil",
"status.pinned": "Toot fixado",
"status.reblog": "Compartilhar",
"status.reblogged_by": "{name} compartilhou",
"status.reply": "Responder",
@ -250,7 +255,6 @@
"status.show_more": "Mostrar mais",
"status.unmute_conversation": "Desativar silêncio desta conversa",
"status.unpin": "Desafixar do perfil",
"tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Página inicial",
"tabs_bar.local_timeline": "Local",
@ -259,7 +263,7 @@
"upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar mídia",
"upload_form.description": "Descreva a imagem para deficientes visuais",
"upload_form.focus": "Crop",
"upload_form.focus": "Recortar",
"upload_form.undo": "Desfazer",
"upload_progress.label": "Salvando...",
"video.close": "Fechar vídeo",

View File

@ -1,7 +1,9 @@
{
"account.block": "Bloquear @{name}",
"account.block_domain": "Esconder tudo do domínio {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -13,6 +15,7 @@
"account.moved_to": "{name} mudou a sua conta para:",
"account.mute": "Silenciar @{name}",
"account.mute_notifications": "Silenciar notificações de @{name}",
"account.muted": "Muted",
"account.posts": "Posts",
"account.posts_with_replies": "Toots with replies",
"account.report": "Denunciar @{name}",
@ -216,6 +219,7 @@
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
"search_popout.search_format": "Formato avançado de pesquisa",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir",
"status.pin": "Fixar no perfil",
"status.pinned": "Pinned toot",
"status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou",
"status.reply": "Responder",
@ -250,7 +255,6 @@
"status.show_more": "Mostrar mais",
"status.unmute_conversation": "Deixar de silenciar esta conversa",
"status.unpin": "Não fixar no perfil",
"tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Блокировать",
"account.block_domain": "Блокировать все с {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Изменить профиль",
"account.follow": "Подписаться",
"account.followers": "Подписаны",
@ -13,6 +15,7 @@
"account.moved_to": "Ищите {name} здесь:",
"account.mute": "Заглушить",
"account.mute_notifications": "Скрыть уведомления от @{name}",
"account.muted": "Muted",
"account.posts": "Посты",
"account.posts_with_replies": "Toots with replies",
"account.report": "Пожаловаться",
@ -216,6 +219,7 @@
"report.target": "Жалуемся на",
"search.placeholder": "Поиск",
"search_popout.search_format": "Продвинутый формат поиска",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "хэштег",
"search_popout.tips.status": "статус",
"search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
@ -238,6 +242,7 @@
"status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус",
"status.pin": "Закрепить в профиле",
"status.pinned": "Pinned toot",
"status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)",
"status.reply": "Ответить",
@ -250,7 +255,6 @@
"status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда",
"status.unpin": "Открепить от профиля",
"tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",
"tabs_bar.local_timeline": "Локальная",

View File

@ -1,20 +1,23 @@
{
"account.block": "Blokovať @{name}",
"account.block_domain": "Ukryť všetko z {domain}",
"account.blocked": "Blokovaný/á",
"account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Upraviť profil",
"account.follow": "Následovať",
"account.followers": "Sledujúci",
"account.follows": "Sledujete",
"account.follows_you": "Následuje vás",
"account.follows_you": "Následuje ťa",
"account.hide_reblogs": "Skryť povýšenia od @{name}",
"account.media": "Médiá",
"account.mention": "Spomeňte @{name}",
"account.moved_to": "{name} sa presunul/a na:",
"account.mute": "Ignorovať @{name}",
"account.mute_notifications": "Stĺmiť notifikácie od @{name}",
"account.muted": "Utíšený/á",
"account.posts": "Hlášky",
"account.posts_with_replies": "Toots with replies",
"account.posts_with_replies": "Príspevky s odpoveďami",
"account.report": "Nahlásiť @{name}",
"account.requested": "Čaká na schválenie. Kliknite pre zrušenie žiadosti",
"account.share": "Zdieľať @{name} profil",
@ -156,8 +159,8 @@
"navigation_bar.preferences": "Možnosti",
"navigation_bar.public_timeline": "Federovaná časová os",
"notification.favourite": "{name} sa páči tvoj status",
"notification.follow": "{name} vás začal(a) sledovať",
"notification.mention": "{name} vás spomenul",
"notification.follow": "{name} ťa začal/a následovať",
"notification.mention": "{name} ťa spomenul/a",
"notification.reblog": "{name} re-tootol tvoj status",
"notifications.clear": "Vyčistiť zoznam notifikácii",
"notifications.clear_confirmation": "Naozaj chcete nenávratne prečistiť všetky vaše notifikácie?",
@ -208,21 +211,22 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Zrušiť",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward": "Posuň ku {target}",
"report.forward_hint": "Tento účet je z iného serveru. Chceš poslať anonymnú kópiu reportu aj tam?",
"report.hint": "Toto nahlásenie bude zaslané správcom servera. Môžeš napísať odvôvodnenie prečo si nahlásil/a tento účet:",
"report.placeholder": "Ďalšie komentáre",
"report.submit": "Poslať",
"report.target": "Nahlásenie {target}",
"search.placeholder": "Hľadať",
"search_popout.search_format": "Pokročilý formát vyhľadávania",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "haštag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Jednoduchý text vráti zhodujúce sa mená, prezývky a hashtagy",
"search_popout.tips.user": "používateľ",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.accounts": "Ľudia",
"search_results.hashtags": "Haštagy",
"search_results.statuses": "Príspevky",
"search_results.total": "{count, number} {count, plural, one {result} ostatné {results}}",
"standalone.public_title": "Pohľad dovnútra...",
"status.block": "Blokovať @{name}",
@ -238,6 +242,7 @@
"status.mute_conversation": "Ignorovať konverzáciu",
"status.open": "Otvoriť tento status",
"status.pin": "Pripnúť na profil",
"status.pinned": "Pripnutý príspevok",
"status.reblog": "Povýšiť",
"status.reblogged_by": "{name} povýšil",
"status.reply": "Odpovedať",
@ -250,7 +255,6 @@
"status.show_more": "Zobraz viac",
"status.unmute_conversation": "Prestať ignorovať konverzáciu",
"status.unpin": "Odopnúť z profilu",
"tabs_bar.compose": "Napísať",
"tabs_bar.federated_timeline": "Federovaná",
"tabs_bar.home": "Domov",
"tabs_bar.local_timeline": "Lokálna",
@ -259,7 +263,7 @@
"upload_area.title": "Ťahaj a pusti pre nahratie",
"upload_button.label": "Pridať médiá",
"upload_form.description": "Opis pre slabo vidiacich",
"upload_form.focus": "Crop",
"upload_form.focus": "Vystrihni",
"upload_form.undo": "Navrátiť",
"upload_progress.label": "Nahráva sa...",
"video.close": "Zavrieť video",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blokiraj korisnika @{name}",
"account.block_domain": "Sakrij sve sa domena {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Navedene informacije možda ne odslikavaju korisnički profil u potpunosti.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Izmeni profil",
"account.follow": "Zaprati",
"account.followers": "Pratioca",
@ -13,6 +15,7 @@
"account.moved_to": "{name} se pomerio na:",
"account.mute": "Ućutkaj korisnika @{name}",
"account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
"account.muted": "Muted",
"account.posts": "Statusa",
"account.posts_with_replies": "Toots with replies",
"account.report": "Prijavi @{name}",
@ -216,6 +219,7 @@
"report.target": "Prijavljujem {target}",
"search.placeholder": "Pretraga",
"search_popout.search_format": "Napredni format pretrage",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hešteg",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Traženjem običnog teksta ćete dobiti sva pronađena imena, sva korisnička imena i sve nađene heštegove",
@ -238,6 +242,7 @@
"status.mute_conversation": "Ućutkaj prepisku",
"status.open": "Proširi ovaj status",
"status.pin": "Prikači na profil",
"status.pinned": "Pinned toot",
"status.reblog": "Podrži",
"status.reblogged_by": "{name} podržao(la)",
"status.reply": "Odgovori",
@ -250,7 +255,6 @@
"status.show_more": "Prikaži više",
"status.unmute_conversation": "Uključi prepisku",
"status.unpin": "Otkači sa profila",
"tabs_bar.compose": "Napiši",
"tabs_bar.federated_timeline": "Federisano",
"tabs_bar.home": "Početna",
"tabs_bar.local_timeline": "Lokalno",

View File

@ -1,7 +1,9 @@
{
"account.block": "Блокирај корисника @{name}",
"account.block_domain": "Сакриј све са домена {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Наведене информације можда не одсликавају кориснички профил у потпуности.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Измени профил",
"account.follow": "Запрати",
"account.followers": "Пратиоца",
@ -13,6 +15,7 @@
"account.moved_to": "{name} се померио на:",
"account.mute": "Ућуткај корисника @{name}",
"account.mute_notifications": "Искључи обавештења од корисника @{name}",
"account.muted": "Muted",
"account.posts": "Статуса",
"account.posts_with_replies": "Toots with replies",
"account.report": "Пријави @{name}",
@ -216,6 +219,7 @@
"report.target": "Пријављујем {target}",
"search.placeholder": "Претрага",
"search_popout.search_format": "Напредни формат претраге",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "хештег",
"search_popout.tips.status": "статус",
"search_popout.tips.text": "Тражењем обичног текста ћете добити сва пронађена имена, сва корисничка имена и све нађене хештегове",
@ -238,6 +242,7 @@
"status.mute_conversation": "Ућуткај преписку",
"status.open": "Прошири овај статус",
"status.pin": "Прикачи на профил",
"status.pinned": "Pinned toot",
"status.reblog": "Подржи",
"status.reblogged_by": "{name} подржао(ла)",
"status.reply": "Одговори",
@ -250,7 +255,6 @@
"status.show_more": "Прикажи више",
"status.unmute_conversation": "Укључи преписку",
"status.unpin": "Откачи са профила",
"tabs_bar.compose": "Напиши",
"tabs_bar.federated_timeline": "Федерисано",
"tabs_bar.home": "Почетна",
"tabs_bar.local_timeline": "Локално",

View File

@ -1,7 +1,9 @@
{
"account.block": "Blockera @{name}",
"account.block_domain": "Dölj allt från {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Informationen nedan kan spegla användarens profil ofullständigt.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Redigera profil",
"account.follow": "Följ",
"account.followers": "Följare",
@ -13,6 +15,7 @@
"account.moved_to": "{name} har flyttat till:",
"account.mute": "Tysta @{name}",
"account.mute_notifications": "Stäng av notifieringar från @{name}",
"account.muted": "Muted",
"account.posts": "Inlägg",
"account.posts_with_replies": "Toots with replies",
"account.report": "Rapportera @{name}",
@ -216,6 +219,7 @@
"report.target": "Rapporterar {target}",
"search.placeholder": "Sök",
"search_popout.search_format": "Avancerat sökformat",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Tysta konversation",
"status.open": "Utvidga denna status",
"status.pin": "Fäst i profil",
"status.pinned": "Pinned toot",
"status.reblog": "Knuff",
"status.reblogged_by": "{name} knuffade",
"status.reply": "Svara",
@ -250,7 +255,6 @@
"status.show_more": "Visa mer",
"status.unmute_conversation": "Öppna konversation",
"status.unpin": "Ångra fäst i profil",
"tabs_bar.compose": "Skriv",
"tabs_bar.federated_timeline": "Förenad",
"tabs_bar.home": "Hem",
"tabs_bar.local_timeline": "Lokal",

View File

@ -1,7 +1,9 @@
{
"account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Edit profile",
"account.follow": "Follow",
"account.followers": "Followers",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Posts",
"account.posts_with_replies": "Toots with replies",
"account.report": "Report @{name}",
@ -216,6 +219,7 @@
"report.target": "Reporting",
"search.placeholder": "Search",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boosted",
"status.reply": "Reply",
@ -250,7 +255,6 @@
"status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",

View File

@ -1,7 +1,9 @@
{
"account.block": "Engelle @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Profili düzenle",
"account.follow": "Takip et",
"account.followers": "Takipçiler",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Sustur @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Gönderiler",
"account.posts_with_replies": "Toots with replies",
"account.report": "Rapor et @{name}",
@ -216,6 +219,7 @@
"report.target": "Raporlama",
"search.placeholder": "Ara",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Bu gönderiyi genişlet",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Boost'la",
"status.reblogged_by": "{name} boost etti",
"status.reply": "Cevapla",
@ -250,7 +255,6 @@
"status.show_more": "Daha fazlası",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Oluştur",
"tabs_bar.federated_timeline": "Federe",
"tabs_bar.home": "Ana sayfa",
"tabs_bar.local_timeline": "Yerel",

View File

@ -1,7 +1,9 @@
{
"account.block": "Заблокувати",
"account.block_domain": "Заглушити {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Налаштування профілю",
"account.follow": "Підписатися",
"account.followers": "Підписники",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Заглушити",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Пости",
"account.posts_with_replies": "Toots with replies",
"account.report": "Поскаржитися",
@ -216,6 +219,7 @@
"report.target": "Скаржимося на",
"search.placeholder": "Пошук",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "Заглушити діалог",
"status.open": "Розгорнути допис",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.reblog": "Передмухнути",
"status.reblogged_by": "{name} передмухнув(-ла)",
"status.reply": "Відповісти",
@ -250,7 +255,6 @@
"status.show_more": "Розгорнути",
"status.unmute_conversation": "Зняти глушення з діалогу",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Написати",
"tabs_bar.federated_timeline": "Глобальна",
"tabs_bar.home": "Головна",
"tabs_bar.local_timeline": "Локальна",

View File

@ -1,7 +1,9 @@
{
"account.block": "屏蔽 @{name}",
"account.block_domain": "隐藏来自 {domain} 的内容",
"account.blocked": "Blocked",
"account.disclaimer_full": "此处显示的信息可能不是全部内容。",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "修改个人资料",
"account.follow": "关注",
"account.followers": "关注者",
@ -13,6 +15,7 @@
"account.moved_to": "{name} 已经迁移到:",
"account.mute": "隐藏 @{name}",
"account.mute_notifications": "隐藏来自 @{name} 的通知",
"account.muted": "Muted",
"account.posts": "嘟文",
"account.posts_with_replies": "Toots with replies",
"account.report": "举报 @{name}",
@ -216,6 +219,7 @@
"report.target": "举报 {target}",
"search.placeholder": "搜索",
"search_popout.search_format": "高级搜索格式",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "话题标签",
"search_popout.tips.status": "嘟文",
"search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
@ -238,6 +242,7 @@
"status.mute_conversation": "隐藏此对话",
"status.open": "展开嘟文",
"status.pin": "在个人资料页面置顶",
"status.pinned": "Pinned toot",
"status.reblog": "转嘟",
"status.reblogged_by": "{name} 转嘟了",
"status.reply": "回复",
@ -250,7 +255,6 @@
"status.show_more": "显示内容",
"status.unmute_conversation": "不再隐藏此对话",
"status.unpin": "在个人资料页面取消置顶",
"tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页",
"tabs_bar.local_timeline": "本站",

View File

@ -1,7 +1,9 @@
{
"account.block": "封鎖 @{name}",
"account.block_domain": "隱藏來自 {domain} 的一切文章",
"account.blocked": "Blocked",
"account.disclaimer_full": "下列資料不一定完整。",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "修改個人資料",
"account.follow": "關注",
"account.followers": "關注的人",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "將 @{name} 靜音",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "文章",
"account.posts_with_replies": "Toots with replies",
"account.report": "舉報 @{name}",
@ -216,6 +219,7 @@
"report.target": "舉報",
"search.placeholder": "搜尋",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "靜音對話",
"status.open": "展開文章",
"status.pin": "置頂到資料頁",
"status.pinned": "Pinned toot",
"status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推",
"status.reply": "回應",
@ -250,7 +255,6 @@
"status.show_more": "顯示更多",
"status.unmute_conversation": "解禁對話",
"status.unpin": "解除置頂",
"tabs_bar.compose": "撰寫",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁",
"tabs_bar.local_timeline": "本站",

View File

@ -1,7 +1,9 @@
{
"account.block": "封鎖 @{name}",
"account.block_domain": "隱藏來自 {domain} 的一切貼文",
"account.blocked": "Blocked",
"account.disclaimer_full": "下列資料不一定完整。",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "編輯用者資訊",
"account.follow": "關注",
"account.followers": "專注者",
@ -13,6 +15,7 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "消音 @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "貼文",
"account.posts_with_replies": "Toots with replies",
"account.report": "檢舉 @{name}",
@ -216,6 +219,7 @@
"report.target": "通報中",
"search.placeholder": "搜尋",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
@ -238,6 +242,7 @@
"status.mute_conversation": "消音對話",
"status.open": "展開這個狀態",
"status.pin": "置頂到個人資訊頁",
"status.pinned": "Pinned toot",
"status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推了",
"status.reply": "回應",
@ -250,7 +255,6 @@
"status.show_more": "看更多",
"status.unmute_conversation": "不消音對話",
"status.unpin": "解除置頂",
"tabs_bar.compose": "編輯",
"tabs_bar.federated_timeline": "聯盟",
"tabs_bar.home": "家",
"tabs_bar.local_timeline": "本地",

View File

@ -102,7 +102,7 @@ const initialState = ImmutableMap();
export default function accounts(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.merge(action.state.get('accounts'));
return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS()));
case ACCOUNT_FETCH_SUCCESS:
case NOTIFICATIONS_UPDATE:
return normalizeAccount(state, action.account);

View File

@ -16,6 +16,8 @@ import {
COMPOSE_SUGGESTIONS_CLEAR,
COMPOSE_SUGGESTIONS_READY,
COMPOSE_SUGGESTION_SELECT,
COMPOSE_SUGGESTION_TAGS_UPDATE,
COMPOSE_TAG_HISTORY_UPDATE,
COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE,
@ -54,6 +56,7 @@ const initialState = ImmutableMap({
default_sensitive: false,
resetFileKey: Math.floor((Math.random() * 0x10000)),
idempotencyKey: null,
tagHistory: ImmutableList(),
});
function statusToTextMentions(state, status) {
@ -87,7 +90,6 @@ function appendMedia(state, media) {
map.update('media_attachments', list => list.push(media));
map.set('is_uploading', false);
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
map.set('focusDate', new Date());
map.set('idempotencyKey', uuid());
@ -98,12 +100,10 @@ function appendMedia(state, media) {
};
function removeMedia(state, mediaId) {
const media = state.get('media_attachments').find(item => item.get('id') === mediaId);
const prevSize = state.get('media_attachments').size;
return state.withMutations(map => {
map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
map.update('text', text => text.replace(media.get('text_url'), '').trim());
map.set('idempotencyKey', uuid());
if (prevSize === 1) {
@ -122,6 +122,18 @@ const insertSuggestion = (state, position, token, completion) => {
});
};
const updateSuggestionTags = (state, token) => {
const prefix = token.slice(1);
return state.merge({
suggestions: state.get('tagHistory')
.filter(tag => tag.startsWith(prefix))
.slice(0, 4)
.map(tag => '#' + tag),
suggestion_token: token,
});
};
const insertEmoji = (state, position, emojiData) => {
const emoji = emojiData.native;
@ -252,6 +264,10 @@ export default function compose(state = initialState, action) {
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
case COMPOSE_SUGGESTION_SELECT:
return insertSuggestion(state, action.position, action.token, action.completion);
case COMPOSE_SUGGESTION_TAGS_UPDATE:
return updateSuggestionTags(state, action.token);
case COMPOSE_TAG_HISTORY_UPDATE:
return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE:
if (action.id === state.get('in_reply_to')) {
return state.set('in_reply_to', null);

View File

@ -0,0 +1,18 @@
import Immutable from 'immutable';
import {
DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE,
} from '../actions/dropdown_menu';
const initialState = Immutable.Map({ openId: null, placement: null });
export default function dropdownMenu(state = initialState, action) {
switch (action.type) {
case DROPDOWN_MENU_OPEN:
return state.merge({ openId: action.id, placement: action.placement });
case DROPDOWN_MENU_CLOSE:
return state.get('openId') === action.id ? state.set('openId', null) : state;
default:
return state;
}
}

View File

@ -1,4 +1,5 @@
import { combineReducers } from 'redux-immutable';
import dropdown_menu from './dropdown_menu';
import timelines from './timelines';
import meta from './meta';
import alerts from './alerts';
@ -26,6 +27,7 @@ import lists from './lists';
import listEditor from './list_editor';
const reducers = {
dropdown_menu,
timelines,
meta,
alerts,

View File

@ -44,3 +44,4 @@ export default class Settings {
}
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
export const tagHistory = new Settings('mastodon_tag_history');

View File

@ -194,6 +194,28 @@ $small-breakpoint: 960px;
}
}
.closed-registrations-message {
margin-top: 20px;
&,
p {
text-align: center;
font-size: 12px;
line-height: 18px;
color: $ui-primary-color;
margin-bottom: 0;
a {
color: $ui-highlight-color;
text-decoration: underline;
}
}
p:last-child {
margin-bottom: 0;
}
}
em {
display: inline;
margin: 0;
@ -832,8 +854,13 @@ $small-breakpoint: 960px;
}
&__features {
& > p {
padding-right: 60px;
}
.features-list {
margin: 40px 0 !important;
margin: 40px 0;
margin-top: 30px;
}
&__action {
@ -842,17 +869,11 @@ $small-breakpoint: 960px;
}
.features-list {
margin-top: 20px;
.features-list__row {
display: flex;
padding: 10px 0;
justify-content: space-between;
&:first-child {
padding-top: 0;
}
.visual {
flex: 0 0 auto;
display: flex;
@ -878,6 +899,14 @@ $small-breakpoint: 960px;
}
}
}
@media screen and (min-width: $small-breakpoint) {
display: grid;
grid-gap: 30px;
grid-template-columns: 1fr 1fr;
grid-auto-columns: 50%;
grid-auto-rows: max-content;
}
}
.extended-description {

View File

@ -105,6 +105,16 @@
margin-bottom: 30px;
}
h4 {
text-transform: uppercase;
font-size: 13px;
font-weight: 500;
color: $ui-primary-color;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
h6 {
font-size: 16px;
color: $ui-secondary-color;

View File

@ -862,12 +862,27 @@
border-bottom: 1px solid $ui-secondary-color;
display: flex;
.status__content {
flex: 1 1 auto;
padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.status-check-box__status {
margin: 10px 0 10px 10px;
flex: 1;
.media-gallery {
max-width: 250px;
}
.status__content {
padding: 0;
white-space: normal;
}
.video-player {
margin-top: 8px;
max-width: 250px;
}
.media-gallery__item-thumbnail {
cursor: default;
}
}
}
@ -1418,36 +1433,29 @@
.image-loader {
position: relative;
width: 100%;
height: 100%;
&.image-loader--loading {
display: flex;
align-content: center;
.image-loader__preview-canvas {
filter: blur(2px);
}
}
.image-loader__img {
position: absolute;
top: 0;
left: 0;
right: 0;
max-width: 100%;
max-height: 100%;
background-image: none;
&.image-loader--amorphous .image-loader__preview-canvas {
display: none;
}
}
&.image-loader--amorphous {
position: static;
.image-loader__preview-canvas {
display: none;
}
.image-loader__img {
position: static;
width: auto;
height: auto;
}
}
.zoomable-image {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-content: center;
}
.navigation-bar {
@ -2784,29 +2792,6 @@ a.status-card {
}
}
.modal-container__nav {
align-items: center;
background: rgba($base-overlay-background, 0.5);
box-sizing: border-box;
border: 0;
color: $primary-text-color;
cursor: pointer;
display: flex;
font-size: 24px;
height: 100%;
padding: 30px 15px;
position: absolute;
top: 0;
}
.modal-container__nav--left {
left: -61px;
}
.modal-container__nav--right {
right: -61px;
}
.account--follows-info {
color: $primary-text-color;
position: absolute;
@ -2823,6 +2808,22 @@ a.status-card {
border-radius: 4px;
}
.account--muting-info {
color: $primary-text-color;
position: absolute;
top: 40px;
left: 10px;
opacity: 0.7;
display: inline-block;
vertical-align: top;
background-color: rgba($base-overlay-background, 0.4);
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
padding: 4px;
border-radius: 4px;
}
.account--action-button {
position: absolute;
top: 10px;
@ -3403,29 +3404,27 @@ a.status-card {
z-index: 9999;
}
.video-modal {
max-width: 100vw;
max-height: 100vh;
position: relative;
}
.media-modal {
max-width: 80vw;
max-height: 80vh;
width: 100%;
height: 100%;
position: relative;
.extended-video-player,
img,
canvas,
video {
max-width: 80vw;
max-height: 80vh;
max-width: 100vw;
max-height: 100vh;
width: auto;
height: auto;
margin: auto;
}
.extended-video-player,
video {
display: flex;
width: 80vw;
height: 80vh;
}
img,
canvas {
display: block;
@ -3434,12 +3433,65 @@ a.status-card {
}
.react-swipeable-view-container {
max-width: 80vw;
width: 100vw;
height: 100%;
}
}
.media-modal__content {
background: $base-overlay-background;
.media-modal__closer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.media-modal__navigation {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
transition: opacity 0.3s linear;
will-change: opacity;
* {
pointer-events: auto;
}
&.media-modal__navigation--hidden {
opacity: 0;
* {
pointer-events: none;
}
}
}
.media-modal__nav {
background: rgba($base-overlay-background, 0.5);
box-sizing: border-box;
border: 0;
color: $primary-text-color;
cursor: pointer;
display: flex;
align-items: center;
font-size: 24px;
height: 20vmax;
margin: auto 0;
padding: 30px 15px;
position: absolute;
top: 0;
bottom: 0;
}
.media-modal__nav--left {
left: 0;
}
.media-modal__nav--right {
right: 0;
}
.media-modal__pagination {
@ -3447,7 +3499,8 @@ a.status-card {
text-align: center;
position: absolute;
left: 0;
bottom: -40px;
bottom: 20px;
pointer-events: none;
}
.media-modal__page-dot {
@ -3471,8 +3524,8 @@ a.status-card {
.media-modal__close {
position: absolute;
right: 4px;
top: 4px;
right: 8px;
top: 8px;
z-index: 100;
}
@ -4169,45 +4222,59 @@ a.status-card {
border-radius: 4px;
margin-top: 14px;
overflow: hidden;
}
.attachment-list__icon {
flex: 0 0 auto;
color: $ui-base-lighter-color;
padding: 8px 18px;
cursor: default;
border-right: 1px solid lighten($ui-base-color, 8%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 26px;
.fa {
display: block;
}
}
.attachment-list__list {
list-style: none;
padding: 4px 0;
padding-left: 8px;
display: flex;
flex-direction: column;
justify-content: center;
li {
display: block;
padding: 4px 0;
}
a {
text-decoration: none;
&__icon {
flex: 0 0 auto;
color: $ui-base-lighter-color;
font-weight: 500;
padding: 8px 18px;
cursor: default;
border-right: 1px solid lighten($ui-base-color, 8%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 26px;
&:hover {
text-decoration: underline;
.fa {
display: block;
}
}
&__list {
list-style: none;
padding: 4px 0;
padding-left: 8px;
display: flex;
flex-direction: column;
justify-content: center;
li {
display: block;
padding: 4px 0;
}
a {
text-decoration: none;
color: $ui-base-lighter-color;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
&.compact {
border: 0;
margin-top: 4px;
.attachment-list__list {
padding: 0;
display: block;
}
.fa {
color: $ui-base-lighter-color;
}
}
}
@ -4613,7 +4680,7 @@ a.status-card {
background-size: cover;
background-position: center;
position: absolute;
color: inherit;
color: $ui-primary-color;
text-decoration: none;
border-radius: 4px;
@ -4621,6 +4688,7 @@ a.status-card {
&:active,
&:focus {
outline: 0;
color: $ui-secondary-color;
&::before {
content: "";
@ -4632,6 +4700,14 @@ a.status-card {
}
}
}
&__icons {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
}
.account__section-headline {

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