mirror of https://framagit.org/tykayn/mastodon.git
Merge pull request #1715 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
2c8bb17453
3
Gemfile
3
Gemfile
|
@ -40,6 +40,7 @@ end
|
|||
gem 'net-ldap', '~> 0.17'
|
||||
gem 'omniauth-cas', '~> 2.0'
|
||||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'gitlab-omniauth-openid-connect', '~>0.5.0', require: 'omniauth_openid_connect'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||
|
||||
|
@ -115,7 +116,7 @@ end
|
|||
group :test do
|
||||
gem 'capybara', '~> 3.36'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.19'
|
||||
gem 'faker', '~> 2.20'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
|
|
55
Gemfile.lock
55
Gemfile.lock
|
@ -68,6 +68,7 @@ GEM
|
|||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
airbrussh (1.4.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
android_key_attestation (0.3.0)
|
||||
|
@ -77,6 +78,7 @@ GEM
|
|||
ast (2.4.2)
|
||||
attr_encrypted (3.1.0)
|
||||
encryptor (~> 3.0.0)
|
||||
attr_required (1.0.1)
|
||||
awrence (1.1.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.558.0)
|
||||
|
@ -126,7 +128,7 @@ GEM
|
|||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (2.0.1)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-rails (1.6.1)
|
||||
capistrano-rails (1.6.2)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (>= 1.1, < 3)
|
||||
capistrano-rbenv (2.2.0)
|
||||
|
@ -210,8 +212,8 @@ GEM
|
|||
tzinfo
|
||||
excon (0.76.0)
|
||||
fabrication (2.27.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faker (2.20.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.9.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
|
@ -260,6 +262,10 @@ GEM
|
|||
fuubar (2.5.1)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
gitlab-omniauth-openid-connect (0.5.0)
|
||||
addressable (~> 2.7)
|
||||
omniauth (~> 1.9)
|
||||
openid_connect (~> 1.2)
|
||||
globalid (1.0.0)
|
||||
activesupport (>= 5.0)
|
||||
hamlit (2.13.0)
|
||||
|
@ -288,10 +294,11 @@ GEM
|
|||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
http_accept_language (2.1.1)
|
||||
httpclient (2.8.3)
|
||||
httplog (1.5.0)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.9.1)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.37)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -308,6 +315,10 @@ GEM
|
|||
jmespath (1.6.0)
|
||||
json (2.5.1)
|
||||
json-canonicalization (0.3.0)
|
||||
json-jwt (1.13.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
bindata
|
||||
json-ld (3.2.0)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.3)
|
||||
|
@ -408,6 +419,16 @@ GEM
|
|||
omniauth-saml (1.10.3)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.9)
|
||||
openid_connect (1.2.0)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
json-jwt (>= 1.5.0)
|
||||
rack-oauth2 (>= 1.6.1)
|
||||
swd (>= 1.0.0)
|
||||
tzinfo
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (>= 1.0.1)
|
||||
openssl (2.2.0)
|
||||
openssl-signature_algorithm (0.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
|
@ -451,6 +472,12 @@ GEM
|
|||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-oauth2 (1.16.0)
|
||||
activesupport
|
||||
attr_required
|
||||
httpclient
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-proxy (0.7.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
|
@ -498,7 +525,7 @@ GEM
|
|||
rdf (~> 3.2)
|
||||
redcarpet (3.5.1)
|
||||
redis (4.5.1)
|
||||
redis-namespace (1.8.1)
|
||||
redis-namespace (1.8.2)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.2.0)
|
||||
request_store (1.5.0)
|
||||
|
@ -606,11 +633,15 @@ GEM
|
|||
sshkit (1.21.2)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.17)
|
||||
stackprof (0.2.19)
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (2.2.1)
|
||||
strong_migrations (0.7.9)
|
||||
activerecord (>= 5)
|
||||
swd (1.2.0)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
temple (0.8.2)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
@ -645,6 +676,12 @@ GEM
|
|||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
validate_url (1.0.13)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webauthn (3.0.0.alpha1)
|
||||
|
@ -657,6 +694,9 @@ GEM
|
|||
safety_net_attestation (~> 0.4.0)
|
||||
securecompare (~> 1.0)
|
||||
tpm-key_attestation (~> 0.9.0)
|
||||
webfinger (1.1.0)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
webmock (3.14.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
|
@ -714,12 +754,13 @@ DEPENDENCIES
|
|||
dotenv-rails (~> 2.7)
|
||||
ed25519 (~> 1.3)
|
||||
fabrication (~> 2.27)
|
||||
faker (~> 2.19)
|
||||
faker (~> 2.20)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
fog-openstack (~> 0.3)
|
||||
fuubar (~> 2.5)
|
||||
gitlab-omniauth-openid-connect (~> 0.5.0)
|
||||
hamlit-rails (~> 0.2)
|
||||
hcaptcha (~> 7.1)
|
||||
hiredis (~> 0.6)
|
||||
|
|
|
@ -57,7 +57,7 @@ class StatusesIndex < Chewy::Index
|
|||
field :id, type: 'long'
|
||||
field :account_id, type: 'long'
|
||||
|
||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.ordered_media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||
field :stemmed, type: 'text', analyzer: 'content'
|
||||
end
|
||||
|
||||
|
|
|
@ -56,10 +56,6 @@ module Admin
|
|||
end
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @domain_block, :show?
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @domain_block, :destroy?
|
||||
UnblockDomainService.new.call(@domain_block)
|
||||
|
|
|
@ -4,28 +4,26 @@ module Admin
|
|||
class InstancesController < BaseController
|
||||
before_action :set_instances, only: :index
|
||||
before_action :set_instance, except: :index
|
||||
before_action :set_exhausted_deliveries_days, only: :show
|
||||
|
||||
def index
|
||||
authorize :instance, :index?
|
||||
preload_delivery_failures!
|
||||
end
|
||||
|
||||
def show
|
||||
authorize :instance, :show?
|
||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize :instance, :destroy?
|
||||
|
||||
Admin::DomainPurgeWorker.perform_async(@instance.domain)
|
||||
|
||||
log_action :destroy, @instance
|
||||
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
|
||||
end
|
||||
|
||||
def clear_delivery_errors
|
||||
authorize :delivery, :clear_delivery_errors?
|
||||
|
||||
@instance.delivery_failure_tracker.clear_failures!
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
end
|
||||
|
@ -33,11 +31,9 @@ module Admin
|
|||
def restart_delivery
|
||||
authorize :delivery, :restart_delivery?
|
||||
|
||||
last_unavailable_domain = unavailable_domain
|
||||
|
||||
if last_unavailable_domain.present?
|
||||
if @instance.unavailable?
|
||||
@instance.delivery_failure_tracker.track_success!
|
||||
log_action :destroy, last_unavailable_domain
|
||||
log_action :destroy, @instance.unavailable_domain
|
||||
end
|
||||
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
|
@ -45,8 +41,7 @@ module Admin
|
|||
|
||||
def stop_delivery
|
||||
authorize :delivery, :stop_delivery?
|
||||
|
||||
UnavailableDomain.create(domain: @instance.domain)
|
||||
unavailable_domain = UnavailableDomain.create!(domain: @instance.domain)
|
||||
log_action :create, unavailable_domain
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
end
|
||||
|
@ -57,12 +52,11 @@ module Admin
|
|||
@instance = Instance.find(params[:id])
|
||||
end
|
||||
|
||||
def set_exhausted_deliveries_days
|
||||
@exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days
|
||||
end
|
||||
|
||||
def set_instances
|
||||
@instances = filtered_instances.page(params[:page])
|
||||
end
|
||||
|
||||
def preload_delivery_failures!
|
||||
warning_domains_map = DeliveryFailureTracker.warning_domains_map
|
||||
|
||||
@instances.each do |instance|
|
||||
|
@ -70,10 +64,6 @@ module Admin
|
|||
end
|
||||
end
|
||||
|
||||
def unavailable_domain
|
||||
UnavailableDomain.find_by(domain: @instance.domain)
|
||||
end
|
||||
|
||||
def filtered_instances
|
||||
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
before_action :set_thread, only: [:create]
|
||||
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
override_rate_limit_headers :update, family: :statuses
|
||||
|
||||
# This API was originally unlimited, pagination cannot be introduced without
|
||||
# breaking backwards-compatibility. Arbitrarily high number to cover most
|
||||
|
|
|
@ -4,8 +4,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
skip_before_action :verify_authenticity_token
|
||||
|
||||
def self.provides_callback_for(provider)
|
||||
provider_id = provider.to_s.chomp '_oauth2'
|
||||
|
||||
define_method provider do
|
||||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||
|
||||
|
@ -20,7 +18,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
)
|
||||
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
|
||||
set_flash_message(:notice, :success, kind: Devise.omniauth_configs[provider].strategy.display_name.capitalize) if is_navigational_format?
|
||||
else
|
||||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||
redirect_to new_user_registration_url
|
||||
|
@ -33,7 +31,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
if resource.email_verified?
|
||||
if resource.email_present?
|
||||
root_path
|
||||
else
|
||||
auth_setup_path(missing_email: '1')
|
||||
|
|
|
@ -241,6 +241,15 @@ module LanguagesHelper
|
|||
code
|
||||
end
|
||||
|
||||
def valid_locale_cascade(*arr)
|
||||
arr.each do |str|
|
||||
locale = valid_locale_or_nil(str)
|
||||
return locale if locale.present?
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def valid_locale?(locale)
|
||||
locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
|
||||
end
|
||||
|
|
|
@ -132,7 +132,7 @@ module StatusesHelper
|
|||
end
|
||||
|
||||
def render_video_component(status, **options)
|
||||
video = status.media_attachments.first
|
||||
video = status.ordered_media_attachments.first
|
||||
|
||||
meta = video.file.meta || {}
|
||||
|
||||
|
@ -150,12 +150,12 @@ module StatusesHelper
|
|||
}.merge(**options)
|
||||
|
||||
react_component :video, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
def render_audio_component(status, **options)
|
||||
audio = status.media_attachments.first
|
||||
audio = status.ordered_media_attachments.first
|
||||
|
||||
meta = audio.file.meta || {}
|
||||
|
||||
|
@ -170,7 +170,7 @@ module StatusesHelper
|
|||
}.merge(**options)
|
||||
|
||||
react_component :audio, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -178,11 +178,11 @@ module StatusesHelper
|
|||
component_params = {
|
||||
sensitive: sensitized?(status, current_account),
|
||||
autoplay: prefers_autoplay?,
|
||||
media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
|
||||
media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
|
||||
}.merge(**options)
|
||||
|
||||
react_component :media_gallery, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -68,12 +68,12 @@ export default class Counter extends React.PureComponent {
|
|||
);
|
||||
} else {
|
||||
const measure = data[0];
|
||||
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||
const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
|
||||
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
|
||||
<span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
|
||||
{measure.previous_total && (<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components';
|
||||
import Bundle from 'flavours/glitch/features/ui/components/bundle';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
export default class MediaAttachments extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
height: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
revealed: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
height: 110,
|
||||
width: 239,
|
||||
};
|
||||
|
||||
updateOnProps = [
|
||||
'status',
|
||||
];
|
||||
|
||||
renderLoadingMediaGallery = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='media-gallery' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingVideoPlayer = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='video-player' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingAudioPlayer = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='audio-player' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, width, height, revealed } = this.props;
|
||||
const mediaAttachments = status.get('media_attachments');
|
||||
|
||||
if (mediaAttachments.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
|
||||
const audio = mediaAttachments.get(0);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
width={width}
|
||||
height={height}
|
||||
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={audio.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={audio.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={audio.getIn(['meta', 'colors', 'accent'])}
|
||||
duration={audio.getIn(['meta', 'original', 'duration'], 0)}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
|
||||
const video = mediaAttachments.get(0);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={width}
|
||||
height={height}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
revealed={revealed}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => (
|
||||
<Component
|
||||
media={mediaAttachments}
|
||||
sensitive={status.get('sensitive')}
|
||||
defaultWidth={width}
|
||||
revealed={revealed}
|
||||
height={height}
|
||||
onOpenMedia={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -40,7 +40,17 @@ class ColumnSettings extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onSelect = mode => value => this.props.onChange(['tags', mode], value);
|
||||
onSelect = mode => value => {
|
||||
const oldValue = this.tags(mode);
|
||||
|
||||
// Prevent changes that add more than 4 tags, but allow removing
|
||||
// tags that were already added before
|
||||
if ((value.length > 4) && !(value < oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange(['tags', mode], value);
|
||||
};
|
||||
|
||||
onToggle = () => {
|
||||
if (this.state.open && this.hasTags()) {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from 'flavours/glitch/components/status_content';
|
||||
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
||||
import Bundle from 'flavours/glitch/features/ui/components/bundle';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import Option from './option';
|
||||
import MediaAttachments from 'flavours/glitch/components/media_attachments';
|
||||
|
||||
export default class StatusCheckBox extends React.PureComponent {
|
||||
|
||||
|
@ -27,53 +25,10 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
render () {
|
||||
const { status, checked } = this.props;
|
||||
|
||||
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')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
revealed={false}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
revealed={false}
|
||||
height={110}
|
||||
onOpenMedia={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelComponent = (
|
||||
<div className='status-check-box__status poll__option__text'>
|
||||
<div className='detailed-status__display-name'>
|
||||
|
@ -84,7 +39,7 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
<div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} media={media} />
|
||||
<StatusContent status={status} media={<MediaAttachments status={status} revealed={false} />} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import escapeTextContentForBrowser from 'escape-html';
|
|||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import MediaAttachments from 'flavours/glitch/components/media_attachments';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
versions: state.getIn(['history', statusId, 'items']),
|
||||
|
@ -70,6 +71,25 @@ class CompareHistoryModal extends React.PureComponent {
|
|||
)}
|
||||
|
||||
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{!!currentVersion.get('poll') && (
|
||||
<div className='poll'>
|
||||
<ul>
|
||||
{currentVersion.getIn(['poll', 'options']).map(option => (
|
||||
<li key={option.get('title')}>
|
||||
<span className='poll__input disabled' />
|
||||
|
||||
<span
|
||||
className='poll__option__text translate'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MediaAttachments status={currentVersion} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -367,6 +367,21 @@ body,
|
|||
}
|
||||
}
|
||||
|
||||
.positive-hint,
|
||||
.negative-hint,
|
||||
.neutral-hint {
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.positive-hint {
|
||||
color: $valid-value-color;
|
||||
font-weight: 500;
|
||||
|
@ -1612,3 +1627,38 @@ a.sparkline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.availability-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
|
||||
&__hint {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
&__graphic {
|
||||
display: flex;
|
||||
margin: 0 -2px;
|
||||
|
||||
&__item {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 4px;
|
||||
height: 21px;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
|
||||
&.positive {
|
||||
background: $valid-value-color;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
background: $error-value-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1563,41 +1563,6 @@ button.icon-button.active i.fa-retweet {
|
|||
filter: none;
|
||||
}
|
||||
|
||||
.compare-history-modal {
|
||||
.report-modal__target {
|
||||
border-bottom: 1px solid $ui-secondary-color;
|
||||
}
|
||||
|
||||
&__container {
|
||||
padding: 30px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.status__content {
|
||||
color: $inverted-text-color;
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
|
||||
.emojione {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: -1px 0 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0.25rem;
|
||||
padding: 0;
|
||||
background-color: $ui-secondary-color;
|
||||
border: 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
background-color: $ui-highlight-color;
|
||||
height: 3px;
|
||||
|
|
|
@ -1041,6 +1041,47 @@
|
|||
}
|
||||
}
|
||||
|
||||
.compare-history-modal {
|
||||
.report-modal__target {
|
||||
border-bottom: 1px solid $ui-secondary-color;
|
||||
}
|
||||
|
||||
&__container {
|
||||
padding: 30px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.status__content {
|
||||
color: $inverted-text-color;
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
|
||||
.emojione {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: -1px 0 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0.25rem;
|
||||
padding: 0;
|
||||
background-color: $ui-secondary-color;
|
||||
border: 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-gallery,
|
||||
.audio-player,
|
||||
.video-player {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-modal {
|
||||
width: auto;
|
||||
max-width: 80vw;
|
||||
|
|
|
@ -538,7 +538,7 @@
|
|||
.media-gallery,
|
||||
.audio-player,
|
||||
.video-player {
|
||||
margin-top: 8px;
|
||||
margin-top: 15px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
|
|
|
@ -229,15 +229,21 @@
|
|||
.mute-modal,
|
||||
.block-modal,
|
||||
.report-modal,
|
||||
.report-dialog-modal,
|
||||
.embed-modal,
|
||||
.error-modal,
|
||||
.onboarding-modal,
|
||||
.report-modal__comment .setting-text__wrapper,
|
||||
.report-modal__comment .setting-text {
|
||||
.report-modal__comment .setting-text,
|
||||
.report-dialog-modal__textarea {
|
||||
background: $white;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.report-dialog-modal .dialog-option .poll__input {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.report-modal__comment {
|
||||
border-right-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.horizontal-table {
|
||||
border-collapse: collapse;
|
||||
border-style: hidden;
|
||||
|
||||
& > tbody > tr > th,
|
||||
& > tbody > tr > td {
|
||||
padding: 11px 10px;
|
||||
background: transparent;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
& > tbody > tr > th {
|
||||
color: $darker-text-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.batch-table {
|
||||
& > thead > tr > th {
|
||||
background: $ui-base-color;
|
||||
|
|
|
@ -68,12 +68,12 @@ export default class Counter extends React.PureComponent {
|
|||
);
|
||||
} else {
|
||||
const measure = data[0];
|
||||
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||
const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
|
||||
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
|
||||
<span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
|
||||
{measure.previous_total && (<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { MediaGallery, Video, Audio } from 'mastodon/features/ui/util/async-components';
|
||||
import Bundle from 'mastodon/features/ui/components/bundle';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
export default class MediaAttachments extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
height: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
height: 110,
|
||||
width: 239,
|
||||
};
|
||||
|
||||
updateOnProps = [
|
||||
'status',
|
||||
];
|
||||
|
||||
renderLoadingMediaGallery = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='media-gallery' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingVideoPlayer = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='video-player' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingAudioPlayer = () => {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className='audio-player' style={{ height, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, width, height } = this.props;
|
||||
const mediaAttachments = status.get('media_attachments');
|
||||
|
||||
if (mediaAttachments.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
|
||||
const audio = mediaAttachments.get(0);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
width={width}
|
||||
height={height}
|
||||
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={audio.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={audio.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={audio.getIn(['meta', 'colors', 'accent'])}
|
||||
duration={audio.getIn(['meta', 'original', 'duration'], 0)}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
|
||||
const video = mediaAttachments.get(0);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={width}
|
||||
height={height}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => (
|
||||
<Component
|
||||
media={mediaAttachments}
|
||||
sensitive={status.get('sensitive')}
|
||||
defaultWidth={width}
|
||||
height={height}
|
||||
onOpenMedia={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -40,7 +40,17 @@ class ColumnSettings extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onSelect = mode => value => this.props.onChange(['tags', mode], value);
|
||||
onSelect = mode => value => {
|
||||
const oldValue = this.tags(mode);
|
||||
|
||||
// Prevent changes that add more than 4 tags, but allow removing
|
||||
// tags that were already added before
|
||||
if ((value.length > 4) && !(value < oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange(['tags', mode], value);
|
||||
};
|
||||
|
||||
onToggle = () => {
|
||||
if (this.state.open && this.hasTags()) {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from 'mastodon/components/status_content';
|
||||
import { MediaGallery, Video } from 'mastodon/features/ui/util/async-components';
|
||||
import Bundle from 'mastodon/features/ui/components/bundle';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import DisplayName from 'mastodon/components/display_name';
|
||||
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||
import Option from './option';
|
||||
import MediaAttachments from 'mastodon/components/media_attachments';
|
||||
|
||||
export default class StatusCheckBox extends React.PureComponent {
|
||||
|
||||
|
@ -27,51 +25,10 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
render () {
|
||||
const { status, checked } = this.props;
|
||||
|
||||
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')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelComponent = (
|
||||
<div className='status-check-box__status poll__option__text'>
|
||||
<div className='detailed-status__display-name'>
|
||||
|
@ -79,12 +36,13 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
<Avatar account={status.get('account')} size={46} />
|
||||
</div>
|
||||
|
||||
<div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
|
||||
<div>
|
||||
<DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} />
|
||||
|
||||
{media}
|
||||
<MediaAttachments status={status} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import escapeTextContentForBrowser from 'escape-html';
|
|||
import InlineAccount from 'mastodon/components/inline_account';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||
import MediaAttachments from 'mastodon/components/media_attachments';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
versions: state.getIn(['history', statusId, 'items']),
|
||||
|
@ -70,6 +71,25 @@ class CompareHistoryModal extends React.PureComponent {
|
|||
)}
|
||||
|
||||
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{!!currentVersion.get('poll') && (
|
||||
<div className='poll'>
|
||||
<ul>
|
||||
{currentVersion.getIn(['poll', 'options']).map(option => (
|
||||
<li key={option.get('title')}>
|
||||
<span className='poll__input disabled' />
|
||||
|
||||
<span
|
||||
className='poll__option__text translate'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MediaAttachments status={currentVersion} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
"account.followers": "Followers",
|
||||
"account.followers.empty": "No one follows this user yet.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
|
||||
"account.following": "Following",
|
||||
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
|
||||
"account.follows.empty": "This user doesn't follow anyone yet.",
|
||||
"account.follows_you": "Follows you",
|
||||
"account.hide_reblogs": "Hide boosts from @{name}",
|
||||
"account.joined": "Joined {date}",
|
||||
"account.last_status": "Last active",
|
||||
"account.link_verified_on": "Ownership of this link was checked on {date}",
|
||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||
"account.media": "Media",
|
||||
|
@ -32,7 +32,6 @@
|
|||
"account.mute": "Mute @{name}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||
"account.muted": "Muted",
|
||||
"account.never_active": "Never",
|
||||
"account.posts": "Toots",
|
||||
"account.posts_with_replies": "Toots and replies",
|
||||
"account.report": "Report @{name}",
|
||||
|
@ -42,10 +41,12 @@
|
|||
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unblock_domain": "Unblock domain {domain}",
|
||||
"account.unblock_short": "Unblock",
|
||||
"account.unendorse": "Don't feature on profile",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account.unmute_short": "Unmute",
|
||||
"account_note.placeholder": "Click to add a note",
|
||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
"account.followers": "المُتابِعون",
|
||||
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم حتى الآن.",
|
||||
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two{مُتابعانِ اِثنان} few{{counter} مُتابِعين} many{{counter} مُتابِعًا} other {{counter} مُتابع}}",
|
||||
"account.following": "Following",
|
||||
"account.following_counter": "{count, plural, zero{لا يُتابِع} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}",
|
||||
"account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.",
|
||||
"account.follows_you": "يُتابِعُك",
|
||||
"account.hide_reblogs": "إخفاء مشاركات @{name}",
|
||||
"account.joined": "انضم في {date}",
|
||||
"account.last_status": "آخر نشاط",
|
||||
"account.link_verified_on": "تمَّ التَّحقق مِن مِلْكيّة هذا الرابط بتاريخ {date}",
|
||||
"account.locked_info": "تمَّ تعيين حالة خصوصية هذا الحساب إلى مُقفَل. يُراجع المالك يدويًا من يمكنه متابعته.",
|
||||
"account.media": "وسائط",
|
||||
|
@ -32,7 +32,6 @@
|
|||
"account.mute": "كَتم @{name}",
|
||||
"account.mute_notifications": "كَتم الإشعارات من @{name}",
|
||||
"account.muted": "مَكتوم",
|
||||
"account.never_active": "أبدًا",
|
||||
"account.posts": "منشورات",
|
||||
"account.posts_with_replies": "المنشورات والرُدود",
|
||||
"account.report": "الإبلاغ عن @{name}",
|
||||
|
@ -42,10 +41,12 @@
|
|||
"account.statuses_counter": "{count, plural, zero {لَا تَبويقات} one {تَبويقةٌ واحدة} two {تَبويقَتانِ اِثنتان} few {{counter} تَبويقات} many {{counter} تَبويقتًا} other {{counter} تَبويقة}}",
|
||||
"account.unblock": "إلغاء الحَظر عن @{name}",
|
||||
"account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}",
|
||||
"account.unblock_short": "Unblock",
|
||||
"account.unendorse": "لا تُرَوِّج لهُ في الملف الشخصي",
|
||||
"account.unfollow": "إلغاء المُتابعة",
|
||||
"account.unmute": "إلغاء الكَتم عن @{name}",
|
||||
"account.unmute_notifications": "إلغاء كَتم الإشعارات عن @{name}",
|
||||
"account.unmute_short": "Unmute",
|
||||
"account_note.placeholder": "اضغط لإضافة مُلاحظة",
|
||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
"account.followers": "Siguidores",
|
||||
"account.followers.empty": "Naide sigue a esti usuariu entá.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
|
||||
"account.following": "Following",
|
||||
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
|
||||
"account.follows.empty": "Esti usuariu entá nun sigue a naide.",
|
||||
"account.follows_you": "Síguete",
|
||||
"account.hide_reblogs": "Anubrir les comparticiones de @{name}",
|
||||
"account.joined": "Joined {date}",
|
||||
"account.last_status": "Cabera actividá",
|
||||
"account.link_verified_on": "La propiedá d'esti enllaz foi comprobada'l {date}",
|
||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||
"account.media": "Media",
|
||||
|
@ -32,7 +32,6 @@
|
|||
"account.mute": "Silenciar a @{name}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||