From 9edab463682f5d5beb0a7f4e4574d708c95bbc52 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 27 Feb 2019 14:57:14 +0100 Subject: Fix mention processing for unknwon accounts on incoming ActivityPub Notes (#10125) `::FetchRemoteAccountService` is not `ActivityPub::FetchRemoteAccountService`, its second argument is the pre-fetched body. Passing `id: false` actually passed a `Hash` as the prefetched body, instead of properly resolving unknown remote accounts. --- app/lib/activitypub/activity/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index d7bd65c80..6d58aba70 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -159,7 +159,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if tag['href'].blank? account = account_from_uri(tag['href']) - account = ::FetchRemoteAccountService.new.call(tag['href'], id: false) if account.nil? + account = ::FetchRemoteAccountService.new.call(tag['href']) if account.nil? return if account.nil? -- cgit From df0120670398246739144466bf327193383ba530 Mon Sep 17 00:00:00 2001 From: trwnh Date: Wed, 27 Feb 2019 08:03:28 -0600 Subject: Allow getting-started to scroll on short screens (#10075) At 480px height, there is not enough space to fully display the footer. --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d88557559..0163e90b9 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2336,6 +2336,7 @@ a.account__display-name { .getting-started { color: $dark-text-color; + overflow: auto; &__footer { flex: 0 0 auto; -- cgit From 6e8743d17a92438bd2332e97f824bc212fa4b96e Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 27 Feb 2019 19:13:16 +0100 Subject: Fix direct timeline pagination in the WebUI (#10126) The `hasMore` property of timelines in redux store was set whenever an API request returned only one page of results, *even* if the query only requested newer conversations (using `since_id`), causing `hasMore` to be incorrectly set to false whenever fetching new toots in the direct timeline, which happens each time the direct message column is opened. (Basically #9516 for direct messages) --- app/javascript/mastodon/actions/conversations.js | 7 +++++-- app/javascript/mastodon/reducers/conversations.js | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js index 3c2ea9680..c6e062ef7 100644 --- a/app/javascript/mastodon/actions/conversations.js +++ b/app/javascript/mastodon/actions/conversations.js @@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); } + const isLoadingRecent = !!params.since_id; + api(getState).get('/api/v1/conversations', { params }) .then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); - dispatch(expandConversationsSuccess(response.data, next ? next.uri : null)); + dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent)); }) .catch(err => dispatch(expandConversationsFail(err))); }; @@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({ type: CONVERSATIONS_FETCH_REQUEST, }); -export const expandConversationsSuccess = (conversations, next) => ({ +export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({ type: CONVERSATIONS_FETCH_SUCCESS, conversations, next, + isLoadingRecent, }); export const expandConversationsFail = error => ({ diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js index 955a07754..9564bffcd 100644 --- a/app/javascript/mastodon/reducers/conversations.js +++ b/app/javascript/mastodon/reducers/conversations.js @@ -35,7 +35,7 @@ const updateConversation = (state, item) => state.update('items', list => { } }); -const expandNormalizedConversations = (state, conversations, next) => { +const expandNormalizedConversations = (state, conversations, next, isLoadingRecent) => { let items = ImmutableList(conversations.map(conversationToMap)); return state.withMutations(mutable => { @@ -66,7 +66,7 @@ const expandNormalizedConversations = (state, conversations, next) => { }); } - if (!next) { + if (!next && !isLoadingRecent) { mutable.set('hasMore', false); } @@ -81,7 +81,7 @@ export default function conversations(state = initialState, action) { case CONVERSATIONS_FETCH_FAIL: return state.set('isLoading', false); case CONVERSATIONS_FETCH_SUCCESS: - return expandNormalizedConversations(state, action.conversations, action.next); + return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent); case CONVERSATIONS_UPDATE: return updateConversation(state, action.conversation); case CONVERSATIONS_MOUNT: -- cgit From 9d3c6f1849120e732a9230959cb302575765ea8c Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 28 Feb 2019 15:22:21 +0100 Subject: Improved remote thread fetching (#10106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fetch up to 5 replies when discovering a new remote status This is used for resolving threads downwards. The originating server must add a “replies” attributes with such replies for it to be useful. * Add some tests for ActivityPub::FetchRepliesWorker * Add specs for ActivityPub::FetchRepliesService * Serialize up to 5 public self-replies for ActivityPub notes * Add specs for ActivityPub::NoteSerializer * Move exponential backoff logic to a worker concern * Fetch first page of paginated collections when fetching thread replies * Add specs for paginated collections in replies * Move Note replies serialization to a first CollectionPage The collection isn't actually paginable yet as it has no id nor a `next` field. This may come in another PR. * Use pluck(:uri) instead of map(&:uri) to improve performances * Fix fetching replies when they are in a CollectionPage --- app/lib/activitypub/activity/create.rb | 10 ++ app/models/concerns/status_threading_concern.rb | 4 + app/presenters/activitypub/collection_presenter.rb | 2 +- .../activitypub/collection_serializer.rb | 5 +- app/serializers/activitypub/note_serializer.rb | 13 +++ app/services/activitypub/fetch_replies_service.rb | 60 ++++++++++ app/workers/activitypub/fetch_replies_worker.rb | 12 ++ app/workers/concerns/exponential_backoff.rb | 11 ++ app/workers/fetch_reply_worker.rb | 12 ++ app/workers/thread_resolve_worker.rb | 5 +- spec/serializers/activitypub/note_spec.rb | 44 ++++++++ .../activitypub/fetch_replies_service_spec.rb | 122 +++++++++++++++++++++ .../activitypub/fetch_replies_worker_spec.rb | 40 +++++++ 13 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 app/services/activitypub/fetch_replies_service.rb create mode 100644 app/workers/activitypub/fetch_replies_worker.rb create mode 100644 app/workers/concerns/exponential_backoff.rb create mode 100644 app/workers/fetch_reply_worker.rb create mode 100644 spec/serializers/activitypub/note_spec.rb create mode 100644 spec/services/activitypub/fetch_replies_service_spec.rb create mode 100644 spec/workers/activitypub/fetch_replies_worker_spec.rb (limited to 'app') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 6d58aba70..0980f94ba 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -40,6 +40,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end resolve_thread(@status) + fetch_replies(@status) distribute(@status) forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? end @@ -213,6 +214,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) end + def fetch_replies(status) + collection = @object['replies'] + return if collection.nil? + replies = ActivityPub::FetchRepliesService.new.call(status, collection, false) + return if replies.present? + uri = value_or_id(collection) + ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil? + end + def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index b9c800c2a..15eb695cd 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -11,6 +11,10 @@ module StatusThreadingConcern find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account, promote: true) end + def self_replies(limit) + account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted]).reorder(id: :asc).limit(limit) + end + private def ancestor_ids(limit) diff --git a/app/presenters/activitypub/collection_presenter.rb b/app/presenters/activitypub/collection_presenter.rb index ec84ab1a3..28331f0c4 100644 --- a/app/presenters/activitypub/collection_presenter.rb +++ b/app/presenters/activitypub/collection_presenter.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ActivityPub::CollectionPresenter < ActiveModelSerializers::Model - attributes :id, :type, :size, :items, :part_of, :first, :last, :next, :prev + attributes :id, :type, :size, :items, :page, :part_of, :first, :last, :next, :prev end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index e8960131b..b03609957 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -7,7 +7,8 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer super end - attributes :id, :type + attribute :id, if: -> { object.id.present? } + attribute :type attribute :total_items, if: -> { object.size.present? } attribute :next, if: -> { object.next.present? } attribute :prev, if: -> { object.prev.present? } @@ -37,6 +38,6 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer end def page? - object.part_of.present? + object.part_of.present? || object.page.present? end end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index c9d23e25f..6b0978ad3 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -13,6 +13,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer has_many :media_attachments, key: :attachment has_many :virtual_tags, key: :tag + has_one :replies, serializer: ActivityPub::CollectionSerializer + def id ActivityPub::TagManager.instance.uri_for(object) end @@ -33,6 +35,17 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer { object.language => Formatter.instance.format(object) } end + def replies + ActivityPub::CollectionPresenter.new( + type: :unordered, + first: ActivityPub::CollectionPresenter.new( + type: :unordered, + page: true, + items: object.self_replies(5).pluck(:uri) + ) + ) + end + def language? object.language.present? end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb new file mode 100644 index 000000000..95c486a43 --- /dev/null +++ b/app/services/activitypub/fetch_replies_service.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class ActivityPub::FetchRepliesService < BaseService + include JsonLdHelper + + def call(parent_status, collection_or_uri, allow_synchronous_requests = true) + @account = parent_status.account + @allow_synchronous_requests = allow_synchronous_requests + + @items = collection_items(collection_or_uri) + return if @items.nil? + + FetchReplyWorker.push_bulk(filtered_replies) + + @items + end + + private + + def collection_items(collection_or_uri) + collection = fetch_collection(collection_or_uri) + return unless collection.is_a?(Hash) + + collection = fetch_collection(collection['first']) if collection['first'].present? + return unless collection.is_a?(Hash) + + case collection['type'] + when 'Collection', 'CollectionPage' + collection['items'] + when 'OrderedCollection', 'OrderedCollectionPage' + collection['orderedItems'] + end + end + + def fetch_collection(collection_or_uri) + return collection_or_uri if collection_or_uri.is_a?(Hash) + return unless @allow_synchronous_requests + return if invalid_origin?(collection_or_uri) + collection = fetch_resource_without_id_validation(collection_or_uri) + raise Mastodon::UnexpectedResponseError if collection.nil? + collection + end + + def filtered_replies + # Only fetch replies to the same server as the original status to avoid + # amplification attacks. + + # Also limit to 5 fetched replies to limit potential for DoS. + @items.map { |item| value_or_id(item) }.reject { |uri| invalid_origin?(uri) }.take(5) + end + + def invalid_origin?(url) + return true if unsupported_uri_scheme?(url) + + needle = Addressable::URI.parse(url).host + haystack = Addressable::URI.parse(@account.uri).host + + !haystack.casecmp(needle).zero? + end +end diff --git a/app/workers/activitypub/fetch_replies_worker.rb b/app/workers/activitypub/fetch_replies_worker.rb new file mode 100644 index 000000000..bf466db54 --- /dev/null +++ b/app/workers/activitypub/fetch_replies_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ActivityPub::FetchRepliesWorker + include Sidekiq::Worker + include ExponentialBackoff + + sidekiq_options queue: 'pull', retry: 3 + + def perform(parent_status_id, replies_uri) + ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri) + end +end diff --git a/app/workers/concerns/exponential_backoff.rb b/app/workers/concerns/exponential_backoff.rb new file mode 100644 index 000000000..f2b931e33 --- /dev/null +++ b/app/workers/concerns/exponential_backoff.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ExponentialBackoff + extend ActiveSupport::Concern + + included do + sidekiq_retry_in do |count| + 15 + 10 * (count**4) + rand(10 * (count**4)) + end + end +end diff --git a/app/workers/fetch_reply_worker.rb b/app/workers/fetch_reply_worker.rb new file mode 100644 index 000000000..f7aa25e81 --- /dev/null +++ b/app/workers/fetch_reply_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class FetchReplyWorker + include Sidekiq::Worker + include ExponentialBackoff + + sidekiq_options queue: 'pull', retry: 3 + + def perform(child_url) + FetchRemoteStatusService.new.call(child_url) + end +end diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index c18a778d5..8bba9ca75 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -2,13 +2,10 @@ class ThreadResolveWorker include Sidekiq::Worker + include ExponentialBackoff sidekiq_options queue: 'pull', retry: 3 - sidekiq_retry_in do |count| - 15 + 10 * (count**4) + rand(10 * (count**4)) - end - def perform(child_status_id, parent_url) child_status = Status.find(child_status_id) parent_status = FetchRemoteStatusService.new.call(parent_url) diff --git a/spec/serializers/activitypub/note_spec.rb b/spec/serializers/activitypub/note_spec.rb new file mode 100644 index 000000000..55bfbc16b --- /dev/null +++ b/spec/serializers/activitypub/note_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ActivityPub::NoteSerializer do + let!(:account) { Fabricate(:account) } + let!(:other) { Fabricate(:account) } + let!(:parent) { Fabricate(:status, account: account, visibility: :public) } + let!(:reply1) { Fabricate(:status, account: account, thread: parent, visibility: :public) } + let!(:reply2) { Fabricate(:status, account: account, thread: parent, visibility: :public) } + let!(:reply3) { Fabricate(:status, account: other, thread: parent, visibility: :public) } + let!(:reply4) { Fabricate(:status, account: account, thread: parent, visibility: :public) } + let!(:reply5) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } + + before(:each) do + @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) + end + + subject { JSON.parse(@serialization.to_json) } + + it 'has a Note type' do + expect(subject['type']).to eql('Note') + end + + it 'has a replies collection' do + expect(subject['replies']['type']).to eql('Collection') + end + + it 'has a replies collection with a first Page' do + expect(subject['replies']['first']['type']).to eql('CollectionPage') + end + + it 'includes public self-replies in its replies collection' do + expect(subject['replies']['first']['items']).to include(reply1.uri, reply2.uri, reply4.uri) + end + + it 'does not include replies from others in its replies collection' do + expect(subject['replies']['first']['items']).to_not include(reply3.uri) + end + + it 'does not include replies with direct visibility in its replies collection' do + expect(subject['replies']['first']['items']).to_not include(reply5.uri) + end +end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb new file mode 100644 index 000000000..65c453341 --- /dev/null +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -0,0 +1,122 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::FetchRepliesService, type: :service do + let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } + let(:status) { Fabricate(:status, account: actor) } + let(:collection_uri) { 'http://example.com/replies/1' } + + let(:items) do + [ + 'http://example.com/self-reply-1', + 'http://example.com/self-reply-2', + 'http://example.com/self-reply-3', + 'http://other.com/other-reply-1', + 'http://other.com/other-reply-2', + 'http://other.com/other-reply-3', + 'http://example.com/self-reply-4', + 'http://example.com/self-reply-5', + 'http://example.com/self-reply-6', + ] + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_uri, + items: items, + }.with_indifferent_access + end + + subject { described_class.new } + + describe '#call' do + context 'when the payload is a Collection with inlined replies' do + context 'when passing the collection itself' do + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, payload) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + + context 'when passing the URL to the collection' do + before do + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + end + + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, collection_uri) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + end + + context 'when the payload is an OrderedCollection with inlined replies' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: collection_uri, + orderedItems: items, + }.with_indifferent_access + end + + context 'when passing the collection itself' do + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, payload) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + + context 'when passing the URL to the collection' do + before do + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + end + + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, collection_uri) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + end + + context 'when the payload is a paginated Collection with inlined replies' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_uri, + first: { + type: 'CollectionPage', + partOf: collection_uri, + items: items, + } + }.with_indifferent_access + end + + context 'when passing the collection itself' do + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, payload) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + + context 'when passing the URL to the collection' do + before do + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + end + + it 'spawns workers for up to 5 replies on the same server' do + allow(FetchReplyWorker).to receive(:push_bulk) + subject.call(status, collection_uri) + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) + end + end + end + end +end diff --git a/spec/workers/activitypub/fetch_replies_worker_spec.rb b/spec/workers/activitypub/fetch_replies_worker_spec.rb new file mode 100644 index 000000000..91ef3c4b9 --- /dev/null +++ b/spec/workers/activitypub/fetch_replies_worker_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ActivityPub::FetchRepliesWorker do + subject { described_class.new } + + let(:account) { Fabricate(:account, uri: 'https://example.com/user/1') } + let(:status) { Fabricate(:status, account: account) } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/statuses_replies/1', + type: 'Collection', + items: [], + } + end + + let(:json) { Oj.dump(payload) } + + describe 'perform' do + it 'performs a request if the collection URI is from the same host' do + stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json) + subject.perform(status.id, 'https://example.com/statuses_replies/1') + expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once + end + + it 'does not perform a request if the collection URI is from a different host' do + stub_request(:get, 'https://other.com/statuses_replies/1').to_return(status: 200) + subject.perform(status.id, 'https://other.com/statuses_replies/1') + expect(a_request(:get, 'https://other.com/statuses_replies/1')).to_not have_been_made + end + + it 'raises when request fails' do + stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 500) + expect { subject.perform(status.id, 'https://example.com/statuses_replies/1') }.to raise_error Mastodon::UnexpectedResponseError + end + end +end -- cgit From d8498b39838eb352a9b268e0f22b1b025320dc73 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 28 Feb 2019 18:16:34 +0100 Subject: Give the `replies` collection an identifier and enable pagination (#10128) --- app/controllers/statuses_controller.rb | 53 ++++++++++++++++++++++++++ app/lib/activitypub/tag_manager.rb | 6 +++ app/serializers/activitypub/note_serializer.rb | 10 +++-- config/routes.rb | 1 + 4 files changed, 67 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 15d59fd89..3686bd9fd 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -18,6 +18,7 @@ class StatusesController < ApplicationController before_action :redirect_to_original, only: [:show] before_action :set_referrer_policy_header, only: [:show] before_action :set_cache_headers + before_action :set_replies, only: [:replies] content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -63,8 +64,37 @@ class StatusesController < ApplicationController render 'stream_entries/embed', layout: 'embedded' end + def replies + skip_session! + + render json: replies_collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json', + skip_activities: true + end + private + def replies_collection_presenter + page = ActivityPub::CollectionPresenter.new( + id: replies_account_status_url(@account, @status, page_params), + type: :unordered, + part_of: replies_account_status_url(@account, @status), + next: next_page, + items: @replies.map { |status| status.local ? status : status.id } + ) + if page_requested? + page + else + ActivityPub::CollectionPresenter.new( + id: replies_account_status_url(@account, @status), + type: :unordered, + first: page + ) + end + end + def create_descendant_thread(starting_depth, statuses) depth = starting_depth + statuses.size if depth < DESCENDANTS_DEPTH_LIMIT @@ -174,4 +204,27 @@ class StatusesController < ApplicationController return if @status.public_visibility? || @status.unlisted_visibility? response.headers['Referrer-Policy'] = 'origin' end + + def page_requested? + params[:page] == 'true' + end + + def set_replies + @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) + end + + def next_page + last_reply = @replies.last + return if last_reply.nil? + same_account = last_reply.account_id == @account.id + return unless same_account || @replies.size == DESCENDANTS_LIMIT + same_account = false unless @replies.size == DESCENDANTS_LIMIT + replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) + end + + def page_params + { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index be3a562d0..892bb9974 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -48,6 +48,12 @@ class ActivityPub::TagManager activity_account_status_url(target.account, target) end + def replies_uri_for(target, page_params = nil) + raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? + + replies_account_status_url(target.account, target, page_params) + end + # Primary audience of a status # Public statuses go out to primarily the public collection # Unlisted and private statuses go out primarily to the followers collection diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 6b0978ad3..4aab993a9 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -13,7 +13,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer has_many :media_attachments, key: :attachment has_many :virtual_tags, key: :tag - has_one :replies, serializer: ActivityPub::CollectionSerializer + has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local? def id ActivityPub::TagManager.instance.uri_for(object) @@ -36,12 +36,16 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def replies + replies = object.self_replies(5).pluck(:id, :uri) + last_id = replies.last&.first ActivityPub::CollectionPresenter.new( type: :unordered, + id: ActivityPub::TagManager.instance.replies_uri_for(object), first: ActivityPub::CollectionPresenter.new( type: :unordered, - page: true, - items: object.self_replies(5).pluck(:uri) + part_of: ActivityPub::TagManager.instance.replies_uri_for(object), + items: replies.map(&:second), + next: last_id ? ActivityPub::TagManager.instance.replies_uri_for(object, page: true, min_id: last_id) : nil ) ) end diff --git a/config/routes.rb b/config/routes.rb index ded62981d..9a83d0f88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,7 @@ Rails.application.routes.draw do member do get :activity get :embed + get :replies end end -- cgit From ee7d79c0ce411ff7754e3ac31dbf27cb9e298f73 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 28 Feb 2019 21:35:16 +0100 Subject: Fix serialization of boosts (#10129) The condition introduced by #9998 was wrong, serializing boosts that weren't self-boosts, and not serializing self-boosts. --- app/serializers/activitypub/activity_serializer.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index b51e8c544..c001e28aa 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -3,8 +3,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer attributes :id, :type, :actor, :published, :to, :cc - has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce? - attribute :proper_uri, key: :object, if: :owned_announce? + has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? + attribute :proper_uri, key: :object, unless: :serialize_object? attribute :atom_uri, if: :announce? def id @@ -43,7 +43,9 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer object.reblog? end - def owned_announce? - announce? && object.account == object.proper.account && object.proper.private_visibility? + def serialize_object? + return true unless announce? + # Serialize private self-boosts of local toots + object.account == object.proper.account && object.proper.private_visibility? && object.local? end end -- cgit From 3e0ed36e8ede7f1994ab9c46c4cb86e613569440 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 1 Mar 2019 11:11:35 +0100 Subject: Fix home timeline perpetually reloading when empty (#10130) Regression from #6876 --- app/javascript/mastodon/features/home_timeline/index.js | 2 +- app/javascript/mastodon/reducers/timelines.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 3ffa7a681..097f91c16 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -16,7 +16,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, - isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null, + isPartial: state.getIn(['timelines', 'home', 'isPartial']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 1f7ece812..38af9cd09 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -29,6 +29,8 @@ const initialTimeline = ImmutableMap({ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); + mMap.set('isPartial', isPartial); + if (!next && !isLoadingRecent) mMap.set('hasMore', false); if (!statuses.isEmpty()) { -- cgit From 12a0dd71beae4f8988adfcaae5455a6cb528d198 Mon Sep 17 00:00:00 2001 From: "Mélanie Chauvel (ariasuni)" Date: Wed, 27 Feb 2019 13:47:27 +0100 Subject: [Glitch] Make the column header of toot/thread view look like the others Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/status/index.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 86c4db283..880372de5 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -54,6 +54,7 @@ const messages = defineMessages({ detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' }, }); const makeMapStateToProps = () => { @@ -453,6 +454,8 @@ export default class Status extends ImmutablePureComponent { return ( -- cgit From 282ac61500a6246593e593ab515dec5807a336d4 Mon Sep 17 00:00:00 2001 From: "Mélanie Chauvel (ariasuni)" Date: Wed, 27 Feb 2019 13:36:40 +0100 Subject: [Glitch] Make the column header of profile view look like the others, too Signed-off-by: Thibaut Girka --- .../account/components/profile_column_header.js | 29 ++++++++++++++++++++++ .../glitch/features/account_gallery/index.js | 4 +-- .../glitch/features/account_timeline/index.js | 4 +-- .../flavours/glitch/features/followers/index.js | 4 +-- .../flavours/glitch/features/following/index.js | 5 ++-- 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/account/components/profile_column_header.js (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js new file mode 100644 index 000000000..32776be75 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ColumnHeader from '../../../components/column_header'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, +}); + +export default @injectIntl +class ProfileColumnHeader extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + }; + + render() { + const { intl } = this.props; + + return ( + + + ) + } +} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index a5fa01444..a9ea5088e 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -6,7 +6,7 @@ import { fetchAccount } from 'flavours/glitch/actions/accounts'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { getAccountGallery } from 'flavours/glitch/selectors'; import MediaItem from './components/media_item'; @@ -113,7 +113,7 @@ export default class AccountGallery extends ImmutablePureComponent { return ( - +
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 6f887a145..415e3be20 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -7,8 +7,8 @@ import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/g import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from './containers/header_container'; -import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; @@ -74,7 +74,7 @@ export default class AccountTimeline extends ImmutablePureComponent { return ( - + } diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index a977142ed..124004cb6 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -11,9 +11,9 @@ import { import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; import LoadMore from 'flavours/glitch/components/load_more'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ @@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent { return ( - +
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index 70aeefaad..bf8fd2262 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -11,9 +11,9 @@ import { import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; import LoadMore from 'flavours/glitch/components/load_more'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ @@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent { return ( - +
@@ -94,5 +94,4 @@ export default class Following extends ImmutablePureComponent { ); } - } -- cgit From 2a4ce7458a16c64029842fde210089453be2ede1 Mon Sep 17 00:00:00 2001 From: "Mélanie Chauvel (ariasuni)" Date: Wed, 27 Feb 2019 13:38:27 +0100 Subject: [Glitch] Fix errors found by eslint Signed-off-by: Thibaut Girka --- .../glitch/features/account/components/profile_column_header.js | 8 ++++---- app/javascript/flavours/glitch/features/following/index.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js index 32776be75..1a6abef37 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ColumnHeader from '../../../components/column_header'; -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, @@ -22,8 +22,8 @@ class ProfileColumnHeader extends React.PureComponent { icon='user-circle' title={intl.formatMessage(messages.profile)} showBackButton - > - - ) + /> + ); } + } diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index bf8fd2262..656100dad 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -94,4 +94,5 @@ export default class Following extends ImmutablePureComponent { ); } + } -- cgit From 99dc212ae5d7b2527d835744bf903293398ce946 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 3 Mar 2019 15:38:47 +0100 Subject: Fix lists export (#10136) --- app/models/export.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/export.rb b/app/models/export.rb index fc4bb6964..9bf866d35 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -23,7 +23,7 @@ class Export def to_lists_csv CSV.generate do |csv| - account.owned_lists.select(:title).each do |list| + account.owned_lists.select(:title, :id).each do |list| list.accounts.select(:username, :domain).each do |account| csv << [list.title, acct(account)] end -- cgit From 230a012f0090c496fc5cdb011bcc8ed732fd0f5c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 22:18:23 +0100 Subject: Add polls (#10111) * Add polls Fix #1629 * Add tests * Fixes * Change API for creating polls * Use name instead of content for votes * Remove poll validation for remote polls * Add polls to public pages * When updating the poll, update options just in case they were changed * Fix public pages showing both poll and other media --- app/controllers/api/v1/polls/votes_controller.rb | 29 +++++ app/controllers/api/v1/polls_controller.rb | 13 ++ app/controllers/api/v1/statuses_controller.rb | 18 ++- app/javascript/mastodon/actions/importer/index.js | 19 ++- .../mastodon/actions/importer/normalizer.js | 4 + app/javascript/mastodon/actions/polls.js | 53 ++++++++ app/javascript/mastodon/components/poll.js | 144 +++++++++++++++++++++ app/javascript/mastodon/components/status.js | 5 +- .../mastodon/containers/media_container.js | 6 +- .../mastodon/containers/poll_container.js | 8 ++ .../features/status/components/detailed_status.js | 5 +- app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/polls.js | 19 +++ app/javascript/styles/application.scss | 1 + app/javascript/styles/mastodon/components.scss | 4 + app/javascript/styles/mastodon/polls.scss | 95 ++++++++++++++ app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/activity/create.rb | 38 +++++- app/models/concerns/account_associations.rb | 1 + app/models/poll.rb | 90 +++++++++++++ app/models/poll_vote.rb | 29 +++++ app/models/status.rb | 11 ++ app/policies/poll_policy.rb | 7 + app/serializers/activitypub/note_serializer.rb | 65 +++++++++- app/serializers/activitypub/vote_serializer.rb | 48 +++++++ app/serializers/rest/poll_serializer.rb | 38 ++++++ app/serializers/rest/status_serializer.rb | 1 + .../activitypub/fetch_remote_poll_service.rb | 51 ++++++++ app/services/post_status_service.rb | 11 +- app/services/vote_service.rb | 40 ++++++ app/validators/poll_validator.rb | 19 +++ app/validators/vote_validator.rb | 13 ++ .../stream_entries/_detailed_status.html.haml | 4 +- app/views/stream_entries/_simple_status.html.haml | 4 +- config/locales/en.yml | 10 ++ config/routes.rb | 4 + db/migrate/20190225031541_create_polls.rb | 17 +++ db/migrate/20190225031625_create_poll_votes.rb | 11 ++ .../20190226003449_add_poll_id_to_statuses.rb | 5 + db/schema.rb | 33 ++++- spec/controllers/api/v1/filter_controller_spec.rb | 87 ------------- spec/controllers/api/v1/filters_controller_spec.rb | 87 +++++++++++++ .../api/v1/polls/votes_controller_spec.rb | 34 +++++ spec/controllers/api/v1/polls_controller_spec.rb | 23 ++++ spec/fabricators/poll_fabricator.rb | 8 ++ spec/fabricators/poll_vote_fabricator.rb | 5 + spec/models/poll_spec.rb | 5 + spec/models/poll_vote_spec.rb | 5 + 48 files changed, 1125 insertions(+), 106 deletions(-) create mode 100644 app/controllers/api/v1/polls/votes_controller.rb create mode 100644 app/controllers/api/v1/polls_controller.rb create mode 100644 app/javascript/mastodon/actions/polls.js create mode 100644 app/javascript/mastodon/components/poll.js create mode 100644 app/javascript/mastodon/containers/poll_container.js create mode 100644 app/javascript/mastodon/reducers/polls.js create mode 100644 app/javascript/styles/mastodon/polls.scss create mode 100644 app/models/poll.rb create mode 100644 app/models/poll_vote.rb create mode 100644 app/policies/poll_policy.rb create mode 100644 app/serializers/activitypub/vote_serializer.rb create mode 100644 app/serializers/rest/poll_serializer.rb create mode 100644 app/services/activitypub/fetch_remote_poll_service.rb create mode 100644 app/services/vote_service.rb create mode 100644 app/validators/poll_validator.rb create mode 100644 app/validators/vote_validator.rb create mode 100644 db/migrate/20190225031541_create_polls.rb create mode 100644 db/migrate/20190225031625_create_poll_votes.rb create mode 100644 db/migrate/20190226003449_add_poll_id_to_statuses.rb delete mode 100644 spec/controllers/api/v1/filter_controller_spec.rb create mode 100644 spec/controllers/api/v1/filters_controller_spec.rb create mode 100644 spec/controllers/api/v1/polls/votes_controller_spec.rb create mode 100644 spec/controllers/api/v1/polls_controller_spec.rb create mode 100644 spec/fabricators/poll_fabricator.rb create mode 100644 spec/fabricators/poll_vote_fabricator.rb create mode 100644 spec/models/poll_spec.rb create mode 100644 spec/models/poll_vote_spec.rb (limited to 'app') diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb new file mode 100644 index 000000000..3fa0b6a76 --- /dev/null +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Polls::VotesController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:statuses' } + before_action :require_user! + before_action :set_poll + + respond_to :json + + def create + VoteService.new.call(current_account, @poll, vote_params[:choices]) + render json: @poll, serializer: REST::PollSerializer + end + + private + + def set_poll + @poll = Poll.attached.find(params[:poll_id]) + authorize @poll.status, :show? + rescue Mastodon::NotPermittedError + raise ActiveRecord::RecordNotFound + end + + def vote_params + params.permit(choices: []) + end +end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb new file mode 100644 index 000000000..4f4a6858d --- /dev/null +++ b/app/controllers/api/v1/polls_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Api::V1::PollsController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show + + respond_to :json + + def show + @poll = Poll.attached.find(params[:id]) + ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale? + render json: @poll, serializer: REST::PollSerializer, include_results: true + end +end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 29b420c67..f9506971a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController visibility: status_params[:visibility], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, + poll: status_params[:poll], idempotency: request.headers['Idempotency-Key']) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer @@ -73,12 +74,25 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end def status_params - params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, media_ids: []) + params.permit( + :status, + :in_reply_to_id, + :sensitive, + :spoiler_text, + :visibility, + :scheduled_at, + media_ids: [], + poll: [ + :multiple, + :hide_totals, + :expires_in, + options: [], + ] + ) end def pagination_params(core_params) diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 931711f4b..13ad5d1e1 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,11 +1,10 @@ -// import { autoPlayGif } from '../../initial_state'; -// import { putAccounts, putStatuses } from '../../storage/modifier'; import { normalizeAccount, normalizeStatus } from './normalizer'; -export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; -export const STATUS_IMPORT = 'STATUS_IMPORT'; +export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -29,6 +28,10 @@ export function importStatuses(statuses) { return { type: STATUSES_IMPORT, statuses }; } +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) { } accounts.forEach(processAccount); - //putAccounts(normalAccounts, !autoPlayGif); return importAccounts(normalAccounts); } @@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) { return (dispatch, getState) => { const accounts = []; const normalStatuses = []; + const polls = []; function processStatus(status) { pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); @@ -66,12 +69,16 @@ export function importFetchedStatuses(statuses) { if (status.reblog && status.reblog.id) { processStatus(status.reblog); } + + if (status.poll && status.poll.id) { + pushUnique(polls, status.poll); + } } statuses.forEach(processStatus); - //putStatuses(normalStatuses); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); + dispatch(importPolls(polls)); }; } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 34a4150fa..3085cd537 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.reblog = status.reblog.id; } + if (status.poll && status.poll.id) { + normalStatus.poll = status.poll.id; + } + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer if (normalOldStatus) { diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js new file mode 100644 index 000000000..bee4c48a6 --- /dev/null +++ b/app/javascript/mastodon/actions/polls.js @@ -0,0 +1,53 @@ +import api from '../api'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch, getState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => dispatch(voteSuccess(data))) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch, getState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then(({ data }) => dispatch(fetchPollSuccess(data))) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js new file mode 100644 index 000000000..d4b9f283a --- /dev/null +++ b/app/javascript/mastodon/components/poll.js @@ -0,0 +1,144 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { vote, fetchPoll } from 'mastodon/actions/polls'; +import Motion from 'mastodon/features/ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; + +const messages = defineMessages({ + moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, + seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, + minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, + hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, + days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, +}); + +const SECOND = 1000; +const MINUTE = 1000 * 60; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; + +const timeRemainingString = (intl, date, now) => { + const delta = date.getTime() - now; + + let relativeTime; + + if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.moments); + } else if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); + } else { + relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); + } + + return relativeTime; +}; + +export default @injectIntl +class Poll extends ImmutablePureComponent { + + static propTypes = { + poll: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func, + disabled: PropTypes.bool, + }; + + state = { + selected: {}, + }; + + handleOptionChange = e => { + const { target: { value } } = e; + + if (this.props.poll.get('multiple')) { + const tmp = { ...this.state.selected }; + tmp[value] = true; + this.setState({ selected: tmp }); + } else { + const tmp = {}; + tmp[value] = true; + this.setState({ selected: tmp }); + } + }; + + handleVote = () => { + if (this.props.disabled) { + return; + } + + this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected))); + }; + + handleRefresh = () => { + if (this.props.disabled) { + return; + } + + this.props.dispatch(fetchPoll(this.props.poll.get('id'))); + }; + + renderOption (option, optionIndex) { + const { poll } = this.props; + const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const showResults = poll.get('voted') || poll.get('expired'); + + return ( +
  • + {showResults && ( + + {({ width }) => + + } + + )} + + +
  • + ); + } + + render () { + const { poll, intl } = this.props; + const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const showResults = poll.get('voted') || poll.get('expired'); + const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + + return ( +
    +
      + {poll.get('options').map((option, i) => this.renderOption(option, i))} +
    + +
    + {!showResults && } + {showResults && !this.props.disabled && · } + · {timeRemaining} +
    +
    + ); + } + +} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 6270d3c92..e10faedf8 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -16,6 +16,7 @@ import { MediaGallery, Video } from '../features/ui/util/async-components'; import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import PollContainer from 'mastodon/containers/poll_container'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -270,7 +271,9 @@ class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (status.get('media_attachments').size > 0) { + if (status.get('poll')) { + media = ; + } else if (status.get('media_attachments').size > 0) { if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = ( { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; - const { media, card, ...props } = JSON.parse(component.getAttribute('data-props')); + const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props')); Object.assign(props, { ...(media ? { media: fromJS(media) } : {}), ...(card ? { card: fromJS(card) } : {}), + ...(poll ? { poll: fromJS(poll) } : {}), ...(componentName === 'Video' ? { onOpenVideo: this.handleOpenVideo, diff --git a/app/javascript/mastodon/containers/poll_container.js b/app/javascript/mastodon/containers/poll_container.js new file mode 100644 index 000000000..cd7216de7 --- /dev/null +++ b/app/javascript/mastodon/containers/poll_container.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import Poll from 'mastodon/components/poll'; + +const mapStateToProps = (state, { pollId }) => ({ + poll: state.getIn(['polls', pollId]), +}); + +export default connect(mapStateToProps)(Poll); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 49bc43a7b..5cd50f055 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -14,6 +14,7 @@ import Video from '../../video'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import PollContainer from 'mastodon/containers/poll_container'; export default class DetailedStatus extends ImmutablePureComponent { @@ -105,7 +106,9 @@ export default class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } - if (status.get('media_attachments').size > 0) { + if (status.get('poll')) { + media = ; + } else if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = ; } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 0f0de849f..a7e9c4d0f 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -29,6 +29,7 @@ import listAdder from './list_adder'; import filters from './filters'; import conversations from './conversations'; import suggestions from './suggestions'; +import polls from './polls'; const reducers = { dropdown_menu, @@ -61,6 +62,7 @@ const reducers = { filters, conversations, suggestions, + polls, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/polls.js b/app/javascript/mastodon/reducers/polls.js new file mode 100644 index 000000000..53d9b1d8c --- /dev/null +++ b/app/javascript/mastodon/reducers/polls.js @@ -0,0 +1,19 @@ +import { POLL_VOTE_SUCCESS, POLL_FETCH_SUCCESS } from 'mastodon/actions/polls'; +import { POLLS_IMPORT } from 'mastodon/actions/importer'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll)))); + +const initialState = ImmutableMap(); + +export default function polls(state = initialState, action) { + switch(action.type) { + case POLLS_IMPORT: + return importPolls(state, action.polls); + case POLL_VOTE_SUCCESS: + case POLL_FETCH_SUCCESS: + return importPolls(state, [action.poll]); + default: + return state; + } +} diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 4bce74187..6db3bc3dc 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -16,6 +16,7 @@ @import 'mastodon/stream_entries'; @import 'mastodon/boost'; @import 'mastodon/components'; +@import 'mastodon/polls'; @import 'mastodon/introduction'; @import 'mastodon/modal'; @import 'mastodon/emoji_picker'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0163e90b9..ceb28dd19 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -105,6 +105,10 @@ border-color: lighten($ui-primary-color, 4%); color: lighten($darker-text-color, 4%); } + + &:disabled { + opacity: 0.5; + } } &.button--block { diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss new file mode 100644 index 000000000..b93d36e92 --- /dev/null +++ b/app/javascript/styles/mastodon/polls.scss @@ -0,0 +1,95 @@ +.poll { + margin-top: 16px; + font-size: 14px; + + li { + margin-bottom: 10px; + position: relative; + } + + &__chart { + position: absolute; + top: 0; + left: 0; + height: 100%; + display: inline-block; + border-radius: 4px; + background: darken($ui-primary-color, 14%); + + &.leading { + background: $ui-highlight-color; + } + } + + &__text { + position: relative; + display: inline-block; + padding: 6px 0; + line-height: 18px; + cursor: default; + + input[type=radio], + input[type=checkbox] { + display: none; + } + + &.selectable { + cursor: pointer; + } + } + + &__input { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + margin-right: 10px; + top: -1px; + border-radius: 4px; + vertical-align: middle; + + &.active { + border-color: $valid-value-color; + background: $valid-value-color; + } + } + + &__number { + display: inline-block; + width: 36px; + font-weight: 700; + padding: 0 10px; + text-align: right; + } + + &__footer { + padding-top: 6px; + padding-bottom: 5px; + color: $dark-text-color; + } + + &__link { + display: inline; + background: transparent; + padding: 0; + margin: 0; + border: 0; + color: $dark-text-color; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + .button { + height: 36px; + padding: 0 16px; + margin-right: 10px; + font-size: 14px; + } +} diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 11fa3363a..54b175613 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -4,7 +4,7 @@ class ActivityPub::Activity include JsonLdHelper include Redisable - SUPPORTED_TYPES = %w(Note).freeze + SUPPORTED_TYPES = %w(Note Question).freeze CONVERTED_TYPES = %w(Image Video Article Page).freeze def initialize(json, account, **options) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 0980f94ba..793e20dbe 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -6,7 +6,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity RedisLock.acquire(lock_options) do |lock| if lock.acquired? - return if delete_arrived_first?(object_uri) + return if delete_arrived_first?(object_uri) || poll_vote? @status = find_existing_status @@ -68,6 +68,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), + owned_poll: process_poll, } end end @@ -209,6 +210,41 @@ class ActivityPub::Activity::Create < ActivityPub::Activity media_attachments end + def process_poll + return unless @object['type'] == 'Question' && (@object['anyOf'].is_a?(Array) || @object['oneOf'].is_a?(Array)) + + expires_at = begin + if @object['closed'].is_a?(String) + @object['closed'] + elsif !@object['closed'].is_a?(FalseClass) + Time.now.utc + else + @object['endTime'] + end + end + + if @object['anyOf'].is_a?(Array) + multiple = true + items = @object['anyOf'] + else + multiple = false + items = @object['oneOf'] + end + + Poll.new( + account: @account, + multiple: multiple, + expires_at: expires_at, + options: items.map { |item| item['name'].presence || item['content'] }, + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + ) + end + + def poll_vote? + return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) + replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name'])) + end + def resolve_thread(status) return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri) ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 397ec4a22..a8ba8fef1 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -26,6 +26,7 @@ module AccountAssociations # Media has_many :media_attachments, dependent: :destroy + has_many :polls, dependent: :destroy # PuSH subscriptions has_many :subscriptions, dependent: :destroy diff --git a/app/models/poll.rb b/app/models/poll.rb new file mode 100644 index 000000000..ba0b17f91 --- /dev/null +++ b/app/models/poll.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: polls +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# status_id :bigint(8) +# expires_at :datetime +# options :string default([]), not null, is an Array +# cached_tallies :bigint(8) default([]), not null, is an Array +# multiple :boolean default(FALSE), not null +# hide_totals :boolean default(FALSE), not null +# votes_count :bigint(8) default(0), not null +# last_fetched_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class Poll < ApplicationRecord + include Expireable + + belongs_to :account + belongs_to :status + + has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy + + validates :options, presence: true + validates :expires_at, presence: true, if: :local? + validates_with PollValidator, if: :local? + + scope :attached, -> { where.not(status_id: nil) } + scope :unattached, -> { where(status_id: nil) } + + before_validation :prepare_votes_count + after_initialize :prepare_cached_tallies + after_commit :reset_parent_cache, on: :update + + def loaded_options + options.map.with_index { |title, key| Option.new(self, key.to_s, title, cached_tallies[key]) } + end + + def unloaded_options + options.map.with_index { |title, key| Option.new(self, key.to_s, title, nil) } + end + + def possibly_stale? + remote? && last_fetched_before_expiration? && time_passed_since_last_fetch? + end + + delegate :local?, to: :account + + def remote? + !local? + end + + class Option < ActiveModelSerializers::Model + attributes :id, :title, :votes_count, :poll + + def initialize(poll, id, title, votes_count) + @poll = poll + @id = id + @title = title + @votes_count = votes_count + end + end + + private + + def prepare_cached_tallies + self.cached_tallies = options.map { 0 } if cached_tallies.empty? + end + + def prepare_votes_count + self.votes_count = cached_tallies.sum unless cached_tallies.empty? + end + + def reset_parent_cache + return if status_id.nil? + Rails.cache.delete("statuses/#{status_id}") + end + + def last_fetched_before_expiration? + last_fetched_at.nil? || expires_at.nil? || last_fetched_at < expires_at + end + + def time_passed_since_last_fetch? + last_fetched_at.nil? || last_fetched_at < 1.minute.ago + end +end diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb new file mode 100644 index 000000000..57781d616 --- /dev/null +++ b/app/models/poll_vote.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: poll_votes +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# poll_id :bigint(8) +# choice :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class PollVote < ApplicationRecord + belongs_to :account + belongs_to :poll, inverse_of: :votes + + validates :choice, presence: true + validates_with VoteValidator + + after_create_commit :increment_counter_cache + + private + + def increment_counter_cache + poll.cached_tallies[choice] = (poll.cached_tallies[choice] || 0) + 1 + poll.save + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 035423b40..db3c130de 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -21,6 +21,7 @@ # account_id :bigint(8) not null # application_id :bigint(8) # in_reply_to_account_id :bigint(8) +# poll_id :bigint(8) # class Status < ApplicationRecord @@ -44,6 +45,7 @@ class Status < ApplicationRecord belongs_to :account, inverse_of: :statuses belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true belongs_to :conversation, optional: true + belongs_to :poll, optional: true belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true @@ -61,6 +63,7 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity, inverse_of: :status has_one :status_stat, inverse_of: :status + has_one :owned_poll, class_name: 'Poll', inverse_of: :status, dependent: :destroy validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } @@ -101,6 +104,7 @@ class Status < ApplicationRecord :tags, :preview_cards, :stream_entry, + :poll, account: :account_stat, active_mentions: { account: :account_stat }, reblog: [ @@ -111,6 +115,7 @@ class Status < ApplicationRecord :media_attachments, :conversation, :status_stat, + :poll, account: :account_stat, active_mentions: { account: :account_stat }, ], @@ -250,6 +255,8 @@ class Status < ApplicationRecord before_validation :set_conversation before_validation :set_local + before_save :set_poll_id + class << self def selectable_visibilities visibilities.keys - %w(direct limited) @@ -438,6 +445,10 @@ class Status < ApplicationRecord self.reblog = reblog.reblog if reblog? && reblog.reblog? end + def set_poll_id + self.poll_id = owned_poll.id unless owned_poll.nil? + end + def set_visibility self.visibility = (account.locked? ? :private : :public) if visibility.nil? self.visibility = reblog.visibility if reblog? diff --git a/app/policies/poll_policy.rb b/app/policies/poll_policy.rb new file mode 100644 index 000000000..0d839f240 --- /dev/null +++ b/app/policies/poll_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PollPolicy < ApplicationPolicy + def vote? + !current_account.blocking?(record.account) && !record.account.blocking?(current_account) + end +end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 4aab993a9..b2c92fdc1 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -15,12 +15,18 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local? + has_many :poll_loaded_options, key: :one_of, if: :poll_and_not_multiple? + has_many :poll_loaded_options, key: :any_of, if: :poll_and_multiple? + + attribute :end_time, if: :poll_and_expires? + attribute :closed, if: :poll_and_expired? + def id ActivityPub::TagManager.instance.uri_for(object) end def type - 'Note' + object.poll ? 'Question' : 'Note' end def summary @@ -38,6 +44,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer def replies replies = object.self_replies(5).pluck(:id, :uri) last_id = replies.last&.first + ActivityPub::CollectionPresenter.new( type: :unordered, id: ActivityPub::TagManager.instance.replies_uri_for(object), @@ -114,6 +121,32 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer object.account.local? end + def poll_loaded_options + object.poll.loaded_options + end + + def poll_and_multiple? + object.poll&.multiple? + end + + def poll_and_not_multiple? + object.poll && !object.poll.multiple? + end + + def closed + object.poll.expires_at.iso8601 + end + + alias end_time closed + + def poll_and_expires? + object.poll&.expires_at&.present? + end + + def poll_and_expired? + object.poll&.expired? + end + class MediaAttachmentSerializer < ActiveModel::Serializer include RoutingHelper @@ -181,4 +214,34 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer class CustomEmojiSerializer < ActivityPub::EmojiSerializer end + + class OptionSerializer < ActiveModel::Serializer + class RepliesSerializer < ActiveModel::Serializer + attributes :type, :total_items + + def type + 'Collection' + end + + def total_items + object.votes_count + end + end + + attributes :type, :name + + has_one :replies, serializer: ActivityPub::NoteSerializer::OptionSerializer::RepliesSerializer + + def type + 'Note' + end + + def name + object.title + end + + def replies + object + end + end end diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb new file mode 100644 index 000000000..5489fbcd3 --- /dev/null +++ b/app/serializers/activitypub/vote_serializer.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class ActivityPub::VoteSerializer < ActiveModel::Serializer + class NoteSerializer < ActiveModel::Serializer + attributes :id, :type, :name, :attributed_to, + :in_reply_to, :to + + def id + nil + end + + def type + 'Note' + end + + def name + object.poll.options[object.choice.to_i] + end + + def attributed_to + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.uri_for(object.poll.account) + end + end + + attributes :id, :type, :actor, :to + + has_one :object, serializer: ActivityPub::VoteSerializer::NoteSerializer + + def id + nil + end + + def type + 'Create' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + ActivityPub::TagManager.instance.uri_for(object.poll.account) + end +end diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb new file mode 100644 index 000000000..b02e8ca93 --- /dev/null +++ b/app/serializers/rest/poll_serializer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class REST::PollSerializer < ActiveModel::Serializer + attributes :id, :expires_at, :expired, + :multiple, :votes_count + + has_many :dynamic_options, key: :options + + attribute :voted, if: :current_user? + + def id + object.id.to_s + end + + def dynamic_options + if !object.expired? && object.hide_totals? + object.unloaded_options + else + object.loaded_options + end + end + + def expired + object.expired? + end + + def voted + object.votes.where(account: current_user.account).exists? + end + + def current_user? + !current_user.nil? + end + + class OptionSerializer < ActiveModel::Serializer + attributes :title, :votes_count + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 66e19be56..30edf397b 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -21,6 +21,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :emojis, serializer: REST::CustomEmojiSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer + has_one :poll, serializer: REST::PollSerializer def id object.id.to_s diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb new file mode 100644 index 000000000..6f0ac5624 --- /dev/null +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class ActivityPub::FetchRemotePollService < BaseService + include JsonLdHelper + + def call(poll, on_behalf_of = nil) + @json = fetch_resource(poll.status.uri, true, on_behalf_of) + + return unless supported_context? && expected_type? + + expires_at = begin + if @json['closed'].is_a?(String) + @json['closed'] + elsif !@json['closed'].is_a?(FalseClass) + Time.now.utc + else + @json['endTime'] + end + end + + items = begin + if @json['anyOf'].is_a?(Array) + @json['anyOf'] + else + @json['oneOf'] + end + end + + latest_options = items.map { |item| item['name'].presence || item['content'] } + + # If for some reasons the options were changed, it invalidates all previous + # votes, so we need to remove them + poll.votes.delete_all if latest_options != poll.options + + poll.update!( + expires_at: expires_at, + options: latest_options, + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + ) + end + + private + + def supported_context? + super(@json) + end + + def expected_type? + equals_or_includes_any?(@json['type'], 'Question') + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 686b10c58..aed680672 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -15,6 +15,7 @@ class PostStatusService < BaseService # @option [String] :spoiler_text # @option [String] :language # @option [String] :scheduled_at + # @option [Hash] :poll Optional poll to attach # @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Doorkeeper::Application] :application # @option [String] :idempotency Optional idempotency key @@ -28,6 +29,7 @@ class PostStatusService < BaseService return idempotency_duplicate if idempotency_given? && idempotency_duplicate? validate_media! + validate_poll! preprocess_attributes! if scheduled? @@ -93,13 +95,19 @@ class PostStatusService < BaseService def validate_media! return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) - raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll_id].present? @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?) end + def validate_poll! + return if @options[:poll].blank? + + @poll = @account.polls.new(@options[:poll]) + end + def language_from_option(str) ISO_639.find(str)&.alpha2 end @@ -152,6 +160,7 @@ class PostStatusService < BaseService text: @text, media_attachments: @media || [], thread: @in_reply_to, + owned_poll: @poll, sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?, spoiler_text: @options[:spoiler_text] || '', visibility: @visibility, diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb new file mode 100644 index 000000000..8bab2810e --- /dev/null +++ b/app/services/vote_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class VoteService < BaseService + include Authorization + + def call(account, poll, choices) + authorize_with account, poll, :vote? + + @account = account + @poll = poll + @choices = choices + @votes = [] + + ApplicationRecord.transaction do + @choices.each do |choice| + @votes << @poll.votes.create!(account: @account, choice: choice) + end + end + + return if @poll.account.local? + + @votes.each do |vote| + ActivityPub::DeliveryWorker.perform_async( + build_json(vote), + @account.id, + @poll.account.inbox_url + ) + end + end + + private + + def build_json(vote) + ActiveModelSerializers::SerializableResource.new( + vote, + serializer: ActivityPub::VoteSerializer, + adapter: ActivityPub::Adapter + ).to_json + end +end diff --git a/app/validators/poll_validator.rb b/app/validators/poll_validator.rb new file mode 100644 index 000000000..a1c4f6851 --- /dev/null +++ b/app/validators/poll_validator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class PollValidator < ActiveModel::Validator + MAX_OPTIONS = 4 + MAX_OPTION_CHARS = 25 + MAX_EXPIRATION = 7.days.freeze + MIN_EXPIRATION = 1.day.freeze + + def validate(poll) + current_time = Time.now.utc + + poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1 + poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS + poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS } + poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size + poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time >= MAX_EXPIRATION + poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && poll.expires_at - current_time <= MIN_EXPIRATION + end +end diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb new file mode 100644 index 000000000..e2a68d1f3 --- /dev/null +++ b/app/validators/vote_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class VoteValidator < ActiveModel::Validator + def validate(vote) + vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll.expired? + + if vote.poll.multiple? && vote.poll.votes.where(account: vote.account, choice: vote.choice).exists? + vote.errors.add(:base, I18n.t('polls.errors.already_voted')) + elsif vote.poll.votes.where(account: vote.account).exists? + vote.errors.add(:base, I18n.t('polls.errors.already_voted')) + end + end +end diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index e123d657f..b9327a546 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,7 +22,9 @@ %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if !status.media_attachments.empty? + - if status.poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json + - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index b0b6e80c8..a000c02f4 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -26,7 +26,9 @@ %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if !status.media_attachments.empty? + - if status.poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json + - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do diff --git a/config/locales/en.yml b/config/locales/en.yml index 7363a7457..b1f6166b4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -734,6 +734,16 @@ en: older: Older prev: Prev truncate: "…" + polls: + errors: + already_voted: You have already voted on this poll + duplicate_options: contain duplicate items + duration_too_long: is too far into the future + duration_too_short: is too soon + expired: The poll has already ended + over_character_limit: cannot be longer than %{MAX} characters each + too_few_options: must have more than one item + too_many_options: can't contain more than %{MAX} items preferences: languages: Languages other: Other diff --git a/config/routes.rb b/config/routes.rb index 9a83d0f88..ac10f9fba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -362,6 +362,10 @@ Rails.application.routes.draw do resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' end + resources :polls, only: [:create, :show] do + resources :votes, only: :create, controller: 'polls/votes' + end + namespace :push do resource :subscription, only: [:create, :show, :update, :destroy] end diff --git a/db/migrate/20190225031541_create_polls.rb b/db/migrate/20190225031541_create_polls.rb new file mode 100644 index 000000000..ea9ad0425 --- /dev/null +++ b/db/migrate/20190225031541_create_polls.rb @@ -0,0 +1,17 @@ +class CreatePolls < ActiveRecord::Migration[5.2] + def change + create_table :polls do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.belongs_to :status, foreign_key: { on_delete: :cascade } + t.datetime :expires_at + t.string :options, null: false, array: true, default: [] + t.bigint :cached_tallies, null: false, array: true, default: [] + t.boolean :multiple, null: false, default: false + t.boolean :hide_totals, null: false, default: false + t.bigint :votes_count, null: false, default: 0 + t.datetime :last_fetched_at + + t.timestamps + end + end +end diff --git a/db/migrate/20190225031625_create_poll_votes.rb b/db/migrate/20190225031625_create_poll_votes.rb new file mode 100644 index 000000000..a0849d3a5 --- /dev/null +++ b/db/migrate/20190225031625_create_poll_votes.rb @@ -0,0 +1,11 @@ +class CreatePollVotes < ActiveRecord::Migration[5.2] + def change + create_table :poll_votes do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.belongs_to :poll, foreign_key: { on_delete: :cascade } + t.integer :choice, null: false, default: 0 + + t.timestamps + end + end +end diff --git a/db/migrate/20190226003449_add_poll_id_to_statuses.rb b/db/migrate/20190226003449_add_poll_id_to_statuses.rb new file mode 100644 index 000000000..692e8f814 --- /dev/null +++ b/db/migrate/20190226003449_add_poll_id_to_statuses.rb @@ -0,0 +1,5 @@ +class AddPollIdToStatuses < ActiveRecord::Migration[5.2] + def change + add_column :statuses, :poll_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index e9fb358f8..d5d516e06 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_02_03_180359) do +ActiveRecord::Schema.define(version: 2019_02_26_003449) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -441,6 +441,32 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at" end + create_table "poll_votes", force: :cascade do |t| + t.bigint "account_id" + t.bigint "poll_id" + t.integer "choice", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_poll_votes_on_account_id" + t.index ["poll_id"], name: "index_poll_votes_on_poll_id" + end + + create_table "polls", force: :cascade do |t| + t.bigint "account_id" + t.bigint "status_id" + t.datetime "expires_at" + t.string "options", default: [], null: false, array: true + t.bigint "cached_tallies", default: [], null: false, array: true + t.boolean "multiple", default: false, null: false + t.boolean "hide_totals", default: false, null: false + t.bigint "votes_count", default: 0, null: false + t.datetime "last_fetched_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_polls_on_account_id" + t.index ["status_id"], name: "index_polls_on_status_id" + end + create_table "preview_cards", force: :cascade do |t| t.string "url", default: "", null: false t.string "title", default: "", null: false @@ -581,6 +607,7 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do t.bigint "account_id", null: false t.bigint "application_id" t.bigint "in_reply_to_account_id" + t.bigint "poll_id" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc } t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" @@ -746,6 +773,10 @@ ActiveRecord::Schema.define(version: 2019_02_03_180359) do add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id", name: "fk_f5fc4c1ee3", on_delete: :cascade add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id", name: "fk_e84df68546", on_delete: :cascade add_foreign_key "oauth_applications", "users", column: "owner_id", name: "fk_b0988c7c0a", on_delete: :cascade + add_foreign_key "poll_votes", "accounts", on_delete: :cascade + add_foreign_key "poll_votes", "polls", on_delete: :cascade + add_foreign_key "polls", "accounts", on_delete: :cascade + add_foreign_key "polls", "statuses", on_delete: :cascade add_foreign_key "report_notes", "accounts", on_delete: :cascade add_foreign_key "report_notes", "reports", on_delete: :cascade add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filter_controller_spec.rb deleted file mode 100644 index 5948809e3..000000000 --- a/spec/controllers/api/v1/filter_controller_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::V1::FiltersController, type: :controller do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - let(:scopes) { 'read:filters' } - let!(:filter) { Fabricate(:custom_filter, account: user.account) } - - it 'returns http success' do - get :index - expect(response).to have_http_status(200) - end - end - - describe 'POST #create' do - let(:scopes) { 'write:filters' } - - before do - post :create, params: { phrase: 'magic', context: %w(home), irreversible: true } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates a filter' do - filter = user.account.custom_filters.first - expect(filter).to_not be_nil - expect(filter.phrase).to eq 'magic' - expect(filter.context).to eq %w(home) - expect(filter.irreversible?).to be true - expect(filter.expires_at).to be_nil - end - end - - describe 'GET #show' do - let(:scopes) { 'read:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - - it 'returns http success' do - get :show, params: { id: filter.id } - expect(response).to have_http_status(200) - end - end - - describe 'PUT #update' do - let(:scopes) { 'write:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - - before do - put :update, params: { id: filter.id, phrase: 'updated' } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'updates the filter' do - expect(filter.reload.phrase).to eq 'updated' - end - end - - describe 'DELETE #destroy' do - let(:scopes) { 'write:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - - before do - delete :destroy, params: { id: filter.id } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'removes the filter' do - expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound - end - end -end diff --git a/spec/controllers/api/v1/filters_controller_spec.rb b/spec/controllers/api/v1/filters_controller_spec.rb new file mode 100644 index 000000000..5948809e3 --- /dev/null +++ b/spec/controllers/api/v1/filters_controller_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' + +RSpec.describe Api::V1::FiltersController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + let(:scopes) { 'read:filters' } + let!(:filter) { Fabricate(:custom_filter, account: user.account) } + + it 'returns http success' do + get :index + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + let(:scopes) { 'write:filters' } + + before do + post :create, params: { phrase: 'magic', context: %w(home), irreversible: true } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates a filter' do + filter = user.account.custom_filters.first + expect(filter).to_not be_nil + expect(filter.phrase).to eq 'magic' + expect(filter.context).to eq %w(home) + expect(filter.irreversible?).to be true + expect(filter.expires_at).to be_nil + end + end + + describe 'GET #show' do + let(:scopes) { 'read:filters' } + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + it 'returns http success' do + get :show, params: { id: filter.id } + expect(response).to have_http_status(200) + end + end + + describe 'PUT #update' do + let(:scopes) { 'write:filters' } + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + before do + put :update, params: { id: filter.id, phrase: 'updated' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'updates the filter' do + expect(filter.reload.phrase).to eq 'updated' + end + end + + describe 'DELETE #destroy' do + let(:scopes) { 'write:filters' } + let(:filter) { Fabricate(:custom_filter, account: user.account) } + + before do + delete :destroy, params: { id: filter.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes the filter' do + expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound + end + end +end diff --git a/spec/controllers/api/v1/polls/votes_controller_spec.rb b/spec/controllers/api/v1/polls/votes_controller_spec.rb new file mode 100644 index 000000000..0ee3aa040 --- /dev/null +++ b/spec/controllers/api/v1/polls/votes_controller_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Polls::VotesController, type: :controller do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'write:statuses' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + before { allow(controller).to receive(:doorkeeper_token) { token } } + + describe 'POST #create' do + let(:poll) { Fabricate(:poll) } + + before do + post :create, params: { poll_id: poll.id, choices: %w(1) } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates a vote' do + vote = poll.votes.where(account: user.account).first + + expect(vote).to_not be_nil + expect(vote.choice).to eq 1 + end + + it 'updates poll tallies' do + expect(poll.reload.cached_tallies).to eq [0, 1] + end + end +end diff --git a/spec/controllers/api/v1/polls_controller_spec.rb b/spec/controllers/api/v1/polls_controller_spec.rb new file mode 100644 index 000000000..2b8d5f3ef --- /dev/null +++ b/spec/controllers/api/v1/polls_controller_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe Api::V1::PollsController, type: :controller do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'read:statuses' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + before { allow(controller).to receive(:doorkeeper_token) { token } } + + describe 'GET #show' do + let(:poll) { Fabricate(:poll) } + + before do + get :show, params: { id: poll.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/fabricators/poll_fabricator.rb b/spec/fabricators/poll_fabricator.rb new file mode 100644 index 000000000..746610f7c --- /dev/null +++ b/spec/fabricators/poll_fabricator.rb @@ -0,0 +1,8 @@ +Fabricator(:poll) do + account + status + expires_at { 7.days.from_now } + options %w(Foo Bar) + multiple false + hide_totals false +end diff --git a/spec/fabricators/poll_vote_fabricator.rb b/spec/fabricators/poll_vote_fabricator.rb new file mode 100644 index 000000000..51f9b006e --- /dev/null +++ b/spec/fabricators/poll_vote_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:poll_vote) do + account + poll + choice 0 +end diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb new file mode 100644 index 000000000..666f8ca68 --- /dev/null +++ b/spec/models/poll_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Poll, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/poll_vote_spec.rb b/spec/models/poll_vote_spec.rb new file mode 100644 index 000000000..354afd535 --- /dev/null +++ b/spec/models/poll_vote_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe PollVote, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end -- cgit From 8fe93b0701ea754fe8727b1d4ef11f7a33903f81 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 23:41:30 +0100 Subject: Fix vote validation for polls with multiple choices (#10138) --- app/validators/vote_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb index e2a68d1f3..2e1818bdb 100644 --- a/app/validators/vote_validator.rb +++ b/app/validators/vote_validator.rb @@ -6,7 +6,7 @@ class VoteValidator < ActiveModel::Validator if vote.poll.multiple? && vote.poll.votes.where(account: vote.account, choice: vote.choice).exists? vote.errors.add(:base, I18n.t('polls.errors.already_voted')) - elsif vote.poll.votes.where(account: vote.account).exists? + elsif !vote.poll.multiple? && vote.poll.votes.where(account: vote.account).exists? vote.errors.add(:base, I18n.t('polls.errors.already_voted')) end end -- cgit From 26c56d0c10ca036291d8b08b34f971f981217e8c Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 3 Mar 2019 23:44:52 +0100 Subject: Insert polls in redux stores before statuses so it avoids crashes (#10140) --- app/javascript/mastodon/actions/importer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 13ad5d1e1..abadee817 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -77,8 +77,8 @@ export function importFetchedStatuses(statuses) { statuses.forEach(processStatus); + dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); - dispatch(importPolls(polls)); }; } -- cgit From 5dfa4336985616cf5652de2f1cf794d8f740424e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 23:45:02 +0100 Subject: Fix web UI crash on page load when detailed status has a poll (#10139) --- app/javascript/mastodon/components/poll.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index d4b9f283a..c18ee1505 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -45,7 +45,7 @@ export default @injectIntl class Poll extends ImmutablePureComponent { static propTypes = { - poll: ImmutablePropTypes.map.isRequired, + poll: ImmutablePropTypes.map, intl: PropTypes.object.isRequired, dispatch: PropTypes.func, disabled: PropTypes.bool, @@ -122,9 +122,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); - const showResults = poll.get('voted') || poll.get('expired'); - const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + + if (!poll) { + return null; + } + + const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const showResults = poll.get('voted') || poll.get('expired'); + const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); return (
    -- cgit From e13d3792f322c6313fde10d639b13bc31723ec63 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 00:39:06 +0100 Subject: Make sure the poll is created before storing its id (#10142) * Make sure the poll is created before storing its id * Fix updating poll results * Support fetching Question activities from the search bar --- app/models/status.rb | 4 ++-- app/services/activitypub/fetch_remote_poll_service.rb | 2 +- app/services/resolve_url_service.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/models/status.rb b/app/models/status.rb index db3c130de..74deeeb50 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -255,7 +255,7 @@ class Status < ApplicationRecord before_validation :set_conversation before_validation :set_local - before_save :set_poll_id + after_create :set_poll_id class << self def selectable_visibilities @@ -446,7 +446,7 @@ class Status < ApplicationRecord end def set_poll_id - self.poll_id = owned_poll.id unless owned_poll.nil? + update_column(:poll_id, owned_poll.id) unless owned_poll.nil? end def set_visibility diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index 6f0ac5624..ea75e8ef9 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -46,6 +46,6 @@ class ActivityPub::FetchRemotePollService < BaseService end def expected_type? - equals_or_includes_any?(@json['type'], 'Question') + equals_or_includes_any?(@json['type'], %w(Question)) end end diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index ed0c56923..b98759bf6 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -20,7 +20,7 @@ class ResolveURLService < BaseService def process_url if equals_or_includes_any?(type, %w(Application Group Organization Person Service)) FetchRemoteAccountService.new.call(atom_url, body, protocol) - elsif equals_or_includes_any?(type, %w(Note Article Image Video Page)) + elsif equals_or_includes_any?(type, %w(Note Article Image Video Page Question)) FetchRemoteStatusService.new.call(atom_url, body, protocol) end end -- cgit From f821eca3b3ed5f3fe8d1656a3ed6d6d2c0435f96 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 00:40:21 +0100 Subject: Correctly make polls and media mutually exclusive (#10141) --- app/services/post_status_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index aed680672..c045a553e 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -95,7 +95,7 @@ class PostStatusService < BaseService def validate_media! return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) - raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll_id].present? + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present? @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) -- cgit From ae1b9cf70a5c7426054947bef8cc836fd402c173 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 00:44:34 +0100 Subject: Fix remote poll expiration time (#10144) --- app/lib/activitypub/activity/create.rb | 2 +- app/services/activitypub/fetch_remote_poll_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 793e20dbe..08c46be44 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -216,7 +216,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity expires_at = begin if @object['closed'].is_a?(String) @object['closed'] - elsif !@object['closed'].is_a?(FalseClass) + elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass) Time.now.utc else @object['endTime'] diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index ea75e8ef9..2f40625d6 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -11,7 +11,7 @@ class ActivityPub::FetchRemotePollService < BaseService expires_at = begin if @json['closed'].is_a?(String) @json['closed'] - elsif !@json['closed'].is_a?(FalseClass) + elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass) Time.now.utc else @json['endTime'] -- cgit From 878a75ba215e061c91516057f79a5dc96b84f426 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 00:50:56 +0100 Subject: Fix typo in ActivityPub::FetchRemotePollService (#10145) --- app/services/activitypub/fetch_remote_poll_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index 2f40625d6..3701f8339 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -11,7 +11,7 @@ class ActivityPub::FetchRemotePollService < BaseService expires_at = begin if @json['closed'].is_a?(String) @json['closed'] - elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass) + elsif !@json['closed'].nil? && !@object['closed'].is_a?(FalseClass) Time.now.utc else @json['endTime'] -- cgit From e6900b167b046c846d1276f5bf86b675542bc09b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 00:52:18 +0100 Subject: Fix another typo in ActivityPub::FetchRemotePollService (#10146) --- app/services/activitypub/fetch_remote_poll_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index 3701f8339..c87a2f84d 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -11,7 +11,7 @@ class ActivityPub::FetchRemotePollService < BaseService expires_at = begin if @json['closed'].is_a?(String) @json['closed'] - elsif !@json['closed'].nil? && !@object['closed'].is_a?(FalseClass) + elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass) Time.now.utc else @json['endTime'] -- cgit From 3cf98aac660aaed9acd533cac07c0fc092bbec5a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 01:06:19 +0100 Subject: Fix missing in_reply_to in ActivityPub::VoteSerializer (#10148) --- app/serializers/activitypub/vote_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index 5489fbcd3..ba1bd7b1d 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -21,6 +21,10 @@ class ActivityPub::VoteSerializer < ActiveModel::Serializer ActivityPub::TagManager.instance.uri_for(object.account) end + def in_reply_to + ActivityPub::TagManager.instance.uri_for(object.poll.status) + end + def to ActivityPub::TagManager.instance.uri_for(object.poll.account) end -- cgit From 0e6998da3cdc0ac73845d1c3c3c4c75972ea28ee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 01:13:42 +0100 Subject: Add tests for ActivityPub poll processing (#10143) --- app/lib/activitypub/activity/create.rb | 3 +- spec/lib/activitypub/activity/create_spec.rb | 42 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 08c46be44..fc4c45692 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -231,8 +231,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity items = @object['oneOf'] end - Poll.new( - account: @account, + @account.polls.new( multiple: multiple, expires_at: expires_at, options: items.map { |item| item['name'].presence || item['content'] }, diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 26cb84871..ac6237c86 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Create do - let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') } + let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') } let(:json) do { @@ -407,6 +407,46 @@ RSpec.describe ActivityPub::Activity::Create do expect(status).to_not be_nil end end + + context 'with poll' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Question', + content: 'Which color was the submarine?', + oneOf: [ + { + name: 'Yellow', + replies: { + type: 'Collection', + totalItems: 10, + }, + }, + { + name: 'Blue', + replies: { + type: 'Collection', + totalItems: 3, + } + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.poll).to_not be_nil + end + + it 'creates a poll' do + poll = sender.polls.first + expect(poll).to_not be_nil + expect(poll.status).to_not be_nil + expect(poll.options).to eq %w(Yellow Blue) + expect(poll.cached_tallies).to eq [10, 3] + end + end end context 'when sender is followed by local users' do -- cgit From 1a7de769a318bbb9c01ec520c2033fffee4e89c3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Mar 2019 01:53:58 +0100 Subject: Fix ActivityPub votes having nil IDs (#10151) --- app/serializers/activitypub/vote_serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index ba1bd7b1d..655d04d22 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::VoteSerializer < ActiveModel::Serializer :in_reply_to, :to def id - nil + [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join end def type @@ -35,7 +35,7 @@ class ActivityPub::VoteSerializer < ActiveModel::Serializer has_one :object, serializer: ActivityPub::VoteSerializer::NoteSerializer def id - nil + [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id, '/activity'].join end def type -- cgit From 4ced609497bc736cb2b1aec921ba5ca7a23a7f53 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 01:54:14 +0100 Subject: Fixes to the polls UI (#10150) * Allow unselecting choices in multiple choice polls * Properly disable checkboxes/radio buttons for polls in public pages * Visually differentiate checkboxes and radio buttons --- app/javascript/mastodon/components/poll.js | 19 ++++++++++++------- app/javascript/styles/mastodon/polls.scss | 6 +++++- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index c18ee1505..45ce107aa 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -60,7 +60,11 @@ class Poll extends ImmutablePureComponent { if (this.props.poll.get('multiple')) { const tmp = { ...this.state.selected }; - tmp[value] = true; + if (tmp[value]) { + delete tmp[value]; + } else { + tmp[value] = true; + } this.setState({ selected: tmp }); } else { const tmp = {}; @@ -86,11 +90,11 @@ class Poll extends ImmutablePureComponent { }; renderOption (option, optionIndex) { - const { poll } = this.props; - const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + const { poll, disabled } = this.props; + const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const showResults = poll.get('voted') || poll.get('expired'); return (
  • @@ -109,9 +113,10 @@ class Poll extends ImmutablePureComponent { value={optionIndex} checked={active} onChange={this.handleOptionChange} + disabled={disabled} /> - {!showResults && } + {!showResults && } {showResults && {Math.floor(percent)}%} {option.get('title')} diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index b93d36e92..f42496559 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -47,9 +47,13 @@ height: 18px; margin-right: 10px; top: -1px; - border-radius: 4px; + border-radius: 50%; vertical-align: middle; + &.checkbox { + border-radius: 4px; + } + &.active { border-color: $valid-value-color; background: $valid-value-color; -- cgit From 3de71887d849103ed62e8b04b54c630763881010 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 18:03:12 +0100 Subject: Add non-JS fallback for polls on public pages (#10155) --- .../stream_entries/_detailed_status.html.haml | 3 ++- app/views/stream_entries/_poll.html.haml | 25 ++++++++++++++++++++++ app/views/stream_entries/_simple_status.html.haml | 3 ++- config/locales/en.yml | 5 +++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 app/views/stream_entries/_poll.html.haml (limited to 'app') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index b9327a546..b19d2452a 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -23,7 +23,8 @@ .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'stream_entries/poll', locals: { poll: status.poll } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml new file mode 100644 index 000000000..974aff9bd --- /dev/null +++ b/app/views/stream_entries/_poll.html.haml @@ -0,0 +1,25 @@ +- options = (!poll.expired? && poll.hide_totals?) ? poll.unloaded_options : poll.loaded_options +- voted = poll.votes.where(account: current_user.account).exists? +- show_results = voted || poll.expired? + +.poll + %ul + - options.each do |option| + %li + - if show_results + - percent = 100 * option.votes_count / poll.votes_count + %span.poll__chart{ style: "width: #{percent}%" } + %label.poll__text>< + %span.poll__number= percent + = option.title + - else + %label.poll__text>< + %span.poll__input{ class: poll.multiple ? 'checkbox' : nil}>< + = option.title + .poll__footer + - unless show_results + %button.button.button-secondary{ disabled: true } + = t('statuses.poll.vote') + %span= t('statuses.poll.total_votes', count: poll.votes_count) + · + %span= poll.expires_at diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index a000c02f4..68e48edbb 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -27,7 +27,8 @@ .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'stream_entries/poll', locals: { poll: status.poll } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/config/locales/en.yml b/config/locales/en.yml index b1f6166b4..2d23b1eb6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -853,6 +853,11 @@ en: ownership: Someone else's toot cannot be pinned private: Non-public toot cannot be pinned reblog: A boost cannot be pinned + poll: + total_votes: + one: "%{count} vote" + other: "%{count} votes" + vote: Vote show_more: Show more sign_in_to_participate: Sign in to participate in the conversation title: '%{name}: "%{quote}"' -- cgit From f2a1b8b96b031db3a479de029a29e04e28c55352 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:46:38 +0100 Subject: Widen allowed time windows for polls (#10162) --- app/validators/poll_validator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/validators/poll_validator.rb b/app/validators/poll_validator.rb index a1c4f6851..d4ae4c16a 100644 --- a/app/validators/poll_validator.rb +++ b/app/validators/poll_validator.rb @@ -3,8 +3,8 @@ class PollValidator < ActiveModel::Validator MAX_OPTIONS = 4 MAX_OPTION_CHARS = 25 - MAX_EXPIRATION = 7.days.freeze - MIN_EXPIRATION = 1.day.freeze + MAX_EXPIRATION = 1.month.freeze + MIN_EXPIRATION = 5.minutes.freeze def validate(poll) current_time = Time.now.utc -- cgit From 7a25bb858a2a7f3662d2ad2a8cba9ac7ea141aca Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:47:47 +0100 Subject: Ensure only people allowed to see the poll can actually vote (#10161) --- app/policies/poll_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/policies/poll_policy.rb b/app/policies/poll_policy.rb index 0d839f240..9d69eb5bb 100644 --- a/app/policies/poll_policy.rb +++ b/app/policies/poll_policy.rb @@ -2,6 +2,6 @@ class PollPolicy < ApplicationPolicy def vote? - !current_account.blocking?(record.account) && !record.account.blocking?(current_account) + StatusPolicy.new(current_account, record.status).show? && !current_account.blocking?(record.account) && !record.account.blocking?(current_account) end end -- cgit From 833ffce2df68ae3b673e230fcb273da5d8c4681f Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:51:23 +0100 Subject: Store remote votes URI (#10158) * Store remote votes URI * Add spec for accepting remote votes * Make poll vote id generation work the same way as follows --- app/lib/activitypub/activity/create.rb | 2 +- app/models/poll_vote.rb | 3 +++ app/serializers/activitypub/vote_serializer.rb | 2 +- db/migrate/20190304152020_add_uri_to_poll_votes.rb | 5 +++++ db/schema.rb | 3 ++- spec/lib/activitypub/activity/create_spec.rb | 21 +++++++++++++++++++++ 6 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20190304152020_add_uri_to_poll_votes.rb (limited to 'app') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index fc4c45692..07ef16bf3 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -241,7 +241,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def poll_vote? return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) - replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name'])) + replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id']) end def resolve_thread(status) diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb index 57781d616..aec678968 100644 --- a/app/models/poll_vote.rb +++ b/app/models/poll_vote.rb @@ -9,6 +9,7 @@ # choice :integer default(0), not null # created_at :datetime not null # updated_at :datetime not null +# uri :string # class PollVote < ApplicationRecord @@ -20,6 +21,8 @@ class PollVote < ApplicationRecord after_create_commit :increment_counter_cache + delegate :local?, to: :account + private def increment_counter_cache diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index 655d04d22..248190404 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::VoteSerializer < ActiveModel::Serializer :in_reply_to, :to def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#votes/', object.id].join end def type diff --git a/db/migrate/20190304152020_add_uri_to_poll_votes.rb b/db/migrate/20190304152020_add_uri_to_poll_votes.rb new file mode 100644 index 000000000..f6b81f1ba --- /dev/null +++ b/db/migrate/20190304152020_add_uri_to_poll_votes.rb @@ -0,0 +1,5 @@ +class AddUriToPollVotes < ActiveRecord::Migration[5.2] + def change + add_column :poll_votes, :uri, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index d5d516e06..161619dcf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_02_26_003449) do +ActiveRecord::Schema.define(version: 2019_03_04_152020) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -447,6 +447,7 @@ ActiveRecord::Schema.define(version: 2019_02_26_003449) do t.integer "choice", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "uri" t.index ["account_id"], name: "index_poll_votes_on_account_id" t.index ["poll_id"], name: "index_poll_votes_on_poll_id" end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index ac6237c86..4780c29c8 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -447,6 +447,27 @@ RSpec.describe ActivityPub::Activity::Create do expect(poll.cached_tallies).to eq [10, 3] end end + + context 'when a vote to a local poll' do + let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } + let!(:local_status) { Fabricate(:status, owned_poll: poll) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + name: 'Yellow', + inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status) + } + end + + it 'adds a vote to the poll with correct uri' do + vote = poll.votes.first + expect(vote).to_not be_nil + expect(vote.uri).to eq object_json[:id] + expect(poll.reload.cached_tallies).to eq [1, 0] + end + end end context 'when sender is followed by local users' do -- cgit From cda6ece760f08974e6118887641e6cc8c0f8c9e0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:52:41 +0100 Subject: Display closed polls as such (#10156) --- app/javascript/mastodon/components/poll.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 45ce107aa..e9124aefa 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -14,6 +14,7 @@ const messages = defineMessages({ minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, + closed: { id: 'poll.closed', defaultMessage: 'Closed' }, }); const SECOND = 1000; @@ -132,7 +133,7 @@ class Poll extends ImmutablePureComponent { return null; } - const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); const showResults = poll.get('voted') || poll.get('expired'); const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); -- cgit From 05dfd632c73b605232a77f27ff8d5b888efe5b00 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 03:45:56 +0100 Subject: Fix poll options not being stripped of surrounding whitespace on save (#10168) --- app/models/poll.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'app') diff --git a/app/models/poll.rb b/app/models/poll.rb index ba0b17f91..ab7236d45 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -32,8 +32,11 @@ class Poll < ApplicationRecord scope :attached, -> { where.not(status_id: nil) } scope :unattached, -> { where(status_id: nil) } + before_validation :prepare_options before_validation :prepare_votes_count + after_initialize :prepare_cached_tallies + after_commit :reset_parent_cache, on: :update def loaded_options @@ -75,6 +78,10 @@ class Poll < ApplicationRecord self.votes_count = cached_tallies.sum unless cached_tallies.empty? end + def prepare_options + self.options = options.map(&:strip).reject(&:blank?) + end + def reset_parent_cache return if status_id.nil? Rails.cache.delete("statuses/#{status_id}") -- cgit From 5d3e7cee991d6d3ce989337a42ef6fd352348385 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 03:46:24 +0100 Subject: Fix featured tag form not failing on failed tag validations (#10167) --- app/models/featured_tag.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index b5a10ad2d..d06ae26a8 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -18,11 +18,12 @@ class FeaturedTag < ApplicationRecord delegate :name, to: :tag, allow_nil: true - validates :name, presence: true + validates_associated :tag, on: :create + validates :name, presence: true, on: :create validate :validate_featured_tags_limit, on: :create def name=(str) - self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) + self.tag = Tag.find_or_initialize_by(name: str.strip.delete('#').mb_chars.downcase.to_s) end def increment(timestamp) -- cgit From a198add83bb527c32fa0e01404338562b157da99 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 03:51:18 +0100 Subject: Fix various issues in polls (#10165) * Fix ActivityPub poll results being serialized even with hide_totals * Fix poll refresh button having a different font size * Display poll in OpenGraph description * Fix NoMethodError when serializing votes Regression from #10158 * Fix polls on public pages being broken for non-logged-in users * Do not show time remaining if poll has no expiration date --- app/helpers/stream_entries_helper.rb | 12 +++++++++++- app/javascript/mastodon/components/poll.js | 5 +++-- app/javascript/styles/mastodon/polls.scss | 1 + app/models/poll_vote.rb | 4 ++++ app/serializers/activitypub/note_serializer.rb | 12 ++++++++---- app/views/stream_entries/_poll.html.haml | 16 ++++++++++------ 6 files changed, 37 insertions(+), 13 deletions(-) (limited to 'app') diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 7a74c0b7d..8392afa73 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -104,9 +104,19 @@ module StreamEntriesHelper I18n.t('statuses.content_warning', warning: status.spoiler_text) end + def poll_summary(status) + return unless status.poll + status.poll.options.map { |o| "[ ] #{o}" }.join("\n") + end + def status_description(status) components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] - components << status.text if status.spoiler_text.blank? + + if status.spoiler_text.blank? + components << status.text + components << poll_summary(status) + end + components.reject(&:blank?).join("\n\n") end diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index e9124aefa..182491af8 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -118,7 +118,7 @@ class Poll extends ImmutablePureComponent { /> {!showResults && } - {showResults && {Math.floor(percent)}%} + {showResults && {Math.round(percent)}%} {option.get('title')} @@ -146,7 +146,8 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } {showResults && !this.props.disabled && · } - · {timeRemaining} + + {poll.get('expires_at') && · {timeRemaining}}
  • ); diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index f42496559..7c6e61d63 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -82,6 +82,7 @@ border: 0; color: $dark-text-color; text-decoration: underline; + font-size: inherit; &:hover, &:focus, diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb index aec678968..9ad66bbf8 100644 --- a/app/models/poll_vote.rb +++ b/app/models/poll_vote.rb @@ -23,6 +23,10 @@ class PollVote < ApplicationRecord delegate :local?, to: :account + def object_type + :vote + end + private def increment_counter_cache diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index b2c92fdc1..b2a5f53e0 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -15,8 +15,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local? - has_many :poll_loaded_options, key: :one_of, if: :poll_and_not_multiple? - has_many :poll_loaded_options, key: :any_of, if: :poll_and_multiple? + has_many :poll_options, key: :one_of, if: :poll_and_not_multiple? + has_many :poll_options, key: :any_of, if: :poll_and_multiple? attribute :end_time, if: :poll_and_expires? attribute :closed, if: :poll_and_expired? @@ -121,8 +121,12 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer object.account.local? end - def poll_loaded_options - object.poll.loaded_options + def poll_options + if !object.expired? && object.hide_totals? + object.poll.unloaded_options + else + object.poll.loaded_options + end end def poll_and_multiple? diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml index 974aff9bd..c7e5e0c63 100644 --- a/app/views/stream_entries/_poll.html.haml +++ b/app/views/stream_entries/_poll.html.haml @@ -1,5 +1,5 @@ -- options = (!poll.expired? && poll.hide_totals?) ? poll.unloaded_options : poll.loaded_options -- voted = poll.votes.where(account: current_user.account).exists? +- options = (!poll.expired? && poll.hide_totals?) ? poll.unloaded_options : poll.loaded_options +- voted = user_signed_in? && poll.votes.where(account: current_account).exists? - show_results = voted || poll.expired? .poll @@ -9,17 +9,21 @@ - if show_results - percent = 100 * option.votes_count / poll.votes_count %span.poll__chart{ style: "width: #{percent}%" } + %label.poll__text>< - %span.poll__number= percent + %span.poll__number= percent.round = option.title - else %label.poll__text>< - %span.poll__input{ class: poll.multiple ? 'checkbox' : nil}>< + %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< = option.title .poll__footer - unless show_results %button.button.button-secondary{ disabled: true } = t('statuses.poll.vote') + %span= t('statuses.poll.total_votes', count: poll.votes_count) - · - %span= poll.expires_at + + - unless poll.expires_at.nil? + · + %span= l poll.expires_at -- cgit From 4037b5eb1eca4858c9f1a93ccafb87a6c849e65b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 04:10:01 +0100 Subject: Fix last_fetched_at not being set on polls (#10170) --- app/services/activitypub/fetch_remote_poll_service.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index c87a2f84d..1dd587d73 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -33,6 +33,7 @@ class ActivityPub::FetchRemotePollService < BaseService poll.votes.delete_all if latest_options != poll.options poll.update!( + last_fetched_at: Time.now.utc, expires_at: expires_at, options: latest_options, cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } -- cgit From ac99b3465e3e0eff44c88f7c557dcc12eafff389 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 05:09:01 +0100 Subject: Fix NoMethodError in ActivityPub::NoteSerializer (#10172) --- app/serializers/activitypub/note_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index b2a5f53e0..3a9e388a5 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -122,7 +122,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def poll_options - if !object.expired? && object.hide_totals? + if !object.poll.expired? && object.poll.hide_totals? object.poll.unloaded_options else object.poll.loaded_options -- cgit From d785497ba5ed44e794dd67660b8779380f81ef42 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 5 Mar 2019 15:19:54 +0100 Subject: Fix suspended account's fields being set as empty dict instead of list (#10178) Fixes #10177 --- app/models/account.rb | 1 + app/services/suspend_account_service.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/account.rb b/app/models/account.rb index 87ce90178..b1abd8f0d 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -241,6 +241,7 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] old_fields = self[:fields] || [] + old_fields = [] if old_fields.is_a?(Hash) if attributes.is_a?(Hash) attributes.each_value do |attr| diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index fc3bc03a5..b2ae3a47c 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -84,7 +84,7 @@ class SuspendAccountService < BaseService @account.locked = false @account.display_name = '' @account.note = '' - @account.fields = {} + @account.fields = [] @account.statuses_count = 0 @account.followers_count = 0 @account.following_count = 0 -- cgit From df5924a1db08f362fcc8cf873ffaed72a2ce9f19 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 5 Mar 2019 15:21:14 +0100 Subject: Do not error out on unsalvageable errors in FetchRepliesService (#10175) * Do not error out on unsalvageable errors in FetchRepliesService Fixes #10152 * Fix FetchRepliesWorker erroring out on deleted statuses --- app/helpers/jsonld_helper.rb | 16 +++++++++++++++- app/services/activitypub/fetch_replies_service.rb | 4 +--- app/workers/activitypub/fetch_replies_worker.rb | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 59e4ae685..f0a19e332 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -63,13 +63,19 @@ module JsonLdHelper json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) build_request(uri, on_behalf_of).perform do |response| + unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response + end return body_to_json(response.body_with_limit) if response.code == 200 end # If request failed, retry without doing it on behalf of a user return if on_behalf_of.nil? build_request(uri).perform do |response| + unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response + end response.code == 200 ? body_to_json(response.body_with_limit) : nil end end @@ -92,6 +98,14 @@ module JsonLdHelper private + def response_successful?(response) + (200...300).cover?(response.code) + end + + def response_error_unsalvageable?(response) + (400...500).cover?(response.code) && response.code != 429 + end + def build_request(uri, on_behalf_of = nil) request = Request.new(:get, uri) request.on_behalf_of(on_behalf_of) if on_behalf_of diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index 95c486a43..569d0d7c1 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -36,9 +36,7 @@ class ActivityPub::FetchRepliesService < BaseService return collection_or_uri if collection_or_uri.is_a?(Hash) return unless @allow_synchronous_requests return if invalid_origin?(collection_or_uri) - collection = fetch_resource_without_id_validation(collection_or_uri) - raise Mastodon::UnexpectedResponseError if collection.nil? - collection + fetch_resource_without_id_validation(collection_or_uri, nil, true) end def filtered_replies diff --git a/app/workers/activitypub/fetch_replies_worker.rb b/app/workers/activitypub/fetch_replies_worker.rb index bf466db54..54d98f228 100644 --- a/app/workers/activitypub/fetch_replies_worker.rb +++ b/app/workers/activitypub/fetch_replies_worker.rb @@ -8,5 +8,7 @@ class ActivityPub::FetchRepliesWorker def perform(parent_status_id, replies_uri) ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri) + rescue ActiveRecord::RecordNotFound + true end end -- cgit From 0c43c320dbae8f72f5f5c40e7a7944196cce368a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 15:21:31 +0100 Subject: Fix status creation API silently discarding invalid poll (#10171) --- app/models/status.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/models/status.rb b/app/models/status.rb index 74deeeb50..f33130dd6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -70,6 +70,7 @@ class Status < ApplicationRecord validates_with StatusLengthValidator validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? + validates_associated :owned_poll default_scope { recent } -- cgit From cce1c3252f271bb5daa5ace05c971d6e1b8f298d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 1 Mar 2019 11:11:35 +0100 Subject: Fix home timeline perpetually reloading when empty Port 3e0ed36e8ede7f1994ab9c46c4cb86e613569440 to glitch-soc --- app/javascript/flavours/glitch/features/home_timeline/index.js | 2 +- app/javascript/flavours/glitch/reducers/timelines.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 7d124ba01..8eb79fa60 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -16,7 +16,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, - isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null, + isPartial: state.getIn(['timelines', 'home', 'isPartial']), }); @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index d7d5ac43f..4179cf477 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -29,6 +29,8 @@ const initialTimeline = ImmutableMap({ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); + mMap.set('isPartial', isPartial); + if (!next && !isLoadingRecent) mMap.set('hasMore', false); if (!statuses.isEmpty()) { -- cgit From 636db1f54fbd1690d083276b18f980cc5a51bcf0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 5 Mar 2019 21:09:18 +0100 Subject: When serializing polls over OStatus, serialize poll options to text (#10160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * When serializing polls over OStatus, serialize poll options to text * Do the same for RSS feeds * Use “[ ] ” as a prefix for poll options instead of “- ” --- app/lib/formatter.rb | 4 ++++ app/lib/ostatus/atom_serializer.rb | 2 +- app/serializers/rss/account_serializer.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 0653214f5..b9845cb45 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -19,6 +19,10 @@ class Formatter raw_content = status.text + if options[:inline_poll_options] && status.poll + raw_content = raw_content + '\n\n' + status.poll.options.map { |title| "[ ] #{title}" }.join('\n') + end + return '' if raw_content.blank? unless status.local? diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 7a181fb40..9a05d96cf 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -352,7 +352,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local? append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? - append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language) + append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language) status.active_mentions.sort_by(&:id).each do |mentioned| append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index bde360a41..712b1347a 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -22,7 +22,7 @@ class RSS::AccountSerializer item.title(status.title) .link(TagManager.instance.url_for(status)) .pub_date(status.created_at) - .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str) + .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) status.media_attachments.each do |media| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size) -- cgit From 0d19fcc2fb8579a61b87206a9376cf113d82ccf4 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 5 Mar 2019 20:15:43 +0100 Subject: Port upstream refactoring of reducers and actions Also includes 9e45b051cfea667f9ca3d3c72d13022259315090 --- app/javascript/flavours/glitch/actions/accounts.js | 27 ++-- app/javascript/flavours/glitch/actions/blocks.js | 3 + .../flavours/glitch/actions/bookmarks.js | 3 + app/javascript/flavours/glitch/actions/compose.js | 3 +- .../flavours/glitch/actions/favourites.js | 3 + .../flavours/glitch/actions/importer/index.js | 75 +++++++++++ .../flavours/glitch/actions/importer/normalizer.js | 63 +++++++++ .../flavours/glitch/actions/interactions.js | 51 +++---- app/javascript/flavours/glitch/actions/lists.js | 15 ++- app/javascript/flavours/glitch/actions/mutes.js | 3 + .../flavours/glitch/actions/notifications.js | 44 ++++-- .../flavours/glitch/actions/pin_statuses.js | 2 + app/javascript/flavours/glitch/actions/search.js | 9 ++ app/javascript/flavours/glitch/actions/statuses.js | 8 +- app/javascript/flavours/glitch/actions/store.js | 2 + .../flavours/glitch/actions/timelines.js | 6 +- .../flavours/glitch/reducers/accounts.js | 149 +-------------------- .../flavours/glitch/reducers/accounts_counters.js | 128 ++---------------- .../flavours/glitch/reducers/statuses.js | 104 ++------------ 19 files changed, 284 insertions(+), 414 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/importer/index.js create mode 100644 app/javascript/flavours/glitch/actions/importer/normalizer.js (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index d67ab112e..b659e4ff3 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,4 +1,5 @@ import api, { getLinks } from 'flavours/glitch/util/api'; +import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -94,7 +95,9 @@ export function fetchAccount(id) { dispatch(fetchAccountRequest(id)); api(getState).get(`/api/v1/accounts/${id}`).then(response => { - dispatch(fetchAccountSuccess(response.data)); + dispatch(importFetchedAccount(response.data)); + }).then(() => { + dispatch(fetchAccountSuccess()); }).catch(error => { dispatch(fetchAccountFail(id, error)); }); @@ -108,10 +111,9 @@ export function fetchAccountRequest(id) { }; }; -export function fetchAccountSuccess(account) { +export function fetchAccountSuccess() { return { type: ACCOUNT_FETCH_SUCCESS, - account, }; }; @@ -338,6 +340,7 @@ export function fetchFollowers(id) { api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => { @@ -383,6 +386,7 @@ export function expandFollowers(id) { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => { @@ -422,6 +426,7 @@ export function fetchFollowing(id) { api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => { @@ -467,6 +472,7 @@ export function expandFollowing(id) { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => { @@ -548,6 +554,7 @@ export function fetchFollowRequests() { api(getState).get('/api/v1/follow_requests').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); }).catch(error => dispatch(fetchFollowRequestsFail(error))); }; @@ -586,6 +593,7 @@ export function expandFollowRequests() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); }).catch(error => dispatch(expandFollowRequestsFail(error))); }; @@ -749,9 +757,10 @@ export function fetchPinnedAccounts() { return (dispatch, getState) => { dispatch(fetchPinnedAccountsRequest()); - api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }) - .then(({ data }) => dispatch(fetchPinnedAccountsSuccess(data))) - .catch(err => dispatch(fetchPinnedAccountsFail(err))); + api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }).then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchPinnedAccountsSuccess(response.data)); + }).catch(err => dispatch(fetchPinnedAccountsFail(err))); }; }; @@ -785,8 +794,10 @@ export function fetchPinnedAccountsSuggestions(q) { following: true, }; - api(getState).get('/api/v1/accounts/search', { params }) - .then(({ data }) => dispatch(fetchPinnedAccountsSuggestionsReady(q, data))); + api(getState).get('/api/v1/accounts/search', { params }).then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data)); + }); }; }; diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index fe44ca19a..498ce519f 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,5 +1,6 @@ import api, { getLinks } from 'flavours/glitch/util/api'; import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; @@ -15,6 +16,7 @@ export function fetchBlocks() { api(getState).get('/api/v1/blocks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(fetchBlocksFail(error))); @@ -54,6 +56,7 @@ export function expandBlocks() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(expandBlocksFail(error))); diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js index fb5d49ad3..83dbf5407 100644 --- a/app/javascript/flavours/glitch/actions/bookmarks.js +++ b/app/javascript/flavours/glitch/actions/bookmarks.js @@ -1,4 +1,5 @@ import api, { getLinks } from 'flavours/glitch/util/api'; +import { importFetchedStatuses } from './importer'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; @@ -18,6 +19,7 @@ export function fetchBookmarkedStatuses() { api(getState).get('/api/v1/bookmarks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchBookmarkedStatusesFail(error)); @@ -58,6 +60,7 @@ export function expandBookmarkedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandBookmarkedStatusesFail(error)); diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 0dd1766bc..fc32277b2 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -6,7 +6,7 @@ import { useEmoji } from './emojis'; import { tagHistory } from 'flavours/glitch/util/settings'; import { recoverHashtags } from 'flavours/glitch/util/hashtag'; import resizeImage from 'flavours/glitch/util/resize_image'; - +import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; @@ -338,6 +338,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => limit: 4, }, }).then(response => { + dispatch(importFetchedAccounts(response.data)); dispatch(readyComposeSuggestionsAccounts(token, response.data)); }).catch(error => { if (!isCancel(error)) { diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 28eca8e5f..0d8bfb14d 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -1,4 +1,5 @@ import api, { getLinks } from 'flavours/glitch/util/api'; +import { importFetchedStatuses } from './importer'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; @@ -18,6 +19,7 @@ export function fetchFavouritedStatuses() { api(getState).get('/api/v1/favourites').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchFavouritedStatusesFail(error)); @@ -61,6 +63,7 @@ export function expandFavouritedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandFavouritedStatusesFail(error)); diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js new file mode 100644 index 000000000..735d1552c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -0,0 +1,75 @@ +import { normalizeAccount, normalizeStatus } from './normalizer'; + +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; +export const STATUS_IMPORT = 'STATUS_IMPORT'; +export const STATUSES_IMPORT = 'STATUSES_IMPORT'; + +function pushUnique(array, object) { + if (array.every(element => element.id !== object.id)) { + array.push(object); + } +} + +export function importAccount(account) { + return { type: ACCOUNT_IMPORT, account }; +} + +export function importAccounts(accounts) { + return { type: ACCOUNTS_IMPORT, accounts }; +} + +export function importStatus(status) { + return { type: STATUS_IMPORT, status }; +} + +export function importStatuses(statuses) { + return { type: STATUSES_IMPORT, statuses }; +} + +export function importFetchedAccount(account) { + return importFetchedAccounts([account]); +} + +export function importFetchedAccounts(accounts) { + const normalAccounts = []; + + function processAccount(account) { + pushUnique(normalAccounts, normalizeAccount(account)); + + if (account.moved) { + processAccount(account.moved); + } + } + + accounts.forEach(processAccount); + //putAccounts(normalAccounts, !autoPlayGif); + + return importAccounts(normalAccounts); +} + +export function importFetchedStatus(status) { + return importFetchedStatuses([status]); +} + +export function importFetchedStatuses(statuses) { + return (dispatch, getState) => { + const accounts = []; + const normalStatuses = []; + + function processStatus(status) { + pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); + pushUnique(accounts, status.account); + + if (status.reblog && status.reblog.id) { + processStatus(status.reblog); + } + } + + statuses.forEach(processStatus); + //putStatuses(normalStatuses); + + dispatch(importFetchedAccounts(accounts)); + dispatch(importStatuses(normalStatuses)); + }; +} diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js new file mode 100644 index 000000000..a2dabb5b2 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -0,0 +1,63 @@ +import escapeTextContentForBrowser from 'escape-html'; +import emojify from 'flavours/glitch/util/emoji'; +import { unescapeHTML } from 'flavours/glitch/util/html'; +import { expandSpoilers } from 'flavours/glitch/util/initial_state'; + +const domParser = new DOMParser(); + +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + +export function normalizeAccount(account) { + account = { ...account }; + + const emojiMap = makeEmojiMap(account); + const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); + + if (account.fields) { + account.fields = account.fields.map(pair => ({ + ...pair, + name_emojified: emojify(escapeTextContentForBrowser(pair.name)), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), + })); + } + + if (account.moved) { + account.moved = account.moved.id; + } + + return account; +} + +export function normalizeStatus(status, normalOldStatus) { + const normalStatus = { ...status }; + normalStatus.account = status.account.id; + + if (status.reblog && status.reblog.id) { + normalStatus.reblog = status.reblog.id; + } + + // Only calculate these values when status first encountered + // Otherwise keep the ones already in the reducer + if (normalOldStatus) { + normalStatus.search_index = normalOldStatus.get('search_index'); + normalStatus.contentHtml = normalOldStatus.get('contentHtml'); + normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); + } else { + const spoilerText = normalStatus.spoiler_text || ''; + const searchContent = [spoilerText, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

    /g, '\n\n'); + const emojiMap = makeEmojiMap(normalStatus); + + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); + } + + return normalStatus; +} diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index edd8961f9..4407f8b6e 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -1,4 +1,5 @@ import api from 'flavours/glitch/util/api'; +import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -47,7 +48,8 @@ export function reblog(status) { api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper - dispatch(reblogSuccess(status, response.data.reblog)); + dispatch(importFetchedStatus(response.data.reblog)); + dispatch(reblogSuccess(status)); }).catch(function (error) { dispatch(reblogFail(status, error)); }); @@ -59,7 +61,8 @@ export function unreblog(status) { dispatch(unreblogRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { - dispatch(unreblogSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(unreblogSuccess(status)); }).catch(error => { dispatch(unreblogFail(status, error)); }); @@ -73,11 +76,10 @@ export function reblogRequest(status) { }; }; -export function reblogSuccess(status, response) { +export function reblogSuccess(status) { return { type: REBLOG_SUCCESS, status: status, - response: response, }; }; @@ -96,11 +98,10 @@ export function unreblogRequest(status) { }; }; -export function unreblogSuccess(status, response) { +export function unreblogSuccess(status) { return { type: UNREBLOG_SUCCESS, status: status, - response: response, }; }; @@ -117,7 +118,8 @@ export function favourite(status) { dispatch(favouriteRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { - dispatch(favouriteSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(favouriteSuccess(status)); }).catch(function (error) { dispatch(favouriteFail(status, error)); }); @@ -129,7 +131,8 @@ export function unfavourite(status) { dispatch(unfavouriteRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { - dispatch(unfavouriteSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(unfavouriteSuccess(status)); }).catch(error => { dispatch(unfavouriteFail(status, error)); }); @@ -143,11 +146,10 @@ export function favouriteRequest(status) { }; }; -export function favouriteSuccess(status, response) { +export function favouriteSuccess(status) { return { type: FAVOURITE_SUCCESS, status: status, - response: response, }; }; @@ -166,11 +168,10 @@ export function unfavouriteRequest(status) { }; }; -export function unfavouriteSuccess(status, response) { +export function unfavouriteSuccess(status) { return { type: UNFAVOURITE_SUCCESS, status: status, - response: response, }; }; @@ -187,7 +188,8 @@ export function bookmark(status) { dispatch(bookmarkRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) { - dispatch(bookmarkSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(bookmarkSuccess(status)); }).catch(function (error) { dispatch(bookmarkFail(status, error)); }); @@ -199,7 +201,8 @@ export function unbookmark(status) { dispatch(unbookmarkRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { - dispatch(unbookmarkSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(unbookmarkSuccess(status)); }).catch(error => { dispatch(unbookmarkFail(status, error)); }); @@ -213,11 +216,10 @@ export function bookmarkRequest(status) { }; }; -export function bookmarkSuccess(status, response) { +export function bookmarkSuccess(status) { return { type: BOOKMARK_SUCCESS, status: status, - response: response, }; }; @@ -236,11 +238,10 @@ export function unbookmarkRequest(status) { }; }; -export function unbookmarkSuccess(status, response) { +export function unbookmarkSuccess(status) { return { type: UNBOOKMARK_SUCCESS, status: status, - response: response, }; }; @@ -257,6 +258,7 @@ export function fetchReblogs(id) { dispatch(fetchReblogsRequest(id)); api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { + dispatch(importFetchedAccounts(response.data)); dispatch(fetchReblogsSuccess(id, response.data)); }).catch(error => { dispatch(fetchReblogsFail(id, error)); @@ -291,6 +293,7 @@ export function fetchFavourites(id) { dispatch(fetchFavouritesRequest(id)); api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { + dispatch(importFetchedAccounts(response.data)); dispatch(fetchFavouritesSuccess(id, response.data)); }).catch(error => { dispatch(fetchFavouritesFail(id, error)); @@ -325,7 +328,8 @@ export function pin(status) { dispatch(pinRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { - dispatch(pinSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(pinSuccess(status)); }).catch(error => { dispatch(pinFail(status, error)); }); @@ -339,11 +343,10 @@ export function pinRequest(status) { }; }; -export function pinSuccess(status, response) { +export function pinSuccess(status) { return { type: PIN_SUCCESS, status, - response, }; }; @@ -360,7 +363,8 @@ export function unpin (status) { dispatch(unpinRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { - dispatch(unpinSuccess(status, response.data)); + dispatch(importFetchedStatus(response.data)); + dispatch(unpinSuccess(status)); }).catch(error => { dispatch(unpinFail(status, error)); }); @@ -374,11 +378,10 @@ export function unpinRequest(status) { }; }; -export function unpinSuccess(status, response) { +export function unpinSuccess(status) { return { type: UNPIN_SUCCESS, status, - response, }; }; diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index f29ca1e01..c2309b8c2 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,4 +1,5 @@ import api from 'flavours/glitch/util/api'; +import { importFetchedAccounts } from './importer'; import { showAlertForError } from './alerts'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; @@ -208,9 +209,10 @@ export const deleteListFail = (id, error) => ({ export const fetchListAccounts = listId => (dispatch, getState) => { dispatch(fetchListAccountsRequest(listId)); - api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }) - .then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data))) - .catch(err => dispatch(fetchListAccountsFail(listId, err))); + api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListAccountsSuccess(listId, data)); + }).catch(err => dispatch(fetchListAccountsFail(listId, err))); }; export const fetchListAccountsRequest = id => ({ @@ -239,9 +241,10 @@ export const fetchListSuggestions = q => (dispatch, getState) => { following: true, }; - api(getState).get('/api/v1/accounts/search', { params }) - .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data))) - .catch(error => dispatch(showAlertForError(error))); + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); }; export const fetchListSuggestionsReady = (query, accounts) => ({ diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index e06130533..927fc7415 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -1,5 +1,6 @@ import api, { getLinks } from 'flavours/glitch/util/api'; import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; import { openModal } from 'flavours/glitch/actions/modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; @@ -19,6 +20,7 @@ export function fetchMutes() { api(getState).get('/api/v1/mutes').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(fetchMutesFail(error))); @@ -58,6 +60,7 @@ export function expandMutes() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(expandMutesFail(error))); diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 3cfad90a1..f89b4cb36 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,6 +1,12 @@ import api, { getLinks } from 'flavours/glitch/util/api'; import IntlMessageFormat from 'intl-messageformat'; import { fetchRelationships } from './accounts'; +import { + importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, +} from './importer'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/util/html'; @@ -47,9 +53,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => { export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { - const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); - const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFilters(getState(), { contextType: 'notifications' }); + const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); + const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); + const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); + const filters = getFilters(getState(), { contextType: 'notifications' }); let filtered = false; @@ -60,15 +67,26 @@ export function updateNotifications(notification, intlMessages, intlLocale) { filtered = regex && regex.test(searchIndex); } - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification, - account: notification.account, - status: notification.status, - meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, - }); + if (showInColumn) { + dispatch(importFetchedAccount(notification.account)); + + if (notification.status) { + dispatch(importFetchedStatus(notification.status)); + } + + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification, + meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, + }); - fetchRelatedRelationships(dispatch, [notification]); + fetchRelatedRelationships(dispatch, [notification]); + } else if (playSound && !filtered) { + dispatch({ + type: NOTIFICATIONS_UPDATE_NOOP, + meta: { sound: 'boop' }, + }); + } // Desktop notifications if (typeof window.Notification !== 'undefined' && showAlert && !filtered) { @@ -120,6 +138,10 @@ export function expandNotifications({ maxId } = {}, done = noOp) { api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); fetchRelatedRelationships(dispatch, response.data); done(); diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index d3d1a154f..77dfb9c7f 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,4 +1,5 @@ import api from 'flavours/glitch/util/api'; +import { importFetchedStatuses } from './importer'; export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; @@ -11,6 +12,7 @@ export function fetchPinnedStatuses() { dispatch(fetchPinnedStatusesRequest()); api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { + dispatch(importFetchedStatuses(response.data)); dispatch(fetchPinnedStatusesSuccess(response.data, null)); }).catch(error => { dispatch(fetchPinnedStatusesFail(error)); diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index ec65bdf28..bc094eed5 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,5 +1,6 @@ import api from 'flavours/glitch/util/api'; import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; export const SEARCH_CHANGE = 'SEARCH_CHANGE'; export const SEARCH_CLEAR = 'SEARCH_CLEAR'; @@ -38,6 +39,14 @@ export function submitSearch() { resolve: true, }, }).then(response => { + if (response.data.accounts) { + dispatch(importFetchedAccounts(response.data.accounts)); + } + + if (response.data.statuses) { + dispatch(importFetchedStatuses(response.data.statuses)); + } + dispatch(fetchSearchSuccess(response.data)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 6183f3c03..13ce782e6 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -1,6 +1,7 @@ import api from 'flavours/glitch/util/api'; import { deleteFromTimelines } from './timelines'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -45,17 +46,17 @@ export function fetchStatus(id) { dispatch(fetchStatusRequest(id, skipLoading)); api(getState).get(`/api/v1/statuses/${id}`).then(response => { - dispatch(fetchStatusSuccess(response.data, skipLoading)); + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(skipLoading)); }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; }; -export function fetchStatusSuccess(status, skipLoading) { +export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, - status, skipLoading, }; }; @@ -127,6 +128,7 @@ export function fetchContext(id) { dispatch(fetchContextRequest(id)); api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { + dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants))); dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index 2dd94a998..34dcafc51 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,5 +1,6 @@ import { Iterable, fromJS } from 'immutable'; import { hydrateCompose } from './compose'; +import { importFetchedAccounts } from './importer'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; @@ -18,5 +19,6 @@ export function hydrateStore(rawState) { }); dispatch(hydrateCompose()); + dispatch(importFetchedAccounts(Object.values(rawState.accounts))); }; }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index bc21b4d5e..c6866f81f 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,3 +1,4 @@ +import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from 'flavours/glitch/util/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -14,11 +15,13 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export function updateTimeline(timeline, status, accept) { - return (dispatch, getState) => { + return dispatch => { if (typeof accept === 'function' && !accept(status)) { return; } + dispatch(importFetchedStatus(status)); + dispatch({ type: TIMELINE_UPDATE, timeline, @@ -77,6 +80,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); done(); }).catch(error => { diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js index 860c13534..530ed8e60 100644 --- a/app/javascript/flavours/glitch/reducers/accounts.js +++ b/app/javascript/flavours/glitch/reducers/accounts.js @@ -1,68 +1,7 @@ -import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, - PINNED_ACCOUNTS_FETCH_SUCCESS, - PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, -} from 'flavours/glitch/actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - BOOKMARK_SUCCESS, - UNBOOKMARK_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from 'flavours/glitch/actions/interactions'; -import { - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from 'flavours/glitch/actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/favourites'; -import { - BOOKMARKED_STATUSES_FETCH_SUCCESS, - BOOKMARKED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/bookmarks'; -import { - LIST_ACCOUNTS_FETCH_SUCCESS, - LIST_EDITOR_SUGGESTIONS_READY, -} from 'flavours/glitch/actions/lists'; -import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; -import emojify from 'flavours/glitch/util/emoji'; +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; -import { unescapeHTML } from 'flavours/glitch/util/html'; -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; -}, {}); +const initialState = ImmutableMap(); const normalizeAccount = (state, account) => { account = { ...account }; @@ -71,25 +10,6 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; - const emojiMap = makeEmojiMap(account); - const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); - account.note_emojified = emojify(account.note, emojiMap); - - if (account.fields) { - account.fields = account.fields.map(pair => ({ - ...pair, - name_emojified: emojify(escapeTextContentForBrowser(pair.name)), - value_emojified: emojify(pair.value, emojiMap), - value_plain: unescapeHTML(pair.value), - })); - } - - if (account.moved) { - state = normalizeAccount(state, account.moved); - account.moved = account.moved.id; - } - return state.set(account.id, fromJS(account)); }; @@ -101,71 +21,12 @@ const normalizeAccounts = (state, accounts) => { return state; }; -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - -const initialState = ImmutableMap(); - export default function accounts(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: - return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS())); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: + case ACCOUNT_IMPORT: return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - case LIST_ACCOUNTS_FETCH_SUCCESS: - case LIST_EDITOR_SUGGESTIONS_READY: - case PINNED_ACCOUNTS_FETCH_SUCCESS: - case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case BOOKMARKED_STATUSES_FETCH_SUCCESS: - case BOOKMARKED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - case BOOKMARK_SUCCESS: - case UNBOOKMARK_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); + case ACCOUNTS_IMPORT: + return normalizeAccounts(state, action.accounts); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/accounts_counters.js b/app/javascript/flavours/glitch/reducers/accounts_counters.js index acf363ca5..9ebf72af9 100644 --- a/app/javascript/flavours/glitch/reducers/accounts_counters.js +++ b/app/javascript/flavours/glitch/reducers/accounts_counters.js @@ -1,59 +1,8 @@ import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, -} from 'flavours/glitch/actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - BOOKMARK_SUCCESS, - UNBOOKMARK_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from 'flavours/glitch/actions/interactions'; -import { - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from 'flavours/glitch/actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/favourites'; -import { - BOOKMARKED_STATUSES_FETCH_SUCCESS, - BOOKMARKED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/bookmarks'; -import { - LIST_ACCOUNTS_FETCH_SUCCESS, - LIST_EDITOR_SUGGESTIONS_READY, -} from 'flavours/glitch/actions/lists'; -import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; +} from '../actions/accounts'; +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeAccount = (state, account) => state.set(account.id, fromJS({ @@ -70,80 +19,19 @@ const normalizeAccounts = (state, accounts) => { return state; }; -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - const initialState = ImmutableMap(); export default function accountsCounters(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('accounts').map(item => fromJS({ - followers_count: item.get('followers_count'), - following_count: item.get('following_count'), - statuses_count: item.get('statuses_count'), - }))); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: + case ACCOUNT_IMPORT: return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - case LIST_ACCOUNTS_FETCH_SUCCESS: - case LIST_EDITOR_SUGGESTIONS_READY: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case BOOKMARKED_STATUSES_FETCH_SUCCESS: - case BOOKMARKED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - case BOOKMARK_SUCCESS: - case UNBOOKMARK_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); + case ACCOUNTS_IMPORT: + return normalizeAccounts(state, action.accounts); case ACCOUNT_FOLLOW_SUCCESS: - if (action.alreadyFollowing) { - return state; - } - return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : num + 1); + return action.alreadyFollowing ? state : + state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); case ACCOUNT_UNFOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : Math.max(0, num - 1)); + return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 1beaf73e1..96c9c6d04 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -1,93 +1,25 @@ import { REBLOG_REQUEST, - REBLOG_SUCCESS, REBLOG_FAIL, - UNREBLOG_SUCCESS, FAVOURITE_REQUEST, - FAVOURITE_SUCCESS, FAVOURITE_FAIL, - UNFAVOURITE_SUCCESS, BOOKMARK_REQUEST, - BOOKMARK_SUCCESS, BOOKMARK_FAIL, - UNBOOKMARK_SUCCESS, - PIN_SUCCESS, - UNPIN_SUCCESS, } from 'flavours/glitch/actions/interactions'; import { - COMPOSE_SUBMIT_SUCCESS, -} from 'flavours/glitch/actions/compose'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, } from 'flavours/glitch/actions/statuses'; import { - TIMELINE_UPDATE, TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS, } from 'flavours/glitch/actions/timelines'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/favourites'; -import { - BOOKMARKED_STATUSES_FETCH_SUCCESS, - BOOKMARKED_STATUSES_EXPAND_SUCCESS, -} from 'flavours/glitch/actions/bookmarks'; -import { - PINNED_STATUSES_FETCH_SUCCESS, -} from 'flavours/glitch/actions/pin_statuses'; -import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search'; -import emojify from 'flavours/glitch/util/emoji'; +import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; - -const domParser = new DOMParser(); - -const normalizeStatus = (state, status) => { - if (!status) { - return state; - } - - const normalStatus = { ...status }; - normalStatus.account = status.account.id; - if (status.reblog && status.reblog.id) { - state = normalizeStatus(state, status.reblog); - normalStatus.reblog = status.reblog.id; - } - - // Only calculate these values when status first encountered - // Otherwise keep the ones already in the reducer - if (!state.has(status.id)) { - const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/
    /g, '\n').replace(/<\/p>

    /g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); - - normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); - } +const importStatus = (state, status) => state.set(status.id, fromJS(status)); - return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); -}; - -const normalizeStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeStatus(state, status); - }); - - return state; -}; +const importStatuses = (state, statuses) => + state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); const deleteStatus = (state, id, references) => { references.forEach(ref => { @@ -101,20 +33,10 @@ const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - case COMPOSE_SUBMIT_SUCCESS: - return normalizeStatus(state, action.status); - case REBLOG_SUCCESS: - case UNREBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNFAVOURITE_SUCCESS: - case BOOKMARK_SUCCESS: - case UNBOOKMARK_SUCCESS: - case PIN_SUCCESS: - case UNPIN_SUCCESS: - return normalizeStatus(state, action.response); + case STATUS_IMPORT: + return importStatus(state, action.status); + case STATUSES_IMPORT: + return importStatuses(state, action.statuses); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); case FAVOURITE_FAIL: @@ -131,16 +53,6 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'muted'], true); case STATUS_UNMUTE_SUCCESS: return state.setIn([action.id, 'muted'], false); - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case BOOKMARKED_STATUSES_FETCH_SUCCESS: - case BOOKMARKED_STATUSES_EXPAND_SUCCESS: - case PINNED_STATUSES_FETCH_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeStatuses(state, action.statuses); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); default: -- cgit From 8d70a8a19b2d8436a1361b2bdeb42e7949acc7d0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 22:18:23 +0100 Subject: Add polls Port front-end parts of 230a012f0090c496fc5cdb011bcc8ed732fd0f5c to glitch-soc --- .../flavours/glitch/actions/importer/index.js | 17 ++- .../flavours/glitch/actions/importer/normalizer.js | 4 + app/javascript/flavours/glitch/actions/polls.js | 53 ++++++++ app/javascript/flavours/glitch/components/poll.js | 144 +++++++++++++++++++++ .../flavours/glitch/components/status.js | 5 +- .../flavours/glitch/containers/media_container.js | 6 +- .../flavours/glitch/containers/poll_container.js | 8 ++ .../features/status/components/detailed_status.js | 5 +- app/javascript/flavours/glitch/reducers/index.js | 2 + app/javascript/flavours/glitch/reducers/polls.js | 19 +++ .../flavours/glitch/styles/components/index.scss | 4 + app/javascript/flavours/glitch/styles/index.scss | 1 + app/javascript/flavours/glitch/styles/polls.scss | 95 ++++++++++++++ 13 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/polls.js create mode 100644 app/javascript/flavours/glitch/components/poll.js create mode 100644 app/javascript/flavours/glitch/containers/poll_container.js create mode 100644 app/javascript/flavours/glitch/reducers/polls.js create mode 100644 app/javascript/flavours/glitch/styles/polls.scss (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 735d1552c..13ad5d1e1 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -1,9 +1,10 @@ import { normalizeAccount, normalizeStatus } from './normalizer'; -export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; -export const STATUS_IMPORT = 'STATUS_IMPORT'; +export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -27,6 +28,10 @@ export function importStatuses(statuses) { return { type: STATUSES_IMPORT, statuses }; } +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -43,7 +48,6 @@ export function importFetchedAccounts(accounts) { } accounts.forEach(processAccount); - //putAccounts(normalAccounts, !autoPlayGif); return importAccounts(normalAccounts); } @@ -56,6 +60,7 @@ export function importFetchedStatuses(statuses) { return (dispatch, getState) => { const accounts = []; const normalStatuses = []; + const polls = []; function processStatus(status) { pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); @@ -64,12 +69,16 @@ export function importFetchedStatuses(statuses) { if (status.reblog && status.reblog.id) { processStatus(status.reblog); } + + if (status.poll && status.poll.id) { + pushUnique(polls, status.poll); + } } statuses.forEach(processStatus); - //putStatuses(normalStatuses); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); + dispatch(importPolls(polls)); }; } diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index a2dabb5b2..f57fb70b4 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.reblog = status.reblog.id; } + if (status.poll && status.poll.id) { + normalStatus.poll = status.poll.id; + } + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer if (normalOldStatus) { diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js new file mode 100644 index 000000000..bee4c48a6 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -0,0 +1,53 @@ +import api from '../api'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch, getState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => dispatch(voteSuccess(data))) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch, getState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then(({ data }) => dispatch(fetchPollSuccess(data))) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js new file mode 100644 index 000000000..d4b9f283a --- /dev/null +++ b/app/javascript/flavours/glitch/components/poll.js @@ -0,0 +1,144 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { vote, fetchPoll } from 'mastodon/actions/polls'; +import Motion from 'mastodon/features/ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; + +const messages = defineMessages({ + moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, + seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, + minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, + hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, + days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, +}); + +const SECOND = 1000; +const MINUTE = 1000 * 60; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; + +const timeRemainingString = (intl, date, now) => { + const delta = date.getTime() - now; + + let relativeTime; + + if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.moments); + } else if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); + } else { + relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); + } + + return relativeTime; +}; + +export default @injectIntl +class Poll extends ImmutablePureComponent { + + static propTypes = { + poll: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func, + disabled: PropTypes.bool, + }; + + state = { + selected: {}, + }; + + handleOptionChange = e => { + const { target: { value } } = e; + + if (this.props.poll.get('multiple')) { + const tmp = { ...this.state.selected }; + tmp[value] = true; + this.setState({ selected: tmp }); + } else { + const tmp = {}; + tmp[value] = true; + this.setState({ selected: tmp }); + } + }; + + handleVote = () => { + if (this.props.disabled) { + return; + } + + this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected))); + }; + + handleRefresh = () => { + if (this.props.disabled) { + return; + } + + this.props.dispatch(fetchPoll(this.props.poll.get('id'))); + }; + + renderOption (option, optionIndex) { + const { poll } = this.props; + const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const showResults = poll.get('voted') || poll.get('expired'); + + return ( +

  • + {showResults && ( + + {({ width }) => + + } + + )} + + +
  • + ); + } + + render () { + const { poll, intl } = this.props; + const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const showResults = poll.get('voted') || poll.get('expired'); + const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + + return ( +
    +
      + {poll.get('options').map((option, i) => this.renderOption(option, i))} +
    + +
    + {!showResults && } + {showResults && !this.props.disabled && · } + · {timeRemaining} +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 349f9c6cc..b38bebe11 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -15,6 +15,7 @@ import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import classNames from 'classnames'; import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; +import PollContainer from 'flavours/glitch/containers/poll_container'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -437,7 +438,9 @@ export default class Status extends ImmutablePureComponent { // `media`, we snatch the thumbnail to use as our `background` if media // backgrounds for collapsed statuses are enabled. attachments = status.get('media_attachments'); - if (attachments.size > 0) { + if (status.get('poll')) { + media = ; + } else if (attachments.size > 0) { if (muted || attachments.some(item => item.get('type') === 'unknown')) { media = ( { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; - const { media, card, ...props } = JSON.parse(component.getAttribute('data-props')); + const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props')); Object.assign(props, { ...(media ? { media: fromJS(media) } : {}), ...(card ? { card: fromJS(card) } : {}), + ...(poll ? { poll: fromJS(poll) } : {}), ...(componentName === 'Video' ? { onOpenVideo: this.handleOpenVideo, diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js new file mode 100644 index 000000000..cd7216de7 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/poll_container.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import Poll from 'mastodon/components/poll'; + +const mapStateToProps = (state, { pollId }) => ({ + poll: state.getIn(['polls', pollId]), +}); + +export default connect(mapStateToProps)(Poll); diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 120ae6817..ad60320ef 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -14,6 +14,7 @@ import Video from 'flavours/glitch/features/video'; import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import classNames from 'classnames'; +import PollContainer from 'flavours/glitch/containers/poll_container'; export default class DetailedStatus extends ImmutablePureComponent { @@ -118,7 +119,9 @@ export default class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } - if (status.get('media_attachments').size > 0) { + if (status.get('poll')) { + media = ; + } else if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = ; } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 5b1ec4abc..7b3e0f651 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -29,6 +29,7 @@ import listEditor from './list_editor'; import listAdder from './list_adder'; import filters from './filters'; import pinnedAccountsEditor from './pinned_accounts_editor'; +import polls from './polls'; const reducers = { dropdown_menu, @@ -61,6 +62,7 @@ const reducers = { listAdder, filters, pinnedAccountsEditor, + polls, }; export default combineReducers(reducers); diff --git a/app/javascript/flavours/glitch/reducers/polls.js b/app/javascript/flavours/glitch/reducers/polls.js new file mode 100644 index 000000000..53d9b1d8c --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/polls.js @@ -0,0 +1,19 @@ +import { POLL_VOTE_SUCCESS, POLL_FETCH_SUCCESS } from 'mastodon/actions/polls'; +import { POLLS_IMPORT } from 'mastodon/actions/importer'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll)))); + +const initialState = ImmutableMap(); + +export default function polls(state = initialState, action) { + switch(action.type) { + case POLLS_IMPORT: + return importPolls(state, action.polls); + case POLL_VOTE_SUCCESS: + case POLL_FETCH_SUCCESS: + return importPolls(state, [action.poll]); + default: + return state; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 8e90aa545..b9811f25c 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -89,6 +89,10 @@ border-color: lighten($ui-primary-color, 4%); color: lighten($darker-text-color, 4%); } + + &:disabled { + opacity: 0.5; + } } &.button--block { diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index 3cb592499..323b2e7fe 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -16,6 +16,7 @@ @import 'accounts'; @import 'stream_entries'; @import 'components/index'; +@import 'polls'; @import 'about'; @import 'tables'; @import 'admin'; diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss new file mode 100644 index 000000000..b93d36e92 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -0,0 +1,95 @@ +.poll { + margin-top: 16px; + font-size: 14px; + + li { + margin-bottom: 10px; + position: relative; + } + + &__chart { + position: absolute; + top: 0; + left: 0; + height: 100%; + display: inline-block; + border-radius: 4px; + background: darken($ui-primary-color, 14%); + + &.leading { + background: $ui-highlight-color; + } + } + + &__text { + position: relative; + display: inline-block; + padding: 6px 0; + line-height: 18px; + cursor: default; + + input[type=radio], + input[type=checkbox] { + display: none; + } + + &.selectable { + cursor: pointer; + } + } + + &__input { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + margin-right: 10px; + top: -1px; + border-radius: 4px; + vertical-align: middle; + + &.active { + border-color: $valid-value-color; + background: $valid-value-color; + } + } + + &__number { + display: inline-block; + width: 36px; + font-weight: 700; + padding: 0 10px; + text-align: right; + } + + &__footer { + padding-top: 6px; + padding-bottom: 5px; + color: $dark-text-color; + } + + &__link { + display: inline; + background: transparent; + padding: 0; + margin: 0; + border: 0; + color: $dark-text-color; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + .button { + height: 36px; + padding: 0 16px; + margin-right: 10px; + font-size: 14px; + } +} -- cgit From 2156765448f3abe295aa39cac382de7748f1aa11 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 3 Mar 2019 23:44:52 +0100 Subject: Insert polls in redux stores before statuses so it avoids crashes Port 26c56d0c10ca036291d8b08b34f971f981217e8c to glitch-soc --- app/javascript/flavours/glitch/actions/importer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 13ad5d1e1..abadee817 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -77,8 +77,8 @@ export function importFetchedStatuses(statuses) { statuses.forEach(processStatus); + dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); - dispatch(importPolls(polls)); }; } -- cgit From f14eda23e97c2f70b9d060f7575ed2801e5f1f5d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Mar 2019 23:45:02 +0100 Subject: Fix web UI crash on page load when detailed status has a poll Port 5dfa4336985616cf5652de2f1cf794d8f740424e to glitch-soc --- app/javascript/flavours/glitch/components/poll.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index d4b9f283a..c18ee1505 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -45,7 +45,7 @@ export default @injectIntl class Poll extends ImmutablePureComponent { static propTypes = { - poll: ImmutablePropTypes.map.isRequired, + poll: ImmutablePropTypes.map, intl: PropTypes.object.isRequired, dispatch: PropTypes.func, disabled: PropTypes.bool, @@ -122,9 +122,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); - const showResults = poll.get('voted') || poll.get('expired'); - const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + + if (!poll) { + return null; + } + + const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const showResults = poll.get('voted') || poll.get('expired'); + const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); return (
    -- cgit From 530151466e8431d048d97992c648e46272754f4e Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 01:54:14 +0100 Subject: Fixes to the polls UI Port 4ced609497bc736cb2b1aec921ba5ca7a23a7f53 to glitch-soc --- app/javascript/flavours/glitch/components/poll.js | 19 ++++++++++++------- app/javascript/flavours/glitch/styles/polls.scss | 6 +++++- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index c18ee1505..45ce107aa 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -60,7 +60,11 @@ class Poll extends ImmutablePureComponent { if (this.props.poll.get('multiple')) { const tmp = { ...this.state.selected }; - tmp[value] = true; + if (tmp[value]) { + delete tmp[value]; + } else { + tmp[value] = true; + } this.setState({ selected: tmp }); } else { const tmp = {}; @@ -86,11 +90,11 @@ class Poll extends ImmutablePureComponent { }; renderOption (option, optionIndex) { - const { poll } = this.props; - const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + const { poll, disabled } = this.props; + const percent = (option.get('votes_count') / poll.get('votes_count')) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const showResults = poll.get('voted') || poll.get('expired'); return (
  • @@ -109,9 +113,10 @@ class Poll extends ImmutablePureComponent { value={optionIndex} checked={active} onChange={this.handleOptionChange} + disabled={disabled} /> - {!showResults && } + {!showResults && } {showResults && {Math.floor(percent)}%} {option.get('title')} diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index b93d36e92..f42496559 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -47,9 +47,13 @@ height: 18px; margin-right: 10px; top: -1px; - border-radius: 4px; + border-radius: 50%; vertical-align: middle; + &.checkbox { + border-radius: 4px; + } + &.active { border-color: $valid-value-color; background: $valid-value-color; -- cgit From 7f27ee35a811c596522513efc0d5e38d85008d04 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 4 Mar 2019 22:52:41 +0100 Subject: Display closed polls as such Port cda6ece760f08974e6118887641e6cc8c0f8c9e0 to glitch-soc --- app/javascript/flavours/glitch/components/poll.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 45ce107aa..e9124aefa 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -14,6 +14,7 @@ const messages = defineMessages({ minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, + closed: { id: 'poll.closed', defaultMessage: 'Closed' }, }); const SECOND = 1000; @@ -132,7 +133,7 @@ class Poll extends ImmutablePureComponent { return null; } - const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); + const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now()); const showResults = poll.get('voted') || poll.get('expired'); const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); -- cgit From ba6ca3cd69118a764628cc4ee0269293aedc8ee0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 03:51:18 +0100 Subject: Fix various issues in polls Port front-end changes from a198add83bb527c32fa0e01404338562b157da99 to glitch-soc --- app/javascript/flavours/glitch/components/poll.js | 5 +++-- app/javascript/flavours/glitch/styles/polls.scss | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index e9124aefa..182491af8 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -118,7 +118,7 @@ class Poll extends ImmutablePureComponent { /> {!showResults && } - {showResults && {Math.floor(percent)}%} + {showResults && {Math.round(percent)}%} {option.get('title')} @@ -146,7 +146,8 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } {showResults && !this.props.disabled && · } - · {timeRemaining} + + {poll.get('expires_at') && · {timeRemaining}}
  • ); diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index f42496559..7c6e61d63 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -82,6 +82,7 @@ border: 0; color: $dark-text-color; text-decoration: underline; + font-size: inherit; &:hover, &:focus, -- cgit From d604489b5e29ef5addaaab41f1f6cd0410261d25 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Mar 2019 21:52:40 +0100 Subject: Weblate translations (2019-03-05) (#10180) * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Czech) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/cs/ * Translated using Weblate (Czech) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/cs/ * Translated using Weblate (Greek) Currently translated at 98,4% (752 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/el/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (98 of 98 strings) Translation: Mastodon/Doorkeeper Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 66,8% (510 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/kk/ * Translated using Weblate (Italian) Currently translated at 99,7% (360 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/it/ * Translated using Weblate (Italian) Currently translated at 100,0% (63 of 63 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/it/ * Translated using Weblate (Italian) Currently translated at 100,0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/it/ * Translated using Weblate (Kazakh) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/kk/ * Translated using Weblate (Italian) Currently translated at 88,9% (679 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/it/ * Translated using Weblate (Dutch) Currently translated at 100,0% (764 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/ * Translated using Weblate (Persian) Currently translated at 99,7% (762 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/ * Translated using Weblate (Arabic) Currently translated at 93,2% (712 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ar/ * Translated using Weblate (Arabic) Currently translated at 96,4% (107 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ar/ * Translated using Weblate (Arabic) Currently translated at 100.0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ar/ * Translated using Weblate (Arabic) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ar/ * Translated using Weblate (Korean) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ko/ * Translated using Weblate (Korean) Currently translated at 100.0% (764 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ko/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Korean) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ko/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Corsican) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/co/ * Translated using Weblate (Corsican) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/co/ * Translated using Weblate (French) Currently translated at 99,7% (360 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/fr/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Lithuanian) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/lt/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/kk/ * Translated using Weblate (Kazakh) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/kk/ * Translated using Weblate (Greek) Currently translated at 99,9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/el/ * Translated using Weblate (Greek) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/el/ * Added translation using Weblate (Bengali) * Translated using Weblate (Bengali) Currently translated at 2,1% (16 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/bn/ * Translated using Weblate (Japanese) Currently translated at 98,1% (354 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/ja/ * Translated using Weblate (Galician) Currently translated at 100,0% (764 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/ * Translated using Weblate (Slovak) Currently translated at 99.7% (762 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/sk/ * Translated using Weblate (French) Currently translated at 99.9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fr/ * Translated using Weblate (Telugu) Currently translated at 99.4% (359 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/te/ * Translated using Weblate (Spanish) Currently translated at 84.7% (647 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/es/ * Translated using Weblate (Slovak) Currently translated at 100.0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/sk/ * Translated using Weblate (Slovak) Currently translated at 99.7% (762 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/sk/ * Translated using Weblate (Esperanto) Currently translated at 98.2% (109 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/eo/ * Translated using Weblate (Esperanto) Currently translated at 99.9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/eo/ * Translated using Weblate (Japanese) Currently translated at 98.3% (751 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ja/ * Translated using Weblate (Esperanto) Currently translated at 98.4% (62 of 63 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/eo/ * Translated using Weblate (Esperanto) Currently translated at 99.9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/eo/ * Translated using Weblate (Czech) Currently translated at 99.9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/cs/ * Translated using Weblate (Czech) Currently translated at 100.0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/cs/ * Translated using Weblate (Czech) Currently translated at 100.0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/cs/ * Translated using Weblate (Czech) Currently translated at 100.0% (98 of 98 strings) Translation: Mastodon/Doorkeeper Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/cs/ * Translated using Weblate (Persian) Currently translated at 100.0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/fa/ * Translated using Weblate (Dutch) Currently translated at 100.0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/nl/ * Translated using Weblate (Czech) Currently translated at 100.0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/cs/ * Translated using Weblate (Occitan) Currently translated at 95,7% (731 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/oc/ * Translated using Weblate (Occitan) Currently translated at 100,0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/oc/ * Translated using Weblate (Persian) Currently translated at 99,7% (762 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/ * Translated using Weblate (Occitan) Currently translated at 100,0% (361 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/oc/ * Translated using Weblate (Occitan) Currently translated at 100,0% (63 of 63 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/oc/ * Translated using Weblate (Occitan) Currently translated at 96.9% (740 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/oc/ * Translated using Weblate (French) Currently translated at 99.9% (763 of 764 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fr/ * Translated using Weblate (Czech) Currently translated at 100.0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/cs/ * Translated using Weblate (Thai) Currently translated at 14.4% (52 of 361 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/th/ * Translated using Weblate (Greek) Currently translated at 99.9% (771 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/el/ * Translated using Weblate (Korean) Currently translated at 100.0% (772 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ko/ * Translated using Weblate (Corsican) Currently translated at 99.9% (771 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/co/ * Translated using Weblate (Slovak) Currently translated at 99.7% (770 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (111 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/sk/ * Translated using Weblate (French) Currently translated at 99.9% (771 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fr/ * Translated using Weblate (Czech) Currently translated at 99.0% (764 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/cs/ * Translated using Weblate (Persian) Currently translated at 99.7% (770 of 772 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/fa/ * Translated using Weblate (Czech) Currently translated at 98.7% (765 of 775 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/cs/ * Translated using Weblate (Czech) Currently translated at 99.9% (774 of 775 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/cs/ * Translated using Weblate (Swedish) Currently translated at 64,0% (71 of 111 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/sv/ * Translated using Weblate (Corsican) Currently translated at 99,9% (774 of 775 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/co/ * Translated using Weblate (Greek) Currently translated at 99,9% (774 of 775 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/el/ * Translated using Weblate (Czech) Currently translated at 100.0% (63 of 63 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/cs/ * Translated using Weblate (Galician) Currently translated at 100,0% (775 of 775 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (63 of 63 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pt/ * i18n-tasks normalize * yarn manage:translations * Fix missing plural keys * Fix inconsistent interpolations --- app/javascript/mastodon/locales/ar.json | 25 +- app/javascript/mastodon/locales/ast.json | 9 + app/javascript/mastodon/locales/bg.json | 9 + app/javascript/mastodon/locales/ca.json | 9 + app/javascript/mastodon/locales/co.json | 15 +- app/javascript/mastodon/locales/cs.json | 47 +- app/javascript/mastodon/locales/cy.json | 9 + app/javascript/mastodon/locales/da.json | 9 + app/javascript/mastodon/locales/de.json | 9 + .../mastodon/locales/defaultMessages.json | 41 ++ app/javascript/mastodon/locales/el.json | 15 +- app/javascript/mastodon/locales/en.json | 9 + app/javascript/mastodon/locales/eo.json | 9 + app/javascript/mastodon/locales/es.json | 9 + app/javascript/mastodon/locales/eu.json | 9 + app/javascript/mastodon/locales/fa.json | 11 +- app/javascript/mastodon/locales/fi.json | 9 + app/javascript/mastodon/locales/fr.json | 15 +- app/javascript/mastodon/locales/gl.json | 9 + app/javascript/mastodon/locales/he.json | 9 + app/javascript/mastodon/locales/hr.json | 9 + app/javascript/mastodon/locales/hu.json | 9 + app/javascript/mastodon/locales/hy.json | 9 + app/javascript/mastodon/locales/id.json | 9 + app/javascript/mastodon/locales/io.json | 9 + app/javascript/mastodon/locales/it.json | 25 +- app/javascript/mastodon/locales/ja.json | 15 +- app/javascript/mastodon/locales/ka.json | 9 + app/javascript/mastodon/locales/kk.json | 721 +++++++++++---------- app/javascript/mastodon/locales/ko.json | 15 +- app/javascript/mastodon/locales/lv.json | 9 + app/javascript/mastodon/locales/ms.json | 9 + app/javascript/mastodon/locales/nl.json | 15 +- app/javascript/mastodon/locales/no.json | 9 + app/javascript/mastodon/locales/oc.json | 15 +- app/javascript/mastodon/locales/pl.json | 9 + app/javascript/mastodon/locales/pt-BR.json | 9 + app/javascript/mastodon/locales/pt.json | 9 + app/javascript/mastodon/locales/ro.json | 9 + app/javascript/mastodon/locales/ru.json | 9 + app/javascript/mastodon/locales/sk.json | 9 + app/javascript/mastodon/locales/sl.json | 9 + app/javascript/mastodon/locales/sq.json | 9 + app/javascript/mastodon/locales/sr-Latn.json | 9 + app/javascript/mastodon/locales/sr.json | 9 + app/javascript/mastodon/locales/sv.json | 9 + app/javascript/mastodon/locales/ta.json | 9 + app/javascript/mastodon/locales/te.json | 17 +- app/javascript/mastodon/locales/th.json | 29 +- app/javascript/mastodon/locales/tr.json | 9 + app/javascript/mastodon/locales/uk.json | 9 + app/javascript/mastodon/locales/zh-CN.json | 9 + app/javascript/mastodon/locales/zh-HK.json | 9 + app/javascript/mastodon/locales/zh-TW.json | 9 + config/locales/ar.yml | 23 + config/locales/bn.yml | 22 + config/locales/co.yml | 16 + config/locales/cs.yml | 65 +- config/locales/devise.cs.yml | 18 +- config/locales/devise.eo.yml | 6 +- config/locales/devise.it.yml | 6 +- config/locales/devise.oc.yml | 6 +- config/locales/devise.pt.yml | 19 +- config/locales/doorkeeper.cs.yml | 6 +- config/locales/doorkeeper.kk.yml | 4 +- config/locales/el.yml | 28 +- config/locales/eo.yml | 162 ++++- config/locales/es.yml | 6 +- config/locales/fa.yml | 13 +- config/locales/fr.yml | 13 +- config/locales/gl.yml | 16 + config/locales/it.yml | 88 ++- config/locales/ja.yml | 3 +- config/locales/kk.yml | 570 +++++++++++++++- config/locales/ko.yml | 11 + config/locales/lt.yml | 1 + config/locales/nl.yml | 1 + config/locales/oc.yml | 18 +- config/locales/simple_form.ar.yml | 2 +- config/locales/simple_form.cs.yml | 8 +- config/locales/simple_form.eo.yml | 26 +- config/locales/simple_form.it.yml | 10 +- config/locales/simple_form.oc.yml | 3 + config/locales/simple_form.sk.yml | 10 +- config/locales/simple_form.sv.yml | 14 +- config/locales/sk.yml | 67 +- 86 files changed, 2038 insertions(+), 595 deletions(-) create mode 100644 config/locales/bn.yml (limited to 'app') diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 7de65f91f..5cd494314 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -128,7 +128,7 @@ "empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.", "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", - "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام", + "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", "follow_request.authorize": "ترخيص", "follow_request.reject": "رفض", "getting_started.developers": "المُطوِّرون", @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "و {additional}", "hashtag.column_header.tag_mode.any": "أو {additional}", "hashtag.column_header.tag_mode.none": "بدون {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "لم يُعثَر على أي اقتراح", + "hashtag.column_settings.select.placeholder": "قم بإدخال وسوم…", "hashtag.column_settings.tag_mode.all": "كلها", "hashtag.column_settings.tag_mode.any": "أي كان مِن هذه", "hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه", @@ -206,7 +206,7 @@ "lists.account.remove": "إحذف من القائمة", "lists.delete": "Delete list", "lists.edit": "تعديل القائمة", - "lists.edit.submit": "Change title", + "lists.edit.submit": "تعديل العنوان", "lists.new.create": "إنشاء قائمة", "lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", @@ -227,7 +227,7 @@ "navigation_bar.favourites": "المفضلة", "navigation_bar.filters": "الكلمات المكتومة", "navigation_bar.follow_requests": "طلبات المتابعة", - "navigation_bar.info": "معلومات إضافية", + "navigation_bar.info": "عن هذا الخادم", "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح", "navigation_bar.lists": "القوائم", "navigation_bar.logout": "خروج", @@ -260,6 +260,10 @@ "notifications.filter.follows": "يتابِع", "notifications.filter.mentions": "الإشارات", "notifications.group": "{count} إشعارات", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "إضبط خصوصية المنشور", "privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط", "privacy.direct.short": "مباشر", @@ -279,7 +283,7 @@ "reply_indicator.cancel": "إلغاء", "report.forward": "التحويل إلى {target}", "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟", - "report.hint": "سوف يتم إرسال التقرير إلى مُشرِفي مثيل خادومكم. بإمكانك الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله :", + "report.hint": "سوف يتم إرسال التقرير إلى المُشرِفين على خادومكم. بإمكانكم الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله:", "report.placeholder": "تعليقات إضافية", "report.submit": "إرسال", "report.target": "إبلاغ", @@ -300,7 +304,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "إلغاء الترقية", "status.cannot_reblog": "تعذرت ترقية هذا المنشور", - "status.copy": "Copy link to status", + "status.copy": "نسخ رابط المنشور", "status.delete": "إحذف", "status.detailed_status": "تفاصيل المحادثة", "status.direct": "رسالة خاصة إلى @{name}", @@ -342,11 +346,16 @@ "tabs_bar.local_timeline": "المحلي", "tabs_bar.notifications": "الإخطارات", "tabs_bar.search": "البحث", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "upload_area.title": "إسحب ثم أفلت للرفع", "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)", - "upload_error.limit": "File upload limit exceeded.", + "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.", "upload_form.description": "وصف للمعاقين بصريا", "upload_form.focus": "قص", "upload_form.undo": "حذف", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index 1e7ecb550..3da1030fb 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} avisos", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Llocal", "tabs_bar.notifications": "Avisos", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "El borrador va perdese si coles de Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 9e5d46503..080200ebc 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Известия", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 8d1d4777b..bc572d7a2 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Seguiments", "notifications.filter.mentions": "Mencions", "notifications.group": "{count} notificacions", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajusta l'estat de privacitat", "privacy.direct.long": "Publicar només per als usuaris esmentats", "privacy.direct.short": "Directe", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificacions", "tabs_bar.search": "Cerca", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, una {person} altres {people}} parlant", "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.", "upload_area.title": "Arrossega i deixa anar per carregar", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index c308d807a..6d5d11e48 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "è {additional}", "hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.none": "senza {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Nisuna sugestione trova", + "hashtag.column_settings.select.placeholder": "Entrà l'hashtag…", "hashtag.column_settings.tag_mode.all": "Tutti quessi", "hashtag.column_settings.tag_mode.any": "Unu di quessi", "hashtag.column_settings.tag_mode.none": "Nisunu di quessi", @@ -206,7 +206,7 @@ "lists.account.remove": "Toglie di a lista", "lists.delete": "Supprime a lista", "lists.edit": "Mudificà a lista", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cambià u titulu", "lists.new.create": "Aghjustà una lista", "lists.new.title_placeholder": "Titulu di a lista", "lists.search": "Circà indè i vostr'abbunamenti", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Abbunamenti", "notifications.filter.mentions": "Minzione", "notifications.group": "{count} nutificazione", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Mudificà a cunfidenzialità di u statutu", "privacy.direct.long": "Mandà solu à quelli chì so mintuvati", "privacy.direct.short": "Direttu", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lucale", "tabs_bar.notifications": "Nutificazione", "tabs_bar.search": "Cercà", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu", "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", "upload_area.title": "Drag & drop per caricà un fugliale", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index d26ef8284..a9442d803 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -72,7 +72,7 @@ "compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.", "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledující.", "compose_form.lock_disclaimer.lock": "uzamčen", - "compose_form.placeholder": "Co máte na mysli?", + "compose_form.placeholder": "Co se vám honí hlavou?", "compose_form.publish": "Tootnout", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive.marked": "Mediální obsah je označen jako citlivý", @@ -84,7 +84,7 @@ "confirmations.block.confirm": "Blokovat", "confirmations.block.message": "Jste si jistý/á, že chcete zablokovat uživatele {name}?", "confirmations.delete.confirm": "Smazat", - "confirmations.delete.message": "Jste si jistý/á, že chcete smazat tento příspěvek?", + "confirmations.delete.message": "Jste si jistý/á, že chcete smazat tento toot?", "confirmations.delete_list.confirm": "Smazat", "confirmations.delete_list.message": "Jste si jistý/á, že chcete tento seznam navždy vymazat?", "confirmations.domain_block.confirm": "Skrýt celou doménu", @@ -92,12 +92,12 @@ "confirmations.mute.confirm": "Ignorovat", "confirmations.mute.message": "Jste si jistý/á, že chcete ignorovat uživatele {name}?", "confirmations.redraft.confirm": "Vymazat a přepsat", - "confirmations.redraft.message": "Jste si jistý/á, že chcete vymazat a přepsat tento příspěvek? Oblíbení a boosty budou ztraceny a odpovědi na původní příspěvek budou opuštěny.", + "confirmations.redraft.message": "Jste si jistý/á, že chcete vymazat a přepsat tento toot? Oblíbení a boosty budou ztraceny a odpovědi na původní příspěvek budou opuštěny.", "confirmations.reply.confirm": "Odpovědět", "confirmations.reply.message": "Odpovězením nyní přepíšete zprávu, kterou aktuálně píšete. Jste si jistý/á, že chcete pokračovat?", "confirmations.unfollow.confirm": "Přestat sledovat", "confirmations.unfollow.message": "jste si jistý/á, že chcete přestat sledovat uživatele {name}?", - "embed.instructions": "Pro přidání příspěvku na vaši webovou stránku zkopírujte níže uvedený kód.", + "embed.instructions": "Pro přidání tootu na vaši webovou stránku zkopírujte níže uvedený kód.", "embed.preview": "Takhle to bude vypadat:", "emoji_button.activity": "Aktivita", "emoji_button.custom": "Vlastní", @@ -108,7 +108,7 @@ "emoji_button.not_found": "Žádná emoji!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Předměty", "emoji_button.people": "Lidé", - "emoji_button.recent": "Často používané", + "emoji_button.recent": "Často používaná", "emoji_button.search": "Hledat...", "emoji_button.search_results": "Výsledky hledání", "emoji_button.symbols": "Symboly", @@ -124,7 +124,7 @@ "empty_column.hashtag": "Pod tímto hashtagem ještě nic není.", "empty_column.home": "Vaše domovská časová osa je prázdná! Začněte navštívením {public} nebo použijte hledání a seznamte se s dalšími uživateli.", "empty_column.home.public_timeline": "veřejné časové osy", - "empty_column.list": "V tomto seznamu ještě nic není. Pokud budou členové tohoto seznamu psát nové příspěvky, objeví se zde.", + "empty_column.list": "V tomto seznamu ještě nic není. Pokud budou členové tohoto seznamu psát nové tooty, objeví se zde.", "empty_column.lists": "Ještě nemáte žádný seznam. Pokud nějaký vytvoříte, zobrazí se zde.", "empty_column.mutes": "Ještě neignorujete žádné uživatele.", "empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte konverzaci komunikováním s ostatními.", @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "a {additional}", "hashtag.column_header.tag_mode.any": "nebo {additional}", "hashtag.column_header.tag_mode.none": "bez {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Žádné návrhy nenalezeny", + "hashtag.column_settings.select.placeholder": "Zadejte hashtagy…", "hashtag.column_settings.tag_mode.all": "Všechny z těchto", "hashtag.column_settings.tag_mode.any": "Jakékoliv z těchto", "hashtag.column_settings.tag_mode.none": "Žádné z těchto", @@ -171,12 +171,12 @@ "keyboard_shortcuts.back": "k návratu zpět", "keyboard_shortcuts.blocked": "k otevření seznamu blokovaných uživatelů", "keyboard_shortcuts.boost": "k boostnutí", - "keyboard_shortcuts.column": "k zaměření na příspěvek v jednom ze sloupců", + "keyboard_shortcuts.column": "k zaměření na toot v jednom ze sloupců", "keyboard_shortcuts.compose": "k zaměření na psací prostor", "keyboard_shortcuts.description": "Popis", "keyboard_shortcuts.direct": "k otevření sloupce s přímými zprávami", "keyboard_shortcuts.down": "k posunutí dolů v seznamu", - "keyboard_shortcuts.enter": "k otevření příspěvku", + "keyboard_shortcuts.enter": "k otevření tootu", "keyboard_shortcuts.favourite": "k oblíbení", "keyboard_shortcuts.favourites": "k otevření seznamu oblíbených", "keyboard_shortcuts.federated": "k otevření federované časové osy", @@ -206,7 +206,7 @@ "lists.account.remove": "Odebrat ze seznamu", "lists.delete": "Smazat seznam", "lists.edit": "Upravit seznam", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Změnit název", "lists.new.create": "Přidat seznam", "lists.new.title_placeholder": "Název nového seznamu", "lists.search": "Hledejte mezi lidmi, které sledujete", @@ -237,10 +237,10 @@ "navigation_bar.preferences": "Předvolby", "navigation_bar.public_timeline": "Federovaná časová osa", "navigation_bar.security": "Zabezpečení", - "notification.favourite": "{name} si oblíbil/a váš příspěvek", + "notification.favourite": "{name} si oblíbil/a váš toot", "notification.follow": "{name} vás začal/a sledovat", "notification.mention": "{name} vás zmínil/a", - "notification.reblog": "{name} boostnul/a váš příspěvek", + "notification.reblog": "{name} boostnul/a váš toot", "notifications.clear": "Vymazat oznámení", "notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?", "notifications.column_settings.alert": "Desktopová oznámení", @@ -260,7 +260,11 @@ "notifications.filter.follows": "Sledování", "notifications.filter.mentions": "Zmínky", "notifications.group": "{count} oznámení", - "privacy.change": "Změnit soukromí příspěvku", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "privacy.change": "Změnit soukromí tootu", "privacy.direct.long": "Odeslat pouze zmíněným uživatelům", "privacy.direct.short": "Přímý", "privacy.private.long": "Odeslat pouze sledujícím", @@ -285,9 +289,9 @@ "report.target": "Nahlášení uživatele {target}", "search.placeholder": "Hledat", "search_popout.search_format": "Pokročilé hledání", - "search_popout.tips.full_text": "Jednoduchý textový výpis příspěvků, které jste napsal/a, oblíbil/a si, boostnul/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, zobrazovaných jmen a hashtagů.", + "search_popout.tips.full_text": "Jednoduchý textový výpis tootů, které jste napsal/a, oblíbil/a si, boostnul/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, zobrazovaných jmen a hashtagů.", "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "příspěvek", + "search_popout.tips.status": "toot", "search_popout.tips.text": "Jednoduchý textový výpis odpovídajících zobrazovaných jmen, přezdívek a hashtagů", "search_popout.tips.user": "uživatel", "search_results.accounts": "Lidé", @@ -296,11 +300,11 @@ "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}", "standalone.public_title": "Nahlédněte dovnitř...", "status.admin_account": "Otevřít moderační rozhraní pro uživatele @{name}", - "status.admin_status": "Otevřít tento příspěvek v moderačním rozhraní", + "status.admin_status": "Otevřít tento toot v moderačním rozhraní", "status.block": "Zablokovat uživatele @{name}", "status.cancel_reblog_private": "Zrušit boost", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", - "status.copy": "Kopírovat odkaz k příspěvku", + "status.copy": "Kopírovat odkaz k tootu", "status.delete": "Smazat", "status.detailed_status": "Detailní zobrazení konverzace", "status.direct": "Poslat přímou zprávu uživateli @{name}", @@ -313,7 +317,7 @@ "status.more": "Více", "status.mute": "Ignorovat uživatele @{name}", "status.mute_conversation": "Ignorovat konverzaci", - "status.open": "Rozbalit tento příspěvek", + "status.open": "Rozbalit tento toot", "status.pin": "Připnout na profil", "status.pinned": "Připnutý toot", "status.read_more": "Číst více", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Místní", "tabs_bar.notifications": "Oznámení", "tabs_bar.search": "Hledat", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří", "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.", "upload_area.title": "Přetažením nahrajete", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 95c8632f7..828508b2a 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Yn dilyn", "notifications.filter.mentions": "Crybwylliadau", "notifications.group": "{count} o hysbysiadau", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Addasu preifatrwdd y tŵt", "privacy.direct.long": "Cyhoeddi i'r defnyddwyr sy'n cael eu crybwyll yn unig", "privacy.direct.short": "Uniongyrchol", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lleol", "tabs_bar.notifications": "Hysbysiadau", "tabs_bar.search": "Chwilio", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad", "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.", "upload_area.title": "Llusgwch & gollwing i uwchlwytho", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index f383f2c9c..7e8f4d3f7 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Følger", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifikationer", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ændre status privatliv", "privacy.direct.long": "Post til kun de nævnte brugere", "privacy.direct.short": "Direkte", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Notifikationer", "tabs_bar.search": "Søg", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} snakker", "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.", "upload_area.title": "Træk og slip for at uploade", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 18e496b0e..44d8e76fa 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Folgende", "notifications.filter.mentions": "Erwähnungen", "notifications.group": "{count} Benachrichtigungen", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Sichtbarkeit des Beitrags anpassen", "privacy.direct.long": "Beitrag nur an erwähnte Profile", "privacy.direct.short": "Direkt", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Mitteilungen", "tabs_bar.search": "Suchen", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "upload_area.title": "Zum Hochladen hereinziehen", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 157b69847..0afe3955b 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -197,6 +197,47 @@ ], "path": "app/javascript/mastodon/components/missing_indicator.json" }, + { + "descriptors": [ + { + "defaultMessage": "Moments remaining", + "id": "time_remaining.moments" + }, + { + "defaultMessage": "{number, plural, one {# second} other {# seconds}} left", + "id": "time_remaining.seconds" + }, + { + "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left", + "id": "time_remaining.minutes" + }, + { + "defaultMessage": "{number, plural, one {# hour} other {# hours}} left", + "id": "time_remaining.hours" + }, + { + "defaultMessage": "{number, plural, one {# day} other {# days}} left", + "id": "time_remaining.days" + }, + { + "defaultMessage": "Closed", + "id": "poll.closed" + }, + { + "defaultMessage": "Vote", + "id": "poll.vote" + }, + { + "defaultMessage": "Refresh", + "id": "poll.refresh" + }, + { + "defaultMessage": "{count, plural, one {# vote} other {# votes}}", + "id": "poll.total_votes" + } + ], + "path": "app/javascript/mastodon/components/poll.json" + }, { "descriptors": [ { diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index a36f41ce5..a9ed36243 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "και {additional}", "hashtag.column_header.tag_mode.any": "ή {additional}", "hashtag.column_header.tag_mode.none": "χωρίς {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Δεν βρέθηκαν προτάσεις", + "hashtag.column_settings.select.placeholder": "Γράψε μερικές ταμπέλες…", "hashtag.column_settings.tag_mode.all": "Όλα αυτα", "hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά", "hashtag.column_settings.tag_mode.none": "Κανένα από αυτά", @@ -206,7 +206,7 @@ "lists.account.remove": "Βγάλε από τη λίστα", "lists.delete": "Διαγραφή λίστας", "lists.edit": "Επεξεργασία λίστας", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Αλλαγή τίτλου", "lists.new.create": "Προσθήκη λίστας", "lists.new.title_placeholder": "Τίτλος νέας λίστα", "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Ακόλουθοι", "notifications.filter.mentions": "Αναφορές", "notifications.group": "{count} ειδοποιήσεις", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης", "privacy.direct.long": "Δημοσίευση μόνο σε όσους και όσες αναφέρονται", "privacy.direct.short": "Προσωπικά", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Τοπικά", "tabs_bar.notifications": "Ειδοποιήσεις", "tabs_bar.search": "Αναζήτηση", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} μιλάνε", "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.", "upload_area.title": "Drag & drop για να ανεβάσεις", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 7f7abf372..b0d7488ca 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index f8d427c80..47820da90 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Sekvoj", "notifications.filter.mentions": "Mencioj", "notifications.group": "{count} sciigoj", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Agordi mesaĝan privatecon", "privacy.direct.long": "Afiŝi nur al menciitaj uzantoj", "privacy.direct.short": "Rekta", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Loka tempolinio", "tabs_bar.notifications": "Sciigoj", "tabs_bar.search": "Serĉi", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, pluraj, unu {person} alia(j) {people}} parolas", "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", "upload_area.title": "Altreni kaj lasi por alŝuti", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 99dce8ffe..7bb1a304e 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notificaciones", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajustar privacidad", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.short": "Directo", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", "tabs_bar.search": "Buscar", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 63a2354ae..76f1c24f0 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Jarraipenak", "notifications.filter.mentions": "Aipamenak", "notifications.group": "{count} jakinarazpen", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Doitu mezuaren pribatutasuna", "privacy.direct.long": "Bidali aipatutako erabiltzaileei besterik ez", "privacy.direct.short": "Zuzena", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokala", "tabs_bar.notifications": "Jakinarazpenak", "tabs_bar.search": "Bilatu", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} hitz egiten", "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.", "upload_area.title": "Arrastatu eta jaregin igotzeko", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index f2f144e78..5cdcf2441 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -258,8 +258,12 @@ "notifications.filter.boosts": "بازبوق‌ها", "notifications.filter.favourites": "پسندیده‌ها", "notifications.filter.follows": "پیگیری‌ها", - "notifications.filter.mentions": "نام‌بردن‌ها", + "notifications.filter.mentions": "گفتگوها", "notifications.group": "{count} اعلان", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "تنظیم حریم خصوصی نوشته‌ها", "privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده", "privacy.direct.short": "مستقیم", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "محلی", "tabs_bar.notifications": "اعلان‌ها", "tabs_bar.search": "جستجو", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشته‌اند}}", "ui.beforeunload": "اگر از ماستدون خارج شوید پیش‌نویس شما پاک خواهد شد.", "upload_area.title": "برای بارگذاری به این‌جا بکشید", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 537280223..6ddd5a02d 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Säädä tuuttauksen näkyvyyttä", "privacy.direct.long": "Julkaise vain mainituille käyttäjille", "privacy.direct.short": "Suora viesti", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Paikallinen", "tabs_bar.notifications": "Ilmoitukset", "tabs_bar.search": "Hae", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index f88f29b11..91ac04fcd 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "et {additional}", "hashtag.column_header.tag_mode.any": "ou {additional}", "hashtag.column_header.tag_mode.none": "sans {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Aucune suggestion trouvée", + "hashtag.column_settings.select.placeholder": "Ajouter des hashtags…", "hashtag.column_settings.tag_mode.all": "Tous ces éléments", "hashtag.column_settings.tag_mode.any": "Au moins un de ces éléments", "hashtag.column_settings.tag_mode.none": "Aucun de ces éléments", @@ -206,7 +206,7 @@ "lists.account.remove": "Supprimer de la liste", "lists.delete": "Effacer la liste", "lists.edit": "Éditer la liste", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Changer le titre", "lists.new.create": "Ajouter une liste", "lists.new.title_placeholder": "Titre de la nouvelle liste", "lists.search": "Rechercher parmi les gens que vous suivez", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Abonné·e·s", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajuster la confidentialité du message", "privacy.direct.long": "N’envoyer qu’aux personnes mentionnées", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Fil public local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Chercher", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent", "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.", "upload_area.title": "Glissez et déposez pour envoyer", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 789624d38..29638d348 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Seguimentos", "notifications.filter.mentions": "Mencións", "notifications.group": "{count} notificacións", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Axustar a intimidade do estado", "privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas", "privacy.direct.short": "Directa", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificacións", "tabs_bar.search": "Buscar", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando", "ui.beforeunload": "O borrador perderase se sae de Mastodon.", "upload_area.title": "Arrastre e solte para subir", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 56d474170..d40e339a8 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "שינוי פרטיות ההודעה", "privacy.direct.long": "הצג רק למי שהודעה זו פונה אליו", "privacy.direct.short": "הודעה ישירה", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "ציר זמן מקומי", "tabs_bar.notifications": "התראות", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "upload_area.title": "ניתן להעלות על ידי Drag & drop", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index cc4f5725a..b17aa8058 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Podesi status privatnosti", "privacy.direct.long": "Prikaži samo spomenutim korisnicima", "privacy.direct.short": "Direktno", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Notifikacije", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Povuci i spusti kako bi uploadao", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index a82b3e94d..f0c686212 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Státusz láthatóságának módosítása", "privacy.direct.long": "Posztolás csak az említett felhasználóknak", "privacy.direct.short": "Egyenesen", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Értesítések", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.", "upload_area.title": "Húzza ide a feltöltéshez", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 07b239f87..f9ef89fa9 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Կարգավորել թթի գաղտնիությունը", "privacy.direct.long": "Թթել միայն նշված օգտատերերի համար", "privacy.direct.short": "Հասցեագրված", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Տեղական", "tabs_bar.notifications": "Ծանուցումներ", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index a23c0a547..3f6c420a6 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Tentukan privasi status", "privacy.direct.long": "Kirim hanya ke pengguna yang disebut", "privacy.direct.short": "Langsung", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Notifikasi", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.", "upload_area.title": "Seret & lepaskan untuk mengunggah", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index e375314bd..3b7d86ab0 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Aranjar privateso di mesaji", "privacy.direct.long": "Sendar nur a mencionata uzeri", "privacy.direct.short": "Direte", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokala", "tabs_bar.notifications": "Savigi", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Tranar faligar por kargar", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 6825d8d05..8be3e6163 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "e {additional}", "hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.none": "senza {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Nessun suggerimento trovato", + "hashtag.column_settings.select.placeholder": "Inserisci hashtag…", "hashtag.column_settings.tag_mode.all": "Tutti questi", "hashtag.column_settings.tag_mode.any": "Uno o più di questi", "hashtag.column_settings.tag_mode.none": "Nessuno di questi", @@ -206,7 +206,7 @@ "lists.account.remove": "Togli dalla lista", "lists.delete": "Delete list", "lists.edit": "Modifica lista", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cambia titolo", "lists.new.create": "Aggiungi lista", "lists.new.title_placeholder": "Titolo della nuova lista", "lists.search": "Cerca tra le persone che segui", @@ -227,7 +227,7 @@ "navigation_bar.favourites": "Apprezzati", "navigation_bar.filters": "Parole silenziate", "navigation_bar.follow_requests": "Richieste di amicizia", - "navigation_bar.info": "Informazioni estese", + "navigation_bar.info": "Informazioni su questo server", "navigation_bar.keyboard_shortcuts": "Tasti di scelta rapida", "navigation_bar.lists": "Liste", "navigation_bar.logout": "Esci", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Seguaci", "notifications.filter.mentions": "Menzioni", "notifications.group": "{count} notifiche", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", "privacy.direct.short": "Diretto", @@ -279,7 +283,7 @@ "reply_indicator.cancel": "Annulla", "report.forward": "Inoltra a {target}", "report.forward_hint": "Questo account appartiene a un altro server. Mandare anche là una copia anonima del rapporto?", - "report.hint": "La segnalazione sarà inviata ai moderatori della tua istanza. Di seguito, puoi fornire il motivo per il quale stai segnalando questo account:", + "report.hint": "La segnalazione sarà inviata ai moderatori del tuo server. Di seguito, puoi fornire il motivo per il quale stai segnalando questo account:", "report.placeholder": "Commenti aggiuntivi", "report.submit": "Invia", "report.target": "Invio la segnalazione {target}", @@ -297,10 +301,10 @@ "standalone.public_title": "Un'occhiata all'interno...", "status.admin_account": "Apri interfaccia di moderazione per @{name}", "status.admin_status": "Apri questo status nell'interfaccia di moderazione", - "status.block": "Block @{name}", + "status.block": "Blocca @{name}", "status.cancel_reblog_private": "Annulla condivisione", "status.cannot_reblog": "Questo post non può essere condiviso", - "status.copy": "Copy link to status", + "status.copy": "Copia link allo status", "status.delete": "Elimina", "status.detailed_status": "Vista conversazione dettagliata", "status.direct": "Messaggio diretto @{name}", @@ -342,11 +346,16 @@ "tabs_bar.local_timeline": "Locale", "tabs_bar.notifications": "Notifiche", "tabs_bar.search": "Cerca", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona ne sta} other {persone ne stanno}} parlando", "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.", "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", - "upload_error.limit": "File upload limit exceeded.", + "upload_error.limit": "Limite al caricamento di file superato.", "upload_form.description": "Descrizione per utenti con disabilità visive", "upload_form.focus": "Modifica anteprima", "upload_form.undo": "Cancella", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 4fb5d8e27..b7c38738c 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "と {additional}", "hashtag.column_header.tag_mode.any": "か {additional}", "hashtag.column_header.tag_mode.none": "({additional} を除く)", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "提案はありません", + "hashtag.column_settings.select.placeholder": "ハッシュタグを入力してください…", "hashtag.column_settings.tag_mode.all": "すべてを含む", "hashtag.column_settings.tag_mode.any": "いずれかを含む", "hashtag.column_settings.tag_mode.none": "これらを除く", @@ -206,7 +206,7 @@ "lists.account.remove": "リストから外す", "lists.delete": "リストを削除", "lists.edit": "リストを編集", - "lists.edit.submit": "Change title", + "lists.edit.submit": "タイトルを変更", "lists.new.create": "リストを作成", "lists.new.title_placeholder": "新規リスト名", "lists.search": "フォローしている人の中から検索", @@ -260,6 +260,10 @@ "notifications.filter.follows": "フォロー", "notifications.filter.mentions": "返信", "notifications.group": "{count} 件の通知", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "公開範囲を変更", "privacy.direct.long": "メンションしたユーザーだけに公開", "privacy.direct.short": "ダイレクト", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "ローカル", "tabs_bar.notifications": "通知", "tabs_bar.search": "検索", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {人} other {人}} がトゥート", "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index 2f7cdc70d..2821d75e4 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} შეტყობინება", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "სტატუსის კონფიდენციალურობის მითითება", "privacy.direct.long": "დაიპოსტოს მხოლოდ დასახელებულ მომხმარებლებთან", "privacy.direct.short": "პირდაპირი", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "ლოკალური", "tabs_bar.notifications": "შეტყობინებები", "tabs_bar.search": "ძებნა", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} საუბრობს", "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.", "upload_area.title": "გადმოწიეთ და ჩააგდეთ ასატვირთათ", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index fc0c88dd9..529459cf2 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -1,363 +1,372 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", - "account.block": "Block @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.blocked": "Blocked", - "account.direct": "Direct message @{name}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", - "account.domain_blocked": "Domain hidden", - "account.edit_profile": "Edit profile", - "account.endorse": "Feature on profile", - "account.follow": "Follow", - "account.followers": "Followers", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "Follows", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "Follows you", - "account.hide_reblogs": "Hide boosts from @{name}", - "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", - "account.mention": "Mention @{name}", - "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 and replies", - "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", - "account.share": "Share @{name}'s profile", - "account.show_reblogs": "Show boosts from @{name}", - "account.unblock": "Unblock @{name}", - "account.unblock_domain": "Unhide {domain}", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "Unfollow", - "account.unmute": "Unmute @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", - "account.view_full_profile": "View full profile", - "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", - "boost_modal.combo": "You can press {combo} to skip this next time", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", - "column.blocks": "Blocked users", - "column.community": "Local timeline", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", - "column.home": "Home", - "column.lists": "Lists", - "column.mutes": "Muted users", + "account.add_or_remove_from_list": "Тізімге қосу немесе жою", + "account.badges.bot": "Бот", + "account.block": "Бұғаттау @{name}", + "account.block_domain": "Домендегі барлығын бұғатта {domain}", + "account.blocked": "Бұғатталды", + "account.direct": "Жеке хат @{name}", + "account.disclaimer_full": "Қолданушы туралы барлық мәліметті көрсетпеуі мүмкін.", + "account.domain_blocked": "Домен жабық", + "account.edit_profile": "Профильді өңдеу", + "account.endorse": "Профильде рекомендеу", + "account.follow": "Жазылу", + "account.followers": "Оқырмандар", + "account.followers.empty": "Әлі ешкім жазылмаған.", + "account.follows": "Жазылғандары", + "account.follows.empty": "Ешкімге жазылмапты.", + "account.follows_you": "Сізге жазылыпты", + "account.hide_reblogs": "@{name} атты қолданушының әрекеттерін жасыру", + "account.link_verified_on": "Сілтеме меншігі расталған күн {date}", + "account.locked_info": "Бұл қолданушы өзі туралы мәліметтерді жасырған. Тек жазылғандар ғана көре алады.", + "account.media": "Медиа", + "account.mention": "Аталым @{name}", + "account.moved_to": "{name} көшіп кетті:", + "account.mute": "Үнсіз қылу @{name}", + "account.mute_notifications": "@{name} туралы ескертпелерді жасыру", + "account.muted": "Үнсіз", + "account.posts": "Жазбалар", + "account.posts_with_replies": "Жазбалар мен жауаптар", + "account.report": "Шағымдану @{name}", + "account.requested": "Растауын күтіңіз. Жазылудан бас тарту үшін басыңыз", + "account.share": "@{name} профилін бөлісу\"", + "account.show_reblogs": "@{name} бөліскендерін көрсету", + "account.unblock": "Бұғаттан шығару @{name}", + "account.unblock_domain": "Бұғаттан шығару {domain}", + "account.unendorse": "Профильде рекомендемеу", + "account.unfollow": "Оқымау", + "account.unmute": "@{name} ескертпелерін қосу", + "account.unmute_notifications": "@{name} ескертпелерін көрсету", + "account.view_full_profile": "Толқы профилін көрсету", + "alert.unexpected.message": "Бір нәрсе дұрыс болмады.", + "alert.unexpected.title": "Өй!", + "boost_modal.combo": "Келесіде өткізіп жіберу үшін басыңыз {combo}", + "bundle_column_error.body": "Бұл компонентті жүктеген кезде бір қате пайда болды.", + "bundle_column_error.retry": "Қайтадан көріңіз", + "bundle_column_error.title": "Желі қатесі", + "bundle_modal_error.close": "Жабу", + "bundle_modal_error.message": "Бұл компонентті жүктеген кезде бір қате пайда болды.", + "bundle_modal_error.retry": "Қайтадан көріңіз", + "column.blocks": "Бұғатталғандар", + "column.community": "Жергілікті желі", + "column.direct": "Жеке хаттар", + "column.domain_blocks": "Жасырылған домендер", + "column.favourites": "Таңдаулылар", + "column.follow_requests": "Жазылу сұранымдары", + "column.home": "Басты бет", + "column.lists": "Тізімдер", + "column.mutes": "Үнсіз қолданушылар", "column.notifications": "Notifications", - "column.pins": "Pinned toot", - "column.public": "Federated timeline", - "column_back_button.label": "Back", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", - "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", - "compose_form.placeholder": "What is on your mind?", - "compose_form.publish": "Toot", + "column.pins": "Жабыстырылған жазбалар", + "column.public": "Жаһандық желі", + "column_back_button.label": "Артқа", + "column_header.hide_settings": "Баптауларды жасыр", + "column_header.moveLeft_settings": "Бағананы солға жылжыту", + "column_header.moveRight_settings": "Бағананы оңға жылжыту", + "column_header.pin": "Жабыстыру", + "column_header.show_settings": "Баптауларды көрсет", + "column_header.unpin": "Алып тастау", + "column_subheading.settings": "Баптаулар", + "community.column_settings.media_only": "Тек медиа", + "compose_form.direct_message_warning": "Тек аталған қолданушыларға.", + "compose_form.direct_message_warning_learn_more": "Көбірек білу", + "compose_form.hashtag_warning": "Бұл пост іздеуде хэштегпен шықпайды, өйткені ол бәріне ашық емес. Тек ашық жазбаларды ғана хэштег арқылы іздеп табуға болады.", + "compose_form.lock_disclaimer": "Аккаунтыңыз {locked} емес. Кез келген адам жазылып, сізді оқи алады.", + "compose_form.lock_disclaimer.lock": "жабық", + "compose_form.placeholder": "Не бөліскіңіз келеді?", + "compose_form.publish": "Түрт", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.marked": "Media is marked as sensitive", - "compose_form.sensitive.unmarked": "Media is not marked as sensitive", - "compose_form.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", - "compose_form.spoiler_placeholder": "Write your warning here", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.redraft.confirm": "Delete & redraft", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.account_timeline": "No toots here!", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", - "getting_started.heading": "Getting started", - "getting_started.invite": "Invite people", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", - "home.column_settings.basic": "Basic", - "home.column_settings.show_reblogs": "Show boosts", - "home.column_settings.show_replies": "Show replies", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "to navigate back", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "to boost", - "keyboard_shortcuts.column": "to focus a status in one of the columns", - "keyboard_shortcuts.compose": "to focus the compose textarea", - "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "to move down in the list", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", - "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.home": "to open home timeline", - "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toot": "to start a brand new toot", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", - "lightbox.close": "Close", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.edit.submit": "Change title", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", - "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", - "navigation_bar.edit_profile": "Edit profile", - "navigation_bar.favourites": "Favourites", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.info": "About this server", - "navigation_bar.keyboard_shortcuts": "Hotkeys", - "navigation_bar.lists": "Lists", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Muted users", - "navigation_bar.personal": "Personal", - "navigation_bar.pins": "Pinned toots", - "navigation_bar.preferences": "Preferences", - "navigation_bar.public_timeline": "Federated timeline", - "navigation_bar.security": "Security", - "notification.favourite": "{name} favourited your status", - "notification.follow": "{name} followed you", - "notification.mention": "{name} mentioned you", - "notification.reblog": "{name} boosted your status", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.group": "{count} notifications", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", + "compose_form.sensitive.marked": "Медиа нәзік деп белгіленген", + "compose_form.sensitive.unmarked": "Медиа нәзік деп белгіленбеген", + "compose_form.spoiler.marked": "Мәтін ескертумен жасырылған", + "compose_form.spoiler.unmarked": "Мәтін жасырылмаған", + "compose_form.spoiler_placeholder": "Ескертуіңізді осында жазыңыз", + "confirmation_modal.cancel": "Қайтып алу", + "confirmations.block.confirm": "Бұғаттау", + "confirmations.block.message": "{name} атты қолданушыны бұғаттайтыныңызға сенімдісіз бе?", + "confirmations.delete.confirm": "Өшіру", + "confirmations.delete.message": "Бұл жазбаны өшіресіз бе?", + "confirmations.delete_list.confirm": "Өшіру", + "confirmations.delete_list.message": "Бұл тізімді жоясыз ба шынымен?", + "confirmations.domain_block.confirm": "Бұл доменді бұғатта", + "confirmations.domain_block.message": "Бұл домендегі {domain} жазбаларды шынымен бұғаттайсыз ба? Кейде үнсіз қылып тастау да жеткілікті.", + "confirmations.mute.confirm": "Үнсіз қылу", + "confirmations.mute.message": "{name} атты қолданушы үнсіз болсын ба?", + "confirmations.redraft.confirm": "Өшіруді құптау", + "confirmations.redraft.message": "Бұл жазбаны өшіріп, нобайларға жібереміз бе? Барлық жауаптар мен лайктарды жоғалтасыз.", + "confirmations.reply.confirm": "Жауап", + "confirmations.reply.message": "Жауабыңыз жазып жатқан жазбаңыздың үстіне кетеді. Жалғастырамыз ба?", + "confirmations.unfollow.confirm": "Оқымау", + "confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?", + "embed.instructions": "Төмендегі кодты көшіріп алу арқылы жазбаны басқа сайттарға да орналастыра аласыз.", + "embed.preview": "Былай көрінетін болады:", + "emoji_button.activity": "Белсенділік", + "emoji_button.custom": "Жеке", + "emoji_button.flags": "Тулар", + "emoji_button.food": "Тамақ", + "emoji_button.label": "Эмодзи қосу", + "emoji_button.nature": "Табиғат", + "emoji_button.not_found": "Эмодзи жоқ!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Заттар", + "emoji_button.people": "Адамдар", + "emoji_button.recent": "Соңғы", + "emoji_button.search": "Іздеу...", + "emoji_button.search_results": "Іздеу нәтижелері", + "emoji_button.symbols": "Таңбалар", + "emoji_button.travel": "Саяхат", + "empty_column.account_timeline": "Жазба жоқ ешқандай!", + "empty_column.blocks": "Ешкімді бұғаттамағансыз.", + "empty_column.community": "Жергілікті желі бос. Сіз бастап жазыңыз!", + "empty_column.direct": "Әзірше дым хат жоқ. Өзіңіз жазып көріңіз алдымен.", + "empty_column.domain_blocks": "Бұғатталған домен жоқ.", + "empty_column.favourited_statuses": "Ешқандай жазба 'Таңдаулылар' тізіміне қосылмапты. Қосылғаннан кейін осында жинала бастайды.", + "empty_column.favourites": "Бұл постты әлі ешкім 'Таңдаулылар' тізіміне қоспапты. Біреу бастағаннан кейін осында көрінетін болады.", + "empty_column.follow_requests": "Әлі ешқандай жазылуға сұранымдар келмеді. Жаңа сұранымдар осында көрінетін болады.", + "empty_column.hashtag": "Бұндай хэштегпен әлі ешкім жазбапты.", + "empty_column.home": "Әлі ешкімге жазылмапсыз. Бәлкім {public} жазбаларын қарап немесе іздеуді қолданып көрерсіз.", + "empty_column.home.public_timeline": "ашық желі", + "empty_column.list": "Бұл тізімде ештеңе жоқ.", + "empty_column.lists": "Әзірше ешқандай тізіміңіз жоқ. Біреуін құрғаннан кейін осы жерде көрінетін болады.", + "empty_column.mutes": "Әзірше ешқандай үнсізге қойылған қолданушы жоқ.", + "empty_column.notifications": "Әзірше ешқандай ескертпе жоқ. Басқалармен араласуды бастаңыз және пікірталастарға қатысыңыз.", + "empty_column.public": "Ештеңе жоқ бұл жерде! Өзіңіз бастап жазып көріңіз немесе басқаларға жазылыңыз", + "follow_request.authorize": "Авторизация", + "follow_request.reject": "Қабылдамау", + "getting_started.developers": "Жасаушылар тобы", + "getting_started.directory": "Профильдер каталогы", + "getting_started.documentation": "Құжаттама", + "getting_started.heading": "Желіде", + "getting_started.invite": "Адам шақыру", + "getting_started.open_source_notice": "Mastodon - ашық кодты құрылым. Түзету енгізу немесе ұсыныстарды GitHub арқылы жасаңыз {github}.", + "getting_started.security": "Қауіпсіздік", + "getting_started.terms": "Қызмет көрсету шарттары", + "hashtag.column_header.tag_mode.all": "және {additional}", + "hashtag.column_header.tag_mode.any": "немесе {additional}", + "hashtag.column_header.tag_mode.none": "{additional} болмай", + "hashtag.column_settings.select.no_options_message": "Ұсыныстар табылмады", + "hashtag.column_settings.select.placeholder": "Хэштег жазыңыз…", + "hashtag.column_settings.tag_mode.all": "Осының барлығын", + "hashtag.column_settings.tag_mode.any": "Осылардың біреуін", + "hashtag.column_settings.tag_mode.none": "Бұлардың ешқайсысын", + "hashtag.column_settings.tag_toggle": "Осы бағанға қосымша тегтерді қосыңыз", + "home.column_settings.basic": "Негізгі", + "home.column_settings.show_reblogs": "Бөлісулерді көрсету", + "home.column_settings.show_replies": "Жауаптарды көрсету", + "introduction.federation.action": "Келесі", + "introduction.federation.federated.headline": "Жаһандық", + "introduction.federation.federated.text": "Жаһандық желідегі жазбалар осында көрінетін болады.", + "introduction.federation.home.headline": "Басты бет", + "introduction.federation.home.text": "Жазылған адамдарыңыздың жазбалары осында шығады. Кез келген серверден жазылуыңызға болады!", + "introduction.federation.local.headline": "Жергілікті", + "introduction.federation.local.text": "Жергілікті желіде жазылған жазбалар осында шығатын болады.", + "introduction.interactions.action": "Оқулық аяқталды!", + "introduction.interactions.favourite.headline": "Таңдаулы", + "introduction.interactions.favourite.text": "Жазбаларды таңдаулыға сақтауға болады, осылайша авторына ұнағанын білдіре аласыз.", + "introduction.interactions.reblog.headline": "Бөлісу", + "introduction.interactions.reblog.text": "Ұнаған жазбаларды өз оқырмандарыңызбен бөлісе аласыз.", + "introduction.interactions.reply.headline": "Жауап", + "introduction.interactions.reply.text": "Жазбаларға жауап жаза аласыз, осылайша пікірталас өрбітуіңізге болады.", + "introduction.welcome.action": "Кеттік!", + "introduction.welcome.headline": "Алғашқы қадамдар", + "introduction.welcome.text": "Желіге қош келдіңіз! Бірнеше минуттан кейін желіде жазба қалдырып, медиа бөлісіп, басқалармен пікірталасқа қатысып ортаға қосыла аласыз. . Бірақ бұл сервер {domain} - бұл ерекше, ол сіздің профиліңізді қояды, сондықтан оның есімін есіңізде сақтаңыз.", + "keyboard_shortcuts.back": "артқа қайту", + "keyboard_shortcuts.blocked": "бұғатталғандар тізімін ашу", + "keyboard_shortcuts.boost": "жазба бөлісу", + "keyboard_shortcuts.column": "бағандардағы жазбаны оқу", + "keyboard_shortcuts.compose": "пост жазу", + "keyboard_shortcuts.description": "Сипаттама", + "keyboard_shortcuts.direct": "жеке хаттар бағаны", + "keyboard_shortcuts.down": "тізімде төмен түсу", + "keyboard_shortcuts.enter": "жазбаны ашу", + "keyboard_shortcuts.favourite": "таңдаулыға қосу", + "keyboard_shortcuts.favourites": "таңдаулылар тізімін ашу", + "keyboard_shortcuts.federated": "жаңандық желіні ашу", + "keyboard_shortcuts.heading": "Қысқа кодтар тақтасы", + "keyboard_shortcuts.home": "жергілікті жазбаларды қарау", + "keyboard_shortcuts.hotkey": "Ыстық пернелер", + "keyboard_shortcuts.legend": "осы мазмұнды көрсету", + "keyboard_shortcuts.local": "жергілікті желіні ашу", + "keyboard_shortcuts.mention": "авторды атап өту", + "keyboard_shortcuts.muted": "үнсіздер тізімін ашу", + "keyboard_shortcuts.my_profile": "профиліңізді ашу", + "keyboard_shortcuts.notifications": "ескертпелер бағанын ашу", + "keyboard_shortcuts.pinned": "жабыстырылған жазбаларды көру", + "keyboard_shortcuts.profile": "автор профилін қарау", + "keyboard_shortcuts.reply": "жауап жазу", + "keyboard_shortcuts.requests": "жазылу сұранымдарын қарау", + "keyboard_shortcuts.search": "іздеу", + "keyboard_shortcuts.start": "бастапқы бағанға бару", + "keyboard_shortcuts.toggle_hidden": "жабық мәтінді CW ашу/жабу", + "keyboard_shortcuts.toot": "жаңа жазба бастау", + "keyboard_shortcuts.unfocus": "жазба қалдыру алаңынан шығу", + "keyboard_shortcuts.up": "тізімде жоғары шығу", + "lightbox.close": "Жабу", + "lightbox.next": "Келесі", + "lightbox.previous": "Алдыңғы", + "lists.account.add": "Тізімге қосу", + "lists.account.remove": "Тізімнен шығару", + "lists.delete": "Тізімді өшіру", + "lists.edit": "Тізімді өңдеу", + "lists.edit.submit": "Тақырыбын өзгерту", + "lists.new.create": "Тізім құру", + "lists.new.title_placeholder": "Жаңа тізім аты", + "lists.search": "Сіз іздеген адамдар арасында іздеу", + "lists.subheading": "Тізімдеріңіз", + "loading_indicator.label": "Жүктеу...", + "media_gallery.toggle_visible": "Көрінуді қосу", + "missing_indicator.label": "Табылмады", + "missing_indicator.sublabel": "Бұл ресурс табылмады", + "mute_modal.hide_notifications": "Бұл қолданушы ескертпелерін жасырамыз ба?", + "navigation_bar.apps": "Мобиль қосымшаларMobile apps", + "navigation_bar.blocks": "Бұғатталғандар", + "navigation_bar.community_timeline": "Жергілікті желі", + "navigation_bar.compose": "Жаңа жазба бастау", + "navigation_bar.direct": "Жеке хаттар", + "navigation_bar.discover": "шарлау", + "navigation_bar.domain_blocks": "Жабық домендер", + "navigation_bar.edit_profile": "Профиль түзету", + "navigation_bar.favourites": "Таңдаулылар", + "navigation_bar.filters": "Үнсіз сөздер", + "navigation_bar.follow_requests": "Жазылуға сұранғандар", + "navigation_bar.info": "Сервер туралы", + "navigation_bar.keyboard_shortcuts": "Ыстық пернелер", + "navigation_bar.lists": "Тізімдер", + "navigation_bar.logout": "Шығу", + "navigation_bar.mutes": "Үнсіз қолданушылар", + "navigation_bar.personal": "Жеке", + "navigation_bar.pins": "Жабыстырылғандар", + "navigation_bar.preferences": "Басымдықтар", + "navigation_bar.public_timeline": "Жаһандық желі", + "navigation_bar.security": "Қауіпсіздік", + "notification.favourite": "{name} жазбаңызды таңдаулыға қосты", + "notification.follow": "{name} сізге жазылды", + "notification.mention": "{name} сізді атап өтті", + "notification.reblog": "{name} жазбаңызды бөлісті", + "notifications.clear": "Ескертпелерді тазарт", + "notifications.clear_confirmation": "Шынымен барлық ескертпелерді өшіресіз бе?", + "notifications.column_settings.alert": "Үстел ескертпелері", + "notifications.column_settings.favourite": "Таңдаулылар:", + "notifications.column_settings.filter_bar.advanced": "Барлық категорияны көрсет", + "notifications.column_settings.filter_bar.category": "Жедел сүзгі", + "notifications.column_settings.filter_bar.show": "Көрсету", + "notifications.column_settings.follow": "Жаңа оқырмандар:", + "notifications.column_settings.mention": "Аталымдар:", + "notifications.column_settings.push": "Push ескертпелер", + "notifications.column_settings.reblog": "Бөлісулер:", + "notifications.column_settings.show": "Бағанда көрсет", + "notifications.column_settings.sound": "Дыбысын қос", + "notifications.filter.all": "Барлығы", + "notifications.filter.boosts": "Бөлісулер", + "notifications.filter.favourites": "Таңдаулылар", + "notifications.filter.follows": "Жазылулар", + "notifications.filter.mentions": "Аталымдар", + "notifications.group": "{count} ескертпе", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "privacy.change": "Құпиялылықты реттеу", + "privacy.direct.long": "Аталған адамдарға ғана көрінетін жазба", + "privacy.direct.short": "Тікелей", + "privacy.private.long": "Тек оқырмандарға арналған жазба", + "privacy.private.short": "Оқырмандарға ғана", + "privacy.public.long": "Ашық желіге жібер", + "privacy.public.short": "Ашық", "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", - "relative_time.just_now": "now", - "relative_time.minutes": "{number}m", + "privacy.unlisted.short": "Тізімсіз", + "regeneration_indicator.label": "Жүктеу…", + "regeneration_indicator.sublabel": "Жергілікті желі құрылуда!", + "relative_time.days": "{number}күн", + "relative_time.hours": "{number}сағ", + "relative_time.just_now": "жаңа", + "relative_time.minutes": "{number}мин", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "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.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Report {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", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", - "search_results.statuses": "Toots", + "reply_indicator.cancel": "Қайтып алу", + "report.forward": "Жіберу {target}", + "report.forward_hint": "Бұл аккаунт басқа серверден. Аноним шағым жібересіз бе?", + "report.hint": "Шағым сіздің модераторларға жіберіледі. Шағымның себептерін мына жерге жазуыңызға болады:", + "report.placeholder": "Қосымша пікірлер", + "report.submit": "Жіберу", + "report.target": "Шағымдану {target}", + "search.placeholder": "Іздеу", + "search_popout.search_format": "Кеңейтілген іздеу форматы", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, bоosted, 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": "Simple text returns matching display names, usernames аnd hashtags", + "search_popout.tips.user": "қолданушы", + "search_results.accounts": "Адамдар", + "search_results.hashtags": "Хэштегтер", + "search_results.statuses": "Жазбалар", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "standalone.public_title": "A look inside...", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", - "status.delete": "Delete", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", - "status.favourite": "Favourite", - "status.filtered": "Filtered", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", - "status.mention": "Mention @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", - "status.pinned": "Pinned toot", - "status.read_more": "Read more", - "status.reblog": "Boost", - "status.reblog_private": "Boost to original audience", - "status.reblogged_by": "{name} boosted", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", - "status.reply": "Reply", - "status.replyAll": "Reply to thread", - "status.report": "Report @{name}", - "status.sensitive_toggle": "Click to view", - "status.sensitive_warning": "Sensitive content", - "status.share": "Share", - "status.show_less": "Show less", - "status.show_less_all": "Show less for all", - "status.show_more": "Show more", - "status.show_more_all": "Show more for all", - "status.show_thread": "Show thread", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", - "tabs_bar.federated_timeline": "Federated", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notifications", - "tabs_bar.search": "Search", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", - "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "File upload limit exceeded.", - "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", - "upload_form.undo": "Delete", - "upload_progress.label": "Uploading...", - "video.close": "Close video", - "video.exit_fullscreen": "Exit full screen", - "video.expand": "Expand video", - "video.fullscreen": "Full screen", - "video.hide": "Hide video", - "video.mute": "Mute sound", - "video.pause": "Pause", - "video.play": "Play", - "video.unmute": "Unmute sound" + "standalone.public_title": "Ішкі көрініс...", + "status.admin_account": "@{name} үшін модерация интерфейсін аш", + "status.admin_status": "Бұл жазбаны модерация интерфейсінде аш", + "status.block": "Бұғаттау @{name}", + "status.cancel_reblog_private": "Бөліспеу", + "status.cannot_reblog": "Бұл жазба бөлісілмейді", + "status.copy": "Жазба сілтемесін көшір", + "status.delete": "Өшіру", + "status.detailed_status": "Толық пікірталас көрінісі", + "status.direct": "Хат жіберу @{name}", + "status.embed": "Embеd", + "status.favourite": "Таңдаулы", + "status.filtered": "Фильтрленген", + "status.load_more": "Тағы әкел", + "status.media_hidden": "Жабық медиа", + "status.mention": "Аталым @{name}", + "status.more": "Тағы", + "status.mute": "Үнсіз @{name}", + "status.mute_conversation": "Пікірталасты үнсіз қылу", + "status.open": "Жазбаны ашу", + "status.pin": "Профильде жабыстыру", + "status.pinned": "Жабыстырылған жазба", + "status.read_more": "Әрі қарай", + "status.reblog": "Бөлісу", + "status.reblog_private": "Негізгі аудиторияға бөлісу", + "status.reblogged_by": "{name} бөлісті", + "status.reblogs.empty": "Бұл жазбаны әлі ешкім бөліспеді. Біреу бөліскен кезде осында көрінеді.", + "status.redraft": "Өшіру & қайта қарастыру", + "status.reply": "Жауап", + "status.replyAll": "Тақырыпқа жауап", + "status.report": "Шағым @{name}", + "status.sensitive_toggle": "Қарау үшін басыңыз", + "status.sensitive_warning": "Нәзік контент", + "status.share": "Бөлісу", + "status.show_less": "Аздап көрсет", + "status.show_less_all": "Бәрін аздап көрсет", + "status.show_more": "Толығырақ", + "status.show_more_all": "Бәрін толығымен", + "status.show_thread": "Желіні көрсет", + "status.unmute_conversation": "Пікірталасты үнсіз қылмау", + "status.unpin": "Профильден алып тастау", + "suggestions.dismiss": "Өткізіп жіберу", + "suggestions.header": "Қызығуыңыз мүмкін…", + "tabs_bar.federated_timeline": "Жаһандық", + "tabs_bar.home": "Басты бет", + "tabs_bar.local_timeline": "Жергілікті", + "tabs_bar.notifications": "Ескертпелер", + "tabs_bar.search": "Іздеу", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} жазған екен", + "ui.beforeunload": "Mastodon желісінен шықсаңыз, нобайыңыз сақталмайды.", + "upload_area.title": "Жүктеу үшін сүйреп әкеліңіз", + "upload_button.label": "Медиа қосу (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "Файл жүктеу лимитінен асып кеттіңіз.", + "upload_form.description": "Көру қабілеті нашар адамдар үшін сипаттаңыз", + "upload_form.focus": "Превьюді өзгерту", + "upload_form.undo": "Өшіру", + "upload_progress.label": "Жүктеп жатыр...", + "video.close": "Видеоны жабу", + "video.exit_fullscreen": "Толық экраннан шық", + "video.expand": "Видеоны аш", + "video.fullscreen": "Толық экран", + "video.hide": "Видеоны жасыр", + "video.mute": "Дыбысын бас", + "video.pause": "Пауза", + "video.play": "Қосу", + "video.unmute": "Дауысын аш" } diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 040ada2c0..6363e2de7 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "그리고 {additional}", "hashtag.column_header.tag_mode.any": "또는 {additional}", "hashtag.column_header.tag_mode.none": "({additional}를 제외)", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "추천 할 내용이 없습니다", + "hashtag.column_settings.select.placeholder": "해시태그를 입력하세요…", "hashtag.column_settings.tag_mode.all": "모두", "hashtag.column_settings.tag_mode.any": "아무것이든", "hashtag.column_settings.tag_mode.none": "이것들을 제외하고", @@ -206,7 +206,7 @@ "lists.account.remove": "리스트에서 제거", "lists.delete": "리스트 삭제", "lists.edit": "리스트 편집", - "lists.edit.submit": "Change title", + "lists.edit.submit": "제목 수정", "lists.new.create": "리스트 추가", "lists.new.title_placeholder": "새 리스트의 이름", "lists.search": "팔로우 중인 사람들 중에서 찾기", @@ -260,6 +260,10 @@ "notifications.filter.follows": "팔로우", "notifications.filter.mentions": "멘션", "notifications.group": "{count} 개의 알림", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "포스트의 프라이버시 설정을 변경", "privacy.direct.long": "멘션한 사용자에게만 공개", "privacy.direct.short": "다이렉트", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "로컬", "tabs_bar.notifications": "알림", "tabs_bar.search": "검색", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {명} other {명}} 의 사람들이 말하고 있습니다", "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.", "upload_area.title": "드래그 & 드롭으로 업로드", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index eaf3366aa..821d8c4b1 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 536f65462..21f066439 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index fdc1eed4f..f6d1041a0 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -142,8 +142,8 @@ "hashtag.column_header.tag_mode.all": "en {additional}", "hashtag.column_header.tag_mode.any": "of {additional}", "hashtag.column_header.tag_mode.none": "zonder {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Geen voorstellen gevonden", + "hashtag.column_settings.select.placeholder": "Vul hashtags in…", "hashtag.column_settings.tag_mode.all": "Allemaal", "hashtag.column_settings.tag_mode.any": "Een van deze", "hashtag.column_settings.tag_mode.none": "Geen van deze", @@ -206,7 +206,7 @@ "lists.account.remove": "Uit lijst verwijderen", "lists.delete": "Lijst verwijderen", "lists.edit": "Lijst bewerken", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Titel veranderen", "lists.new.create": "Lijst toevoegen", "lists.new.title_placeholder": "Naam nieuwe lijst", "lists.search": "Zoek naar mensen die je volgt", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Die jij volgt", "notifications.filter.mentions": "Vermeldingen", "notifications.group": "{count} meldingen", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Zichtbaarheid toot aanpassen", "privacy.direct.long": "Alleen aan vermelde gebruikers tonen", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "tabs_bar.search": "Zoeken", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover", "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", "upload_area.title": "Hierin slepen om te uploaden", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index d9504c0c5..8b6060d5d 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Justér synlighet", "privacy.direct.long": "Post kun til nevnte brukere", "privacy.direct.short": "Direkte", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Varslinger", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", "upload_area.title": "Dra og slipp for å laste opp", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 5d4897e2b..5c5a583b6 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -128,7 +128,7 @@ "empty_column.lists": "Encara avètz pas cap de lista. Quand ne creetz una, apareisserà aquí.", "empty_column.mutes": "Encara avètz pas mes en silenci degun.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", - "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public", + "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autres servidors per garnir lo flux public", "follow_request.authorize": "Acceptar", "follow_request.reject": "Regetar", "getting_started.developers": "Desvelopaires", @@ -227,7 +227,7 @@ "navigation_bar.favourites": "Favorits", "navigation_bar.filters": "Mots ignorats", "navigation_bar.follow_requests": "Demandas d’abonament", - "navigation_bar.info": "Mai informacions", + "navigation_bar.info": "Tocant aqueste servidor", "navigation_bar.keyboard_shortcuts": "Acorchis clavièr", "navigation_bar.lists": "Listas", "navigation_bar.logout": "Desconnexion", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Seguiments", "notifications.filter.mentions": "Mencions", "notifications.group": "{count} notificacions", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajustar la confidencialitat del messatge", "privacy.direct.long": "Mostrar pas qu’a las personas mencionadas", "privacy.direct.short": "Dirècte", @@ -279,7 +283,7 @@ "reply_indicator.cancel": "Anullar", "report.forward": "Far sègre a {target}", "report.forward_hint": "Lo compte ven d’un autre servidor. Volètz mandar una còpia anonima del rapòrt enlai tanben ?", - "report.hint": "Lo moderator de l’instància aurà lo rapòrt. Podètz fornir una explicacion de vòstre senhalament aquí dejós :", + "report.hint": "Lo moderator del servidor aurà lo rapòrt. Podètz fornir una explicacion de vòstre senhalament aquí dejós  :", "report.placeholder": "Comentaris addicionals", "report.submit": "Mandar", "report.target": "Senhalar {target}", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Flux public local", "tabs_bar.notifications": "Notificacions", "tabs_bar.search": "Recèrcas", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} ne charra other {people}} ne charran", "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.", "upload_area.title": "Lisatz e depausatz per mandar", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index d1d900447..80fc21488 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Śledzenia", "notifications.filter.mentions": "Wspomienia", "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Dostosuj widoczność wpisów", "privacy.direct.long": "Widoczny tylko dla wspomnianych", "privacy.direct.short": "Bezpośrednio", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokalne", "tabs_bar.notifications": "Powiadomienia", "tabs_bar.search": "Szukaj", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym", "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", "upload_area.title": "Przeciągnij i upuść aby wysłać", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 83c2dd0ce..368663a01 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Seguidores", "notifications.filter.mentions": "Menções", "notifications.group": "{count} notificações", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajustar a privacidade da mensagem", "privacy.direct.long": "Apenas para usuários mencionados", "privacy.direct.short": "Direta", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "tabs_bar.search": "Buscar", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre", "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.", "upload_area.title": "Arraste e solte para enviar", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index af48b323c..c9a7cd6a3 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Ajustar a privacidade da mensagem", "privacy.direct.long": "Apenas para utilizadores mencionados", "privacy.direct.short": "Directo", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", "upload_area.title": "Arraste e solte para enviar", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 802e43ce2..a0d5f9a27 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Urmărește", "notifications.filter.mentions": "Menționări", "notifications.group": "{count} notificări", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Cine vede asta", "privacy.direct.long": "Postează doar pentru utilizatorii menționați", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificări", "tabs_bar.search": "Căutare", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} vorbesc", "ui.beforeunload": "Postarea se va pierde dacă părăsești pagina.", "upload_area.title": "Trage și eliberează pentru a încărca", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 7c978bc3f..01c915d71 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} уведомл.", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Изменить видимость статуса", "privacy.direct.long": "Показать только упомянутым", "privacy.direct.short": "Направленный", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Локальная", "tabs_bar.notifications": "Уведомления", "tabs_bar.search": "Поиск", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}", "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 62677471c..c11bebce8 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Sledovania", "notifications.filter.mentions": "Iba spomenutia", "notifications.group": "{count} oboznámení", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Uprav súkromie príspevku", "privacy.direct.long": "Pošli iba spomenutým používateľom", "privacy.direct.short": "Súkromne", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokálna", "tabs_bar.notifications": "Notifikácie", "tabs_bar.search": "Hľadaj", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {človek vraví} other {ľudia vravia}}", "ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.", "upload_area.title": "Pretiahni a pusť pre nahratie", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 213eb8203..b2404d178 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Obvestila", "tabs_bar.search": "Poišči", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", "upload_area.title": "Povlecite in spustite za pošiljanje", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 6e0d7ebb6..9aaec4b46 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Ndjekje", "notifications.filter.mentions": "Përmendje", "notifications.group": "%(count)s njoftime", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Rregulloni privatësi gjendje", "privacy.direct.long": "Postoja vetëm përdoruesve të përmendur", "privacy.direct.short": "I drejtpërdrejtë", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Vendore", "tabs_bar.notifications": "Njoftime", "tabs_bar.search": "Kërkim", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, një {person} {people} të tjerë} po flasin", "ui.beforeunload": "Skica juaj do të humbë nëse dilni nga Mastodon-i.", "upload_area.title": "Merreni & vëreni që të ngarkohet", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 79619c914..59dc24ab3 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Podesi status privatnosti", "privacy.direct.long": "Objavi samo korisnicima koji su pomenuti", "privacy.direct.short": "Direktno", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Obaveštenja", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.", "upload_area.title": "Prevucite ovde da otpremite", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index b72431b34..1097d48fb 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} обавештења", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Подеси статус приватности", "privacy.direct.long": "Објави само корисницима који су поменути", "privacy.direct.short": "Директно", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Локално", "tabs_bar.notifications": "Обавештења", "tabs_bar.search": "Претрага", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {човек} other {људи}} прича", "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.", "upload_area.title": "Превуците овде да отпремите", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 018fdc85f..90ac623af 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} aviseringar", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Justera sekretess", "privacy.direct.long": "Skicka endast till nämnda användare", "privacy.direct.short": "Direkt", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Meddelanden", "tabs_bar.search": "Sök", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, en {person} andra {people}} pratar", "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.", "upload_area.title": "Dra & släpp för att ladda upp", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 536f65462..21f066439 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 1c27fab81..806bc9f6f 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -142,9 +142,9 @@ "hashtag.column_header.tag_mode.all": "మరియు {additional}", "hashtag.column_header.tag_mode.any": "లేదా {additional}", "hashtag.column_header.tag_mode.none": "{additional} లేకుండా", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "ఇవన్నీAll of these", + "hashtag.column_settings.select.no_options_message": "ఎటువంటి సూచనలూ దొరకలేదు", + "hashtag.column_settings.select.placeholder": "హ్యాష్ టాగులు నింపండి…", + "hashtag.column_settings.tag_mode.all": "ఇవన్నీ", "hashtag.column_settings.tag_mode.any": "వీటిలో ఏవైనా", "hashtag.column_settings.tag_mode.none": "ఇవేవీ కావు", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", @@ -227,7 +227,7 @@ "navigation_bar.favourites": "ఇష్టపడినవి", "navigation_bar.filters": "మ్యూట్ చేయబడిన పదాలు", "navigation_bar.follow_requests": "అనుసరించడానికి అభ్యర్ధనలు", - "navigation_bar.info": "ఈ దృష్టాంతం గురించి", + "navigation_bar.info": "ఈ సేవిక గురించి", "navigation_bar.keyboard_shortcuts": "హాట్ కీలు", "navigation_bar.lists": "జాబితాలు", "navigation_bar.logout": "లాగ్ అవుట్ చేయండి", @@ -260,6 +260,10 @@ "notifications.filter.follows": "అనుసరిస్తున్నవి", "notifications.filter.mentions": "పేర్కొన్నవి", "notifications.group": "{count} ప్రకటనలు", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "స్టేటస్ గోప్యతను సర్దుబాటు చేయండి", "privacy.direct.long": "పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయి", "privacy.direct.short": "ప్రత్యక్ష", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "స్థానిక", "tabs_bar.notifications": "ప్రకటనలు", "tabs_bar.search": "శోధన", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} మాట్లాడుతున్నారు", "ui.beforeunload": "మీరు మాస్టొడొన్ను వదిలివేస్తే మీ డ్రాఫ్ట్లు పోతాయి.", "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 5947b04c2..96c1a422b 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -1,20 +1,20 @@ { "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", - "account.block": "Block @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.blocked": "Blocked", + "account.badges.bot": "บอต", + "account.block": "บล็อค @{name}", + "account.block_domain": "ซ่อนทุกอย่างจาก {domain}", + "account.blocked": "ถูกบล็อค", "account.direct": "Direct Message @{name}", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", "account.endorse": "Feature on profile", - "account.follow": "Follow", - "account.followers": "Followers", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "Follows", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "Follows you", + "account.follow": "ติดตาม", + "account.followers": "ผู้ติดตาม", + "account.followers.empty": "ยังไม่มีใครติดตาม", + "account.follows": "ติดตาม", + "account.follows.empty": "ยังไม่ได้ติดตามใคร", + "account.follows_you": "ติดตามคุณ", "account.hide_reblogs": "Hide boosts from @{name}", "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.", @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 76949352f..62bff6cb2 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Gönderi gizliliğini ayarla", "privacy.direct.long": "Sadece bahsedilen kişilere gönder", "privacy.direct.short": "Direkt", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Yerel", "tabs_bar.notifications": "Bildirimler", "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Upload için sürükle bırak yapınız", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index d6c5317e0..02ecc9689 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} сповіщень", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "Змінити видимість допису", "privacy.direct.long": "Показати тільки згаданим користувачам", "privacy.direct.short": "Направлений", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "Локальна", "tabs_bar.notifications": "Сповіщення", "tabs_bar.search": "Пошук", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Вашу чернетку буде втрачено, якщо ви покинете Mastodon.", "upload_area.title": "Перетягніть сюди, щоб завантажити", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 27effba4c..9941d99d1 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} 条通知", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "设置嘟文可见范围", "privacy.direct.long": "只有被提及的用户能看到", "privacy.direct.short": "私信", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "本站", "tabs_bar.notifications": "通知", "tabs_bar.search": "搜索", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} 人正在讨论", "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", "upload_area.title": "将文件拖放到此处开始上传", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 60baffd2e..7e1cf15e6 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} 條通知", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "調整私隱設定", "privacy.direct.long": "只有提及的用戶能看到", "privacy.direct.short": "私人訊息", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "本站", "tabs_bar.notifications": "通知", "tabs_bar.search": "搜尋", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} 位用戶在討論", "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。", "upload_area.title": "將檔案拖放至此上載", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index c0871d379..c2e807103 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -260,6 +260,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.group": "{count} 條通知", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", "privacy.change": "調整隱私狀態", "privacy.direct.long": "只有被提到的使用者能看到", "privacy.direct.short": "私訊", @@ -342,6 +346,11 @@ "tabs_bar.local_timeline": "本站", "tabs_bar.notifications": "通知", "tabs_bar.search": "搜尋", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} 位使用者在討論", "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。", "upload_area.title": "拖放來上傳", diff --git a/config/locales/ar.yml b/config/locales/ar.yml index ec8b15cba..67fa97c59 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -319,6 +319,8 @@ ar: back_to_account: العودة إلى الحساب title: "%{acct} مُتابِعون" instances: + by_domain: النطاق + delivery_available: التسليم متوفر known_accounts: few: "%{count} حسابات معروفة" many: "%{count} حسابات معروفة" @@ -598,10 +600,13 @@ ar: size: الحجم blocks: قمت بحظر csv: CSV + domain_blocks: النطاقات المحظورة follows: أنت تتبع lists: القوائم mutes: قُمتَ بكتم storage: ذاكرة التخزين + featured_tags: + add_new: إضافة واحد filters: contexts: home: الخيط الزمني الرئيسي @@ -643,10 +648,16 @@ ar: two: هناك شيء ما ليس على ما يرام! يُرجى مراجعة الأخطاء الـ %{count} أدناه zero: هناك شيء ما ليس على ما يرام! يُرجى مراجعة الأخطاء الـ %{count} أدناه imports: + modes: + merge: دمج + merge_long: الإبقاء علي التسجيلات الحالية وإضافة الجديدة + overwrite: إعادة الكتابة + overwrite_long: استبدال التسجيلات الحالية بالجديدة preface: بإمكانك استيراد بيانات قد قُمتَ بتصديرها مِن مثيل خادوم آخَر، كقوائم المستخدِمين الذين كنتَ تتابِعهم أو قُمتَ بحظرهم. success: تم تحميل بياناتك بنجاح وسيتم معالجتها في الوقت المناسب types: blocking: قائمة المحظورين + domain_blocking: قائمة النطاقات المحظورة following: قائمة المستخدمين المتبوعين muting: قائمة الكتم upload: تحميل @@ -761,6 +772,16 @@ ar: no_account_html: أليس عندك حساب بعدُ ؟ يُمْكنك التسجيل مِن هنا proceed: أكمل المتابعة prompt: 'إنك بصدد متابعة :' + remote_interaction: + favourite: + proceed: المواصلة إلى المفضلة + prompt: 'ترغب في إضافة هذا التبويق إلى مفضلتك:' + reblog: + proceed: المواصلة إلى الترقية + prompt: 'ترغب في ترقية هذا التبويق:' + reply: + proceed: المواصلة إلى الرد + prompt: 'ترغب في الرد على هذا التبويق:' remote_unfollow: error: خطأ title: العنوان @@ -903,6 +924,8 @@ ar: review_server_policies: مراجعة شروط السيرفر subject: disable: تم تجميد حسابك %{acct} + none: تحذير إلى %{acct} + suspend: لقد تم تعليق حسابك %{acct} title: disable: الحساب مُجمَّد none: تحذير diff --git a/config/locales/bn.yml b/config/locales/bn.yml new file mode 100644 index 000000000..560e74398 --- /dev/null +++ b/config/locales/bn.yml @@ -0,0 +1,22 @@ +--- +bn: + about: + about_hashtag_html: এগুলো প্রকাশ্য লেখা যার হ্যাশট্যাগ #%{hashtag}। আপনি এগুলোর ব্যবহার বা সাথে যুক্ত হতে পারবেন যদি আপনার যুক্তবিশ্বের কোথাও নিবন্ধন থেকে থাকে। + about_mastodon_html: মাস্টাডন উন্মুক্ত ইন্টারনেটজালের নিয়ম এবং স্বাধীন ও মুক্ত উৎসের সফটওয়্যারের ভিত্তিতে তৈরী একটি সামাজিক যোগাযোগ মাধ্যম। এটি ইমেইলের মত বিকেন্দ্রীভূত। + about_this: কি + administered_by: 'পরিচালনা করছেন:' + api: সফটওয়্যার তৈরীর নিয়ম (API) + apps: মোবাইল অ্যাপ + closed_registrations: এই সার্ভারে এখন নিবন্ধন বন্ধ। কিন্তু ! অন্য একটি সার্ভার খুঁজে নিবন্ধন করলেও একই নেটওয়ার্কে ঢুকতে পারবেন। + contact: যোগাযোগ + contact_missing: নেই + contact_unavailable: প্রযোজ্য নয় + documentation: ব্যবহারবিলি + extended_description_html: | +

    নিয়মের জন্য উপযুক্ত জায়গা

    +

    বিস্তারিত বিবরণ এখনো যুক্ত করা হয়নি

    + features: + humane_approach_body: অনন্যা নেটওয়ার্কের ব্যর্থতা থেকে শিখে, মাস্টাডনের লক্ষ্য নৈতিক পরিকল্পনার দ্বারা সামাজিক মাধ্যমের অপব্যবহারের বিরোধিতা করা। + humane_approach_title: একটি মনুষ্যত্বপূর্ণ চেষ্টা + not_a_product_body: মাস্টাডন কোনো ব্যবসায়িক নেটওয়ার্ক না। কোনো বিজ্ঞাপন নেই, কোনো তথ্য খনি নেই, কোনো বাধার দেয়াল নেই। এর কোনো কেন্দ্রীয় কর্তৃপক্ষ নেই। + not_a_product_title: আপনি একজন মানুষ, পণ্য নন diff --git a/config/locales/co.yml b/config/locales/co.yml index 8955f7a68..d30fc9e96 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -302,6 +302,7 @@ co: back_to_account: Rivene à u Contu title: Abbunati à %{acct} instances: + by_domain: Duminiu delivery_available: Rimessa dispunibule known_accounts: one: "%{count} contu cunnisciutu" @@ -733,6 +734,16 @@ co: older: Più vechju prev: Nanzu truncate: "…" + polls: + errors: + already_voted: Avete digià vutatu nant'à stu scandagliu + duplicate_options: cuntene uzzione doppie + duration_too_long: hè troppu luntanu indè u futuru + duration_too_short: hè troppu prossimu + expired: U scandagliu hè digià finitu + over_character_limit: ùn ponu micca esse più longhi chè %{MAX} caratteri + too_few_options: deve avè più d'un'uzzione + too_many_options: ùn pò micca avè più di %{MAX} uzzione preferences: languages: Lingue other: Altre @@ -842,6 +853,11 @@ co: ownership: Pudete puntarulà solu unu di i vostri propii statuti private: Ùn pudete micca puntarulà un statutu ch’ùn hè micca pubblicu reblog: Ùn pudete micca puntarulà una spartera + poll: + total_votes: + one: "%{count} votu" + other: "%{count} voti" + vote: Vutà show_more: Vede di più sign_in_to_participate: Cunnettatevi per participà à a cunversazione title: '%{name}: "%{quote}"' diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 7a4c3f255..b5cc5750d 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -32,8 +32,8 @@ cs: source_code: Zdrojový kód status_count_after: few: příspěvky - one: příspěvek - other: příspěvků + one: toot + other: tootů status_count_before: Kteří napsali terms: Podmínky používání user_count_after: @@ -150,7 +150,7 @@ cs: already_confirmed: Tento uživatel je již potvrzen send: Znovu odeslat potvrzovací e-mail success: Potvrzovací e-mail byl úspěšně odeslán! - reset: Resetovat + reset: Obnovit reset_password: Obnovit heslo resubscribe: Znovu odebírat role: Oprávnění @@ -167,7 +167,7 @@ cs: targeted_reports: Nahlášeni ostatními silence: Utišit silenced: Utišen/a - statuses: Příspěvky + statuses: Tooty subscribe: Odebírat suspended: Pozastaven/a title: Účty @@ -191,7 +191,7 @@ cs: destroy_custom_emoji: "%{name} zničil/a emoji %{target}" destroy_domain_block: "%{name} odblokoval/a doménu %{target}" destroy_email_domain_block: "%{name} odebral/a e-mailovou doménu %{target} z černé listiny" - destroy_status: "%{name} odstranil/a příspěvek uživatele %{target}" + destroy_status: "%{name} odstranil/a toot uživatele %{target}" disable_2fa_user: "%{name} vypnul/a požadavek pro dvoufaktorovou autentikaci pro uživatele %{target}" disable_custom_emoji: "%{name} zakázal/a emoji %{target}" disable_user: "%{name} zakázal/a přihlašování pro uživatele %{target}" @@ -201,7 +201,7 @@ cs: promote_user: "%{name} povýšil/a uživatele %{target}" remove_avatar_user: "%{name} odstranil/a avatar uživatele %{target}" reopen_report: "%{name} znovuotevřel/a nahlášení %{target}" - reset_password_user: "%{name} resetoval/a heslo uživatele %{target}" + reset_password_user: "%{name} obnovil/a heslo uživatele %{target}" resolve_report: "%{name} vyřešil/a nahlášení %{target}" silence_account: "%{name} utišil/a účet uživatele %{target}" suspend_account: "%{name} pozastavil/a účet uživatele %{target}" @@ -209,8 +209,8 @@ cs: unsilence_account: "%{name} odtišil/a účet uživatele %{target}" unsuspend_account: "%{name} zrušil/a pozastavení účtu uživatele %{target}" update_custom_emoji: "%{name} aktualizoval/a emoji %{target}" - update_status: "%{name} aktualizoval/a příspěvek uživatele %{target}" - deleted_status: "(smazaný příspěvek)" + update_status: "%{name} aktualizoval/a toot uživatele %{target}" + deleted_status: "(smazaný toot)" title: Záznam auditu custom_emojis: by_domain: Doména @@ -307,6 +307,7 @@ cs: back_to_account: Zpět na účet title: Sledující uživatele %{acct} instances: + by_domain: Doména delivery_available: Doručení je k dispozici known_accounts: few: "%{count} známé účty" @@ -380,7 +381,7 @@ cs: updated_at: Aktualizováno settings: activity_api_enabled: - desc_html: Počty lokálně publikovaných příspěvků, aktivních uživatelů a nových registrací, v týdenních intervalech + desc_html: Počty lokálně publikovaných tootů, aktivních uživatelů a nových registrací, v týdenních intervalech title: Publikovat hromadné statistiky o uživatelské aktivitě bootstrap_timeline_accounts: desc_html: Je-li uživatelskch jmen více, oddělujte je čárkami. Lze zadat pouze místní a odemknuté účty. Je-li tohle prázdné, jsou výchozí hodnotou všichni místní administrátoři. @@ -455,8 +456,8 @@ cs: media: title: Média no_media: Žádná média - no_status_selected: Nebyly změněny žádné příspěvky, neboť žádné nebyly vybrány - title: Příspěvky účtu + no_status_selected: Nebyly změněny žádné tooty, neboť žádné nebyly vybrány + title: Tooty účtu with_media: S médii subscriptions: callback_url: Zpáteční URL @@ -491,7 +492,7 @@ cs: settings: 'Změnit volby e-mailu: %{link}' view: 'Zobrazit:' view_profile: Zobrazit profil - view_status: Zobrazit příspěvek + view_status: Zobrazit toot applications: created: Aplikace úspěšně vytvořena destroyed: Aplikace úspěšně smazána @@ -508,7 +509,7 @@ cs: delete_account_html: Chcete-li odstranit svůj účet, pokračujte zde. Budete požádán/a o potvrzení. didnt_get_confirmation: Neobdržel/a jste pokyny pro potvrzení? forgot_password: Zapomněl/a jste heslo? - invalid_reset_password_token: Token na obnovu hesla je buď neplatný, nebo vypršel. Prosím vyžádejte si nový. + invalid_reset_password_token: Token pro obnovení hesla je buď neplatný, nebo vypršel. Prosím vyžádejte si nový. login: Přihlásit logout: Odhlásit migrate_account: Přesunout se na jiný účet @@ -598,7 +599,7 @@ cs: featured_tags: add_new: Přidat nový errors: - limit: Již jste nastavil/a maximální počet oblíbených hashtagů + limit: Již jste zvýraznil/a maximální počet hashtagů filters: contexts: home: Domovská časová osa @@ -617,16 +618,16 @@ cs: title: Přidat nový filtr followers: domain: Doména - explanation_html: Chcete-li zaručit soukromí vašich příspěvků, musíte mít na vědomí, kdo vás sleduje. Vaše soukromé příspěvky jsou doručeny na všechny servery, kde máte sledující. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí. + explanation_html: Chcete-li zaručit soukromí vašich tootů, musíte mít na vědomí, kdo vás sleduje. Vaše soukromé tooty jsou doručeny na všechny servery, kde máte sledující. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí. followers_count: Počet sledujících - lock_link: Zamkněte svůj účet + lock_link: Uzamkněte svůj účet purge: Odstranit ze sledujících success: few: V průběhu blokování sledujících ze %{count} domén... one: V průběhu blokování sledujících z jedné domény... other: V průběhu blokování sledujících z %{count} domén... true_privacy_html: Berte prosím na vědomí, že skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování. - unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující. + unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé tooty. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující. unlocked_warning_title: Váš účet není uzamčen footer: developers: Vývojáři @@ -683,7 +684,7 @@ cs: limit: Dosáhl/a jste maximálního počtu seznamů media_attachments: validations: - images_and_video: K příspěvku, který již obsahuje obrázky, nelze připojit video + images_and_video: K tootu, který již obsahuje obrázky, nelze připojit video too_many: Nelze připojit více než 4 soubory migrations: acct: přezdívka@doména nového účtu @@ -707,8 +708,8 @@ cs: other: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418" title: Ve vaší nepřítomnosti... favourite: - body: 'Váš příspěvek si oblíbil/a %{name}:' - subject: "%{name} si oblíbil/a váš příspěvek" + body: 'Váš toot si oblíbil/a %{name}:' + subject: "%{name} si oblíbil/a váš toot" title: Nové oblíbení follow: body: "%{name} vás nyní sleduje!" @@ -725,9 +726,9 @@ cs: subject: Byl/a jste zmíněn/a uživatelem %{name} title: Nová zmínka reblog: - body: 'Váš příspěvek byl boostnutý uživatelem %{name}:' - subject: "%{name} boostnul/a váš příspěvek" - title: Nové boostnutí + body: 'Váš toot byl boostnutý uživatelem %{name}:' + subject: "%{name} boostnul/a váš toot" + title: Nový boost number: human: decimal_units: @@ -744,6 +745,16 @@ cs: older: Starší prev: Před truncate: "…" + polls: + errors: + already_voted: V této anketě jste již hlasoval/a + duplicate_options: obsahuje duplicitní položky + duration_too_long: je příliš daleko v budoucnosti + duration_too_short: je příliš brzy + expired: Anketa již skončila + over_character_limit: nesmí být každá delší než %{MAX} znaků + too_few_options: musí mít více než jednu položku + too_many_options: nesmí obsahovat více než %{MAX} položky preferences: languages: Jazyky other: Ostatní @@ -822,7 +833,7 @@ cs: development: Vývoj edit_profile: Upravit profil export: Export dat - featured_tags: Oblíbené hashtagy + featured_tags: Zvýrazněné hashtagy followers: Autorizovaní sledující import: Import migrate: Přesunutí účtu @@ -856,6 +867,12 @@ cs: ownership: Nelze připnout toot někoho jiného private: Nelze připnout neveřejné tooty reblog: Nelze připnout boostnutí + poll: + total_votes: + few: "%{count} hlasů" + one: "%{count} hlas" + other: "%{count} hlasů" + vote: Hlasovat show_more: Zobrazit více sign_in_to_participate: Chcete-li se účastnit této konverzace, přihlaste se title: "%{name}: „%{quote}“" diff --git a/config/locales/devise.cs.yml b/config/locales/devise.cs.yml index 83534cccd..b87c7472c 100644 --- a/config/locales/devise.cs.yml +++ b/config/locales/devise.cs.yml @@ -3,8 +3,8 @@ cs: devise: confirmations: confirmed: Vaše e-mailová adresa byla úspěšně ověřena. - send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, zkontrolujte si složku „spam“. - send_paranoid_instructions: Pokud tato e-mailová adresa existuje v naší databázi, obdržíte za několik minut e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, zkontrolujte si složku „spam“. + send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro potvrzení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. + send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut e-mail s instrukcemi pro potvrzení vaší e-mailové adresy. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. failure: already_authenticated: Již jste přihlášen/a. inactive: Váš účet ještě není aktivován. @@ -42,7 +42,7 @@ cs: action: Změnit heslo explanation: Vyžádal/a jste si pro svůj účet nové heslo. extra: Pokud jste tohle nevyžádal/a, prosím ignorujte tento e-mail. Vaše heslo nebude změněno, dokud nepřejdete na výše uvedenou adresu a nevytvoříte si nové. - subject: 'Mastodon: Instrukce pro obnovu hesla' + subject: 'Mastodon: Instrukce pro obnovení hesla' title: Obnovení hesla unlock_instructions: subject: 'Mastodon: Instrukce pro odemčení účtu' @@ -50,9 +50,9 @@ cs: failure: Nelze vás ověřit z %{kind}, protože „%{reason}“. success: Úspěšně ověřeno z účtu %{kind}. passwords: - no_token: Tuto stránku nemůžete navštívit, pokud nepřicházíte z e-mailu pro obnovu hesla. Pokud jste z něj přišel/la, ujistěte se, že jste použil/a celé URL z e-mailu. - send_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za pár minut ve vašem e-mailu odkaz pro obnovení hesla. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. - send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za pár minut ve vašem e-mailu odkaz pro obnovení hesla. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. + no_token: Tuto stránku nemůžete navštívit, pokud nepřicházíte z e-mailu pro obnovení hesla. Pokud z něj přicházíte, ujistěte se, že jste použil/a celé URL z e-mailu. + send_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut ve vašem e-mailu odkaz pro obnovení hesla. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. + send_paranoid_instructions: Pokud vaše e-mailová adresa existuje v naší databázi, obdržíte za několik minut ve vašem e-mailu odkaz pro obnovení hesla. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. updated: Vaše heslo bylo úspěšně změněno. Nyní jste přihlášen/a. updated_not_active: Vaše heslo bylo úspěšně změněno. registrations: @@ -61,15 +61,15 @@ cs: signed_up_but_inactive: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet ještě není aktivován. signed_up_but_locked: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet je uzamčen. signed_up_but_unconfirmed: Na vaši e-mailovou adresu byla poslána zpráva s potvrzovacím odkazem. Pro aktivaci účtu přejděte na danou adresu. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam. - update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Prosím zkontrolujte si e-mail a klikněte na odkaz pro potvrzení vaši nové e-mailové adresy. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam. + update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. updated: Váš účet byl úspěšně aktualizován. sessions: already_signed_out: Odhlášení proběhlo úspěšně. signed_in: Přihlášení proběhlo úspěšně. signed_out: Odhlášení proběhlo úspěšně. unlocks: - send_instructions: Za pár minut obdržíte e-mail s instrukcemi pro odemčení vašeho účtu. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. - send_paranoid_instructions: Pokud váš účet existuje, obdržíte za pár minut e-mail s instrukcemi pro odemčení vašeho účtu. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. + send_instructions: Za několik minut obdržíte e-mail s instrukcemi pro odemčení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. + send_paranoid_instructions: Pokud váš účet existuje, obdržíte za několik minut e-mail s instrukcemi pro odemčení vašeho účtu. Pokud tento e-mail neobdržíte, prosím zkontrolujte si složku „spam“. unlocked: Váš účet byl úspěšně odemčen. Pro pokračování se prosím přihlaste. errors: messages: diff --git a/config/locales/devise.eo.yml b/config/locales/devise.eo.yml index 71dd6c1ef..6c9fe4360 100644 --- a/config/locales/devise.eo.yml +++ b/config/locales/devise.eo.yml @@ -19,17 +19,17 @@ eo: confirmation_instructions: action: Konfirmi retadreson explanation: Vi kreis konton en %{host} per ĉi tiu retadreso. Nur klako restas por aktivigi ĝin. Se tio ne estis vi, bonvolu ignori ĉi tiun retmesaĝon. - extra_html: Bonvolu rigardi la regulojn de la nodo kaj niajn uzkondiĉojn. + extra_html: Bonvolu rigardi la regulojn de la servilo kaj niajn uzkondiĉojn. subject: 'Mastodon: Konfirmaj instrukcioj por %{instance}' title: Konfirmi retadreson email_changed: explanation: 'La retadreso de via konto ŝanĝiĝas al:' - extra: Se vi ne volis ŝanĝi vian retadreson, iu verŝajne aliris al via konto. Bonvolu tuj ŝanĝi vian pasvorton aŭ kontakti la administranton de la nodo, se vi estas blokita ekster via konto. + extra: Se vi ne volis ŝanĝi vian retadreson, iu verŝajne aliris al via konto. Bonvolu tuj ŝanĝi vian pasvorton aŭ kontakti la administranton de la servilo, se vi estas blokita ekster via konto. subject: 'Mastodon: Retadreso ŝanĝita' title: Nova retadreso password_change: explanation: La pasvorto de via konto estis ŝanĝita. - extra: Se vi ne ŝanĝis vian pasvorton, iu verŝajne aliris al via konto. Bonvolu ŝanĝi vian pasvorton tuj aŭ kontakti la administranton de la nodo, se vi estas blokita ekster via konto. + extra: Se vi ne ŝanĝis vian pasvorton, iu verŝajne aliris al via konto. Bonvolu ŝanĝi vian pasvorton tuj aŭ kontakti la administranton de la servilo, se vi estas blokita ekster via konto. subject: 'Mastodon: Pasvorto ŝanĝita' title: Pasvorto ŝanĝita reconfirmation_instructions: diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml index 30266e46b..fc36fdbff 100644 --- a/config/locales/devise.it.yml +++ b/config/locales/devise.it.yml @@ -20,17 +20,17 @@ it: action: Verifica indirizzo email action_with_app: Conferma e torna a %{app} explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email. - extra_html: Per favore controllale regole dell'istanza e i nostri termini di servizio. + extra_html: Per favore controllale regole del server e i nostri termini di servizio. subject: 'Mastodon: Istruzioni di conferma per %{instance}' title: Verifica indirizzo email email_changed: explanation: 'L''indirizzo email del tuo account sta per essere cambiato in:' - extra: Se non hai cambiato la tua email, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account. + extra: Se non hai cambiato la tua email, è probabile che qualcuno abbia ottenuto l'accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore del server se non puoi più accedere al tuo account. subject: 'Mastodon: Email cambiata' title: Nuovo indirizzo email password_change: explanation: La password del tuo account è stata cambiata. - extra: Se non hai cambiato la password, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account. + extra: Se non hai cambiato la password, è probabile che qualcuno abbia ottenuto l'accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore del server non puoi più accedere al tuo account. subject: 'Mastodon: Password modificata' title: Password cambiata reconfirmation_instructions: diff --git a/config/locales/devise.oc.yml b/config/locales/devise.oc.yml index 99809b858..e167f7e19 100644 --- a/config/locales/devise.oc.yml +++ b/config/locales/devise.oc.yml @@ -20,17 +20,17 @@ oc: action: Verificar l’adreça de corrièl action_with_app: Confirmar e tornar a %{app} explanation: Venètz de crear un compte sus %{host} amb aquesta adreça de corrièl. Vos manca pas qu’un clic per l’activar. S’èra pas vosautre mercés de far pas cas a aqueste messatge. - extra_html: Pensatz tanben de gaitar las règlas de l’instància e nòstres tèrmes e condicions d’utilizacion. + extra_html: Pensatz tanben de gaitar las règlas del servidor e nòstres tèrmes e condicions d’utilizacion. subject: 'Mastodon : consignas de confirmacion per %{instance}' title: Verificatz l’adreça de corrièl email_changed: explanation: 'L’adreça per aqueste compte es ara :' - extra: S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat. + extra: S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator de servidor se l’accès a vòstre compte vos es barrat. subject: 'Mastodon : corrièl cambiat' title: Nòva adreça de corrièl password_change: explanation: Lo senhal per vòstre compte a cambiat. - extra: S’avètz pas demandat aqueste cambiament de senhal, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat. + extra: S’avètz pas demandat aqueste cambiament de senhal, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator de servidor se l’accès a vòstre compte vos es barrat. subject: Mastodon : senhal cambiat title: Senhal cambiat reconfirmation_instructions: diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml index 5fd54ff50..9b44bbf00 100644 --- a/config/locales/devise.pt.yml +++ b/config/locales/devise.pt.yml @@ -2,34 +2,35 @@ pt: devise: confirmations: - confirmed: O teu endereço de email foi confirmado. - send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos. - send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos. + confirmed: O teu endereço de e-mail foi confirmado com sucesso. + send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail. + send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail. failure: already_authenticated: A tua sessão já está aberta. inactive: A tua conta ainda não está ativada. - invalid: "%{authentication_keys} ou palavra-passe não válida." + invalid: "%{authentication_keys} ou palavra-passe inválida." last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada. locked: A tua conta está bloqueada. - not_found_in_database: "%{authentication_keys} ou palavra-passe não válida." + not_found_in_database: "%{authentication_keys} ou palavra-passe inválida." timeout: A tua sessão expirou. Por favor, entra de novo para continuares. - unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar. + unauthenticated: Precisas de entrar na tua conta ou de te registares antes de continuar. unconfirmed: Tens de confirmar o teu endereço de email antes de continuar. mailer: confirmation_instructions: action: Verificar o endereço de e-mail + action_with_app: Confirmar e regressar a %{app} explanation: Criaste uma conta em %{host} com este endereço de e-mail. Estás a um clique de activá-la. Se não foste tu que fizeste este registo, por favor ignora esta mensagem. - extra_html: Por favor vê as as regras da instância e os termos de serviço. + extra_html: Por favor lê as regras da instância e os nossos termos de serviço. subject: 'Mastodon: Instruções de confirmação %{instance}' title: Verificar o endereço de e-mail email_changed: explanation: 'O e-mail associado à tua conta será alterado para:' - extra: Se não alteraste o teu e-mail é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador da tua instância se ficaste sem acesso à tua conta. + extra: Se não alteraste o teu e-mail é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta. subject: 'Mastodon: Email alterado' title: Novo endereço de e-mail password_change: explanation: A palavra-passe da tua conta foi alterada. - extra: Se não alteraste a tua palavra-passe, é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador da tua instância se ficaste sem acesso à tua conta. + extra: Se não alteraste a tua palavra-passe, é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta. subject: 'Mastodon: Nova palavra-passe' title: Palavra-passe alterada reconfirmation_instructions: diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml index 03b66a0fa..a303891c7 100644 --- a/config/locales/doorkeeper.cs.yml +++ b/config/locales/doorkeeper.cs.yml @@ -127,11 +127,11 @@ cs: read:notifications: vidět vaše oznámení read:reports: vidět vaše nahlášení read:search: vyhledávat za vás - read:statuses: vidět všechny příspěvky + read:statuses: vidět všechny tooty write: měnit všechna data vašeho účtu write:accounts: měnit váš profil write:blocks: blokovat účty a domény - write:favourites: oblibovat si příspěvky + write:favourites: oblibovat si tooty write:filters: vytvářet filtry write:follows: sledovat lidi write:lists: vytvářet seznamy @@ -139,4 +139,4 @@ cs: write:mutes: ignorovat lidi a konverzace write:notifications: vymazávat vaše oznámení write:reports: nahlašovat jiné uživatele - write:statuses: publikovat příspěvky + write:statuses: publikovat tooty diff --git a/config/locales/doorkeeper.kk.yml b/config/locales/doorkeeper.kk.yml index de3a0e155..409435802 100644 --- a/config/locales/doorkeeper.kk.yml +++ b/config/locales/doorkeeper.kk.yml @@ -4,8 +4,8 @@ kk: attributes: doorkeeper/application: name: Application аты - redirect_uri: Redirect URI - scopes: Scopes + redirect_uri: Redirеct URI + scopes: Scopеs website: Application сайты errors: models: diff --git a/config/locales/el.yml b/config/locales/el.yml index f3038e3d0..ca821c4fc 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -302,6 +302,7 @@ el: back_to_account: Επιστροφή στον λογαριασμό title: Ακόλουθοι του/της %{acct} instances: + by_domain: Τομέας delivery_available: Διαθέσιμη παράδοση known_accounts: one: "%{count} γνωστός λογαριασμός" @@ -423,10 +424,10 @@ el: desc_html: Εισαγωγική παράγραφος στην αρχική σελίδα. Περιέγραψε τι κάνει αυτό τον διακομιστή Mastodon διαφορετικό και ό,τι άλλο ενδιαφέρον. Μπορείς να χρησιμοποιήσεις HTML tags, συγκεκριμένα < a> και < em>. title: Περιγραφή κόμβου site_description_extended: - desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Δέχεται και κώδικα HTML + desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Μπορείς να χρησιμοποιήσεις και κώδικα HTML title: Προσαρμοσμένες εκτεταμένες πληροφορίες site_short_description: - desc_html: Εμφανίζεται στην πλαϊνή μπάρα και στα meta tags. Περιέγραψε τι είναι το Mastodon και τι κάνει αυτό τον διακομιστή ιδιαίτερο σε μια παράγραφο. Αν μείνει κενό, θα πάρει την προκαθορισμένη περιγραφή του κόμβου. + desc_html: Εμφανίζεται στην πλαϊνή μπάρα και στα meta tags. Περιέγραψε τι είναι το Mastodon και τι κάνει αυτό τον διακομιστή ιδιαίτερο σε μια παράγραφο. Αν μείνει κενό, θα χρησιμοποιήσει την προκαθορισμένη περιγραφή του κόμβου. title: Σύντομη περιγραφή του κόμβου site_terms: desc_html: Μπορείς να γράψεις τη δική σου πολιτική απορρήτου, όρους χρήσης ή άλλους νομικούς όρους. Μπορείς να χρησιμοποιήσεις HTML tags @@ -564,7 +565,7 @@ el: errors: '403': Δεν έχεις δικαίωμα πρόσβασης σε αυτή τη σελίδα. '404': Η σελίδα που ψάχνεις δεν υπάρχει. - '410': Η σελίδα που έψαχνες δεν υπάρχει πια. + '410': Η σελίδα που έψαχνες δεν υπάρχει πια εδώ. '422': content: Απέτυχε η επιβεβαίωση ασφαλείας. Μήπως μπλοκάρεις τα cookies; title: Η επιβεβαίωση ασφαλείας απέτυχε @@ -732,6 +733,16 @@ el: older: Παλιότερο prev: Προηγούμενο truncate: "…" + polls: + errors: + already_voted: Έχεις ήδη ψηφίσει σε αυτή την ψηφοφορία + duplicate_options: περιέχει επαναλαμβανόμενες επιλογές + duration_too_long: είναι πολύ μακριά στο μέλλον + duration_too_short: είναι πολύ σύντομα + expired: Η ψηφοφορία έχει ήδη λήξει + over_character_limit: δε μπορεί να υπερβαίνει τους %{MAX} χαρακτήρες έκαστη + too_few_options: πρέπει να έχει περισσότερες από μια επιλογές + too_many_options: δεν μπορεί να έχει περισσότερες από %{MAX} επιλογές preferences: languages: Γλώσσες other: Άλλο @@ -841,6 +852,11 @@ el: ownership: Δεν μπορείς να καρφιτσώσεις μη δικό σου τουτ private: Τα μη δημόσια τουτ δεν καρφιτσώνονται reblog: Οι προωθήσεις δεν καρφιτσώνονται + poll: + total_votes: + one: "%{count} ψήφος" + other: "%{count} ψήφοι" + vote: Ψήφισε show_more: Δείξε περισσότερα sign_in_to_participate: Εγγράφου για να συμμετάσχεις στη συζήτηση title: '%{name}: "%{quote}"' @@ -939,9 +955,9 @@ el:

    Οι παραπάνω όροι έχουν προσαρμοστεί από τους αντίστοιχους όρους του Discourse.

    title: Όροι Χρήσης και Πολιτική Απορρήτου του κόμβου %{instance} themes: - contrast: Υψηλή αντίθεση - default: Mastodon - mastodon-light: Mastodon (ανοιχτόχρωμο) + contrast: Mastodon (Υψηλή αντίθεση) + default: Mastodon (Σκοτεινό) + mastodon-light: Mastodon (Ανοιχτόχρωμο) time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/eo.yml b/config/locales/eo.yml index b7dd7ca8b..4e404d9eb 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -7,7 +7,7 @@ eo: administered_by: 'Administrata de:' api: API apps: Poŝtelefonaj aplikaĵoj - closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu nodo. Tamen, vi povas trovi alian nodon por fari konton kaj aliri al la sama reto de tie. + closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu servilo. Tamen, vi povas trovi alian servilon por fari konton kaj aliri al la sama reto de tie. contact: Kontakti contact_missing: Ne elektita contact_unavailable: Ne disponebla @@ -27,7 +27,7 @@ eo: generic_description: "%{domain} estas unu servilo en la reto" hosted_on: "%{domain} estas nodo de Mastodon" learn_more: Lerni pli - other_instances: Listo de nodoj + other_instances: Listo de serviloj privacy_policy: Privateca politiko source_code: Fontkodo status_count_after: @@ -70,6 +70,9 @@ eo: moderator: Kontrolanto unfollow: Ne plu sekvi admin: + account_actions: + action: Plenumi agon + title: Plenumi kontrolan agon al %{acct} account_moderation_notes: create: Lasi noton created_msg: Kontrola noto sukcese kreita! @@ -89,6 +92,7 @@ eo: confirm: Konfirmi confirmed: Konfirmita confirming: Konfirmante + deleted: Forigita demote: Degradi disable: Malebligi disable_two_factor_authentication: Malebligi 2FA @@ -96,26 +100,29 @@ eo: display_name: Montrata nomo domain: Domajno edit: Redakti - email: Retpoŝto - email_status: Retpoŝto Stato + email: Retadreso + email_status: Retadreso Stato enable: Ebligi enabled: Ebligita feed_url: URL de la fluo followers: Sekvantoj followers_url: URL de la sekvantoj follows: Sekvatoj + header: Kapa bildo inbox_url: Enira URL + invited_by: Invitita de ip: IP + joined: Aliĝis location: all: Ĉio - local: Loka - remote: Fora + local: Lokaj + remote: Foraj title: Loko login_status: Ensaluta stato media_attachments: Ligitaj aŭdovidaĵoj memorialize: Ŝanĝi al memoro moderation: - active: Aktiva + active: Aktivaj all: Ĉio silenced: Silentigitaj suspended: Haltigitaj @@ -132,8 +139,9 @@ eo: protocol: Protokolo public: Publika push_subscription_expires: Eksvalidiĝo de la abono al PuSH - redownload: Aktualigi profilbildon + redownload: Aktualigi profilon remove_avatar: Forigi profilbildon + remove_header: Forigi kapan bildon resend_confirmation: already_confirmed: Ĉi tiu uzanto jam estas konfirmita send: Esend konfirmi retpoŝton @@ -151,8 +159,8 @@ eo: search: Serĉi shared_inbox_url: URL de kunhavigita leterkesto show: - created_reports: Signaloj kreitaj de ĉi tiu konto - targeted_reports: Signaloj kreitaj de ĉi tiu konto + created_reports: Kreitaj signaloj + targeted_reports: Signalitaj de aliaj silence: Kaŝi silenced: Silentigita statuses: Mesaĝoj @@ -164,12 +172,14 @@ eo: undo_suspension: Malfari haltigon unsubscribe: Malaboni username: Uzantnomo + warn: Averti web: Reto action_logs: actions: assigned_to_self_report: "%{name} asignis signalon %{target} al si mem" change_email_user: "%{name} ŝanĝis retadreson de uzanto %{target}" confirm_user: "%{name} konfirmis retadreson de uzanto %{target}" + create_account_warning: "%{name} sendis averton al %{target}" create_custom_emoji: "%{name} alŝutis novan emoĝion %{target}" create_domain_block: "%{name} blokis domajnon %{target}" create_email_domain_block: "%{name} metis en nigran liston domajnon %{target}" @@ -261,6 +271,13 @@ eo: title: Nova domajna blokado reject_media: Malakcepti aŭdovidajn dosierojn reject_media_hint: Forigas aŭdovidaĵojn loke konservitajn kaj rifuzas alŝuti ajnan estonte. Senzorge pri haltigoj + reject_reports: Malakcepti raportojn + reject_reports_hint: Ignori ĉiujn raportojn el tiu domajno. Nur gravas por silentigoj + rejecting_media: aŭdovidaj dosieroj malakceptiĝas + rejecting_reports: raportoj malakceptiĝas + severity: + silence: silentigita + suspend: haltigita show: affected_accounts: one: Unu konto en la datumbazo esta influita @@ -281,8 +298,25 @@ eo: create: Aldoni domajnon title: Nova blokado de retadresa domajno title: Nigra listo de retadresaj domajnoj + followers: + back_to_account: Reen al la konto + title: Sekvantoj de %{acct} instances: - title: Konataj nodoj + by_domain: Domajno + delivery_available: Liverado disponeblas + known_accounts: + one: "%{count} konata konto" + other: "%{count} konataj kontoj" + moderation: + all: Ĉiuj + limited: Limigita + title: Kontrolo + title: Federacio + total_blocked_by_us: Blokitaj de ni + total_followed_by_them: Sekvataj de ili + total_followed_by_us: Sekvataj de ni + total_reported: Raportoj pri ili + total_storage: Aŭdovidaj kunsendaĵoj invites: deactivate_all: Malaktivigi ĉion filter: @@ -301,6 +335,7 @@ eo: enable_hint: Post ebligo, via servilo abonos ĉiujn publikajn mesaĝojn de tiu ripetilo, kaj komencos sendi publikajn mesaĝojn de la servilo al ĝi. enabled: Malebligita inbox_url: URL de la ripetilo + pending: Atendante aprobon de la ripetilo save_and_enable: Konservi kaj ebligi setup: Agordi konekton al ripetilo status: Stato @@ -331,12 +366,12 @@ eo: report: 'Signalo #%{id}' reported_account: Signalita konto reported_by: Signalita de - resolved: Solvita + resolved: Solvitaj resolved_msg: Signalo sukcese solvita! status: Mesaĝoj title: Signaloj unassign: Malasigni - unresolved: Nesolvita + unresolved: Nesolvitaj updated_at: Ĝisdatigita settings: activity_api_enabled: @@ -352,11 +387,14 @@ eo: desc_html: Ŝanĝi la aspekton per CSS ŝargita en ĉiu pago title: Propra CSS hero: - desc_html: Montrata en la ĉefpaĝo. Almenaŭ 600x100px rekomendita. Kiam ne agordita, la bildeto de la nodo estos uzata + desc_html: Montrata en la ĉefpaĝo. Almenaŭ 600x100px rekomendita. Kiam ne agordita, la bildeto de la servilo estos uzata title: Kapbildo + mascot: + desc_html: Montrata en pluraj paĝoj. Rekomendataj estas almenaŭ 293x205px. Se ĉi tio ne estas agordita, la defaŭlta maskoto uziĝas + title: Maskota bildo peers_api_enabled: - desc_html: Nomoj de domajnoj, kiujn ĉi tiu nodo renkontis en la fediverse - title: Publikigi liston de malkovritaj nodoj + desc_html: Nomoj de domajnoj, kiujn ĉi tiu servilo renkontis en la federauniverso + title: Publikigi liston de malkovritaj serviloj preview_sensitive_media: desc_html: Antaŭvido de ligiloj en aliaj retejoj montros bildeton eĉ se la aŭdovidaĵo estas markita kiel tikla title: Montri tiklajn aŭdovidaĵojn en la antaŭvidoj de OpenGraph @@ -384,20 +422,20 @@ eo: title: Montri teaman insignon site_description: desc_html: Enkonduka alineo en la ĉefpaĝo. Priskribu la unikaĵojn de ĉi tiu nodo de Mastodon, kaj ĉiujn aliajn gravaĵojn. Vi povas uzi HTML-etikedojn, kiel <a> kaj <em>. - title: Priskribo de la nodo + title: Priskribo de la servilo site_description_extended: - desc_html: Bona loko por viaj sintenaj reguloj, aliaj reguloj, gvidlinioj kaj aliaj aferoj, kiuj apartigas vian nodon. Vi povas uzi HTML-etikedojn + desc_html: Bona loko por viaj sintenaj reguloj, aliaj reguloj, gvidlinioj kaj aliaj aferoj, kiuj apartigas vian serilon. Vi povas uzi HTML-etikedojn title: Propraj detalaj informoj site_short_description: - desc_html: Afiŝita en la flankpanelo kaj metadatumaj etikedoj. Priskribu kio estas Mastodon, kaj kio specialas en ĉi tiu nodo, per unu alineo. Se malplena, la priskribo de la nodo estos uzata. - title: Mallonga priskribo de la nodo + desc_html: Afiŝita en la flankpanelo kaj metadatumaj etikedoj. Priskribu kio estas Mastodon, kaj kio specialas en ĉi tiu nodo, per unu alineo. Se malplena, la priskribo de la servilo estos uzata. + title: Mallonga priskribo de la servilo site_terms: desc_html: Vi povas skribi vian propran privatecan politikon, viajn uzkondiĉojn aŭ aliajn leĝaĵojn. Vi povas uzi HTML-etikedojn title: Propraj uzkondiĉoj - site_title: Nomo de la nodo + site_title: Nomo de la servilo thumbnail: desc_html: Uzata por antaŭvidoj per OpenGraph kaj per API. 1200x630px rekomendita - title: Bildeto de la nodo + title: Bildeto de la servilo timeline_preview: desc_html: Montri publikan tempolinion en komenca paĝo title: Tempolinia antaŭvido @@ -423,12 +461,20 @@ eo: title: WebSub topic: Temo tags: + accounts: Kontoj + hidden: Kaŝitaj hide: Kaŝi de la profilujo name: Kradvorto title: Kradvortoj unhide: Montri en la profilujo - visible: Videbla + visible: Videblaj title: Administrado + warning_presets: + add_new: Aldoni novan + delete: Forigi + edit: Redakti + edit_preset: Redakti avertan antaŭagordon + title: Administri avertajn antaŭagordojn admin_mailer: new_report: body: "%{reporter} signalis %{target}" @@ -450,7 +496,7 @@ eo: warning: Estu tre atenta kun ĉi tiu datumo. Neniam diskonigu ĝin al iu ajn! your_token: Via alira ĵetono auth: - agreement_html: Klakante “Registriĝi” sube, vi konsentas kun la reguloj de la nodo kaj niaj uzkondiĉoj. + agreement_html: Klakante “Registriĝi” sube, vi konsentas kun la reguloj de la servilo kaj niaj uzkondiĉoj. change_password: Pasvorto confirm_email: Konfirmi retadreson delete_account: Forigi konton @@ -504,19 +550,22 @@ eo: description_html: Tio porĉiame kaj neŝanĝeble forigos la enhavon de via konto kaj malaktivigos ĝin. Via uzantnomo restos rezervita por eviti postajn trompojn pri identeco. proceed: Forigi konton success_msg: Via konto estis sukcese forigita - warning_html: La forigo de la enhavo estas certa nur por ĉi tiu aparta nodo. Enhavo, kiu estis disvastigita verŝajne lasos spurojn. Eksterretaj serviloj kaj serviloj, kiuj ne abonas viajn ĝisdatigojn ne ĝisdatigos siajn datumbazojn. + warning_html: La forigo de la enhavo estas certa nur por ĉi tiu aparta servilo. Enhavo, kiu estis disvastigita verŝajne lasos spurojn. Eksterretaj serviloj kaj serviloj, kiuj ne abonas viajn ĝisdatigojn ne ĝisdatigos siajn datumbazojn. warning_title: Disponebleco de disvastigita enhavo directories: directory: Profilujo + enabled: Vi estas listigata en la profilujo. + enabled_but_waiting: Vi elektis esti listigata en la profilujo, sed vi ankoraŭ ne havas la minimuman kvanton da sekvantoj (%{min_followers}) por esti listigata. explanation: Malkovru uzantojn per iliaj interesoj explore_mastodon: Esplori %{title} + how_to_enable: Vi ankoraŭ ne donis permeson listigi vin en la profilujo. Vi povas doni permeson ĉi-sube. Uzu kradvortojn en via biografia teksto por esti listigata sub specifaj kradvortoj! people: one: "%{count} personoj" other: "%{count} personoj" errors: '403': Vi ne havas la rajton por vidi ĉi tiun paĝon. - '404': La paĝo, kiun vi serĉas, ne ekzistas. - '410': La paĝo, kiun vi serĉas, ne plu ekzistas. + '404': La paĝo ke kiun vi serĉas ne ekzistas ĉi tie. + '410': La paĝo, kiun vi serĉas, ne plu ekzistas ĉi tie. '422': content: Sekureca konfirmo malsukcesa. Ĉu vi blokas kuketojn? title: Sekureca konfirmo malsukcesa @@ -537,9 +586,15 @@ eo: size: Grandeco blocks: Vi blokas csv: CSV + domain_blocks: Blokoj de domajnoj follows: Vi sekvas + lists: Listoj mutes: Vi silentigas storage: Aŭdovidaĵa konservado + featured_tags: + add_new: Aldoni novan + errors: + limit: Vi jam elstarigis la maksimuman kvanton da kradvortoj filters: contexts: home: Hejma tempolinio @@ -558,7 +613,7 @@ eo: title: Aldoni novan filtrilon followers: domain: Domajno - explanation_html: Se vi volas esti certa pri la privateco de viaj mesaĝoj, vi bezonas esti atenta pri tiuj, kiuj sekvas vin. Viaj privataj mesaĝoj estas liveritaj al ĉiuj nodoj, kie vi havas sekvantojn. Eble vi ŝatus kontroli ilin, kaj forigi la sekvantojn de la nodoj, kie vi ne certas ĉu via privateco estos respektita de la tiea teamo aŭ programo. + explanation_html: Se vi volas esti certa pri la privateco de viaj mesaĝoj, vi bezonas esti atenta pri tiuj, kiuj sekvas vin. Viaj privataj mesaĝoj estas liveritaj al ĉiuj serviloj, kie vi havas sekvantojn. Eble vi ŝatus kontroli ilin, kaj forigi la sekvantojn de la serviloj, kie vi ne certas ĉu via privateco estos respektita de la tiea teamo aŭ programo. followers_count: Nombro de sekvantoj lock_link: Ŝlosu vian konton purge: Forigi el la sekvantoj @@ -580,10 +635,16 @@ eo: one: Io mise okazis! Bonvolu konsulti la suban erar-raporton other: Io mise okazis! Bonvolu konsulti la subajn %{count} erar-raportojn imports: - preface: Vi povas importi datumojn, kiujn vi eksportis el alia nodo, kiel liston de homoj, kiujn vi sekvas aŭ blokas. + modes: + merge: Kunigi + merge_long: Konservi ekzistajn registrojn kaj aldoni novajn + overwrite: Anstataŭigi + overwrite_long: Anstataŭigi la nunajn registrojn per la novaj + preface: Vi povas importi datumojn, kiujn vi eksportis el alia servilo, kiel liston de homoj, kiujn vi sekvas aŭ blokas. success: Viaj datumoj estis sukcese alŝutitaj kaj estos traktitaj kiel planite types: blocking: Listo de blokitoj + domain_blocking: Listo de blokitaj domajnoj following: Listo de sekvatoj muting: Listo de silentigitoj upload: Alŝuti @@ -605,7 +666,7 @@ eo: one: 1 uzo other: "%{count} uzoj" max_uses_prompt: Neniu limo - prompt: Krei kaj diskonigi ligilojn al aliaj por doni aliron al ĉi tiu nodo + prompt: Krei kaj diskonigi ligilojn al aliaj por doni aliron al ĉi tiu servilo table: expires_at: Eksvalidiĝas je uses: Uzoj @@ -686,10 +747,25 @@ eo: no_account_html: Ĉu vi ne havas konton? Vi povas registriĝi tie proceed: Daŭrigi por eksekvi prompt: 'Vi eksekvos:' + reason_html: "Kial necesas ĉi tiu paŝo?%{instance} povus ne esti la servilo, kie vi registriĝis, do ni unue bezonas alidirekti vin al via hejma servilo." + remote_interaction: + favourite: + proceed: Konfirmi la stelumon + prompt: 'Vi volas stelumi ĉi tiun mesaĝon:' + reblog: + proceed: Konfirmi la diskonigon + prompt: 'Vi volas diskonigi ĉi tiun mesaĝon:' + reply: + proceed: Konfirmi la respondon + prompt: 'Vi volas respondi al ĉi tiu mesaĝo:' remote_unfollow: error: Eraro title: Titolo unfollowed: Ne plu sekvita + scheduled_statuses: + over_daily_limit: Vi transpasis la limigon al %{limit} samtage planitaj mesaĝoj + over_total_limit: Vi transpasis la limigon al %{limit} planitaj mesaĝoj + too_soon: La planita dato devas esti en la estonteco sessions: activity: Lasta ago browser: Retumilo @@ -738,6 +814,7 @@ eo: development: Evoluigado edit_profile: Redakti profilon export: Eksporti datumojn + featured_tags: Elstarigitaj kradvortoj followers: Rajtigitaj sekvantoj import: Importi migrate: Konta migrado @@ -785,9 +862,9 @@ eo: terms: title: Uzkondiĉoj kaj privateca politiko de %{instance} themes: - contrast: Forta kontrasto - default: Mastodon - mastodon-light: Mastodon (hela) + contrast: Mastodon (Forta kontrasto) + default: Mastodon (Malluma) + mastodon-light: Mastodon (Luma) time: formats: default: "%Y-%m-%d %H:%M" @@ -813,6 +890,22 @@ eo: explanation: Vi petis kompletan arkivon de via Mastodon-konto. Ĝi nun pretas por elŝutado! subject: Via arkivo estas preta por elŝutado title: Arkiva elŝuto + warning: + explanation: + disable: Dum via konto estas frostigita, via kontaj datumoj restas intaktaj, sed vi ne povas plenumi iujn agojn ĝis ĝi estas malhaltigita. + silence: Dum via konto estas limigita, nur tiuj, kiuj jam sekvas vin, vidos viajn mesaĝojn en ĉi tiu servilo, kaj vi povus esti ekskludita de diversaj publikaj listoj. Tamen, aliaj ankoraŭ povas mane sekvi vin. + suspend: Via konto estis haltigita, kaj ĉiuj el viaj mesaĝoj kaj alŝutitaj aŭdovidaj dosieroj estis nemalfareble forigitaj de ĉi tiu servilo, kaj de la serviloj, kie vi havis sekvantojn. + review_server_policies: Superrigardi servilajn politikojn + subject: + disable: Via konto %{acct} estas frostigita + none: Averto por %{acct} + silence: Via konto %{acct} estas limigita + suspend: Via konto %{acct} estas haltigita + title: + disable: Konto frostigita + none: Averto + silence: Konto limigita + suspend: Konto haltigita welcome: edit_profile_action: Agordi profilon edit_profile_step: Vi povas proprigi vian profilon per alŝuto de profilbildo, fonbildo, ŝanĝo de via afiŝita nomo kaj pli. Se vi ŝatus kontroli novajn sekvantojn antaŭ ol ili rajtas sekvi vin, vi povas ŝlosi vian konton. @@ -820,7 +913,7 @@ eo: final_action: Ekmesaĝi final_step: 'Ekmesaĝu! Eĉ sen sekvantoj, viaj publikaj mesaĝoj povas esti vidataj de aliaj, ekzemple en la loka tempolinio kaj en la kradvortoj. Eble vi ŝatus prezenti vin per la kradvorto #introductions.' full_handle: Via kompleta uzantnomo - full_handle_hint: Jen kion vi dirus al viaj amikoj, por ke ili mesaĝu aŭ sekvu vin de alia nodo. + full_handle_hint: Jen kion vi dirus al viaj amikoj, por ke ili mesaĝu aŭ sekvu vin de alia servilo. review_preferences_action: Ŝanĝi preferojn review_preferences_step: Estu certa ke vi agordis viajn preferojn, kiel kiujn retmesaĝojn vi ŝatus ricevi, aŭ kiun dekomencan privatecan nivelon vi ŝatus ke viaj mesaĝoj havu. Se tio ne ĝenas vin, vi povas ebligi aŭtomatan ekigon de GIF-oj. subject: Bonvenon en Mastodon @@ -838,4 +931,5 @@ eo: seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj. signed_in_as: 'Ensalutinta kiel:' verification: + explanation_html: 'Vi povas pruvi, ke vi estas la posedanto de la ligiloj en viaj profilaj metadatumoj. Por fari tion, la alligita retejo devas enhavi ligilon reen al via Mastodon-profilo. La religilo devas havi la atributon rel="me". Ne gravas la teksta enhavo de la religilo. Jen ekzemplo:' verification: Kontrolo diff --git a/config/locales/es.yml b/config/locales/es.yml index b221989e8..2bde59dbc 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -7,7 +7,7 @@ es: administered_by: 'Administrado por:' api: API apps: Aplicaciones móviles - closed_registrations: Los registros están actualmente cerrados en esta instancia. + closed_registrations: Los registros están actualmente cerrados en este servidor. Aun así, puedes encontrar un servidor diferente para registrarte y tener acceso a la misma comunidad contact: Contacto contact_missing: No especificado contact_unavailable: N/A @@ -48,6 +48,7 @@ es: other: Seguidores following: Siguiendo joined: Se unió el %{date} + last_active: última conexión link_verified_on: La propiedad de este vínculo fue verificada el %{date} media: Media moved_html: "%{name} se ha trasladado a %{new_profile_link}:" @@ -69,6 +70,8 @@ es: moderator: Moderador unfollow: Dejar de seguir admin: + account_actions: + title: Moderar %{acct} account_moderation_notes: create: Crear created_msg: "¡Nota de moderación creada con éxito!" @@ -88,6 +91,7 @@ es: confirm: Confirmar confirmed: Confirmado confirming: Confirmando + deleted: Borrado demote: Degradar disable: Deshabilitar disable_two_factor_authentication: Desactivar autenticación de dos factores diff --git a/config/locales/fa.yml b/config/locales/fa.yml index fd551d1b6..4cf8f415c 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -61,7 +61,7 @@ fa: posts: one: بوق other: بوق - posts_tab_heading: بوق‌ها + posts_tab_heading: نوشته‌ها posts_with_replies: نوشته‌ها و پاسخ‌ها reserved_username: این نام کاربری در دسترس نیست roles: @@ -302,6 +302,7 @@ fa: back_to_account: بازگشت به حساب title: پیگیران %{acct} instances: + by_domain: دامین delivery_available: پیام آماده است known_accounts: one: "%{count} حساب شناخته‌شده" @@ -733,6 +734,16 @@ fa: older: قدیمی‌تر prev: قبلی truncate: "…" + polls: + errors: + already_voted: شما قبلاً در این نظرسنجی رأی داده‌اید + duplicate_options: دارای موارد تکراری است + duration_too_long: در آیندهٔ خیلی دور است + duration_too_short: در آیندهٔ خیلی نزدیک است + expired: این نظرسنجی به پایان رسیده است + over_character_limit: هر کدام نمی‌تواند از %{MAX} نویسه طولانی‌تر باشد + too_few_options: حتماً باید بیش از یک گزینه داشته باشد + too_many_options: نمی‌تواند بیشتر از %{MAX} گزینه داشته باشد preferences: languages: تنظیمات زبان other: سایر تنظیمات diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5c564fc04..c84dc8063 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -264,7 +264,7 @@ fr: create: Créer le blocage hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. severity: - desc_html: "Silence rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. Suspendre supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez Aucun si vous voulez simplement rejeter les fichiers multimédia." + desc_html: "Masqué rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. Suspendre supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez Aucune si vous voulez simplement rejeter les fichiers multimédia." noop: Aucune silence: Masqué suspend: Suspendre @@ -302,6 +302,7 @@ fr: back_to_account: Retour au compte title: Abonné⋅e⋅s de %{acct} instances: + by_domain: Domaine delivery_available: Livraison disponible known_accounts: one: "%{count} compte connu" @@ -733,6 +734,16 @@ fr: older: Plus ancien prev: Précédent truncate: "…" + polls: + errors: + already_voted: Vous avez déjà voté sur ce sondage + duplicate_options: contient des doublons + duration_too_long: est trop loin dans le futur + duration_too_short: est trop tôt + expired: Ce sondage est déjà terminé + over_character_limit: ne peuvent être plus long que %{MAX} caractères chacun + too_few_options: doit avoir plus qu'une proposition + too_many_options: ne peut contenir plus que %{MAX} propositions preferences: languages: Langues other: Autre diff --git a/config/locales/gl.yml b/config/locales/gl.yml index cadb7cff6..3c58e04f5 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -302,6 +302,7 @@ gl: back_to_account: Voltar a Conta title: Seguidoras de %{acct} instances: + by_domain: Dominio delivery_available: A entrega está dispoñible known_accounts: one: "%{count} conta coñecida" @@ -733,6 +734,16 @@ gl: older: Máis antigo prev: Previo truncate: "…" + polls: + errors: + already_voted: Xa votou en esta sondaxe + duplicate_options: contén elementos duplicados + duration_too_long: está moi lonxe no futuro + duration_too_short: é demasiado cedo + expired: A sondaxe rematou + over_character_limit: non poden ter máis de %{MAX} caracteres cada unha + too_few_options: debe ter máis de unha opción + too_many_options: non pode haber máis de %{MAX} opcións preferences: languages: Idiomas other: Outro @@ -842,6 +853,11 @@ gl: ownership: Non pode fixar a mensaxe de outra usuaria private: As mensaxes non-públicas non poden ser fixadas reblog: Non se poden fixar as mensaxes promovidas + poll: + total_votes: + one: "%{count} voto" + other: "%{count} votos" + vote: Votar show_more: Mostrar máis sign_in_to_participate: Conéctese para participar na conversa title: '%{name}: "%{quote}"' diff --git a/config/locales/it.yml b/config/locales/it.yml index fea39b1fd..76fcb2b91 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -7,7 +7,7 @@ it: administered_by: 'Amministrato da:' api: API apps: Applicazioni Mobile - closed_registrations: Al momento le iscrizioni a questo server sono chiuse. Tuttavia! Puoi provare a cercare un istanza diversa su cui creare un account ed avere accesso alla stessa identica rete. + closed_registrations: Al momento le iscrizioni a questo server sono chiuse. Tuttavia! Puoi provare a cercare un server diverso su cui creare un account ed avere accesso alla stessa identica rete. contact: Contatti contact_missing: Non impostato contact_unavailable: N/D @@ -27,7 +27,7 @@ it: generic_description: "%{domain} è un server nella rete" hosted_on: Mastodon ospitato su %{domain} learn_more: Scopri altro - other_instances: Elenco istanze + other_instances: Elenco server privacy_policy: Politica della privacy source_code: Codice sorgente status_count_after: @@ -69,6 +69,8 @@ it: moderator: Moderatore unfollow: Non seguire più admin: + account_actions: + action: Esegui azione account_moderation_notes: create: Lascia nota created_msg: Nota di moderazione creata con successo! @@ -88,6 +90,7 @@ it: confirm: Conferma confirmed: Confermato confirming: Confermando + deleted: Cancellato demote: Declassa disable: Disabilita disable_two_factor_authentication: Disabilita 2FA @@ -103,7 +106,9 @@ it: followers: Follower followers_url: URL follower follows: Segue + header: Intestazione inbox_url: URL inbox + invited_by: Invitato da ip: IP location: all: Tutto @@ -114,6 +119,7 @@ it: media_attachments: Media allegati memorialize: Trasforma in memoriam moderation: + active: Attivo all: Tutto silenced: Silenziati suspended: Sospesi @@ -132,6 +138,7 @@ it: push_subscription_expires: Sottoscrizione PuSH scaduta redownload: Aggiorna avatar remove_avatar: Rimuovi avatar + remove_header: Rimuovi intestazione resend_confirmation: already_confirmed: Questo utente è già confermato send: Reinvia email di conferma @@ -162,12 +169,14 @@ it: undo_suspension: Rimuovi sospensione unsubscribe: Annulla l'iscrizione username: Nome utente + warn: Avverti web: Web action_logs: actions: assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso" change_email_user: "%{name} ha cambiato l'indirizzo email per l'utente %{target}" confirm_user: "%{name} ha confermato l'indirizzo email per l'utente %{target}" + create_account_warning: "%{name} ha mandato un avvertimento a %{target}" create_custom_emoji: "%{name} ha caricato un nuovo emoji %{target}" create_domain_block: "%{name} ha bloccato il dominio %{target}" create_email_domain_block: "%{name} ha messo il dominio email %{target} nella blacklist" @@ -260,6 +269,9 @@ it: reject_media_hint: Rimuovi i file media salvati in locale e blocca i download futuri. Irrilevante per le sospensioni reject_reports: Respingi rapporti reject_reports_hint: Ignora tutti i rapporti provenienti da questo dominio. Irrilevante per sospensioni + severity: + silence: silenziato + suspend: sospeso show: affected_accounts: one: Interessato un solo account nel database @@ -280,8 +292,22 @@ it: create: Aggiungi dominio title: Nuova voce della lista nera delle email title: Lista nera email + followers: + back_to_account: Torna all'account + title: Seguaci di %{acct} instances: + by_domain: Dominio + known_accounts: + one: "%{count} account noto" + other: "%{count} account noti" + moderation: + limited: Limitato + title: Moderazione title: Istanze conosciute + total_blocked_by_us: Bloccato da noi + total_followed_by_them: Seguito da loro + total_followed_by_us: Seguito da noi + total_storage: Media allegati invites: deactivate_all: Disattiva tutto filter: @@ -351,17 +377,19 @@ it: desc_html: Modifica l'aspetto con il CSS caricato in ogni pagina title: CSS personalizzato hero: - desc_html: Mostrata nella pagina iniziale. Almeno 600x100 px consigliati. Se non impostata, sarà usato il thumbnail dell'istanza + desc_html: Mostrata nella pagina iniziale. Almeno 600x100 px consigliati. Se non impostata, sarà usato il thumbnail del server title: Immagine dell'eroe mascot: desc_html: Mostrata su più pagine. Almeno 293×205px consigliati. Se non impostata, sarò usata la mascotte predefinita title: Immagine della mascotte peers_api_enabled: - desc_html: Nomi di dominio che questa istanza ha incontrato nella fediverse - title: Pubblica elenco di istanze scoperte + desc_html: Nomi di dominio che questo server ha incontrato nel fediverse + title: Pubblica elenco dei server scoperti preview_sensitive_media: desc_html: Le anteprime dei link su altri siti mostreranno un thumbnail anche se il media è segnato come sensibile title: Mostra media sensibili nella anteprime OpenGraph + profile_directory: + title: Attiva directory del profilo registrations: closed_message: desc_html: Mostrato nella pagina iniziale quando le registrazioni sono chiuse. Puoi usare tag HTML @@ -382,20 +410,20 @@ it: title: Mostra badge staff site_description: desc_html: Paragrafo introduttivo nella pagina iniziale. Descrive ciò che rende speciale questo server Mastodon e qualunque altra cosa sia importante dire. Potete usare marcatori HTML, in particolare <a> e <em>. - title: Descrizione istanza + title: Descrizione del server site_description_extended: - desc_html: Un posto adatto per pubblicare regole di comportamento, linee guida e altre cose specifiche della vostra istanza. Potete usare marcatori HTML + desc_html: Un posto adatto per pubblicare regole di comportamento, linee guida e altre cose specifiche del vostro server. Potete usare marcatori HTML title: Informazioni estese personalizzate site_short_description: - desc_html: Mostrato nella barra laterale e nei tag meta. Descrive in un paragrafo che cos'è Mastodon e che cosa rende questo server speciale. Se vuoto, sarà usata la descrizione predefinita dell'istanza. - title: Breve descrizione dell'istanza + desc_html: Mostrato nella barra laterale e nei tag meta. Descrive in un paragrafo che cos'è Mastodon e che cosa rende questo server speciale. Se vuoto, sarà usata la descrizione predefinita del server. + title: Breve descrizione del server site_terms: desc_html: Potete scrivere la vostra politica sulla privacy, condizioni del servizio o altre informazioni legali. Potete usare tag HTML title: Termini di servizio personalizzati - site_title: Nome istanza + site_title: Nome del server thumbnail: desc_html: Usato per anteprime tramite OpenGraph e API. 1200x630px consigliati - title: Thumbnail dell'istanza + title: Thumbnail del server timeline_preview: desc_html: Mostra la timeline pubblica sulla pagina iniziale title: Anteprima timeline @@ -418,7 +446,18 @@ it: confirmed: Confermato expires_in: Scade in topic: Argomento + tags: + accounts: Account + hidden: Nascosto + name: Hashtag + title: Hashtag + unhide: Mostra nella directory + visible: Visibile title: Amministrazione + warning_presets: + add_new: Aggiungi nuovo + delete: Cancella + edit: Modifica application_mailer: notification_preferences: Cambia preferenze email salutation: "%{name}," @@ -434,7 +473,7 @@ it: token_regenerated: Token di accesso rigenerato warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro! auth: - agreement_html: Iscrivendoti, accetti di seguire le regole dell'istanza e le nostre condizioni di servizio. + agreement_html: Iscrivendoti, accetti di seguire le regole del server e le nostre condizioni di servizio. change_password: Password confirm_email: Conferma email delete_account: Elimina account @@ -484,11 +523,14 @@ it: description_html: Questa azione eliminerà in modo permanente e irreversibile tutto il contenuto del tuo account e lo disattiverà. Il tuo nome utente resterà riservato per prevenire che qualcuno in futuro assuma la tua identità. proceed: Cancella l'account success_msg: Il tuo account è stato cancellato - warning_html: È garantita solo la cancellazione del contenuto solo da questa istanza. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + warning_html: È garantita la cancellazione del contenuto solo da questo server. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + directories: + explanation: Scopri utenti in base ai loro interessi + explore_mastodon: Esplora %{title} errors: '403': Non sei autorizzato a visualizzare questa pagina. '404': La pagina che stavi cercando non esiste. - '410': La pagina che stavi cercando non esiste più. + '410': La pagina che stavi cercando qui non esiste più. '422': content: Verifica di sicurezza non riuscita. Stai bloccando i cookies? title: Verifica di sicurezza non riuscita @@ -506,9 +548,13 @@ it: size: Dimensioni blocks: Stai bloccando csv: CSV + domain_blocks: Blocchi di dominio follows: Stai seguendo + lists: Liste mutes: Stai silenziando storage: Archiviazione media + featured_tags: + add_new: Aggiungi nuovo filters: contexts: home: Timeline home @@ -526,7 +572,7 @@ it: title: Aggiungi filtro followers: domain: Dominio - explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. I tuoi status privati vengono inviati a tutte le istanze su cui hai dei seguaci. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quelle istanze. + explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. I tuoi status privati vengono inviati a tutti i server su cui hai dei seguaci. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quei server. followers_count: Numero di seguaci lock_link: Blocca il tuo account purge: Elimina dai seguaci @@ -544,7 +590,9 @@ it: one: Qualcosa ancora non va bene! Per favore, controlla l'errore qui sotto other: Qualcosa ancora non va bene! Per favore, controlla i %{count} errori qui sotto imports: - preface: Puoi importare alcune informazioni, come le persone che segui o hai bloccato su questo server, da file creati da un esportazione su un altro server. + modes: + overwrite: Sovrascrivi + preface: Puoi importare alcune informazioni, come le persone che segui o hai bloccato su questo server, da file creati da un'esportazione su un altro server. success: Le tue impostazioni sono state importate correttamente e verranno applicate in breve tempo types: blocking: Lista dei bloccati @@ -569,7 +617,7 @@ it: one: un uso other: "%{count} utilizzi" max_uses_prompt: Nessun limite - prompt: Genera e condividi dei link ad altri per garantire l'accesso a questa istanza + prompt: Genera e condividi dei link con altri per concedere l'accesso a questo server table: expires_at: Scade uses: Utilizzi @@ -735,8 +783,8 @@ it: terms: title: "%{instance} Termini di servizio e politica della privacy" themes: - contrast: Contrasto elevato - default: Mastodon + contrast: Mastodon (contrasto elevato) + default: Mastodon (scuro) mastodon-light: Mastodon (chiaro) time: formats: @@ -768,7 +816,7 @@ it: final_action: Inizia a postare final_step: 'Inizia a postare! Anche se non hai seguaci, i tuoi messaggi pubblici possono essere visti da altri, ad esempio nelle timeline locali e negli hashtag. Se vuoi puoi presentarti con l''hashtag #introductions.' full_handle: Il tuo nome utente completo - full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un'altra istanza. + full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un altro server. review_preferences_action: Cambia preferenze review_preferences_step: Dovresti impostare le tue preferenze, ad esempio quali email vuoi ricevere oppure il livello predefinito di privacy per i tuoi post. Se le immagini in movimento non ti danno fastidio, puoi abilitare l'animazione automatica delle GIF. subject: Benvenuto/a su Mastodon diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 6d0e19684..7cbe82354 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -287,7 +287,7 @@ ja: suspend: このドメインからの存在するすべてのアカウントの停止を戻す title: "%{domain}のドメインブロックを戻す" undo: 元に戻す - undo: 元に戻す + undo: ドメインブロックを戻す email_domain_blocks: add_new: 新規追加 created_msg: ブラックリストに追加しました @@ -302,6 +302,7 @@ ja: back_to_account: 戻る title: "%{acct}さんのフォロワー" instances: + by_domain: ドメイン delivery_available: 配送可能 known_accounts: one: 既知のアカウント数 %{count} diff --git a/config/locales/kk.yml b/config/locales/kk.yml index 97a0626e6..1c40adeb7 100644 --- a/config/locales/kk.yml +++ b/config/locales/kk.yml @@ -33,7 +33,7 @@ kk: status_count_after: one: жазба other: жазба - status_count_before: Жазылғандар + status_count_before: Барлығы terms: Қолдану шарттары user_count_after: one: қолданушы @@ -104,6 +104,7 @@ kk: email_status: Email статусы enable: Қосу enabled: Қосылды + feed_url: Feеd URL followers: Оқырмандар followers_url: Оқырмандар URL follows: Жазылғандары @@ -147,6 +148,7 @@ kk: success: Құптау хаты сәтті жіберілді! reset: Қалпына келтіру reset_password: Құпиясөзді қалпына келтіру + resubscribe: Resubscribе role: Қайта жазылу roles: admin: Админ @@ -262,6 +264,7 @@ kk: create: Блок құру hint: Домендік блок дерекқорда тіркелгі жазбаларын құруға кедергі жасамайды, бірақ сол есептік жазбаларда ретроактивті және автоматты түрде нақты модерация әдістерін қолданады. severity: + desc_html: "Silence will make the account's posts invisible to anyone who isn't following them. Suspend will remove all of the account's content, media, and profile data. Use None if you just want to reject media filеs." noop: Ештеңе silence: Үнсіз suspend: Тоқтатылған @@ -289,11 +292,17 @@ kk: add_new: Жаңасын қосу created_msg: Қаратізімге email домені қосылды delete: Өшіру + destroyed_msg: Successfully deletеd e-mail domain from blacklist domain: Домен + new: + create: Add dоmain + title: New e-mail blаcklist entry title: E-mail қаратізімі followers: + back_to_account: Back To Accоunt title: "%{acct} оқырмандары" instances: + by_domain: Domаin delivery_available: Жеткізу қол жетімді known_accounts: one: "%{count} таныс аккаунт" @@ -319,9 +328,11 @@ kk: relays: add_new: Жаңа арна қосу delete: Өшіру + description_html: A fedеration relay is an intermediary server that exchanges large volumes of public toots between servers that subscribe and publish to it. It can help small and medium servers discover content from the fediverse, which would otherwise require local users manually following other people on remote servers. disable: Сөндіру disabled: Сөндірілді enable: Қосу + enable_hint: Once enabled, your server will subscribe to all public toots from this rеlay, and will begin sending this server's public toots to it. enabled: Қосылды inbox_url: Арна URL pending: Жаңа арна құпталуын күту @@ -432,6 +443,7 @@ kk: statuses: back_to_account: Аккаунт бетіне оралы batch: + delete: Delеte nsfw_off: Сезімтал емес ретінде белгіле nsfw_on: Сезімтал ретінде белгіле failed_to_execute: Орындалмады @@ -443,3 +455,559 @@ kk: with_media: Медиамен subscriptions: callback_url: Callbаck URL + confirmed: Confirmеd + expires_in: Expirеs in + last_delivery: Last dеlivery + title: WеbSub + topic: Tоpic + tags: + accounts: Accоunts + hidden: Hiddеn + hide: Hidе from directory + name: Hаshtag + title: Hashtаgs + unhide: Shоw in directory + visible: Visiblе + title: Administrаtion + warning_presets: + add_new: Add nеw + delete: Deletе + edit: Еdit + edit_preset: Edit warning prеset + title: Manage warning presеts + admin_mailer: + new_report: + body: "%{reporter} has rеported %{target}" + body_remote: Someone from %{domain} has rеported %{target} + subject: New rеport for %{instance} (#%{id}) + application_mailer: + notification_preferences: Change e-mail prеferences + salutation: "%{name}," + settings: 'Change e-mail preferеnces: %{link}' + view: 'Viеw:' + view_profile: Viеw Profile + view_status: Viеw status + applications: + created: Application succеssfully created + destroyed: Application succеssfully deleted + invalid_url: The providеd URL is invalid + regenerate_token: Regenerate accеss token + token_regenerated: Access token succеssfully regenerated + warning: Be very carеful with this data. Never share it with anyone! + your_token: Your access tokеn + auth: + agreement_html: '"Тіркелу" батырмасын басу арқылы сервер ережелері мен қолдану шарттарына келісесіз.' + change_password: Құпиясөз + confirm_email: Еmаil құптау + delete_account: Аккаунт өшіру + delete_account_html: Аккаунтыңызды жойғыңыз келсе, мына жерді басыңыз. Сізден растау сұралатын болады. + didnt_get_confirmation: Растау хаты келмеді ме? + forgot_password: Құпиясөзіңізді ұмытып қалдыңыз ба? + invalid_reset_password_token: Құпиясөз қайтып алу қолжетімді емес немесе мерзімі аяқталған. Қайтадан сұратыңыз. + login: Кіру + logout: Шығу + migrate_account: Басқа аккаунтқа көшіру + migrate_account_html: Егер аккаунтыңызды басқасына байлағыңыз келсе, мына жерге келіңіз. + or: немесе + or_log_in_with: Немесе былай кіріңіз + providers: + cas: САS + saml: SАML + register: Тіркелу + register_elsewhere: Басқа серверге тіркелу + resend_confirmation: Растау нұсқаулықтарын жіберу + reset_password: Құпиясөзді қалпына келтіру + security: Қауіпсіздік + set_new_password: Жаңа құпиясөз қою + authorize_follow: + already_following: Бұл аккаунтқа жазылғансыз + error: Өкінішке орай, қашықтағы тіркелгіні іздеуде қате пайда болды + follow: Жазылу + follow_request: 'Сіз жазылуға өтініш жібердіңіз:' + following: 'Керемет! Сіз енді жазылдыңыз:' + post_follow: + close: Немесе терезені жаба салыңыз. + return: Қолданушы профилін көрсет + web: Вебте ашу + title: Жазылу %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}сағ" + about_x_months: "%{count}ай" + about_x_years: "%{count}жыл" + almost_x_years: "%{count}жыл" + half_a_minute: Осы бойда + less_than_x_minutes: "%{count}мин" + less_than_x_seconds: Осы бойда + over_x_years: "%{count}жыл" + x_days: "%{count}күн" + x_minutes: "%{count}мин" + x_months: "%{count}ай" + x_seconds: "%{count}сек" + deletes: + bad_password_msg: Болмады ма, хакер бала? Құпиясөз қате + confirm_password: Қазіргі құпиясөзіңізді жазыңыз + description_html: This will permanently, irreversibly remove content from your account аnd deactivate it. Your username will remain reserved to prevent future impersonations. + proceed: Аккаунт өшіру + success_msg: Аккаунтыңыз сәтті өшірілді + warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely sharеd is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. + warning_title: Бөлінген мазмұнның қол жетімділігі + directories: + directory: Профильдер каталогы + enabled: Каталогтағы тізімге ендіңіз. + enabled_but_waiting: Каталогта көрінгіңіз келетінін түсінеміз, бірақ ол үшін кем дегенде (%{min_followers}) оқырманыңыз болуы қажет. + explanation: Қолданушыларды қызығушылықтарына қарай реттеу + explore_mastodon: "%{title} шарлау" + how_to_enable: Сіз қазіргі уақытта каталогқа қосылмағансыз. Төменде қосылуға болады. Арнайы био мәтініндегі хэштегтерді қолданыңыз! + people: + one: "%{count} адам" + other: "%{count} адам" + errors: + '403': Бұны көру үшін сізде рұқсат жоқ. + '404': Сіз іздеген бет бұл жерде емес екен. + '410': Сіз іздеген бет қазір жоқ екен. + '422': + content: Қауіпсіздік растауы қате. кукилерді блоктағансыз ба? + title: Қауіпсіздік растауы жасалмады + '429': Қысқартылған + '500': + content: Кешірерсіз, бірақ қазір бір қате пайда болып тұр. + title: Бұл бет дұрыс емес екен + noscript_html: Mastodon веб қосымшасын қолдану үшін, JavaScript қосыңыз. Болмай жатса, мына қосымшаларды қосып көріңіз, Mastodon қолдану үшін. + exports: + archive_takeout: + date: Уақыты + download: Мұрағатыңызды түсіріп алыңыз + hint_html: Өзіңіздің жазба және медиаларыңыздың мұрағатын сақтап алуыңызға болады. Экспортталатын деректер ActivityPub форматында болады, сәйкес бағдарламамлармен ашуға болады. Әр 7 күн сайын сұратуыңызға болады. + in_progress: Мұрағатыңызды жинақтау... + request: Мұрағат сұрату + size: Өлшемі + blocks: Бұғатталғансыз + csv: СSV + domain_blocks: Домен блоктары + follows: Оқитындарыңыз + lists: Тізімдер + mutes: Үнсіздер + storage: Медиа жинақ + featured_tags: + add_new: Жаңасын қосу + errors: + limit: Хэштег лимитинен асып кеттіңіз + filters: + contexts: + home: Ішкі желі + notifications: Ескертпелер + public: Ашық желі + thread: Пікірталас + edit: + title: Фильтр өңдеу + errors: + invalid_context: Жоқ немесе жарамсыз контекст берілген + invalid_irreversible: Қайтарылмайтын сүзгі тек ішкі немесе ескертпелер контекстімен жұмыс істейді + index: + delete: Өшіру + title: Фильтрлер + new: + title: Жаңа фильтр қосу + followers: + domain: Домен + explanation_html: Егер сіз жазбаларыңыздың құпиялылығын қамтамасыз еткіңіз келсе, сізді кім іздейтінін білуіңіз керек. Сіздің жазбаларыңыз оқырмандарыңыз бар барлық серверлерге жеткізіледі . Оларды оқырмандарыңызға және админдерге немесе осы серверлердің бағдарламалық жасақтамасына жауапты қызметкерлерге сенбесеңіз, оқырмандарыңызды алып тастауыңызға болады. + followers_count: Оқырман саны + lock_link: Аккаунтыңызды құлыптау + purge: Оқырмандар тізімінен шығару + success: + one: Бір доменнен оқырмандарды бұғаттау барысында... + other: "%{count} доменнен оқырмандарды бұғаттау барысында..." + true_privacy_html: Ұмытпаңыз, нақты құпиялылықты шифрлаудан соң ғана қол жеткізуге болатындығын ескеріңіз.. + unlocked_warning_html: Кез келген адам жазбаларыңызды оқу үшін сізге жазыла алады. Жазылушыларды қарап, қабылдамау үшін %{lock_link}. + unlocked_warning_title: Аккаунтыңыз қазір құлыпталды + footer: + developers: Жасаушылар + more: Тағы… + resources: Ресурстар + generic: + changes_saved_msg: Өзгерістер сәтті сақталды! + copy: Көшіру + save_changes: Өзгерістерді сақтау + validation_errors: + one: Бір нәрсе дұрыс емес! Төмендегі қатені қараңыз + other: Бір нәрсе дұрыс емес! Төмендегі %{count} қатені қараңыз + imports: + modes: + merge: Біріктіру + merge_long: Бар жазбаларды сақтаңыз және жаңаларын қосыңыз + overwrite: Үстіне жазу + overwrite_long: Ағымдағы жазбаларды жаңаларына ауыстырыңыз + preface: Басқа серверден экспортталған деректерді импорттауға болады, мысалы, сіз бақылайтын немесе блоктайтын адамдардың тізімін. + success: Деректеріңіз сәтті жүктелді және дер кезінде өңделеді + types: + blocking: Бұғат тізімі + domain_blocking: Домен бұғаттары тізімі + following: Жазылғандар тізімі + muting: Үнсіздер тізімі + upload: Жүктеу + in_memoriam_html: Естеліктерде. + invites: + delete: Ажырату + expired: Мерзімі өткен + expires_in: + '1800': 30 минут + '21600': 6 сағат + '3600': 1 сағат + '43200': 12 сағат + '604800': 1 апта + '86400': 1 күн + expires_in_prompt: Ешқашан + generate: Құру + invited_by: 'Сізді шақырған:' + max_uses: + one: 1 қолданыс + other: "%{count} қолданыс" + max_uses_prompt: Лимитсіз + prompt: Осы серверге кіру рұқсатын беру үшін сілтемелерді жасаңыз және бөлісіңіз + table: + expires_at: Аяқталу мерзімі + uses: Қолданыс + title: Адам шақыру + lists: + errors: + limit: Сіз тізімдердің максимум мөлшеріне жеттіңіз + media_attachments: + validations: + images_and_video: Жазбаға видео қоса алмайсыз, тек сурет қосуға болады + too_many: 4 файлдан артық қосылмайды + migrations: + acct: жаңа аккаунт үшін username@domain + currently_redirecting: 'Профиліңіз көшіріледі:' + proceed: Сақтау + updated_msg: Аккаунт көшіруіңіз сәтті аяқталды! + moderation: + title: Модерация + notification_mailer: + digest: + action: Барлық ескертпелер + body: Міне, соңғы кірген уақыттан кейін келген хаттардың қысқаша мазмұны %{since} + mention: "%{name} сізді атап өтіпті:" + new_followers_summary: + one: Сондай-ақ, сіз бір жаңа оқырман таптыңыз! Алақай! + other: Сондай-ақ, сіз %{count} жаңа оқырман таптыңыз! Керемет! + subject: + one: "Соңғы кіруіңізден кейін 1 ескертпе келіпті \U0001F418" + other: "Соңғы кіруіңізден кейін %{count} ескертпе келіпті \U0001F418" + title: Сіз жоқ кезде... + favourite: + body: 'Жазбаңызды ұнатып, таңдаулыға қосты %{name}:' + subject: "%{name} жазбаңызды таңдаулыға қосты" + title: Жаңа таңдаулы + follow: + body: "%{name} сізге жазылды!" + subject: "%{name} сізге жазылды" + title: Жаңа оқырман + follow_request: + action: Жазылуға сұранымдарды реттеу + body: "%{name} сізге жазылғысы келеді" + subject: 'Жазылғысы келеді: %{name}' + title: Жазылуға сұраным + mention: + action: Жауап + body: 'Сізді атап өтіпті %{name} мында:' + subject: Сізді %{name} атап өтіпті + title: Жаңа аталым + reblog: + body: 'Жазбаңызды бөліскен %{name}:' + subject: "%{name} жазбаңызды бөлісті" + title: Жаңа бөлісім + number: + human: + decimal_units: + format: "%n%u" + units: + billion: В + million: М + quadrillion: Q + thousand: К + trillion: Т + pagination: + newer: Ешқашан + next: Келесі + older: Ерте + prev: Алдыңғы + truncate: "…" + preferences: + languages: Тілдер + other: Басқа + publishing: Жариялау + web: Веб + remote_follow: + acct: Өзіңіздің username@domain теріңіз + missing_resource: Аккаунтыңызға байланған URL табылмады + no_account_html: Әлі тіркелмегенсіз бе? Мына жерден тіркеліп алыңыз + proceed: Жазылу + prompt: 'Жазылғыңыз келеді:' + reason_html: "Неліктен бұл қадам қажет? %{instance} тіркелгіңіз келген сервер болмауы мүмкін, сондықтан сізді алдымен ішкі серверіңізге қайта бағыттау қажет." + remote_interaction: + favourite: + proceed: Таңдаулыға қосу + prompt: 'Мына жазбаны таңдаулыға қосасыз:' + reblog: + proceed: Жазба бөлісу + prompt: 'Сіз мына жазбаны бөлісесіз:' + reply: + proceed: Жауап жазу + prompt: 'Сіз мына жазбаға жауап жазасыз:' + remote_unfollow: + error: Қате + title: Тақырыбы + unfollowed: Жазылудан бас тартылды + scheduled_statuses: + over_daily_limit: Сіз бір күндік %{limit} жазба лимитін тауыстыңыз + over_total_limit: Сіз %{limit} жазба лимитін тауыстыңыз + too_soon: Жоспарланған күн болашақта болуы керек + sessions: + activity: Соңғы әрекеттер + browser: Браузер + browsers: + alipay: Аlipay + blackberry: Blаckberry + chrome: Chrоme + edge: Microsоft Edge + electron: Electrоn + firefox: Firеfox + generic: Белгісіз браузер + ie: Internet Explоrer + micro_messenger: MicroMеssenger + nokia: Nokia S40 Ovi Brоwser + opera: Opеra + otter: Ottеr + phantom_js: PhаntomJS + qq: QQ Brоwser + safari: Safаri + uc_browser: UCBrоwser + weibo: Weibо + current_session: Қазіргі сессия + description: "%{browser} - %{platform}" + explanation: Сіздің аккаунтыңызбен кірілген браузерлер тізімі. + ip: ІР + platforms: + adobe_air: Adobе Air + android: Andrоid + blackberry: Blackbеrry + chrome_os: ChromеOS + firefox_os: Firefоx OS + ios: iОS + linux: Lіnux + mac: Mаc + other: белгісіз платформа + windows: Windоws + windows_mobile: Windows Mоbile + windows_phone: Windоws Phone + revoke: Шығып кету + revoke_success: Сессиялар сәтті жабылды + title: Сессиялар + settings: + authorized_apps: Authorizеd apps + back: Желіге оралу + delete: Аккаунт өшіру + development: Жасаушы топ + edit_profile: Профиль өңдеу + export: Экспорт уақыты + featured_tags: Таңдаулы хэштегтер + followers: Авторизацияланған оқырмандар + import: Импорт + migrate: Аккаунт көшіру + notifications: Ескертпелер + preferences: Таңдаулар + settings: Баптаулар + two_factor_authentication: Екі-факторлы авторизация + your_apps: Қосымшалар + statuses: + attached: + description: 'Жүктелді: %{attached}' + image: + one: "%{count} сурет" + other: "%{count} сурет" + video: + one: "%{count} видео" + other: "%{count} видео" + boosted_from_html: Бөлісілді %{acct_link} + content_warning: 'Контент ескертуі: %{warning}' + disallowed_hashtags: + one: 'рұқсат етілмеген хэштег: %{tags}' + other: 'рұқсат етілмеген хэштегтер: %{tags}' + language_detection: Тілді өздігінен таңда + open_in_web: Вебте ашу + over_character_limit: "%{max} максимум таңбадан асып кетті" + pin_errors: + limit: Жабыстырылатын жазба саны максимумынан асты + ownership: Біреудің жазбасы жабыстырылмайды + private: Жабық жазба жабыстырылмайды + reblog: Бөлісілген жазба жабыстырылмайды + show_more: Тағы әкел + sign_in_to_participate: Сұхбатқа қатысу үшін кіріңіз + title: '%{name}: "%{quote}"' + visibilities: + private: Тек оқырмандарға + private_long: Тек оқырмандарға ғана көрінеді + public: Ашық + public_long: Бәрі көре алады + unlisted: Тізімге енбеген + unlisted_long: Бәрі көре алады, бірақ ашық тізімдерге ене алмайды + stream_entries: + pinned: Жабыстырылған жазба + reblogged: бөлісті + sensitive_content: Нәзік мазмұн + terms: + body_html: | +

    Құпиялылық шарттары

    +

    What information do we collect?

    + +
      +
    • Basic account information: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly.
    • +
    • Posts, following and other public information: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public.
    • +
    • Direct and followers-only posts: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. Please keep in mind that the operators of the server and any receiving server may view such messages, and that recipients may screenshot, copy or otherwise re-share them. Do not share any dangerous information over Mastodon.
    • +
    • IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.
    • +
    + +
    + +

    What do we use your information for?

    + +

    Any of the information we collect from you may be used in the following ways:

    + +
      +
    • To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.
    • +
    • To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.
    • +
    • The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.
    • +
    + +
    + +

    How do we protect your information?

    + +

    We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.

    + +
    + +

    What is our data retention policy?

    + +

    We will make a good faith effort to:

    + +
      +
    • Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.
    • +
    • Retain the IP addresses associated with registered users no more than 12 months.
    • +
    + +

    You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.

    + +

    You may irreversibly delete your account at any time.

    + +
    + +

    Do we use cookies?

    + +

    Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.

    + +

    We use cookies to understand and save your preferences for future visits.

    + +
    + +

    Do we disclose any information to outside parties?

    + +

    We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.

    + +

    Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.

    + +

    When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.

    + +
    + +

    Site usage by children

    + +

    If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.

    + +

    If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.

    + +

    Law requirements can be different if this server is in another jurisdiction.

    + +
    + +

    Changes to our Privacy Policy

    + +

    If we decide to change our privacy policy, we will post those changes on this page.

    + +

    This document is CC-BY-SA. It was last updated March 7, 2018.

    + +

    Originally adapted from the Discourse privacy policy.

    + title: "%{instance} Қызмет көрсету шарттары және Құпиялылық саясаты" + themes: + contrast: Mastodon (Жоғары контраст) + default: Mastodon (Қою) + mastodon-light: Mastodon (Ашық) + time: + formats: + default: "%b %d, %Y, %H:%M" + month: "%b %Y" + two_factor_authentication: + code_hint: Растау үшін түпнұсқалықты растау бағдарламасы арқылы жасалған кодты енгізіңіз + description_html: "екі факторлы түпнұсқалықты растауды қоссаңыз, кіру үшін сізге телефонға кіруіңізді талап етеді, сізге арнайы токен беріледі." + disable: Ажырату + enable: Қосу + enabled: Екі-факторлы авторизация қосылған + enabled_success: Екі-факторлы авторизация сәтті қосылды + generate_recovery_codes: Қалпына келтіру кодтарын жасаңыз + instructions_html: "Мына QR кодты Google Authenticator арқылы скандаңыз немесе ұқсас TOTP бағдарламалары арқылы. Одан кейін желіге кіру үшін токендер берілетін болады." + lost_recovery_codes: Қалпына келтіру кодтары телефонды жоғалтсаңыз, тіркелгіңізге қайта кіруге мүмкіндік береді. Қалпына келтіру кодтарын жоғалтсаңыз, оларды осында қалпына келтіре аласыз. Ескі қалпына келтіру кодтары жарамсыз болады. + manual_instructions: 'Егер сіз QR-кодты сканерлей алмасаңыз және оны қолмен енгізуіңіз қажет болса, мұнда қарапайым нұсқаулық:' + recovery_codes: Қалпына келтіру кодтарын резервтік көшіру + recovery_codes_regenerated: Қалпына келтіру кодтары қалпына келтірілді + recovery_instructions_html: Егер сіз телефонға кіруді жоғалтсаңыз, тіркелгіңізге кіру үшін төмендегі қалпына келтіру кодтарының бірін пайдалануға болады. Қалпына келтіру кодтарын қауіпсіз ұстаңыз . Мысалы, оларды басып шығарып, оларды басқа маңызды құжаттармен сақтауға болады. + setup: Орнату + wrong_code: Енгізілген код жарамсыз! Сервер уақыты мен құрылғының уақыты дұрыс па? + user_mailer: + backup_ready: + explanation: Сіз Mastodon аккаунтыңыздың толық мұрағатын сұрадыңыз. Қазір жүктеуге дайын! + subject: Мұрағатыңыз түсіріп алуға дайын + title: Мұрағатты алу + warning: + explanation: + disable: Аккаунтыңыз қатып қалса, сіздің деректеріңіз өзгеріссіз қалады, бірақ ол құлыптан босатылғанша ешқандай әрекетті орындай алмайсыз. + silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follоw you. + suspend: Сіздің аккаунтыңыз уақытша тоқтатылды және сіздің барлық файлдарыңыз бен жүктеп салынған медиа файлдарыңыз осы серверлерден және оқырманы болған серверлерден қайтарылмайды. + review_server_policies: Сервер саясатын қарап шығыңыз + subject: + disable: Аккаунтыңыз %{acct} уақытша тоқтатылды + none: "%{acct} ескертуі" + silence: "%{acct} аккаунтыңыз шектеулі" + suspend: "%{acct} аккаунт тоқтатылды" + title: + disable: Аккаунт қатырылды + none: Ескерту + silence: Аккаунт шектеулі + suspend: Аккаунт тоқтатылды + welcome: + edit_profile_action: Профиль өңдеу + edit_profile_step: Профиліңізге аватар, мұқаба сурет жүктей аласыз, аты-жөніңізді көрсете аласыз. Оқырмандарыңызға сізбен танысуға рұқсат бермес бұрын аккаунтыңызды уақытша құлыптап қоюға болады. + explanation: Мына кеңестерді шолып өтіңіз + final_action: Жазба жазу + final_step: 'Жазуды бастаңыз! Тіпті оқырмандарыңыз болмаса да, сіздің жалпы жазбаларыңызды басқа адамдар көре алады, мысалы, жергілікті желіде және хэштегтерде. Жазбаларыңызға # протоколды хэштег қоссаңыз болады.' + full_handle: Желі тұтқасы + full_handle_hint: This is what you would tell your friends so they can message or follow you frоm another server. + review_preferences_action: Таңдауларды өзгерту + review_preferences_step: Қандай хат-хабарларын алуды қалайтыныңызды немесе сіздің хабарламаларыңыздың қандай құпиялылық деңгейін алғыңыз келетінін анықтаңыз. Сондай-ақ, сіз GIF автоматты түрде ойнату мүмкіндігін қосуды таңдай аласыз. + subject: Mastodon Желісіне қош келдіңіз + tip_federated_timeline: Жаһандық желі - Mastodon желісінің негізгі құндылығы. + tip_following: Сіз бірден желі админіне жазылған болып саналасыз. Басқа адамдарға жазылу үшін жергілікті және жаһандық желіні шолып шығыңыз. + tip_local_timeline: Жерігілкті желіде маңайыздағы адамдардың белсенділігін көре аласыз %{instance}. Олар - негізгі көршілеріңіз! + tip_mobile_webapp: Мобиль браузеріңіз Mastodon желісін бастапқы бетке қосуды ұсынса, қабылдаңыз. Ескертпелер де шығатын болады. Арнайы қосымша сияқты бұл! + tips: Кеңестер + title: Ортаға қош келдің, %{name}! + users: + follow_limit_reached: Сіз %{limit} лимитінен көп адамға жазыла алмайсыз + invalid_email: Бұл e-mail адрес қате + invalid_otp_token: Қате екі-факторлы код + otp_lost_help_html: Егер кіру жолдарын жоғалтып алсаңыз, сізге %{email} арқылы жіберіледі + seamless_external_login: Сыртқы сервис арқылы кіріпсіз, сондықтан құпиясөз және электрондық пошта параметрлері қол жетімді емес. + signed_in_as: 'Былай кірдіңіз:' + verification: + explanation_html: 'Өзіңіздің профиль метадеректеріңіздегі сілтемелердің иесі ретінде өзіңізді тексере аласыз. Ол үшін байланыстырылған веб-сайтта Mastodon профиліне сілтеме болуы керек. Сілтемеде rel = «me» атрибуты болуы керек. Сілтеме мәтінінің мазмұны маңызды емес. Міне мысал:' + verification: Растау diff --git a/config/locales/ko.yml b/config/locales/ko.yml index ecee8374c..6fa18b12a 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -304,6 +304,7 @@ ko: back_to_account: 계정으로 돌아가기 title: "%{acct}의 팔로워" instances: + by_domain: 도메인 delivery_available: 전송 가능 known_accounts: one: 알려진 계정 %{count}개 @@ -735,6 +736,16 @@ ko: older: 오래된 툿 prev: 이전 truncate: "…" + polls: + errors: + already_voted: 이미 투표에 참여하셨습니다 + duplicate_options: 중복된 항목이 있습니다 + duration_too_long: 너무 먼 미래입니다 + duration_too_short: 너무 가깝습니다 + expired: 투표가 이미 끝났습니다 + over_character_limit: 각각 %{MAX} 글자를 넘을 수 없습니다 + too_few_options: 한가지 이상의 항목을 포함해야 합니다 + too_many_options: 항목은 %{MAX}개를 넘을 수 없습니다 preferences: languages: 언어 other: 기타 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index ad10c7067..fa3469b11 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -309,6 +309,7 @@ lt: back_to_account: Atgal Į Paskyrą title: "%{acct} Sekėjai" instances: + by_domain: Domenas delivery_available: Pristatymas galimas known_accounts: few: "%{count} žinomos paskyros" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 2ba99463b..70094f764 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -302,6 +302,7 @@ nl: back_to_account: Terug naar account title: Volgers van %{acct} instances: + by_domain: Domein delivery_available: Bezorging is mogelijk known_accounts: one: "%{count} bekend account" diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 7bedded41..fc6194527 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -302,6 +302,7 @@ oc: back_to_account: Tornar al compte title: Seguidors de %{acct} instances: + by_domain: Domeni delivery_available: Liurason disponibla known_accounts: one: "%{count} compte conegut" @@ -644,6 +645,10 @@ oc: lists: Listas mutes: Personas rescondudas storage: Mèdias gardats + featured_tags: + add_new: Ajustar una etiqueta nòva + errors: + limit: Avètz ja utilizat lo maximum d’etiquetas filters: contexts: home: Flux d’acuèlh @@ -684,10 +689,16 @@ oc: one: I a quicòm que truca ! Mercés de corregir l’error çai-jos other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos imports: + modes: + merge: Fondre + merge_long: Gardar los enregistraments existents e ajustar los nòus + overwrite: Remplaçar + overwrite_long: Remplaçar los enregistraments actuals pels nòus preface: Podètz importar qualques donadas coma lo monde que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància. success: Vòstras donadas son ben estadas mandadas e seràn tractadas tre que possible types: blocking: Lista de blocatge + domain_blocking: Lista dels domenis blocats following: Lista de monde que seguètz muting: Lista de monde que volètz pas legir upload: Importar @@ -713,7 +724,7 @@ oc: table: expires_at: Expirats uses: Usatges - title: Convidar de mond + title: Convidar de monde lists: errors: limit: Avètz atengut lo maximum de listas @@ -856,9 +867,10 @@ oc: delete: Supression de compte development: Desvolopament edit_profile: Modificar lo perfil - export: Export donadas + export: Exportar de donadas + featured_tags: Etiquetas en avant followers: Seguidors autorizats - import: Importar + import: Importar de donadas migrate: Migracion de compte notifications: Notificacions preferences: Preferéncias diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml index 14fcfdbd9..f0d121135 100644 --- a/config/locales/simple_form.ar.yml +++ b/config/locales/simple_form.ar.yml @@ -109,7 +109,7 @@ ar: username_or_email: إسم المستخدم أو كلمة السر whole_word: الكلمة كاملة featured_tag: - name: وسم + name: الوسم interactions: must_be_follower: حظر الإخطارات القادمة من حسابات لا تتبعك must_be_following: حظر الإخطارات القادمة من الحسابات التي لا تتابعها diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 2107530b5..c67a9bd2c 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -25,14 +25,14 @@ cs: locked: Vyžaduje, abyste ručně schvaloval/a sledující password: Použijte alespoň 8 znaků phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu - scopes: Které API bude aplikace povolena používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat po jednom. + scopes: Která API bude aplikaci povoleno používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě. setting_aggregate_reblogs: Nezobrazovat nové boosty pro tooty, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty) setting_default_language: Jazyk vašich tootů může být detekován automaticky, není to však vždy přesné setting_display_media_default: Skrývat média označená jako citlivá setting_display_media_hide_all: Vždy skrývat všechna média setting_display_media_show_all: Vždy zobrazovat média označená jako citlivá setting_hide_network: Koho sledujete a kdo sleduje vás nebude zobrazeno na vašem profilu - setting_noindex: Ovlivňuje váš veřejný profil a stránky příspěvků + setting_noindex: Ovlivňuje váš veřejný profil a stránky tootů setting_show_application: Aplikace, kterou používáte psaní tootů, bude zobrazena v detailním zobrazení vašich tootů setting_theme: Ovlivňuje jak Mastodon vypadá, jste-li přihlášen na libovolném zařízení. username: Vaše uživatelské jméno bude na %{domain} unikátní @@ -120,11 +120,11 @@ cs: must_be_following_dm: Blokovat přímé zprávy od lidí, které nesledujete notification_emails: digest: Posílat e-maily s přehledem - favourite: Posílat e-maily, když si někdo oblíbí váš příspěvek + favourite: Posílat e-maily, když si někdo oblíbí váš toot follow: Posílat e-maily, když vás někdo začne sledovat follow_request: Posílat e-maily, když vás někdo požádá o sledování mention: Posílat e-maily, když vás někdo zmíní - reblog: Posílat e-maily, když někdo boostne váš příspěvek + reblog: Posílat e-maily, když někdo boostne váš toot report: Posílat e-maily, je-li odesláno nové nahlášení 'no': Ne required: diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index 6cffcc0d1..f3592d584 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -6,6 +6,9 @@ eo: text: Vi povas uzi skribmanierojn de mesaĝoj, kiel URL-ojn, kradvortojn kaj menciojn admin_account_action: send_email_notification: La uzanto ricevos klarigon pri tio, kio okazis al ties konto + text_html: Malnepra. Vi povas uzi skribmanierojn de mesaĝoj. Vi povas aldoni avertajn antaŭagordojn por ŝpari tempon + type_html: Elektu kion fari kun %{acct} + warning_preset_id: Malnepra. Vi povas ankoraŭ aldoni propran tekston al la fino de la antaŭagordo defaults: autofollow: Homoj, kiuj registriĝos per la invito aŭtomate sekvos vin avatar: Formato PNG, GIF aŭ JPG. Ĝis %{size}. Estos malgrandigita al %{dimensions}px @@ -21,17 +24,22 @@ eo: locked: Vi devos aprobi ĉiun peton de sekvado mane password: Uzu almenaŭ 8 signojn phrase: Estos provita senzorge pri la uskleco de teksto aŭ averto pri enhavo de mesaĝo + scopes: Kiujn API-ojn la aplikaĵo permesiĝos atingi. Se vi elektas supran amplekson, vi ne bezonas elekti la individuajn. + setting_aggregate_reblogs: Ne montri novajn diskonigojn de mesaĝoj laste diskonigitaj (nur efikas al novaj diskonigoj) setting_default_language: La lingvo de viaj mesaĝoj povas esti aŭtomate detektitaj, sed tio ne ĉiam ĝustas setting_display_media_default: Kaŝi aŭdovidaĵojn markitajn kiel tiklaj setting_display_media_hide_all: Ĉiam kaŝi ĉiujn aŭdovidaĵojn setting_display_media_show_all: Ĉiam montri aŭdovidaĵojn markitajn kiel tiklaj setting_hide_network: Tiuj, kiujn vi sekvas, kaj tiuj, kiuj sekvas vin ne estos videblaj en via profilo setting_noindex: Influas vian publikan profilon kaj mesaĝajn paĝojn + setting_show_application: La aplikaĵo, kiun vi uzas por afiŝi, estos montrita en la detala vido de viaj mesaĝoj setting_theme: Influas kiel Mastodon aspektas post ensaluto de ajna aparato. username: Via uzantnomo estos unika ĉe %{domain} whole_word: Kiam la vorto aŭ frazo estas nur litera aŭ cifera, ĝi estos uzata nur se ĝi kongruas kun la tuta vorto + featured_tag: + name: 'Vi povus uzi iun el la jenaj:' imports: - data: CSV-dosiero el alia nodo de Mastodon + data: CSV-dosiero el alia Mastodon-servilo sessions: otp: 'Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el viaj realiraj kodoj:' user: @@ -41,6 +49,18 @@ eo: fields: name: Etikedo value: Enhavo + account_warning_preset: + text: Antaŭagordita teksto + admin_account_action: + send_email_notification: Atentigi la uzanton retpoŝte + text: Propra averto + type: Ago + types: + disable: Malebligi + none: Fari nenion + silence: Silentigi + suspend: Haltigi kaj nemalfereble forigi kontajn datumojn + warning_preset_id: Uzi antaŭagorditan averton defaults: autofollow: Inviti al sekvi vian konton avatar: Profilbildo @@ -66,6 +86,7 @@ eo: otp_attempt: Kodo de dufaktora aŭtentigo password: Pasvorto phrase: Vorto aŭ frazo + setting_aggregate_reblogs: Grupigi diskonigojn en tempolinioj setting_auto_play_gif: Aŭtomate ekigi GIF-ojn setting_boost_modal: Montri fenestron por konfirmi antaŭ ol diskonigi setting_default_language: Publikada lingvo @@ -80,6 +101,7 @@ eo: setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn setting_noindex: Ellistiĝi de retserĉila indeksado setting_reduce_motion: Malrapidigi animaciojn + setting_show_application: Publikigi la aplikaĵon uzatan por sendi mesaĝojn setting_system_font_ui: Uzi la dekomencan tiparon de la sistemo setting_theme: Reteja etoso setting_unfollow_modal: Montri fenestron por konfirmi antaŭ ol ĉesi sekvi iun @@ -88,6 +110,8 @@ eo: username: Uzantnomo username_or_email: Uzantnomo aŭ Retadreso whole_word: Tuta vorto + featured_tag: + name: Kradvorto interactions: must_be_follower: Bloki sciigojn de nesekvantoj must_be_following: Bloki sciigojn de homoj, kiujn vi ne sekvas diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index dd43898d2..9061844fe 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -33,11 +33,14 @@ it: setting_display_media_show_all: Nascondi sempre i media segnati come sensibili setting_hide_network: Chi segui e chi segue te non saranno mostrati sul tuo profilo setting_noindex: Ha effetto sul tuo profilo pubblico e sulle pagine degli status + setting_show_application: L'applicazione che usi per pubblicare i toot sarà mostrata nella vista di dettaglio dei tuoi toot setting_theme: Ha effetto sul modo in cui Mastodon verrà visualizzato quando sarai collegato da qualsiasi dispositivo. username: Il tuo nome utente sarà unico su %{domain} whole_word: Quando la parola chiave o la frase è solo alfanumerica, si applica solo se corrisponde alla parola intera + featured_tag: + name: 'Eccone alcuni che potresti usare:' imports: - data: File CSV esportato da un'altra istanza di Mastodon + data: File CSV esportato da un altro server Mastodon sessions: otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:' user: @@ -100,14 +103,17 @@ it: setting_hide_network: Nascondi la tua rete setting_noindex: Non farti indicizzare dai motori di ricerca setting_reduce_motion: Riduci movimento nelle animazioni + setting_show_application: Rendi pubblica l'applicazione usata per inviare i toot setting_system_font_ui: Usa il carattere predefinito del sistema setting_theme: Tema sito - setting_unfollow_modal: Mostra dialogo di conferma prima di smettere di seguire qualcuno + setting_unfollow_modal: Chiedi conferma prima di smettere di seguire qualcuno severity: Severità type: Tipo importazione username: Nome utente username_or_email: Nome utente o email whole_word: Parola intera + featured_tag: + name: Hashtag interactions: must_be_follower: Blocca notifiche da chi non ti segue must_be_following: Blocca notifiche dalle persone che non segui diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml index ac2845335..84633dde4 100644 --- a/config/locales/simple_form.oc.yml +++ b/config/locales/simple_form.oc.yml @@ -103,6 +103,7 @@ oc: setting_hide_network: Amagar vòstre malhum setting_noindex: Èsser pas indexat pels motors de recèrca setting_reduce_motion: Reduire la velocitat de las animacions + setting_show_application: Revelar lo nom de l’aplicacion utilizada per enviar de tuts setting_system_font_ui: Utilizar la polissa del sistèma setting_theme: Tèma del site setting_unfollow_modal: Mostrar una confirmacion abans de quitar de sègre qualqu’un @@ -111,6 +112,8 @@ oc: username: Nom d’utilizaire username_or_email: Nom d’utilizaire o corrièl whole_word: Mot complèt + featured_tag: + name: Etiqueta interactions: must_be_follower: Blocar las notificacions del mond que vos sègon pas must_be_following: Blocar las notificacions del mond que seguètz pas diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 504f909c2..17be44e67 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -26,7 +26,7 @@ sk: password: Zadaj aspoň osem znakov phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom. - setting_aggregate_reblogs: Neukazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných vyzdvihnutí) + setting_aggregate_reblogs: Nezobrazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných povýšení) setting_default_language: Jazyk tvojích príspevkov môže byť zistený automaticky, ale nieje to vždy presné setting_display_media_default: Skryť médiá označené ako citlivé setting_display_media_hide_all: Vždy ukryť všetky médiá @@ -106,7 +106,7 @@ sk: setting_show_application: Zverejni akú aplikáciu používaš na posielanie príspevkov setting_system_font_ui: Použi základné systémové písmo setting_theme: Vzhľad webu - setting_unfollow_modal: Zobrazuj potvrdzovacie okno pred skončením sledovania iného užívateľa + setting_unfollow_modal: Vyžaduj potvrdenie pred skončením sledovania iného užívateľa severity: Závažnosť type: Typ importu username: Prezývka @@ -115,9 +115,9 @@ sk: featured_tag: name: Haštag interactions: - must_be_follower: Blokovať oznámenia od nesledujúcich - must_be_following: Blokovať oznámenia od ľudí, ktorých nesleduješ - must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesleduješ + must_be_follower: Blokuj oboznámenia od užívateľov, ktorí ma nenásledujú + must_be_following: Blokuj oboznámenia od ľudí, ktorých nesledujem + must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesledujem notification_emails: digest: Posielaj súhrnné emaily favourite: Poslať email ak si niekto obľúbi tvoj príspevok diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index 8bc82c609..62d0b3769 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -2,15 +2,23 @@ sv: simple_form: hints: + account_warning_preset: + text: Du kan använda inläggssyntax som webbadresser, hashtaggar och omnämnanden + admin_account_action: + send_email_notification: Användaren kommer att få en förklaring av vad som hände med sitt konto + type_html: Välj vad du vill göra med %{acct} defaults: autofollow: Användarkonton som skapas genom din inbjudan kommer automatiskt följa dig - avatar: Högst %{size}. Kommer att skalas ner till %{dimensions}px + avatar: PNG, GIF eller JPG. Högst %{size}. Kommer att skalas ner till %{dimensions}px bot: Detta konto utför huvudsakligen automatiserade åtgärder och kanske inte övervakas digest: Skickas endast efter en lång period av inaktivitet och endast om du har fått några personliga meddelanden i din frånvaro + email: Ett konfirmationsmeddelande kommer att skickas till dig via epost fields: Du kan ha upp till 4 objekt visade som en tabell på din profil - header: NG, GIF eller JPG. Högst %{size}. Kommer nedskalas till %{dimensions}px - locale: Användargränssnittets språk, e-post och push aviseringar + header: PNG, GIF eller JPG. Högst %{size}. Kommer att skalas ner till %{dimensions}px + irreversible: Filtrerade inlägg kommer att försvinna oåterkalleligt, även om filter tas bort senare + locale: Användargränssnittets språk, e-post och push-aviseringar locked: Kräver att du manuellt godkänner följare + password: Använd minst 8 tecken setting_default_language: Språket av dina inlägg kan upptäckas automatiskt, men det är inte alltid rätt setting_hide_network: Vem du följer och vilka som följer dig kommer inte att visas på din profilsida setting_noindex: Påverkar din offentliga profil och statussidor diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 296589bec..c983e3ccf 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -14,7 +14,7 @@ sk: documentation: Dokumentácia extended_description_html: |

    Pravidlá

    -

    Žiadne zatiaľ nie sú

    +

    Žiadne zatiaľ uvedené nie sú

    features: humane_approach_body: Poučený z chýb iných sociálnych sietí, Mastodon sa snaží bojovať so zneužívaním siete voľbou etických návrhov. humane_approach_title: Ľudskejší prístup @@ -80,10 +80,10 @@ sk: account_moderation_notes: create: Zanechaj poznámku created_msg: Poznámka moderátora bola úspešne vytvorená! - delete: Zmazať - destroyed_msg: Poznámka moderátora bola úspešne zmazaná! + delete: Vymaž + destroyed_msg: Moderátorska poznámka bola úspešne zmazaná! accounts: - are_you_sure: Si si istý? + are_you_sure: Si si istý/á? avatar: Maskot by_domain: Doména change_email: @@ -93,13 +93,13 @@ sk: new_email: Nový email submit: Zmeň email title: Zmeň email pre %{username} - confirm: Potvrdiť + confirm: Potvrď confirmed: Potvrdený confirming: Potvrdzujúci deleted: Zmazané demote: Degradovať - disable: Zablokovať - disable_two_factor_authentication: Zakázať 2FA + disable: Zablokuj + disable_two_factor_authentication: Zakáž 2FA disabled: Blokovaný display_name: Zobraziť meno domain: Doména @@ -134,10 +134,10 @@ sk: moderation_notes: Moderátorské poznámky most_recent_activity: Posledná aktivita most_recent_ip: Posledná IP - no_limits_imposed: Niesú stanovené žiadné obmedzenia - not_subscribed: Nezaregistrované + no_limits_imposed: Nie sú stanovené žiadné obmedzenia + not_subscribed: Neodoberá outbox_url: URL poslaných - perform_full_suspension: Zablokovať + perform_full_suspension: Vylúč profile_url: URL profilu promote: Povýš protocol: Protokol @@ -147,12 +147,12 @@ sk: remove_avatar: Odstrániť avatár remove_header: Odstráň hlavičku resend_confirmation: - already_confirmed: Tento užívateľ už je potvrdený - send: Znovu odoslať potvrdzovací email + already_confirmed: Tento užívateľ je už potvrdený + send: Odošli potvrdzovací email znovu success: Potvrdzujúci email bol úspešne odoslaný! reset: Resetuj reset_password: Obnov heslo - resubscribe: Znovu odoberať + resubscribe: Znovu odoberaj role: Oprávnenia roles: admin: Administrátor @@ -165,15 +165,15 @@ sk: show: created_reports: Vytvorené hlásenia targeted_reports: Nahlásenia od ostatných - silence: Stíšiť - silenced: Utíšení + silence: Stíš + silenced: Utíšený/é statuses: Príspevky subscribe: Odoberať suspended: Zablokovaní title: Účty unconfirmed_email: Nepotvrdený email undo_silenced: Zrušiť stíšenie - undo_suspension: Zrušiť suspendáciu + undo_suspension: Zruš blokovanie unsubscribe: Prestaň odoberať username: Prezývka warn: Varovať @@ -214,7 +214,7 @@ sk: title: Kontrólny záznam custom_emojis: by_domain: Doména - copied_msg: Lokálna kópia emoji úspešne vytvorená + copied_msg: Miestna kópia emoji bola úspešne vytvorená copy: Kopíruj copy_failed_msg: Nebolo možné vytvoriť miestnu kópiu tohto emoji created_msg: Emoji úspešne vytvorené! @@ -228,10 +228,10 @@ sk: image_hint: PNG do 50KB listed: V zozname new: - title: Pridať nový vlastný emoji - overwrite: Prepísať + title: Pridaj nové, vlastné emoji + overwrite: Prepíš shortcode: Skratka - shortcode_hint: Aspoň 2 znaky, povolené sú alfanumerické alebo podčiarkovník + shortcode_hint: Aspoň 2 znaky, povolené sú alfanumerické, alebo podčiarkovník title: Vlastné emoji unlisted: Nie je na zozname update_failed_msg: Nebolo možné aktualizovať toto emoji @@ -265,16 +265,16 @@ sk: destroyed_msg: Blokovanie domény bolo zrušené domain: Doména new: - create: Blokovať doménu - hint: Blokovanie domény stále dovolí vytvárať nové účty v databáze, ale tieto budú automaticky moderované. + create: Vytvor blokovanie domény + hint: Blokovanie domény stále dovolí vytvárať nové účty v databázi, ale tieto budú spätne automaticky moderované. severity: desc_html: "Stíšenie urobí všetky príspevky daného účtu neviditeľné pre všetkých ktorí nenásledujú tento účet. Suspendácia zmaže všetky príspevky, médiá a profilové informácie. Použi Žiadne, ak chceš iba neprijímať súbory médií." noop: Nič - silence: Stíšiť - suspend: Vylúčiť + silence: Stíš + suspend: Vylúč title: Nové blokovanie domény - reject_media: Odmietať súbory s obrázkami alebo videami - reject_media_hint: Zmaže lokálne uložené súbory médií a odmietne ich sťahovanie v budúcnosti. Irelevantné pre suspendáciu + reject_media: Odmietaj súbory s obrázkami, alebo videami + reject_media_hint: Vymaže miestne uložené súbory médií a odmietne ich sťahovanie v budúcnosti. Nepodstatné pri vylúčení reject_reports: Zamietni hlásenia reject_reports_hint: Ignoruj všetky hlásenia prichádzajúce z tejto domény. Nevplýva na blokovania rejecting_media: odmietanie médiálnych súborov @@ -307,6 +307,7 @@ sk: back_to_account: Späť na účet title: Následovatielia užívateľa %{acct} instances: + by_domain: Doména delivery_available: Je v dosahu doručovania known_accounts: few: "%{count} známe účty" @@ -404,7 +405,7 @@ sk: desc_html: Náhľad odkazov z iných serverov, bude zobrazený aj vtedy, keď sú médiá označené ako senzitívne title: Ukazuj aj chúlostivé médiá v náhľadoch OpenGraph profile_directory: - desc_html: Povoliť užívateľom aby boli nájdení + desc_html: Povoľ užívateľom, aby mohli byť nájdení title: Zapni profilový katalóg registrations: closed_message: @@ -742,8 +743,18 @@ sk: newer: Novšie next: Ďalšie older: Staršie - prev: Predošlé + prev: Predchádzajúce truncate: "…" + polls: + errors: + already_voted: V tejto ankete si už hlasoval/a + duplicate_options: obsahuje opakujúce sa položky + duration_too_long: je príliš ďaleko do budúcnosti + duration_too_short: je príliš skoro + expired: Anketa už skončila + over_character_limit: každá nemôže byť dlhšia ako %{MAX} znakov + too_few_options: musí mať viac ako jednu položku + too_many_options: nemôže zahŕňať viac ako %{MAX} položiek preferences: languages: Jazyky other: Ostatné -- cgit From ff827c1f38e91f2ef0ac3ee1c92f0f8a1a3cbdfa Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 5 Mar 2019 22:51:23 +0100 Subject: [Glitch] Perform deep comparison for card data when receiving new props Prevents embedded players from resetting when interacting with the toot --- app/javascript/flavours/glitch/features/status/components/card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index e405a5ef0..f974a87a1 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -75,7 +75,7 @@ export default class Card extends React.PureComponent { }; componentWillReceiveProps (nextProps) { - if (this.props.card !== nextProps.card) { + if (!Immutable.is(this.props.card, nextProps.card)) { this.setState({ embedded: false }); } } -- cgit From efd0fb80880171b1f027b6d0dbd8ace999857062 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 5 Mar 2019 23:58:58 +0100 Subject: Fix newlines in OStatus and RSS serializations (#10183) --- app/lib/formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index b9845cb45..464e1ee7e 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -20,7 +20,7 @@ class Formatter raw_content = status.text if options[:inline_poll_options] && status.poll - raw_content = raw_content + '\n\n' + status.poll.options.map { |title| "[ ] #{title}" }.join('\n') + raw_content = raw_content + "\n\n" + status.poll.options.map { |title| "[ ] #{title}" }.join("\n") end return '' if raw_content.blank? -- cgit From e80fabfd84c1e2d157ca1cd536f68f3c75f011c1 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 5 Mar 2019 23:17:09 +0100 Subject: Fix toots opening in dynamic column when trying to interact with them This fixes inline preview cards and polls, preventing them from opening the toot in detailed view when clicking on an interactive element. --- app/javascript/flavours/glitch/components/status_content.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index c60d63f9a..98a34ebaf 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -100,8 +100,12 @@ export default class StatusContent extends React.PureComponent { const [ startX, startY ] = this.startXY; const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - if (e.target.localName === 'button' || e.target.localName == 'video' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { - return; + let element = e.target; + while (element) { + if (element.localName === 'button' || element.localName === 'video' || element.localName === 'a' || element.localName === 'label') { + return; + } + element = element.parentNode; } if (deltaX + deltaY < 5 && e.button === 0 && parseClick) { -- cgit From 4407f07014096bcbaf5a06015a5791984282846d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 03:57:46 +0100 Subject: Render unicode emoji in polls using emoji pack (#10185) --- app/javascript/mastodon/actions/importer/index.js | 4 ++-- app/javascript/mastodon/actions/importer/normalizer.js | 11 +++++++++++ app/javascript/mastodon/components/poll.js | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index abadee817..e990dc04c 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,4 +1,4 @@ -import { normalizeAccount, normalizeStatus } from './normalizer'; +import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; @@ -71,7 +71,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll && status.poll.id) { - pushUnique(polls, status.poll); + pushUnique(polls, normalizePoll(status.poll)); } } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 3085cd537..ea80c0efb 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -67,3 +67,14 @@ export function normalizeStatus(status, normalOldStatus) { return normalStatus; } + +export function normalizePoll(poll) { + const normalPoll = { ...poll }; + + normalPoll.options = poll.options.map(option => ({ + ...option, + title_emojified: emojify(escapeTextContentForBrowser(option.title)), + })); + + return normalPoll; +} diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 182491af8..c52445c86 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -120,7 +120,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - {option.get('title')} + ); -- cgit From d97cbb0da60f32c9e7e60445af329173b0df1aa7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 04:53:37 +0100 Subject: Add UI for creating polls (#10184) * Add actions and reducers for polls * Add poll button * Disable media upload if poll enabled * Add poll form * Make delete & redraft work with polls --- app/javascript/mastodon/actions/compose.js | 52 ++++++++- app/javascript/mastodon/actions/statuses.js | 6 +- .../features/compose/components/compose_form.js | 4 + .../features/compose/components/poll_button.js | 55 ++++++++++ .../features/compose/components/poll_form.js | 121 +++++++++++++++++++++ .../features/compose/components/upload_button.js | 6 +- .../compose/containers/poll_button_container.js | 24 ++++ .../compose/containers/poll_form_container.js | 29 +++++ .../compose/containers/upload_button_container.js | 1 + app/javascript/mastodon/reducers/compose.js | 35 ++++++ app/javascript/styles/mastodon/polls.scss | 88 +++++++++++++++ 11 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 app/javascript/mastodon/features/compose/components/poll_button.js create mode 100644 app/javascript/mastodon/features/compose/components/poll_form.js create mode 100644 app/javascript/mastodon/features/compose/containers/poll_button_container.js create mode 100644 app/javascript/mastodon/features/compose/containers/poll_form_container.js (limited to 'app') diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 0be2a5cd4..2d58a71fe 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -51,6 +51,13 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; +export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; +export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; +export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; +export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; +export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; + const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, }); @@ -131,6 +138,7 @@ export function submitCompose(routerHistory) { sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), visibility: getState().getIn(['compose', 'privacy']), + poll: getState().getIn(['compose', 'poll'], null), }, { headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), @@ -484,4 +492,46 @@ export function changeComposing(value) { type: COMPOSE_COMPOSING_CHANGE, value, }; -} +}; + +export function addPoll() { + return { + type: COMPOSE_POLL_ADD, + }; +}; + +export function removePoll() { + return { + type: COMPOSE_POLL_REMOVE, + }; +}; + +export function addPollOption(title) { + return { + type: COMPOSE_POLL_OPTION_ADD, + title, + }; +}; + +export function changePollOption(index, title) { + return { + type: COMPOSE_POLL_OPTION_CHANGE, + index, + title, + }; +}; + +export function removePollOption(index) { + return { + type: COMPOSE_POLL_OPTION_REMOVE, + index, + }; +}; + +export function changePollSettings(expiresIn, isMultiple) { + return { + type: COMPOSE_POLL_SETTINGS_CHANGE, + expiresIn, + isMultiple, + }; +}; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index e7c89b4ba..1794538e2 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -140,7 +140,11 @@ export function redraft(status) { export function deleteStatus(id, router, withRedraft = false) { return (dispatch, getState) => { - const status = getState().getIn(['statuses', id]); + let status = getState().getIn(['statuses', id]); + + if (status.get('poll')) { + status = status.set('poll', getState().getIn(['polls', status.get('poll')])); + } dispatch(deleteStatusRequest(id)); diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index a990e7498..4b0bde659 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -5,12 +5,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import PollButtonContainer from '../containers/poll_button_container'; import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; +import PollFormContainer from '../containers/poll_form_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; import { isMobile } from '../../../is_mobile'; @@ -205,11 +207,13 @@ class ComposeForm extends ImmutablePureComponent {
    +
    + diff --git a/app/javascript/mastodon/features/compose/components/poll_button.js b/app/javascript/mastodon/features/compose/components/poll_button.js new file mode 100644 index 000000000..76f96bfa4 --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/poll_button.js @@ -0,0 +1,55 @@ +import React from 'react'; +import IconButton from '../../../components/icon_button'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' }, + remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' }, +}); + +const iconStyle = { + height: null, + lineHeight: '27px', +}; + +export default +@injectIntl +class PollButton extends React.PureComponent { + + static propTypes = { + disabled: PropTypes.bool, + unavailable: PropTypes.bool, + active: PropTypes.bool, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleClick = () => { + this.props.onClick(); + } + + render () { + const { intl, active, unavailable, disabled } = this.props; + + if (unavailable) { + return null; + } + + return ( +
    + +
    + ); + } + +} diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js new file mode 100644 index 000000000..ff0062425 --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import IconButton from 'mastodon/components/icon_button'; +import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; + +const messages = defineMessages({ + option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, + add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, + remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, + poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, + minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, + hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, + days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, +}); + +@injectIntl +class Option extends React.PureComponent { + + static propTypes = { + title: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + isPollMultiple: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleOptionTitleChange = e => { + this.props.onChange(this.props.index, e.target.value); + }; + + handleOptionRemove = () => { + this.props.onRemove(this.props.index); + }; + + render () { + const { isPollMultiple, title, index, intl } = this.props; + + return ( +
  • + + +
    + +
    +
  • + ); + } + +} + +export default +@injectIntl +class PollForm extends ImmutablePureComponent { + + static propTypes = { + options: ImmutablePropTypes.list, + expiresIn: PropTypes.number, + isMultiple: PropTypes.bool, + onChangeOption: PropTypes.func.isRequired, + onAddOption: PropTypes.func.isRequired, + onRemoveOption: PropTypes.func.isRequired, + onChangeSettings: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleAddOption = () => { + this.props.onAddOption(''); + }; + + handleSelectDuration = e => { + this.props.onChangeSettings(e.target.value, this.props.isMultiple); + }; + + render () { + const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; + + if (!options) { + return null; + } + + return ( +
    +
      + {options.map((title, i) =>
    + +
    + {options.size < 4 && ( + + )} + + +
    +
    + ); + } + +} diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index db55ad70b..90e2769f3 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -29,6 +29,7 @@ class UploadButton extends ImmutablePureComponent { static propTypes = { disabled: PropTypes.bool, + unavailable: PropTypes.bool, onSelectFile: PropTypes.func.isRequired, style: PropTypes.object, resetFileKey: PropTypes.number, @@ -51,8 +52,11 @@ class UploadButton extends ImmutablePureComponent { } render () { + const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props; - const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; + if (unavailable) { + return null; + } return (
    diff --git a/app/javascript/mastodon/features/compose/containers/poll_button_container.js b/app/javascript/mastodon/features/compose/containers/poll_button_container.js new file mode 100644 index 000000000..8f1cb7c10 --- /dev/null +++ b/app/javascript/mastodon/features/compose/containers/poll_button_container.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import PollButton from '../components/poll_button'; +import { addPoll, removePoll } from '../../../actions/compose'; + +const mapStateToProps = state => ({ + unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0), + active: state.getIn(['compose', 'poll']) !== null, +}); + +const mapDispatchToProps = dispatch => ({ + + onClick () { + dispatch((_, getState) => { + if (getState().getIn(['compose', 'poll'])) { + dispatch(removePoll()); + } else { + dispatch(addPoll()); + } + }); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(PollButton); diff --git a/app/javascript/mastodon/features/compose/containers/poll_form_container.js b/app/javascript/mastodon/features/compose/containers/poll_form_container.js new file mode 100644 index 000000000..da795a291 --- /dev/null +++ b/app/javascript/mastodon/features/compose/containers/poll_form_container.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; +import PollForm from '../components/poll_form'; +import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose'; + +const mapStateToProps = state => ({ + options: state.getIn(['compose', 'poll', 'options']), + expiresIn: state.getIn(['compose', 'poll', 'expires_in']), + isMultiple: state.getIn(['compose', 'poll', 'multiple']), +}); + +const mapDispatchToProps = dispatch => ({ + onAddOption(title) { + dispatch(addPollOption(title)); + }, + + onRemoveOption(index) { + dispatch(removePollOption(index)); + }, + + onChangeOption(index, title) { + dispatch(changePollOption(index, title)); + }, + + onChangeSettings(expiresIn, isMultiple) { + dispatch(changePollSettings(expiresIn, isMultiple)); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(PollForm); diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js index 1f1d915bc..d8b8c4b6e 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js @@ -4,6 +4,7 @@ import { uploadCompose } from '../../../actions/compose'; const mapStateToProps = state => ({ disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), + unavailable: state.getIn(['compose', 'poll']) !== null, resetFileKey: state.getIn(['compose', 'resetFileKey']), }); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 1622871b8..b45def281 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -29,6 +29,12 @@ import { COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_FAIL, COMPOSE_RESET, + COMPOSE_POLL_ADD, + COMPOSE_POLL_REMOVE, + COMPOSE_POLL_OPTION_ADD, + COMPOSE_POLL_OPTION_CHANGE, + COMPOSE_POLL_OPTION_REMOVE, + COMPOSE_POLL_SETTINGS_CHANGE, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; @@ -55,6 +61,7 @@ const initialState = ImmutableMap({ is_uploading: false, progress: 0, media_attachments: ImmutableList(), + poll: null, suggestion_token: null, suggestions: ImmutableList(), default_privacy: 'public', @@ -64,6 +71,12 @@ const initialState = ImmutableMap({ tagHistory: ImmutableList(), }); +const initialPoll = ImmutableMap({ + options: ImmutableList(['', '']), + expires_in: 24 * 3600, + multiple: false, +}); + function statusToTextMentions(state, status) { let set = ImmutableOrderedSet([]); @@ -85,6 +98,7 @@ function clearAll(state) { map.set('privacy', state.get('default_privacy')); map.set('sensitive', false); map.update('media_attachments', list => list.clear()); + map.set('poll', null); map.set('idempotencyKey', uuid()); }); }; @@ -247,6 +261,7 @@ export default function compose(state = initialState, action) { map.set('spoiler', false); map.set('spoiler_text', ''); map.set('privacy', state.get('default_privacy')); + map.set('poll', null); map.set('idempotencyKey', uuid()); }); case COMPOSE_SUBMIT_REQUEST: @@ -329,7 +344,27 @@ export default function compose(state = initialState, action) { map.set('spoiler', false); map.set('spoiler_text', ''); } + + if (action.status.get('poll')) { + map.set('poll', ImmutableMap({ + options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), + multiple: action.status.getIn(['poll', 'multiple']), + expires_in: 24 * 3600, + })); + } }); + case COMPOSE_POLL_ADD: + return state.set('poll', initialPoll); + case COMPOSE_POLL_REMOVE: + return state.set('poll', null); + case COMPOSE_POLL_OPTION_ADD: + return state.updateIn(['poll', 'options'], options => options.push(action.title)); + case COMPOSE_POLL_OPTION_CHANGE: + return state.setIn(['poll', 'options', action.index], action.title); + case COMPOSE_POLL_OPTION_REMOVE: + return state.updateIn(['poll', 'options'], options => options.delete(action.index)); + case COMPOSE_POLL_SETTINGS_CHANGE: + return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); default: return state; } diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index 7c6e61d63..d86cef44c 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -33,9 +33,34 @@ display: none; } + input[type=text] { + display: block; + box-sizing: border-box; + flex: 1 1 auto; + width: 20px; + font-size: 14px; + color: $inverted-text-color; + display: block; + outline: 0; + font-family: inherit; + background: $simple-background-color; + border: 1px solid darken($simple-background-color, 14%); + border-radius: 4px; + padding: 6px 10px; + + &:focus { + border-color: $highlight-text-color; + } + } + &.selectable { cursor: pointer; } + + &.editable { + display: flex; + align-items: center; + } } &__input { @@ -45,6 +70,7 @@ box-sizing: border-box; width: 18px; height: 18px; + flex: 0 0 auto; margin-right: 10px; top: -1px; border-radius: 50%; @@ -98,3 +124,65 @@ font-size: 14px; } } + +.compose-form__poll-wrapper { + border-top: 1px solid darken($simple-background-color, 8%); + + ul { + padding: 10px; + } + + .poll__footer { + border-top: 1px solid darken($simple-background-color, 8%); + padding: 10px; + display: flex; + align-items: center; + + button, + select { + flex: 1 1 50%; + } + } + + .button.button-secondary { + font-size: 14px; + font-weight: 400; + padding: 6px 10px; + height: auto; + line-height: inherit; + color: $action-button-color; + border-color: $action-button-color; + margin-right: 5px; + } + + li { + display: flex; + align-items: center; + + .poll__text { + flex: 0 0 auto; + width: calc(100% - (23px + 6px)); + margin-right: 6px; + } + } + + select { + appearance: none; + box-sizing: border-box; + font-size: 14px; + color: $inverted-text-color; + display: inline-block; + width: auto; + outline: 0; + font-family: inherit; + background: $simple-background-color url("data:image/svg+xml;utf8,") no-repeat right 8px center / auto 16px; + border: 1px solid darken($simple-background-color, 14%); + border-radius: 4px; + padding: 6px 10px; + padding-right: 30px; + } + + .icon-button.disabled { + color: darken($simple-background-color, 14%); + } +} -- cgit From 5996be994d7a27ffb5075d663acf02dddc32ea7e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 04:54:11 +0100 Subject: Fix poll validation issues (#10186) - Fix missing interpolation argument in PollValidator - Fix PollValidator rejecting exact allowed min/max durations --- app/validators/poll_validator.rb | 4 ++-- config/locales/co.yml | 4 ++-- config/locales/cs.yml | 4 ++-- config/locales/el.yml | 4 ++-- config/locales/en.yml | 4 ++-- config/locales/fa.yml | 4 ++-- config/locales/fr.yml | 4 ++-- config/locales/gl.yml | 4 ++-- config/locales/ko.yml | 4 ++-- config/locales/sk.yml | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) (limited to 'app') diff --git a/app/validators/poll_validator.rb b/app/validators/poll_validator.rb index d4ae4c16a..fd497c8d0 100644 --- a/app/validators/poll_validator.rb +++ b/app/validators/poll_validator.rb @@ -13,7 +13,7 @@ class PollValidator < ActiveModel::Validator poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS } poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size - poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time >= MAX_EXPIRATION - poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && poll.expires_at - current_time <= MIN_EXPIRATION + poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION + poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && poll.expires_at - current_time < MIN_EXPIRATION end end diff --git a/config/locales/co.yml b/config/locales/co.yml index d30fc9e96..8fcb27598 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -741,9 +741,9 @@ co: duration_too_long: hè troppu luntanu indè u futuru duration_too_short: hè troppu prossimu expired: U scandagliu hè digià finitu - over_character_limit: ùn ponu micca esse più longhi chè %{MAX} caratteri + over_character_limit: ùn ponu micca esse più longhi chè %{max} caratteri too_few_options: deve avè più d'un'uzzione - too_many_options: ùn pò micca avè più di %{MAX} uzzione + too_many_options: ùn pò micca avè più di %{max} uzzione preferences: languages: Lingue other: Altre diff --git a/config/locales/cs.yml b/config/locales/cs.yml index e751b5266..fe83bd57a 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -752,9 +752,9 @@ cs: duration_too_long: je příliš daleko v budoucnosti duration_too_short: je příliš brzy expired: Anketa již skončila - over_character_limit: nesmí být každá delší než %{MAX} znaků + over_character_limit: nesmí být každá delší než %{max} znaků too_few_options: musí mít více než jednu položku - too_many_options: nesmí obsahovat více než %{MAX} položky + too_many_options: nesmí obsahovat více než %{max} položky preferences: languages: Jazyky other: Ostatní diff --git a/config/locales/el.yml b/config/locales/el.yml index ca821c4fc..35da50af7 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -740,9 +740,9 @@ el: duration_too_long: είναι πολύ μακριά στο μέλλον duration_too_short: είναι πολύ σύντομα expired: Η ψηφοφορία έχει ήδη λήξει - over_character_limit: δε μπορεί να υπερβαίνει τους %{MAX} χαρακτήρες έκαστη + over_character_limit: δε μπορεί να υπερβαίνει τους %{max} χαρακτήρες έκαστη too_few_options: πρέπει να έχει περισσότερες από μια επιλογές - too_many_options: δεν μπορεί να έχει περισσότερες από %{MAX} επιλογές + too_many_options: δεν μπορεί να έχει περισσότερες από %{max} επιλογές preferences: languages: Γλώσσες other: Άλλο diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d23b1eb6..f714884f9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -741,9 +741,9 @@ en: duration_too_long: is too far into the future duration_too_short: is too soon expired: The poll has already ended - over_character_limit: cannot be longer than %{MAX} characters each + over_character_limit: cannot be longer than %{max} characters each too_few_options: must have more than one item - too_many_options: can't contain more than %{MAX} items + too_many_options: can't contain more than %{max} items preferences: languages: Languages other: Other diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 4cf8f415c..4214e793c 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -741,9 +741,9 @@ fa: duration_too_long: در آیندهٔ خیلی دور است duration_too_short: در آیندهٔ خیلی نزدیک است expired: این نظرسنجی به پایان رسیده است - over_character_limit: هر کدام نمی‌تواند از %{MAX} نویسه طولانی‌تر باشد + over_character_limit: هر کدام نمی‌تواند از %{max} نویسه طولانی‌تر باشد too_few_options: حتماً باید بیش از یک گزینه داشته باشد - too_many_options: نمی‌تواند بیشتر از %{MAX} گزینه داشته باشد + too_many_options: نمی‌تواند بیشتر از %{max} گزینه داشته باشد preferences: languages: تنظیمات زبان other: سایر تنظیمات diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c84dc8063..b9d813e9e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -741,9 +741,9 @@ fr: duration_too_long: est trop loin dans le futur duration_too_short: est trop tôt expired: Ce sondage est déjà terminé - over_character_limit: ne peuvent être plus long que %{MAX} caractères chacun + over_character_limit: ne peuvent être plus long que %{max} caractères chacun too_few_options: doit avoir plus qu'une proposition - too_many_options: ne peut contenir plus que %{MAX} propositions + too_many_options: ne peut contenir plus que %{max} propositions preferences: languages: Langues other: Autre diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 3c58e04f5..2435137f9 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -741,9 +741,9 @@ gl: duration_too_long: está moi lonxe no futuro duration_too_short: é demasiado cedo expired: A sondaxe rematou - over_character_limit: non poden ter máis de %{MAX} caracteres cada unha + over_character_limit: non poden ter máis de %{max} caracteres cada unha too_few_options: debe ter máis de unha opción - too_many_options: non pode haber máis de %{MAX} opcións + too_many_options: non pode haber máis de %{max} opcións preferences: languages: Idiomas other: Outro diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 6fa18b12a..fe347a703 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -743,9 +743,9 @@ ko: duration_too_long: 너무 먼 미래입니다 duration_too_short: 너무 가깝습니다 expired: 투표가 이미 끝났습니다 - over_character_limit: 각각 %{MAX} 글자를 넘을 수 없습니다 + over_character_limit: 각각 %{max} 글자를 넘을 수 없습니다 too_few_options: 한가지 이상의 항목을 포함해야 합니다 - too_many_options: 항목은 %{MAX}개를 넘을 수 없습니다 + too_many_options: 항목은 %{max}개를 넘을 수 없습니다 preferences: languages: 언어 other: 기타 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index b29a7a934..0800b8f8c 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -752,9 +752,9 @@ sk: duration_too_long: je príliš ďaleko do budúcnosti duration_too_short: je príliš skoro expired: Anketa už skončila - over_character_limit: každá nemôže byť dlhšia ako %{MAX} znakov + over_character_limit: každá nemôže byť dlhšia ako %{max} znakov too_few_options: musí mať viac ako jednu položku - too_many_options: nemôže zahŕňať viac ako %{MAX} položiek + too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: languages: Jazyky other: Ostatné -- cgit From 57643557b64bc1853c4aeb65fc652dac3467fa18 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 04:54:32 +0100 Subject: Avoid line breaks in poll options (#10188) --- app/javascript/styles/mastodon/polls.scss | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index d86cef44c..4f8c94d83 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -5,6 +5,7 @@ li { margin-bottom: 10px; position: relative; + height: 18px + 12px; } &__chart { @@ -27,6 +28,9 @@ padding: 6px 0; line-height: 18px; cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; input[type=radio], input[type=checkbox] { -- cgit From fd128b9c7aa5c71adbfc2e223212514c0baee675 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 05:35:52 +0100 Subject: Fix poll options not rendering text after vote/refresh (#10189) * Fix poll options not rendering text after vote/refresh * Fix poll options not showing up on public pages * Fix code style issue --- app/javascript/mastodon/actions/importer/index.js | 6 ++++++ app/javascript/mastodon/actions/polls.js | 11 +++++++++-- app/javascript/mastodon/components/poll.js | 4 +++- app/javascript/mastodon/reducers/polls.js | 4 ---- 4 files changed, 18 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index e990dc04c..f4372fb31 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -82,3 +82,9 @@ export function importFetchedStatuses(statuses) { dispatch(importStatuses(normalStatuses)); }; } + +export function importFetchedPoll(poll) { + return dispatch => { + dispatch(importPolls([normalizePoll(poll)])); + }; +} diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js index bee4c48a6..8e8b82df5 100644 --- a/app/javascript/mastodon/actions/polls.js +++ b/app/javascript/mastodon/actions/polls.js @@ -1,4 +1,5 @@ import api from '../api'; +import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; @@ -12,7 +13,10 @@ export const vote = (pollId, choices) => (dispatch, getState) => { dispatch(voteRequest()); api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) - .then(({ data }) => dispatch(voteSuccess(data))) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) .catch(err => dispatch(voteFail(err))); }; @@ -20,7 +24,10 @@ export const fetchPoll = pollId => (dispatch, getState) => { dispatch(fetchPollRequest()); api(getState).get(`/api/v1/polls/${pollId}`) - .then(({ data }) => dispatch(fetchPollSuccess(data))) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) .catch(err => dispatch(fetchPollFail(err))); }; diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index c52445c86..bfff7b601 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -7,6 +7,8 @@ import classNames from 'classnames'; import { vote, fetchPoll } from 'mastodon/actions/polls'; import Motion from 'mastodon/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; +import escapeTextContentForBrowser from 'escape-html'; +import emojify from 'mastodon/features/emoji/emoji'; const messages = defineMessages({ moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, @@ -120,7 +122,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - + ); diff --git a/app/javascript/mastodon/reducers/polls.js b/app/javascript/mastodon/reducers/polls.js index 53d9b1d8c..9956cf83f 100644 --- a/app/javascript/mastodon/reducers/polls.js +++ b/app/javascript/mastodon/reducers/polls.js @@ -1,4 +1,3 @@ -import { POLL_VOTE_SUCCESS, POLL_FETCH_SUCCESS } from 'mastodon/actions/polls'; import { POLLS_IMPORT } from 'mastodon/actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; @@ -10,9 +9,6 @@ export default function polls(state = initialState, action) { switch(action.type) { case POLLS_IMPORT: return importPolls(state, action.polls); - case POLL_VOTE_SUCCESS: - case POLL_FETCH_SUCCESS: - return importPolls(state, [action.poll]); default: return state; } -- cgit From 7f32d675b05b012a80c2b34c07d361ec8d596bf3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 03:57:46 +0100 Subject: Render unicode emoji in polls using emoji pack Port 4407f07014096bcbaf5a06015a5791984282846d to glitch-soc --- app/javascript/flavours/glitch/actions/importer/index.js | 4 ++-- app/javascript/flavours/glitch/actions/importer/normalizer.js | 11 +++++++++++ app/javascript/flavours/glitch/components/poll.js | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index abadee817..e990dc04c 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -1,4 +1,4 @@ -import { normalizeAccount, normalizeStatus } from './normalizer'; +import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; @@ -71,7 +71,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll && status.poll.id) { - pushUnique(polls, status.poll); + pushUnique(polls, normalizePoll(status.poll)); } } diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index f57fb70b4..ccd84364e 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -65,3 +65,14 @@ export function normalizeStatus(status, normalOldStatus) { return normalStatus; } + +export function normalizePoll(poll) { + const normalPoll = { ...poll }; + + normalPoll.options = poll.options.map(option => ({ + ...option, + title_emojified: emojify(escapeTextContentForBrowser(option.title)), + })); + + return normalPoll; +} diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 182491af8..c52445c86 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -120,7 +120,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - {option.get('title')} + ); -- cgit From 94f1a751bf0313249d891e760dc758e621f1b9ad Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 04:54:32 +0100 Subject: Avoid line breaks in poll options Port 57643557b64bc1853c4aeb65fc652dac3467fa18 to glitch-soc --- app/javascript/flavours/glitch/styles/polls.scss | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 7c6e61d63..ce324b36e 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -5,6 +5,7 @@ li { margin-bottom: 10px; position: relative; + height: 18px + 12px; } &__chart { @@ -27,6 +28,9 @@ padding: 6px 0; line-height: 18px; cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; input[type=radio], input[type=checkbox] { -- cgit From 94a0149ff3e455792050e790d397d1d4d2c2d851 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Mar 2019 05:35:52 +0100 Subject: Fix poll options not rendering text after vote/refresh Port fd128b9c7aa5c71adbfc2e223212514c0baee675 to glitch-soc --- app/javascript/flavours/glitch/actions/importer/index.js | 6 ++++++ app/javascript/flavours/glitch/actions/polls.js | 11 +++++++++-- app/javascript/flavours/glitch/components/poll.js | 4 +++- app/javascript/flavours/glitch/reducers/polls.js | 4 ---- 4 files changed, 18 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index e990dc04c..f4372fb31 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -82,3 +82,9 @@ export function importFetchedStatuses(statuses) { dispatch(importStatuses(normalStatuses)); }; } + +export function importFetchedPoll(poll) { + return dispatch => { + dispatch(importPolls([normalizePoll(poll)])); + }; +} diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index bee4c48a6..8e8b82df5 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,4 +1,5 @@ import api from '../api'; +import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; @@ -12,7 +13,10 @@ export const vote = (pollId, choices) => (dispatch, getState) => { dispatch(voteRequest()); api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) - .then(({ data }) => dispatch(voteSuccess(data))) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) .catch(err => dispatch(voteFail(err))); }; @@ -20,7 +24,10 @@ export const fetchPoll = pollId => (dispatch, getState) => { dispatch(fetchPollRequest()); api(getState).get(`/api/v1/polls/${pollId}`) - .then(({ data }) => dispatch(fetchPollSuccess(data))) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) .catch(err => dispatch(fetchPollFail(err))); }; diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index c52445c86..bfff7b601 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -7,6 +7,8 @@ import classNames from 'classnames'; import { vote, fetchPoll } from 'mastodon/actions/polls'; import Motion from 'mastodon/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; +import escapeTextContentForBrowser from 'escape-html'; +import emojify from 'mastodon/features/emoji/emoji'; const messages = defineMessages({ moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, @@ -120,7 +122,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - + ); diff --git a/app/javascript/flavours/glitch/reducers/polls.js b/app/javascript/flavours/glitch/reducers/polls.js index 53d9b1d8c..9956cf83f 100644 --- a/app/javascript/flavours/glitch/reducers/polls.js +++ b/app/javascript/flavours/glitch/reducers/polls.js @@ -1,4 +1,3 @@ -import { POLL_VOTE_SUCCESS, POLL_FETCH_SUCCESS } from 'mastodon/actions/polls'; import { POLLS_IMPORT } from 'mastodon/actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; @@ -10,9 +9,6 @@ export default function polls(state = initialState, action) { switch(action.type) { case POLLS_IMPORT: return importPolls(state, action.polls); - case POLL_VOTE_SUCCESS: - case POLL_FETCH_SUCCESS: - return importPolls(state, [action.poll]); default: return state; } -- cgit From 1bb23100b1059879a743302253eafc8317daf215 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Wed, 6 Mar 2019 12:21:18 +0100 Subject: Add an icon to highlight polls behind a Content Warning --- app/javascript/flavours/glitch/components/status.js | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index b38bebe11..31f4f1ddd 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -440,6 +440,7 @@ export default class Status extends ImmutablePureComponent { attachments = status.get('media_attachments'); if (status.get('poll')) { media = ; + mediaIcon = 'tasks'; } else if (attachments.size > 0) { if (muted || attachments.some(item => item.get('type') === 'unknown')) { media = ( -- cgit From cb217444b842ac2952acdd83844b5440ebe36209 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 6 Mar 2019 15:09:18 +0100 Subject: Disable file upload when there is a poll (#10195) --- app/javascript/mastodon/actions/compose.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'app') diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 2d58a71fe..da7bd3cb0 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -60,6 +60,7 @@ export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, + uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, }); export function changeCompose(text) { @@ -207,6 +208,12 @@ export function uploadCompose(files) { dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } + + if (getState().getIn(['compose', 'poll'])) { + dispatch(showAlert(undefined, messages.uploadErrorPoll)); + return; + } + dispatch(uploadComposeRequest()); for (const [i, f] of Array.from(files).entries()) { -- cgit From b3668a79eca2d185ea57a9ffc2fa012db52e49f0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 6 Mar 2019 15:10:32 +0100 Subject: Disable the underlying button element when an ItemButton is disabled (#10194) Fixes #10191 --- app/javascript/mastodon/components/icon_button.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index fbb42f78f..9d8a8d06b 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -86,6 +86,7 @@ export default class IconButton extends React.PureComponent { onClick={this.handleClick} style={style} tabIndex={tabIndex} + disabled={disabled} >