Compare commits

...

65 Commits

Author SHA1 Message Date
Eugen Rochko
ab71cf4593 Bump to 1.6.0 2017-09-10 15:10:03 +02:00
Eugen Rochko
c450ddb613 Fix POST /api/v1/follows error when already following (#4878) 2017-09-10 15:09:06 +02:00
yoshipc
15b886a6f0 Fix Japanese translation (#4876)
I translated the additional text ( added by #4871)
2017-09-10 20:34:15 +09:00
Eugen Rochko
4819e2913d Bump version to 1.6.0rc5 2017-09-10 10:26:51 +02:00
Eugen Rochko
72e662bb0d Hide modal loading screen for media/video/boost/confirm/actions modals (#4873) 2017-09-10 10:26:01 +02:00
Eugen Rochko
7d7844a47f Default follows for new users (#4871)
When a new user confirms their e-mail, bootstrap their home timeline
by automatically following a set of accounts. By default, all local
admin accounts (that are unlocked). Can be customized by new admin
setting (comma-separated usernames, local and unlocked only)
2017-09-10 09:58:38 +02:00
Eugen Rochko
f2cbfb2eb3 Fix dimensions of loading component for compose drawer (#4872) 2017-09-10 08:48:11 +02:00
Yamagishi Kazutoshi
3f333a8d31 Set fallback address when empty notification address (#4868) 2017-09-09 21:31:48 +02:00
Quent-in
bc077018b8 i10n minors changes for 1.6 (#4867)
* wrong preposition + typo

* wrong preposition + typo

* Typo

* Typo

* minor changes

* minor changes
2017-09-09 18:22:56 +02:00
Eugen Rochko
90712d4293 Fix errors preventing UnsubscribeService from working (#4866) 2017-09-09 17:36:27 +02:00
Eugen Rochko
6867681c7c Add script to make embedded iframes autosize (#4853) 2017-09-09 16:23:44 +02:00
Eugen Rochko
bdc8b4fd91 Disable mouse-based pause from #4859 (#4865)
It wasn't working ideally and introduced some annoying false positivies
2017-09-09 15:09:50 +02:00
Eugen Rochko
2ff7146b6d Bump version to 1.6.0rc4 2017-09-09 14:53:49 +02:00
unarist
c7908e2d09 Fix scroll behavior and others on paused timeline (#4864)
Resolved:

* Lot of redundant renders while mouse moving
* Scroll jumping when timeline loaded
* Scroll position isn't kept when statuses below the scrollTop was deleted then new status arrived

Unresolved:

* Scroll position isn't kept when statuses over the scrollTop was deleted then new status arrived
-> It needs to know which statuses are over the scrollTop
* New status indicator should be active when new statuses arrived while mouse moved recently
-> It needs a) update indicator in ScrollableList, or b) set scrollTop status while mouse moving
2017-09-09 14:16:11 +02:00
Yamagishi Kazutoshi
c9d04f1c39 Fix second report (regression from 3b81baaaaf) (#4863) 2017-09-09 13:42:48 +02:00
Eugen Rochko
9e15eeec63 Add missing reject_media check before avatar download via ActivityPub (#4862) 2017-09-09 13:41:45 +02:00
Lynx Kotoura
3c45d3963a Scrollable tables in settings pages (#4857)
* Scrollable tables in settings pages

* Add space before curly brace
2017-09-09 02:26:58 +02:00
Eugen Rochko
baa8b82179 Fix #1004 - Temporarily pause timeline if there's been recent mouse movement (#4859) 2017-09-09 02:26:41 +02:00
Eugen Rochko
4b460bc571 Fix #4852 - Check if already requested from FollowService (#4855) 2017-09-09 02:02:44 +02:00
Eugen Rochko
7ca173be47 Fix #4850 - When visibility missing from API call to toot, fallback to user preference (#4861) 2017-09-09 02:02:29 +02:00
unarist
1ae5d49a71 Refresh timeline after toot while the timeline is disconnected (#4858)
To reflect status posting immediately, we've inserted the status into timelines directly. However, status insertion changes "latest status", and it means next timeline refresh only fetches statuses since the inserted status. This behavior is very bad for disconnected timeline and mobile views.

After this patch, it refreshes timeline for disconnected timelines, instead of direct insertion.
2017-09-08 21:43:34 +02:00
unarist
a12572e074 Handle stream_entry URL correctly in ActivityPub (#4854)
In before, the method uses stream_entry id as status id, so replied status was wrongly selected.

This PR uses StatusFinder which was introduced with `Api::Web::EmbedsController`.
2017-09-08 18:20:03 +02:00
Quent-in
dabc309ca3 i10n update OC and FR (#4849)
* Missing "navigation_bar.pins"

* Missing "navigation_bar.pins"
2017-09-08 13:55:47 +02:00
Eugen Rochko
1caf11ddcc Fix language filter codes (#4841)
* Fix language filter codes

CLD3 returns BCP-47 language identifier, filter settings expect
identifiers in the ISO 639-1 format. Convert between formats,
and exclude duplicate languages from filter choices (zh-CN->zh)

* Fix zh name
2017-09-08 12:32:22 +02:00
Eugen Rochko
95f018a3d4 "Mute conversation" option on all own toots, not just in notifications (#4844)
That way you can mute notifications for a toot before you get replies
to it or boosts or favourites
2017-09-08 12:00:30 +02:00
Eugen Rochko
a4caa7eb62 Fetch statuses/following/followers numbers from ActivityPub collections (#4840) 2017-09-08 12:00:17 +02:00
m4sk1n
7c2d84910c i18n: Update Polish translation (#4845)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-08 05:51:48 +09:00
Quent-in
b00cc4b9bd i10n OC / FR update Pinned toots (#4842)
* Added column.pins

New strings

* Added column.pins

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* Update password_change.oc.html.erb

* Update password_change.oc.text.erb

* Update reset_password_instructions.oc.html.erb

* Update reset_password_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb
2017-09-07 22:07:03 +02:00
Eugen Rochko
dd6ede554f Fix #4834 - Adjust Status#local and Status#remote scopes (#4839) 2017-09-07 20:18:34 +02:00
abcang
6859d4c028 Enable UniqueRetryJobMiddleware even when called from sidekiq worker (#4836) 2017-09-07 16:44:14 +02:00
PFM
7d853b514a Use <button> instead of <div role="button"> (#4835) 2017-09-07 16:18:41 +02:00
voidSatisfaction
85c7c42098 Add Pinned toot column (#4817)
* Add Pinned_toot_section

* Fix add frozen_string_literal

* Fix delete no need controller and tests

* Fix replace query strings to axios params

* Fix change value to accountId and disabling more button
2017-09-07 09:58:11 +02:00
voidSatisfaction
8185f98872 Feat add validation for report comment: characters under 1000 valid (#4833) 2017-09-07 09:55:42 +02:00
Joseph Mingrone
5264496240 Use casecmp() instead of casecmp?() for now (#4832)
* Use casecmp() instead of casecmp?() for now

casecmp?() is only available in ruby 2.4.0.  Users running earlier ruby versions
would see errors, e.g., running
RAILS_ENV=production rails mastodon:maintenance:remove_deprecated_preview_cards.

* Correctly check whether casecmp() returns 0
2017-09-07 03:55:06 +02:00
Quent-in
be75b13d68 i10n update OC and FR files (#4824)
* Onboarding: corrections

Some missing letters and spaces or better wording

* Embed

Translated as Intégrer in FR / Embarcar in OC
2017-09-07 08:55:03 +09:00
Olivier Humbert
9417c9bb8f Update fr.json (#4830)
typo
2017-09-06 22:32:49 +02:00
Eugen Rochko
11bddd31ce Fix locking migration on statuses table. Nullable column and NO default value (#4825) 2017-09-06 20:57:52 +02:00
Eugen Rochko
dd5cb5085c Bump version to 1.6.0rc2 2017-09-06 19:02:03 +02:00
Eugen Rochko
e7adbf572a Switch to static URIs, new URI format in both protocols for new statuses (#4815)
* Decouple Status#local? from uri being nil

* Replace on-the-fly URI generation with stored URIs

- Generate URI in after_save hook for local statuses
- Use static value in TagManager when available, fallback to tag format
- Make TagManager use ActivityPub::TagManager to understand new format
- Adjust tests

* Use other heuristic for locality of old statuses, do not perform long query

* Exclude tombstone stream entries from Atom feed

* Prevent nil statuses from landing in Pubsubhubbub::DistributionWorker

* Fix URI not being saved (#4818)

* Add more specs for Status

* Save generated uri immediately

and also fix method order to minimize diff.

* Fix alternate HTML URL in Atom

* Fix tests

* Remove not-null constraint from statuses migration to speed it up
2017-09-06 19:01:28 +02:00
voidSatisfaction
13ffa3c59e Add Smartphone screen favourite back button and adjust styles (#4813)
* Feat add get-back button on favourite columnHeader

* Style adjust nice looking get-back button

* Fix delete media query and add padding right

* fix: restore padding and add lastchild style for back-button
2017-09-06 17:32:15 +02:00
Masoud Abkenar
aec5097d44 i18n: update Persian translation (#4822) 2017-09-06 17:31:54 +02:00
Clworld
1646f622a5 fix scroll position (#4821) 2017-09-06 17:29:56 +02:00
Yamagishi Kazutoshi
e0cda4a851 Update react-intl to version 2.4.0 (#4820) 2017-09-06 16:25:19 +02:00
PFM
d8d2a54741 fix text position of NSFW for video file (#4819) 2017-09-06 13:55:47 +02:00
Yamagishi Kazutoshi
fa21d004c7 Add environment sample for OpenStack Swift (#4816) 2017-09-06 12:13:00 +02:00
Adam Thurlow
6994664a13 swift-enable the paperclip! 📎 (#2322) 2017-09-05 23:17:06 +02:00
abcang
be7ffa2d75 Do not execute the job with the same arguments as the retry job (#4814) 2017-09-05 20:56:20 +02:00
Eugen Rochko
e821c00e74 Fix mentions in direct statuses not being delivered via AP (#4806) 2017-09-05 20:55:25 +02:00
Eugen Rochko
9b994c4aee Fix #4794 - Fake instant follow in API response when account is believed unlocked (#4799) 2017-09-05 17:48:13 +02:00
Lynx Kotoura
4c3dd0b254 Adjust status embeds (#4808)
* Adjust status embeds

Adjust styles of embed code. Adjust styles of embed pages. Fix overflow of embed-modal.

* Remove trailing whitespace

* Using width from the variable
2017-09-05 12:31:24 +02:00
takayamaki
672df4ecc0 add index_notifications_on_id_and_account_id_and_activity_type on notifications table (#4750) 2017-09-05 12:30:01 +02:00
Treyssat-Vincent Nino
aefb4719bc comment correction (#4812) 2017-09-05 12:13:25 +02:00
voidSatisfaction
4d67bf18fe Translation korean added (#4802) 2017-09-04 20:40:01 +02:00
ButterflyOfFire
f09a250a7c Update ar.yml (#4810)
Some little changes to "ar" locale
2017-09-04 19:54:12 +02:00
Eugen Rochko
9b50a9dd83 Fix some ActivityPub JSON bugs (#4796)
- Fix assumption that `url` is always a string. Handle it if it's an
  array of strings, array of objects, object, or string, both for
  accounts and for objects
- `sharedInbox` is actually supposed to be under `endpoints`, handle
  both cases and adjust the serializer
2017-09-04 18:26:33 +02:00
nullkal
2293466edd Show pinned statuses only in the top of the profile page (#4803)
* Show pinned statuses only in the top of the profile page

* Refactor AccountsController#show_pinned_statuses?
2017-09-04 12:53:18 +02:00
voidSatisfaction
b6f3869f8d Fix streaming url to lowercase (#4804) 2017-09-04 12:52:06 +02:00
Eugen Rochko
09cffaaf04 Fix #4551 - Use correct syntax for content preloading (#4798) 2017-09-04 02:14:12 +02:00
abcang
334a633c2a Fix a problem that notification column goes to top (#4792) 2017-09-03 20:31:51 +02:00
Eugen Rochko
8b12e3cc7f Fix short number locales (#4790)
Overwrite values from rails-i18n by manually overwriting in every locale.
We want numbers like 1.5K in every language
2017-09-03 22:17:34 +09:00
Andreas Drop
d3f46a77c3 Make german translation more gender neutral #4755 (#4789) 2017-09-03 15:17:24 +02:00
Akihiko Odaki
a789315361 Use next instead of return in task (#4787) 2017-09-03 12:39:39 +02:00
May Kittens Devour Your Soul
579c7a88e0 Croatian translation - updated (#4183)
* Update hr.json

* Update hr.json
2017-09-03 18:10:53 +09:00
Quent-in
8538170c2d l10n Occitan update for Embed, cancel follow request, ... (#4788)
* Update: some missing strings

* Updates missing strings

* New string

* Update oc.json

* Update oc.yml

* Update oc.json
2017-09-03 18:08:37 +09:00
Akihiko Odaki
249bdc169c Explicitly define attached file of DeprecatedPreviewCard (#4786)
The path template of the attached files must explicitly be defined because
it is contradicting to the name of the class.
2017-09-03 07:58:58 +02:00
156 changed files with 1771 additions and 664 deletions

View File

@@ -26,7 +26,7 @@ LOCAL_HTTPS=true
# ALTERNATE_DOMAINS=example1.com,example2.com # ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets # Application secrets
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) # Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET= PAPERCLIP_SECRET=
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
@@ -36,7 +36,7 @@ OTP_SECRET=
# You should only generate this once per instance. If you later decide to change it, all push subscription will # You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe. # be invalidated, requiring the users to access the website again to resubscribe.
# #
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
# #
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
@@ -98,6 +98,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

View File

@@ -15,6 +15,7 @@ gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.9' gem 'aws-sdk', '~> 2.9'
gem 'fog-openstack', '~> 0.1'
gem 'paperclip', '~> 5.1' gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
@@ -23,6 +24,7 @@ gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'

View File

@@ -154,12 +154,25 @@ GEM
erubis (2.7.0) erubis (2.7.0)
et-orbi (1.0.5) et-orbi (1.0.5)
tzinfo tzinfo
excon (0.58.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.16.2) fabrication (2.16.2)
faker (1.7.3) faker (1.7.3)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
ffi (1.9.18) ffi (1.9.18)
fog-core (1.45.0)
builder
excon (~> 0.58)
formatador (~> 0.2)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-openstack (0.1.21)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
@@ -211,6 +224,8 @@ GEM
rainbow (~> 2.2) rainbow (~> 2.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5) json-ld (2.1.5)
@@ -535,6 +550,7 @@ DEPENDENCIES
fabrication (~> 2.16) fabrication (~> 2.16)
faker (~> 1.7) faker (~> 1.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.0) goldfinger (~> 2.0)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
@@ -545,6 +561,7 @@ DEPENDENCIES
httplog (~> 0.99) httplog (~> 0.99)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1) json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)

View File

@@ -14,7 +14,7 @@ class AccountsController < ApplicationController
return return
end end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty? @next_url = next_url unless @statuses.empty?
@@ -22,7 +22,7 @@ class AccountsController < ApplicationController
format.atom do format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end end
format.json do format.json do
@@ -33,6 +33,10 @@ class AccountsController < ApplicationController
private private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?

View File

@@ -13,6 +13,7 @@ module Admin
closed_registrations_message closed_registrations_message
open_deletion open_deletion
timeline_preview timeline_preview
bootstrap_timeline_accounts
).freeze ).freeze
BOOLEAN_SETTINGS = %w( BOOLEAN_SETTINGS = %w(

View File

@@ -14,6 +14,16 @@ class Api::V1::AccountsController < Api::BaseController
def follow def follow
FollowService.new.call(current_user.account, @account.acct) FollowService.new.call(current_user.account, @account.acct)
unless @account.locked?
relationships = AccountRelationshipsPresenter.new(
[@account.id],
current_user.account_id,
following_map: { @account.id => true },
requested_map: { @account.id => false }
)
end
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end

View File

@@ -10,6 +10,12 @@ class Api::V1::FollowsController < Api::BaseController
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account) @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end

View File

@@ -2,4 +2,10 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth' layout 'auth'
def show
super do |user|
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
end
end
end end

View File

@@ -12,8 +12,14 @@ module RoutingHelper
end end
def full_asset_url(source, options = {}) def full_asset_url(source, options = {})
source = ActionController::Base.helpers.asset_url(source, options) unless Rails.configuration.x.use_s3 source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s URI.join(root_url, source).to_s
end end
private
def use_storage?
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
end
end end

View File

@@ -30,6 +30,7 @@ module SettingsHelper
th: 'ภาษาไทย', th: 'ภาษาไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
zh: '中文',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)', 'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)', 'zh-TW': '繁體中文(臺灣)',
@@ -39,6 +40,10 @@ module SettingsHelper
HUMAN_LOCALES[locale] HUMAN_LOCALES[locale]
end end
def filterable_languages
I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq
end
def hash_to_object(hash) def hash_to_object(hash)
HashObject.new(hash) HashObject.new(hash)
end end

View File

@@ -1,6 +1,11 @@
import api from '../api'; import api from '../api';
import { updateTimeline } from './timelines'; import {
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
@@ -95,16 +100,20 @@ export function submitCompose() {
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns // To make the app more responsive, immediately get the status into the columns
dispatch(updateTimeline('home', { ...response.data }));
const insertOrRefresh = (timelineId, refreshAction) => {
if (getState().getIn(['timelines', timelineId, 'online'])) {
dispatch(updateTimeline(timelineId, { ...response.data }));
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
dispatch(refreshAction());
}
};
insertOrRefresh('home', refreshHomeTimeline);
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
if (getState().getIn(['timelines', 'community', 'loaded'])) { insertOrRefresh('community', refreshCommunityTimeline);
dispatch(updateTimeline('community', { ...response.data })); insertOrRefresh('public', refreshPublicTimeline);
}
if (getState().getIn(['timelines', 'public', 'loaded'])) {
dispatch(updateTimeline('public', { ...response.data }));
}
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));

View File

@@ -0,0 +1,39 @@
import api from '../api';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
const accountId = getState().getIn(['meta', 'me']);
api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
};
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
};
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
};

View File

@@ -5,6 +5,7 @@ import IntersectionObserverArticle from './intersection_observer_article';
import LoadMore from './load_more'; import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent { export default class ScrollableList extends PureComponent {
@@ -26,6 +27,10 @@ export default class ScrollableList extends PureComponent {
trackScroll: true, trackScroll: true,
}; };
state = {
lastMouseMove: null,
};
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => { handleScroll = throttle(() => {
@@ -46,6 +51,14 @@ export default class ScrollableList extends PureComponent {
trailing: true, trailing: true,
}); });
handleMouseMove = throttle(() => {
this._lastMouseMove = new Date();
}, 300);
handleMouseLeave = () => {
this._lastMouseMove = null;
}
componentDidMount () { componentDidMount () {
this.attachScrollListener(); this.attachScrollListener();
this.attachIntersectionObserver(); this.attachIntersectionObserver();
@@ -55,11 +68,15 @@ export default class ScrollableList extends PureComponent {
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
// Reset the scroll position when a new child comes in in order not to // Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page. // jerk the scrollbar around if you're already scrolled down the page.
if (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) { if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) {
if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) {
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition; const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) { if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop; this.node.scrollTop = newScrollTop;
} }
@@ -67,7 +84,6 @@ export default class ScrollableList extends PureComponent {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop; this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
} }
} }
}
componentWillUnmount () { componentWillUnmount () {
this.detachScrollListener(); this.detachScrollListener();
@@ -95,7 +111,12 @@ export default class ScrollableList extends PureComponent {
getFirstChildKey (props) { getFirstChildKey (props) {
const { children } = props; const { children } = props;
const firstChild = Array.isArray(children) ? children[0] : children; let firstChild = children;
if (children instanceof ImmutableList) {
firstChild = children.get(0);
} else if (Array.isArray(children)) {
firstChild = children[0];
}
return firstChild && firstChild.key; return firstChild && firstChild.key;
} }
@@ -108,6 +129,10 @@ export default class ScrollableList extends PureComponent {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} }
_recentlyMoved () {
return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600);
}
handleKeyDown = (e) => { handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => { const article = (() => {
@@ -143,7 +168,7 @@ export default class ScrollableList extends PureComponent {
if (isLoading || childrenCount > 0 || !emptyMessage) { if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = ( scrollableArea = (
<div className='scrollable' ref={this.setRef}> <div className='scrollable' ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}> <div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
{prepend} {prepend}

View File

@@ -134,7 +134,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
menu.push(null); menu.push(null);
if (withDismiss) { if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null); menu.push(null);
} }

View File

@@ -146,29 +146,29 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) { if (!this.state.visible) {
if (sensitive) { if (sensitive) {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}> <button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </button>
); );
} else { } else {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}> <button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </button>
); );
} }
} }
if (this.state.preview && !autoplay) { if (this.state.preview && !autoplay) {
return ( return (
<div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}> <button className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
{spoilerButton} {spoilerButton}
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
</div> </button>
); );
} }

View File

@@ -77,6 +77,7 @@ export default class Favourites extends ImmutablePureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
showBackButton
/> />
<StatusList <StatusList

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@@ -66,15 +67,16 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]); ]);
if (me.get('locked')) { if (me.get('locked')) {
navItems.push(<ColumnLink key='5' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
} }
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='6' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='7' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]); ]);
return ( return (

View File

@@ -106,6 +106,7 @@ export default class Notifications extends React.PureComponent {
const scrollContainer = ( const scrollContainer = (
<ScrollableList <ScrollableList
scrollKey={`notifications-${columnId}`} scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={hasMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
heading: { id: 'column.pins', defaultMessage: 'Pinned toot' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'pins', 'items']),
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
});
@connect(mapStateToProps)
@injectIntl
export default class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchPinnedStatuses());
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { intl, statusIds, hasMore } = this.props;
return (
<Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
<ColumnBackButtonSlim />
<StatusList
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
/>
</Column>
);
}
}

View File

@@ -9,6 +9,7 @@ import { links, getIndex, getLink } from './tabs_bar';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading'; import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
@@ -129,8 +130,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
); );
} }
renderLoading = () => { renderLoading = columnId => () => {
return <ColumnLoading />; return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
} }
renderError = (props) => { renderError = (props) => {
@@ -158,7 +159,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
const params = column.get('params', null) === null ? null : column.get('params').toJS(); const params = column.get('params', null) === null ? null : column.get('params').toJS();
return ( return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading} error={this.renderError}> <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />} {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />}
</BundleContainer> </BundleContainer>
); );

View File

@@ -0,0 +1,11 @@
import React from 'react';
const DrawerLoading = () => (
<div className='drawer'>
<div className='drawer__pager'>
<div className='drawer__inner' />
</div>
</div>
);
export default DrawerLoading;

View File

@@ -33,7 +33,8 @@ export default class EmbedModal extends ImmutablePureComponent {
iframeDocument.close(); iframeDocument.close();
iframeDocument.body.style.margin = 0; iframeDocument.body.style.margin = 0;
this.iframe.height = iframeDocument.body.scrollHeight + 'px'; this.iframe.width = iframeDocument.body.scrollWidth;
this.iframe.height = iframeDocument.body.scrollHeight;
}); });
} }
@@ -71,7 +72,6 @@ export default class EmbedModal extends ImmutablePureComponent {
<iframe <iframe
className='embed-modal__iframe' className='embed-modal__iframe'
scrolling='no'
frameBorder='0' frameBorder='0'
ref={this.setIframeRef} ref={this.setIframeRef}
title='preview' title='preview'

View File

@@ -5,23 +5,23 @@ import spring from 'react-motion/lib/spring';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import BundleModalError from './bundle_modal_error'; import BundleModalError from './bundle_modal_error';
import ModalLoading from './modal_loading'; import ModalLoading from './modal_loading';
import ActionsModal from '../components/actions_modal'; import ActionsModal from './actions_modal';
import MediaModal from './media_modal';
import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import { import {
MediaModal,
OnboardingModal, OnboardingModal,
VideoModal,
BoostModal,
ConfirmationModal,
ReportModal, ReportModal,
EmbedModal, EmbedModal,
} from '../../../features/ui/util/async-components'; } from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
'MEDIA': MediaModal, 'MEDIA': () => Promise.resolve({ default: MediaModal }),
'ONBOARDING': OnboardingModal, 'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal, 'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': BoostModal, 'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': ConfirmationModal, 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'REPORT': ReportModal, 'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal, 'EMBED': EmbedModal,
@@ -82,8 +82,8 @@ export default class ModalRoot extends React.PureComponent {
return { opacity: spring(0), scale: spring(0.98) }; return { opacity: spring(0), scale: spring(0.98) };
} }
renderLoading = () => { renderLoading = modalId => () => {
return <ModalLoading />; return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
} }
renderError = (props) => { renderError = (props) => {
@@ -117,7 +117,7 @@ export default class ModalRoot extends React.PureComponent {
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
<div role='dialog' className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> <div role='dialog' className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}> <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
</BundleContainer> </BundleContainer>
</div> </div>

View File

@@ -35,6 +35,7 @@ import {
FavouritedStatuses, FavouritedStatuses,
Blocks, Blocks,
Mutes, Mutes,
PinnedStatuses,
} from './util/async-components'; } from './util/async-components';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ -208,6 +209,7 @@ export default class UI extends React.PureComponent {
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />

View File

@@ -34,6 +34,10 @@ export function GettingStarted () {
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
} }
export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
}
export function AccountTimeline () { export function AccountTimeline () {
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
} }
@@ -78,26 +82,10 @@ export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes'); return import(/* webpackChunkName: "features/mutes" */'../../mutes');
} }
export function MediaModal () {
return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal');
}
export function OnboardingModal () { export function OnboardingModal () {
return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
} }
export function VideoModal () {
return import(/* webpackChunkName: "modals/video_modal" */'../components/video_modal');
}
export function BoostModal () {
return import(/* webpackChunkName: "modals/boost_modal" */'../components/boost_modal');
}
export function ConfirmationModal () {
return import(/* webpackChunkName: "modals/confirmation_modal" */'../components/confirmation_modal');
}
export function ReportModal () { export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
} }

View File

@@ -26,12 +26,12 @@
"bundle_modal_error.close": "Schließen", "bundle_modal_error.close": "Schließen",
"bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.", "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
"bundle_modal_error.retry": "Erneut versuchen", "bundle_modal_error.retry": "Erneut versuchen",
"column.blocks": "Blockierte Benutzer", "column.blocks": "Blockierte Profile",
"column.community": "Lokale Zeitleiste", "column.community": "Lokale Zeitleiste",
"column.favourites": "Favoriten", "column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen", "column.follow_requests": "Folgeanfragen",
"column.home": "Startseite", "column.home": "Startseite",
"column.mutes": "Stummgeschaltete Benutzer", "column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
@@ -46,7 +46,7 @@
"compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.", "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
"compose_form.lock_disclaimer.lock": "gesperrt", "compose_form.lock_disclaimer.lock": "gesperrt",
"compose_form.placeholder": "Worüber möchtest du schreiben?", "compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.", "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Profile auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
"compose_form.publish": "Tröt", "compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Medien als heikel markieren", "compose_form.sensitive": "Medien als heikel markieren",
@@ -77,18 +77,18 @@
"emoji_button.travel": "Reise und Orte", "emoji_button.travel": "Reise und Orte",
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
"empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.", "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.",
"empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.", "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Profile zu finden.",
"empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.", "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste", "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.", "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.", "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um es aufzufüllen.",
"follow_request.authorize": "Erlauben", "follow_request.authorize": "Erlauben",
"follow_request.reject": "Ablehnen", "follow_request.reject": "Ablehnen",
"getting_started.appsshort": "Anwendungen", "getting_started.appsshort": "Anwendungen",
"getting_started.faq": "Häufig gestellte Fragen", "getting_started.faq": "Häufig gestellte Fragen",
"getting_started.heading": "Erste Schritte", "getting_started.heading": "Erste Schritte",
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"getting_started.userguide": "Nutzeranleitung", "getting_started.userguide": "Bedienungsanleitung",
"home.column_settings.advanced": "Fortgeschritten", "home.column_settings.advanced": "Fortgeschritten",
"home.column_settings.basic": "Einfach", "home.column_settings.basic": "Einfach",
"home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke", "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@@ -101,14 +101,14 @@
"loading_indicator.label": "Lade…", "loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen", "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden", "missing_indicator.label": "Nicht gefunden",
"navigation_bar.blocks": "Blockierte Benutzer", "navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.community_timeline": "Lokale Zeitleiste", "navigation_bar.community_timeline": "Lokale Zeitleiste",
"navigation_bar.edit_profile": "Profil bearbeiten", "navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.favourites": "Favoriten", "navigation_bar.favourites": "Favoriten",
"navigation_bar.follow_requests": "Folgeanfragen", "navigation_bar.follow_requests": "Folgeanfragen",
"navigation_bar.info": "Erweiterte Informationen", "navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden", "navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Benutzer", "navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste", "navigation_bar.public_timeline": "Föderierte Zeitleiste",
"notification.favourite": "{name} favorisierte deinen Status", "notification.favourite": "{name} favorisierte deinen Status",
@@ -132,7 +132,7 @@
"onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.", "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
"onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.", "onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.",
"onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.", "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
"onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Nutzername im Netzwerk {handle}", "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Profilname im Netzwerk {handle}",
"onboarding.page_one.welcome": "Willkommen bei Mastodon!", "onboarding.page_one.welcome": "Willkommen bei Mastodon!",
"onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.", "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
"onboarding.page_six.almost_done": "Fast fertig…", "onboarding.page_six.almost_done": "Fast fertig…",
@@ -143,11 +143,11 @@
"onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!", "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
"onboarding.page_six.various_app": "mobile Anwendungen", "onboarding.page_six.various_app": "mobile Anwendungen",
"onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.", "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.",
"onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Nutzernamen.", "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
"onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.", "onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.",
"onboarding.skip": "Überspringen", "onboarding.skip": "Überspringen",
"privacy.change": "Privatsphäre des Status anpassen", "privacy.change": "Privatsphäre des Status anpassen",
"privacy.direct.long": "Beitrag nur an erwähnte Benutzer", "privacy.direct.long": "Beitrag nur an erwähnte Profile",
"privacy.direct.short": "Direkt", "privacy.direct.short": "Direkt",
"privacy.private.long": "Beitrag nur an Folgende", "privacy.private.long": "Beitrag nur an Folgende",
"privacy.private.short": "Privat", "privacy.private.short": "Privat",

View File

@@ -34,6 +34,7 @@
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column.pins": "Pinned toots",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.pins": "Pinned toots",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",

View File

@@ -63,8 +63,8 @@
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟", "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"confirmations.unfollow.confirm": "لغو پیگیری", "confirmations.unfollow.confirm": "لغو پیگیری",
"confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟", "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
"embed.preview": "Here is what it will look like:", "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
"emoji_button.activity": "فعالیت", "emoji_button.activity": "فعالیت",
"emoji_button.flags": "پرچم‌ها", "emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی", "emoji_button.food": "غذا و نوشیدنی",
@@ -164,14 +164,14 @@
"standalone.public_title": "نگاهی به کاربران این سرور...", "standalone.public_title": "نگاهی به کاربران این سرور...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن", "status.delete": "پاک‌کردن",
"status.embed": "Embed", "status.embed": "جاگذاری",
"status.favourite": "پسندیدن", "status.favourite": "پسندیدن",
"status.load_more": "بیشتر نشان بده", "status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده", "status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}", "status.mention": "نام‌بردن از @{name}",
"status.mute_conversation": "بی‌صداکردن گفتگو", "status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن", "status.open": "این نوشته را باز کن",
"status.pin": "Pin on profile", "status.pin": "نوشتهٔ ثابت نمایه",
"status.reblog": "بازبوقیدن", "status.reblog": "بازبوقیدن",
"status.reblogged_by": "{name} بازبوقید", "status.reblogged_by": "{name} بازبوقید",
"status.reply": "پاسخ", "status.reply": "پاسخ",
@@ -183,7 +183,7 @@
"status.show_less": "نهفتن", "status.show_less": "نهفتن",
"status.show_more": "نمایش", "status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو", "status.unmute_conversation": "باصداکردن گفتگو",
"status.unpin": "Unpin from profile", "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
"tabs_bar.compose": "بنویسید", "tabs_bar.compose": "بنویسید",
"tabs_bar.federated_timeline": "همگانی", "tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه", "tabs_bar.home": "خانه",

View File

@@ -34,6 +34,7 @@
"column.mutes": "Comptes masqués", "column.mutes": "Comptes masqués",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public global", "column.public": "Fil public global",
"column.pins": "Pouets épinglés",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.hide_settings": "Masquer les paramètres", "column_header.hide_settings": "Masquer les paramètres",
"column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@@ -62,9 +63,9 @@
"confirmations.mute.confirm": "Masquer", "confirmations.mute.confirm": "Masquer",
"confirmations.mute.message": "Confirmez vous le masquage de {name}?", "confirmations.mute.message": "Confirmez vous le masquage de {name}?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name}?", "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Il apparaîtra comme cela:",
"emoji_button.activity": "Activités", "emoji_button.activity": "Activités",
"emoji_button.flags": "Drapeaux", "emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger", "emoji_button.food": "Boire et manger",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Comptes masqués", "navigation_bar.mutes": "Comptes masqués",
"navigation_bar.preferences": "Préférences", "navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global", "navigation_bar.public_timeline": "Fil public global",
"navigation_bar.pins": "Pouets épinglés",
"notification.favourite": "{name} a ajouté à ses favoris:", "notification.favourite": "{name} a ajouté à ses favoris:",
"notification.follow": "{name} vous suit.", "notification.follow": "{name} vous suit.",
"notification.mention": "{name} vous a mentionné⋅e:", "notification.mention": "{name} vous a mentionné⋅e:",

View File

@@ -1,7 +1,7 @@
{ {
"account.block": "Blokiraj @{name}", "account.block": "Blokiraj @{name}",
"account.block_domain": "Sakrij sve sa {domain}", "account.block_domain": "Sakrij sve sa {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
"account.edit_profile": "Uredi profil", "account.edit_profile": "Uredi profil",
"account.follow": "Slijedi", "account.follow": "Slijedi",
"account.followers": "Sljedbenici", "account.followers": "Sljedbenici",
@@ -15,7 +15,7 @@
"account.requested": "Čeka pristanak", "account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile", "account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}", "account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}", "account.unblock_domain": "Poništi sakrivanje {domain}",
"account.unfollow": "Prestani slijediti", "account.unfollow": "Prestani slijediti",
"account.unmute": "Poništi utišavanje @{name}", "account.unmute": "Poništi utišavanje @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "View full profile",
@@ -43,7 +43,7 @@
"column_header.unpin": "Unpin", "column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigacija", "column_subheading.navigation": "Navigacija",
"column_subheading.settings": "Postavke", "column_subheading.settings": "Postavke",
"compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti i vidjeti tvoje postove namijenjene samo sljedbenicima.", "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.",
"compose_form.lock_disclaimer.lock": "zaključan", "compose_form.lock_disclaimer.lock": "zaključan",
"compose_form.placeholder": "Što ti je na umu?", "compose_form.placeholder": "Što ti je na umu?",
"compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.", "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.",
@@ -54,13 +54,14 @@
"compose_form.spoiler_placeholder": "Upozorenje o sadržaju", "compose_form.spoiler_placeholder": "Upozorenje o sadržaju",
"confirmation_modal.cancel": "Otkaži", "confirmation_modal.cancel": "Otkaži",
"confirmations.block.confirm": "Blokiraj", "confirmations.block.confirm": "Blokiraj",
"confirmations.block.message": "Jesi li siguran da želiš blokirati {name}?", "confirmations.block.message": "Želiš li sigurno blokirati {name}?",
"confirmations.delete.confirm": "Obriši", "confirmations.delete.confirm": "Obriši",
"confirmations.delete.message": "Jesi li siguran da želiš obrisati ovaj status?", "confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
"confirmations.domain_block.confirm": "Sakrij cijelu domenu", "confirmations.domain_block.confirm": "Sakrij cijelu domenu",
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš blokirati sve sa {domain}? U većini slučajeva nekoliko ciljanih blokiranja ili utišavanja je dostatno i poželjnije.", "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj", "confirmations.mute.confirm": "Utišaj",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?", "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "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.instructions": "Embed this status on your website by copying the code below.",
@@ -69,16 +70,16 @@
"emoji_button.flags": "Zastave", "emoji_button.flags": "Zastave",
"emoji_button.food": "Hrana & Piće", "emoji_button.food": "Hrana & Piće",
"emoji_button.label": "Umetni smajlije", "emoji_button.label": "Umetni smajlije",
"emoji_button.nature": "Nature", "emoji_button.nature": "Priroda",
"emoji_button.objects": "Objekti", "emoji_button.objects": "Objekti",
"emoji_button.people": "Ljudi", "emoji_button.people": "Ljudi",
"emoji_button.search": "Traži...", "emoji_button.search": "Traži...",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Putovanja i Mjesta", "emoji_button.travel": "Putovanja & Mjesta",
"empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!", "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
"empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
"empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
"empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, regenerirat će se uskoro.", "empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, uskoro ćese regenerirati.",
"empty_column.home.public_timeline": "javni timeline", "empty_column.home.public_timeline": "javni timeline",
"empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
"empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
@@ -88,11 +89,11 @@
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Počnimo", "getting_started.heading": "Počnimo",
"getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}.", "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu {github}.",
"getting_started.userguide": "Vodič za korisnike", "getting_started.userguide": "Upute za korištenje",
"home.column_settings.advanced": "Napredno", "home.column_settings.advanced": "Napredno",
"home.column_settings.basic": "Osnovno", "home.column_settings.basic": "Osnovno",
"home.column_settings.filter_regex": "Filtriraj s regularnim izrazima", "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima",
"home.column_settings.show_reblogs": "Pokaži boosts", "home.column_settings.show_reblogs": "Pokaži boostove",
"home.column_settings.show_replies": "Pokaži odgovore", "home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca", "home.settings": "Postavke Stupca",
"lightbox.close": "Zatvori", "lightbox.close": "Zatvori",
@@ -113,7 +114,7 @@
"navigation_bar.public_timeline": "Federalni timeline", "navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status", "notification.favourite": "{name} je lajkao tvoj status",
"notification.follow": "{name} te sada slijedi", "notification.follow": "{name} te sada slijedi",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} te je spomenuo",
"notification.reblog": "{name} je podigao tvoj status", "notification.reblog": "{name} je podigao tvoj status",
"notifications.clear": "Očisti notifikacije", "notifications.clear": "Očisti notifikacije",
"notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?", "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?",
@@ -123,28 +124,28 @@
"notifications.column_settings.mention": "Spominjanja:", "notifications.column_settings.mention": "Spominjanja:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.reblog": "Boostovi:",
"notifications.column_settings.show": "Prikaži u stupcu", "notifications.column_settings.show": "Prikaži u stupcu",
"notifications.column_settings.sound": "Sviraj zvuk", "notifications.column_settings.sound": "Sviraj zvuk",
"onboarding.done": "Učinjeno", "onboarding.done": "Učinjeno",
"onboarding.next": "Sljedeća", "onboarding.next": "Sljedeće",
"onboarding.page_five.public_timelines": "The local timeline prikazuje javne postove svih na {domain}. Federalni timeline pokazuje javne postove svih sa {domain} domena koje slijed. To je sjajan način da otkriješ nove ljude.", "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.",
"onboarding.page_four.home": "The home timeline prikazuje samo postove ljudi koje slijediš.", "onboarding.page_four.home": "The home timeline prikazuje postove ljudi koje slijediš.",
"onboarding.page_four.notifications": "Stupac notifikacija pokazuje kada je netko u interakciji s tobom.", "onboarding.page_four.notifications": "Stupac za notifikacije pokazuje poruke drugih upućene tebi.",
"onboarding.page_one.federation": "Mastodon je mreža nezavisnih servera udruženih kako bi stvorili veću socijalnu mrežu. Te servere zovemo instance.", "onboarding.page_one.federation": "Mastodon čini mreža neovisnih servera udruženih u jednu veću socialnu mrežu. Te servere nazivamo instancama.",
"onboarding.page_one.handle": "Ti si na {domain}, tako da je tvoj potpuni opis {handle}", "onboarding.page_one.handle": "Ti si na {domain}, i tvoja puna handle je {handle}",
"onboarding.page_one.welcome": "Dobro došli u Mastodon!", "onboarding.page_one.welcome": "Dobro došli na Mastodon!",
"onboarding.page_six.admin": "Administrator tvoje instance je {admin}.", "onboarding.page_six.admin": "Administrator tvoje instance je {admin}.",
"onboarding.page_six.almost_done": "Još malo pa gotovo...", "onboarding.page_six.almost_done": "Još malo pa gotovo...",
"onboarding.page_six.appetoot": "Živjeli!", "onboarding.page_six.appetoot": "Živjeli!",
"onboarding.page_six.apps_available": "Postoje {apps} dostupne za iOS, Android i druge platforme.", "onboarding.page_six.apps_available": "Postoje {apps} dostupne za iOS, Android i druge platforme.",
"onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. Možeš prijaviti greške, zahtijevati mogućnosti, ili pridonijeti kodu na {github}.", "onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "smjernice zajednice", "onboarding.page_six.guidelines": "smjernice zajednice",
"onboarding.page_six.read_guidelines": "Molimo, pročitaj {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "Molimo pročitaj {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobilne aplikacije", "onboarding.page_six.various_app": "mobilne aplikacije",
"onboarding.page_three.profile": "Uredi svoj profil mijenjanjem avatara, biografije i imena koje će biti prikazano. Naći ćeš i druge korisne postavke.", "onboarding.page_three.profile": "Uredi svoj profil promjenom svog avatara, biografije, i imena. Ovdje ćeš isto tako pronaći i druge postavke.",
"onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i sadržaj sa određenim hashtagovima, kao što su {illustration} i {introductions}. Da bi našao osobu koja nije na ovoj instanci, upotrijebi njihov puni opis.", "onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i tražio hashtags, kao što su {illustration} i {introductions}. Kako bi pronašao osobu koja nije na ovoj instanci, upotrijebi njen pun handle.",
"onboarding.page_two.compose": "Piši postove u stupcu za njihovo sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.", "onboarding.page_two.compose": "Piši postove u stupcu za sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.",
"onboarding.skip": "Preskoči", "onboarding.skip": "Preskoči",
"privacy.change": "Podesi status privatnosti", "privacy.change": "Podesi status privatnosti",
"privacy.direct.long": "Prikaži samo spomenutim korisnicima", "privacy.direct.long": "Prikaži samo spomenutim korisnicima",
@@ -162,7 +163,7 @@
"search.placeholder": "Traži", "search.placeholder": "Traži",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Ovaj post ne može biti podignut", "status.cannot_reblog": "Ovaj post ne može biti boostan",
"status.delete": "Obriši", "status.delete": "Obriši",
"status.embed": "Embed", "status.embed": "Embed",
"status.favourite": "Označi omiljenim", "status.favourite": "Označi omiljenim",
@@ -196,5 +197,5 @@
"video_player.expand": "Proširi video", "video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk", "video_player.toggle_sound": "Toggle zvuk",
"video_player.toggle_visible": "Preklopi vidljivost", "video_player.toggle_visible": "Preklopi vidljivost",
"video_player.video_error": "Video nije mogao biti prikazan" "video_player.video_error": "Video ne može biti reproduciran"
} }

View File

@@ -34,6 +34,7 @@
"column.mutes": "ミュートしたユーザー", "column.mutes": "ミュートしたユーザー",
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "連合タイムライン", "column.public": "連合タイムライン",
"column.pins": "固定されたトゥート",
"column_back_button.label": "戻る", "column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す", "column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する", "column_header.moveLeft_settings": "カラムを左に移動する",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "ミュートしたユーザー", "navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定", "navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン", "navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.pins": "固定されたトゥート",
"notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました", "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました", "notification.follow": "{name}さんにフォローされました",
"notification.mention": "{name}さんがあなたに返信しました", "notification.mention": "{name}さんがあなたに返信しました",

View File

@@ -34,6 +34,7 @@
"column.mutes": "뮤트 중인 사용자", "column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림", "column.notifications": "알림",
"column.public": "연합 타임라인", "column.public": "연합 타임라인",
"column.pins": "고정된 Toot",
"column_back_button.label": "돌아가기", "column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "뮤트 중인 사용자", "navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.preferences": "사용자 설정", "navigation_bar.preferences": "사용자 설정",
"navigation_bar.public_timeline": "연합 타임라인", "navigation_bar.public_timeline": "연합 타임라인",
"navigation_bar.pins": "고정된 Toot",
"notification.favourite": "{name}님이 즐겨찾기 했습니다", "notification.favourite": "{name}님이 즐겨찾기 했습니다",
"notification.follow": "{name}님이 나를 팔로우 했습니다", "notification.follow": "{name}님이 나를 팔로우 했습니다",
"notification.mention": "{name}님이 답글을 보냈습니다", "notification.mention": "{name}님이 답글을 보냈습니다",

View File

@@ -12,7 +12,7 @@
"account.mute": "Rescondre @{name}", "account.mute": "Rescondre @{name}",
"account.posts": "Estatuts", "account.posts": "Estatuts",
"account.report": "Senhalar @{name}", "account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada", "account.requested": "Invitacion mandada. Clicatz per anullar.",
"account.share": "Partejar lo perfil a @{name}", "account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}", "account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}", "account.unblock_domain": "Desblocar {domain}",
@@ -34,6 +34,7 @@
"column.mutes": "Personas en silenci", "column.mutes": "Personas en silenci",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Flux public global", "column.public": "Flux public global",
"column.pins": "Tuts penjats",
"column_back_button.label": "Tornar", "column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres", "column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@@ -63,8 +64,8 @@
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats", "emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Personas rescondudas", "navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"navigation_bar.pins": "Tuts penjats",
"notification.favourite": "{name} a ajustat a sos favorits:", "notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc", "notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat:", "notification.mention": "{name} vos a mencionat:",
@@ -126,21 +128,21 @@
"notifications.column_settings.reblog": "Partatges:", "notifications.column_settings.reblog": "Partatges:",
"notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son", "notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach", "onboarding.done": "Sortir",
"onboarding.next": "Seguent", "onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon!", "onboarding.page_one.welcome": "Benvengut a Mastodon!",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.", "onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…", "onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetut!", "onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.",
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.", "onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
"onboarding.page_six.guidelines": "guida de la comunitat", "onboarding.page_six.guidelines": "guida de la comunitat",
"onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain}!", "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} de {domain}!",
"onboarding.page_six.various_app": "aplicacions per mobil", "onboarding.page_six.various_app": "aplicacions per mobil",
"onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.", "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.",
"onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.", "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.",
@@ -164,7 +166,7 @@
"standalone.public_title": "Una ulhada dedins…", "standalone.public_title": "Una ulhada dedins…",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar", "status.delete": "Escafar",
"status.embed": "Embed", "status.embed": "Embarcar",
"status.favourite": "Apondre als favorits", "status.favourite": "Apondre als favorits",
"status.load_more": "Cargar mai", "status.load_more": "Cargar mai",
"status.media_hidden": "Mèdia rescondut", "status.media_hidden": "Mèdia rescondut",
@@ -183,7 +185,7 @@
"status.show_less": "Tornar plegar", "status.show_less": "Tornar plegar",
"status.show_more": "Desplegar", "status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat", "status.unmute_conversation": "Conversacions amb silenci levat",
"status.unpin": "Despenjar del perfil", "status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar", "tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global", "tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh", "tabs_bar.home": "Acuèlh",

View File

@@ -12,7 +12,7 @@
"account.mute": "Wycisz @{name}", "account.mute": "Wycisz @{name}",
"account.posts": "Wpisy", "account.posts": "Wpisy",
"account.report": "Zgłoś @{name}", "account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba", "account.requested": "Oczekująca prośba, kliknij aby anulować",
"account.share": "Udostępnij profil @{name}", "account.share": "Udostępnij profil @{name}",
"account.unblock": "Odblokuj @{name}", "account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}", "account.unblock_domain": "Odblokuj domenę {domain}",
@@ -33,6 +33,7 @@
"column.home": "Strona główna", "column.home": "Strona główna",
"column.mutes": "Wyciszeni użytkownicy", "column.mutes": "Wyciszeni użytkownicy",
"column.notifications": "Powiadomienia", "column.notifications": "Powiadomienia",
"column.pins": "Przypięte wpisy",
"column.public": "Globalna oś czasu", "column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć", "column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia", "column_header.hide_settings": "Ukryj ustawienia",
@@ -109,6 +110,7 @@
"navigation_bar.info": "Szczegółowe informacje", "navigation_bar.info": "Szczegółowe informacje",
"navigation_bar.logout": "Wyloguj", "navigation_bar.logout": "Wyloguj",
"navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy",
"navigation_bar.preferences": "Preferencje", "navigation_bar.preferences": "Preferencje",
"navigation_bar.public_timeline": "Oś czasu federacji", "navigation_bar.public_timeline": "Oś czasu federacji",
"notification.favourite": "{name} dodał Twój status do ulubionych", "notification.favourite": "{name} dodał Twój status do ulubionych",
@@ -153,8 +155,8 @@
"privacy.private.short": "Tylko dla śledzących", "privacy.private.short": "Tylko dla śledzących",
"privacy.public.long": "Widoczny na publicznych osiach czasu", "privacy.public.long": "Widoczny na publicznych osiach czasu",
"privacy.public.short": "Publiczny", "privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczne", "privacy.unlisted.short": "Niewidoczny",
"reply_indicator.cancel": "Anuluj", "reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze", "report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij", "report.submit": "Wyślij",

View File

@@ -28,7 +28,7 @@ export default function reports(state = initialState, action) {
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet()); map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
map.setIn(['new', 'comment'], ''); map.setIn(['new', 'comment'], '');
} else { } else if (action.status) {
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
} }
}); });

View File

@@ -2,10 +2,15 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from '../actions/pin_statuses';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { import {
FAVOURITE_SUCCESS, FAVOURITE_SUCCESS,
UNFAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS,
PIN_SUCCESS,
UNPIN_SUCCESS,
} from '../actions/interactions'; } from '../actions/interactions';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@@ -14,6 +19,11 @@ const initialState = ImmutableMap({
loaded: false, loaded: false,
items: ImmutableList(), items: ImmutableList(),
}), }),
pins: ImmutableMap({
next: null,
loaded: false,
items: ImmutableList(),
}),
}); });
const normalizeList = (state, listType, statuses, next) => { const normalizeList = (state, listType, statuses, next) => {
@@ -53,6 +63,12 @@ export default function statusLists(state = initialState, action) {
return prependOneToList(state, 'favourites', action.status); return prependOneToList(state, 'favourites', action.status);
case UNFAVOURITE_SUCCESS: case UNFAVOURITE_SUCCESS:
return removeOneFromList(state, 'favourites', action.status); return removeOneFromList(state, 'favourites', action.status);
case PINNED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'pins', action.statuses, action.next);
case PIN_SUCCESS:
return prependOneToList(state, 'pins', action.status);
case UNPIN_SUCCESS:
return removeOneFromList(state, 'pins', action.status);
default: default:
return state; return state;
} }

View File

@@ -36,6 +36,9 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from '../actions/pin_statuses';
import { SEARCH_FETCH_SUCCESS } from '../actions/search'; import { SEARCH_FETCH_SUCCESS } from '../actions/search';
import emojify from '../emoji'; import emojify from '../emoji';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -138,6 +141,7 @@ export default function statuses(state = initialState, action) {
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS:
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:
case PINNED_STATUSES_FETCH_SUCCESS:
case SEARCH_FETCH_SUCCESS: case SEARCH_FETCH_SUCCESS:
return normalizeStatuses(state, action.statuses); return normalizeStatuses(state, action.statuses);
case TIMELINE_DELETE: case TIMELINE_DELETE:

View File

@@ -1,4 +1,21 @@
import loadPolyfills from '../mastodon/load_polyfills'; import loadPolyfills from '../mastodon/load_polyfills';
import ready from '../mastodon/ready';
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
ready(() => {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
});
});
function main() { function main() {
const { length } = require('stringz'); const { length } = require('stringz');
@@ -6,13 +23,13 @@ function main() {
const { delegate } = require('rails-ujs'); const { delegate } = require('rails-ujs');
const emojify = require('../mastodon/emoji').default; const emojify = require('../mastodon/emoji').default;
const { getLocale } = require('../mastodon/locales'); const { getLocale } = require('../mastodon/locales');
const ready = require('../mastodon/ready').default;
const { localeData } = getLocale(); const { localeData } = getLocale();
localeData.forEach(IntlRelativeFormat.__addLocaleData); localeData.forEach(IntlRelativeFormat.__addLocaleData);
ready(() => { ready(() => {
const locale = document.documentElement.lang; const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, { const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@@ -20,6 +37,7 @@ function main() {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}); });
const relativeFormat = new IntlRelativeFormat(locale); const relativeFormat = new IntlRelativeFormat(locale);
[].forEach.call(document.querySelectorAll('.emojify'), (content) => { [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
@@ -29,12 +47,14 @@ function main() {
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => { [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime); const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate; content.title = formattedDate;
content.textContent = formattedDate; content.textContent = formattedDate;
}); });
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
content.title = dateTimeFormat.format(datetime); content.title = dateTimeFormat.format(datetime);
content.textContent = relativeFormat.format(datetime); content.textContent = relativeFormat.format(datetime);
}); });
@@ -45,10 +65,6 @@ function main() {
window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
}); });
}); });
if (window.parent) {
window.parent.postMessage(['setHeight', document.getElementsByTagName('html')[0].scrollHeight], '*');
}
}); });
delegate(document, '.video-player video', 'click', ({ target }) => { delegate(document, '.video-player video', 'click', ({ target }) => {
@@ -77,6 +93,7 @@ function main() {
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content'); const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') { if (contentEl.style.display === 'block') {
contentEl.style.display = 'none'; contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0; target.parentNode.style.marginBottom = 0;
@@ -84,11 +101,13 @@ function main() {
contentEl.style.display = 'block'; contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null; target.parentNode.style.marginBottom = null;
} }
return false; return false;
}); });
delegate(document, '.account_display_name', 'input', ({ target }) => { delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter'); const nameCounter = document.querySelector('.name-counter');
if (nameCounter) { if (nameCounter) {
nameCounter.textContent = 30 - length(target.value); nameCounter.textContent = 30 - length(target.value);
} }
@@ -96,6 +115,7 @@ function main() {
delegate(document, '.account_note', 'input', ({ target }) => { delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter'); const noteCounter = document.querySelector('.note-counter');
if (noteCounter) { if (noteCounter) {
noteCounter.textContent = 160 - length(target.value); noteCounter.textContent = 160 - length(target.value);
} }
@@ -105,6 +125,7 @@ function main() {
const avatar = document.querySelector('.card.compact .avatar img'); const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
avatar.src = url; avatar.src = url;
}); });
@@ -112,6 +133,7 @@ function main() {
const header = document.querySelector('.card.compact'); const header = document.querySelector('.card.compact');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
header.style.backgroundImage = `url(${url})`; header.style.backgroundImage = `url(${url})`;
}); });
} }

View File

@@ -190,11 +190,15 @@
.filters { .filters {
display: flex; display: flex;
margin-bottom: 20px; flex-wrap: wrap;
.filter-subset { .filter-subset {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 40px; margin: 0 40px 10px 0;
&:last-child {
margin-bottom: 20px;
}
ul { ul {
margin-top: 5px; margin-top: 5px;

View File

@@ -45,6 +45,7 @@ body {
&.embed { &.embed {
background: transparent; background: transparent;
margin: 0; margin: 0;
padding-bottom: 0;
.container { .container {
position: absolute; position: absolute;

View File

@@ -1622,6 +1622,10 @@
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
&:last-child {
padding: 0 15px 0 0;
}
} }
.column-back-button__icon { .column-back-button__icon {
@@ -3801,6 +3805,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer; cursor: pointer;
margin-top: 8px; margin-top: 8px;
position: relative; position: relative;
border: 0;
display: block;
} }
.media-spoiler-video-play-icon { .media-spoiler-video-play-icon {
@@ -3966,6 +3972,24 @@ noscript {
} }
} }
.embed-modal {
max-width: 80vw;
max-height: 80vh;
h4 {
padding: 30px;
font-weight: 500;
font-size: 16px;
text-align: center;
}
.embed-modal__container {
padding: 10px;
.hint {
margin-bottom: 15px;
}
.embed-modal__html { .embed-modal__html {
color: $ui-secondary-color; color: $ui-secondary-color;
outline: 0; outline: 0;
@@ -4000,26 +4024,11 @@ noscript {
} }
} }
.embed-modal {
h4 {
padding: 30px;
font-weight: 500;
font-size: 16px;
text-align: center;
}
.hint {
margin-bottom: 15px;
}
}
.embed-modal__container {
padding: 10px;
}
.embed-modal__iframe { .embed-modal__iframe {
width: 100%; width: 400px;
min-width: 400px; max-width: 100%;
overflow: hidden; overflow: hidden;
border: 0; border: 0;
} }
}
}

View File

@@ -403,29 +403,23 @@
.embed { .embed {
.activity-stream { .activity-stream {
border-radius: 4px;
box-shadow: none; box-shadow: none;
.entry { .entry {
&:last-child {
border-radius: 0 0 4px 4px;
}
&:first-child { .detailed-status.light {
border-radius: 4px 4px 0 0; display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
&:last-child { .detailed-status__display-name {
border-radius: 4px; flex: 1;
} margin: 0 5px 15px 0;
}
}
}
} }
.button.button-secondary.logo-button { .button.button-secondary.logo-button {
position: absolute; flex: 0 auto;
right: 14px;
top: 14px;
font-size: 14px; font-size: 14px;
svg { svg {
@@ -451,3 +445,12 @@
} }
} }
} }
.status__content,
.detailed-status__meta {
flex: 100%;
}
}
}
}
}

View File

@@ -3,7 +3,6 @@
max-width: 100%; max-width: 100%;
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 20px;
th, th,
td { td {
@@ -43,17 +42,15 @@
font-weight: 500; font-weight: 500;
} }
&.inline-table { &.inline-table > tbody > tr:nth-child(odd) > td,
td, &.inline-table > tbody > tr:nth-child(odd) > th {
th {
padding: 8px 2px;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: transparent; background: transparent;
} }
} }
.table-wrapper {
overflow: auto;
margin-bottom: 20px;
} }
samp { samp {

View File

@@ -33,7 +33,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def status_params def status_params
{ {
uri: @object['id'], uri: @object['id'],
url: @object['url'] || @object['id'], url: object_url || @object['id'],
account: @account, account: @account,
text: text_from_content || '', text: text_from_content || '',
language: language_from_content, language: language_from_content,
@@ -147,6 +147,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@object['contentMap'].keys.first @object['contentMap'].keys.first
end end
def object_url
return if @object['url'].blank?
value = first_of_value(@object['url'])
return value if value.is_a?(String)
value['href']
end
def language_map? def language_map?
@object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
end end

View File

@@ -96,12 +96,14 @@ class ActivityPub::TagManager
when 'Account' when 'Account'
klass.find_local(uri_to_local_id(uri, :username)) klass.find_local(uri_to_local_id(uri, :username))
else else
klass.find_by(id: uri_to_local_id(uri)) StatusFinder.new(uri).status
end end
elsif ::TagManager.instance.local_id?(uri) elsif ::TagManager.instance.local_id?(uri)
klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
else else
klass.find_by(uri: uri.split('#').first) klass.find_by(uri: uri.split('#').first)
end end
rescue ActiveRecord::RecordNotFound
nil
end end
end end

View File

@@ -20,7 +20,16 @@ class LanguageDetector
private private
def detected_language_code def detected_language_code
result.language.to_sym if detected_language_reliable? iso6391(result.language).to_sym if detected_language_reliable?
end
def iso6391(bcp47)
iso639 = bcp47.split('-').first
# CLD3 returns grandfathered language code for Hebrew
return 'he' if iso639 == 'iw'
ISO_639.find(iso639).alpha2
end end
def result def result

View File

@@ -65,7 +65,7 @@ class OStatus::AtomSerializer
add_namespaces(entry) if root add_namespaces(entry) if root
append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type)) append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status))
append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'published', stream_entry.created_at.iso8601)
append_element(entry, 'updated', stream_entry.updated_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601)
append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
@@ -86,7 +86,7 @@ class OStatus::AtomSerializer
serialize_status_attributes(entry, stream_entry.status) serialize_status_attributes(entry, stream_entry.status)
end end
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry)) append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?

View File

@@ -49,12 +49,17 @@ class TagManager
def unique_tag_to_local_id(tag, expected_type) def unique_tag_to_local_id(tag, expected_type)
return nil unless local_id?(tag) return nil unless local_id?(tag)
if ActivityPub::TagManager.instance.local_uri?(tag)
ActivityPub::TagManager.instance.uri_to_local_id(tag)
else
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
return matches[1] unless matches.nil? return matches[1] unless matches.nil?
end end
end
def local_id?(id) def local_id?(id)
id.start_with?("tag:#{Rails.configuration.x.local_domain}") id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
end end
def web_domain?(domain) def web_domain?(domain)
@@ -92,7 +97,7 @@ class TagManager
when :person when :person
account_url(target) account_url(target)
when :note, :comment, :activity when :note, :comment, :activity
unique_tag(target.created_at, target.id, 'Status') target.uri || unique_tag(target.created_at, target.id, 'Status')
end end
end end

View File

@@ -24,6 +24,8 @@ class Form::AdminSettings
:open_deletion=, :open_deletion=,
:timeline_preview, :timeline_preview,
:timeline_preview=, :timeline_preview=,
:bootstrap_timeline_accounts,
:bootstrap_timeline_accounts=,
to: Setting to: Setting
) )
end end

View File

@@ -22,6 +22,8 @@ class Report < ApplicationRecord
scope :unresolved, -> { where(action_taken: false) } scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) } scope :resolved, -> { where(action_taken: true) }
validates :comment, length: { maximum: 1000 }
def statuses def statuses
Status.where(id: status_ids).includes(:account, :media_attachments, :mentions) Status.where(id: status_ids).includes(:account, :media_attachments, :mentions)
end end

View File

@@ -22,6 +22,7 @@
# reblogs_count :integer default(0), not null # reblogs_count :integer default(0), not null
# language :string # language :string
# conversation_id :integer # conversation_id :integer
# local :boolean
# #
class Status < ApplicationRecord class Status < ApplicationRecord
@@ -62,8 +63,8 @@ class Status < ApplicationRecord
default_scope { recent } default_scope { recent }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where.not(uri: nil) } scope :remote, -> { where(local: false).or(where.not(uri: nil)) }
scope :local, -> { where(uri: nil) } scope :local, -> { where(local: true).or(where(uri: nil)) }
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
@@ -84,7 +85,7 @@ class Status < ApplicationRecord
end end
def local? def local?
uri.nil? attributes['local'] || uri.nil?
end end
def reblog? def reblog?
@@ -131,11 +132,14 @@ class Status < ApplicationRecord
!sensitive? && media_attachments.any? !sensitive? && media_attachments.any?
end end
after_create :store_uri, if: :local?
before_validation :prepare_contents, if: :local? before_validation :prepare_contents, if: :local?
before_validation :set_reblog before_validation :set_reblog
before_validation :set_visibility before_validation :set_visibility
before_validation :set_conversation before_validation :set_conversation
before_validation :set_sensitivity before_validation :set_sensitivity
before_validation :set_local
class << self class << self
def not_in_filtered_languages(account) def not_in_filtered_languages(account)
@@ -253,6 +257,10 @@ class Status < ApplicationRecord
private private
def store_uri
update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
end
def prepare_contents def prepare_contents
text&.strip! text&.strip!
spoiler_text&.strip! spoiler_text&.strip!
@@ -292,4 +300,8 @@ class Status < ApplicationRecord
thread.account_id thread.account_id
end end
end end
def set_local
self.local = account.local?
end
end end

View File

@@ -4,12 +4,12 @@ class AccountRelationshipsPresenter
attr_reader :following, :followed_by, :blocking, attr_reader :following, :followed_by, :blocking,
:muting, :requested, :domain_blocking :muting, :requested, :domain_blocking
def initialize(account_ids, current_account_id) def initialize(account_ids, current_account_id, options = {})
@following = Account.following_map(account_ids, current_account_id) @following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
@followed_by = Account.followed_by_map(account_ids, current_account_id) @followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
@blocking = Account.blocking_map(account_ids, current_account_id) @blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
@muting = Account.muting_map(account_ids, current_account_id) @muting = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {})
@requested = Account.requested_map(account_ids, current_account_id) @requested = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {})
@domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {})
end end
end end

View File

@@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
include RoutingHelper include RoutingHelper
attributes :id, :type, :following, :followers, attributes :id, :type, :following, :followers,
:inbox, :outbox, :shared_inbox, :inbox, :outbox,
:preferred_username, :name, :summary, :preferred_username, :name, :summary,
:url, :manually_approves_followers :url, :manually_approves_followers
@@ -24,6 +24,18 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
end end
end end
class EndpointsSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :shared_inbox
def shared_inbox
inbox_url
end
end
has_one :endpoints, serializer: EndpointsSerializer
has_one :icon, serializer: ImageSerializer, if: :avatar_exists? has_one :icon, serializer: ImageSerializer, if: :avatar_exists?
has_one :image, serializer: ImageSerializer, if: :header_exists? has_one :image, serializer: ImageSerializer, if: :header_exists?
@@ -51,8 +63,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
account_outbox_url(object) account_outbox_url(object)
end end
def shared_inbox def endpoints
inbox_url object
end end
def preferred_username def preferred_username

View File

@@ -40,13 +40,12 @@ class OEmbedSerializer < ActiveModel::Serializer
attributes = { attributes = {
src: embed_short_account_status_url(object.account, object), src: embed_short_account_status_url(object.account, object),
class: 'mastodon-embed', class: 'mastodon-embed',
frameborder: '0', style: 'max-width: 100%; border: 0',
scrolling: 'no',
width: width, width: width,
height: height, height: height,
} }
content_tag :iframe, nil, attributes content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js'), async: true)
end end
def width def width

View File

@@ -6,13 +6,14 @@ class ActivityPub::ProcessAccountService < BaseService
# Should be called with confirmed valid JSON # Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain # and WebFinger-resolved username and domain
def call(username, domain, json) def call(username, domain, json)
return unless json['inbox'].present? return if json['inbox'].blank?
@json = json @json = json
@uri = @json['id'] @uri = @json['id']
@username = username @username = username
@domain = domain @domain = domain
@account = Account.find_by(uri: @uri) @account = Account.find_by(uri: @uri)
@collections = {}
create_account if @account.nil? create_account if @account.nil?
upgrade_account if @account.ostatus? upgrade_account if @account.ostatus?
@@ -42,16 +43,19 @@ class ActivityPub::ProcessAccountService < BaseService
@account.protocol = :activitypub @account.protocol = :activitypub
@account.inbox_url = @json['inbox'] || '' @account.inbox_url = @json['inbox'] || ''
@account.outbox_url = @json['outbox'] || '' @account.outbox_url = @json['outbox'] || ''
@account.shared_inbox_url = @json['sharedInbox'] || '' @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
@account.followers_url = @json['followers'] || '' @account.followers_url = @json['followers'] || ''
@account.url = @json['url'] || @uri @account.url = url || @uri
@account.display_name = @json['name'] || '' @account.display_name = @json['name'] || ''
@account.note = @json['summary'] || '' @account.note = @json['summary'] || ''
@account.avatar_remote_url = image_url('icon') @account.avatar_remote_url = image_url('icon') unless skip_download?
@account.header_remote_url = image_url('image') @account.header_remote_url = image_url('image') unless skip_download?
@account.public_key = public_key || '' @account.public_key = public_key || ''
@account.locked = @json['manuallyApprovesFollowers'] || false @account.locked = @json['manuallyApprovesFollowers'] || false
@account.save! @account.statuses_count = outbox_total_items if outbox_total_items.present?
@account.following_count = following_total_items if following_total_items.present?
@account.followers_count = followers_total_items if followers_total_items.present?
@account.save_with_optional_media!
end end
def upgrade_account def upgrade_account
@@ -62,7 +66,7 @@ class ActivityPub::ProcessAccountService < BaseService
value = first_of_value(@json[key]) value = first_of_value(@json[key])
return if value.nil? return if value.nil?
return @json[key]['url'] if @json[key].is_a?(Hash) return value['url'] if value.is_a?(Hash)
image = fetch_resource(value) image = fetch_resource(value)
image['url'] if image image['url'] if image
@@ -78,6 +82,43 @@ class ActivityPub::ProcessAccountService < BaseService
key['publicKeyPem'] if key key['publicKeyPem'] if key
end end
def url
return if @json['url'].blank?
value = first_of_value(@json['url'])
return value if value.is_a?(String)
value['href']
end
def outbox_total_items
collection_total_items('outbox')
end
def following_total_items
collection_total_items('following')
end
def followers_total_items
collection_total_items('followers')
end
def collection_total_items(type)
return if @json[type].blank?
return @collections[type] if @collections.key?(type)
collection = fetch_resource(@json[type])
@collections[type] = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
rescue HTTP::Error, OpenSSL::SSL::SSLError
@collections[type] = nil
end
def skip_download?
@account.suspended? || domain_block&.reject_media?
end
def auto_suspend? def auto_suspend?
domain_block && domain_block.suspend? domain_block && domain_block.suspend?
end end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
class BootstrapTimelineService < BaseService
def call(source_account)
bootstrap_timeline_accounts.each do |target_account|
FollowService.new.call(source_account, target_account)
end
end
private
def bootstrap_timeline_accounts
return @bootstrap_timeline_accounts if defined?(@bootstrap_timeline_accounts)
@bootstrap_timeline_accounts = bootstrap_timeline_accounts_usernames.empty? ? admin_accounts : local_unlocked_accounts(bootstrap_timeline_accounts_usernames)
end
def bootstrap_timeline_accounts_usernames
@bootstrap_timeline_accounts_usernames ||= (Setting.bootstrap_timeline_accounts || '').split(',').map { |str| str.strip.gsub(/\A@/, '') }.reject(&:blank?)
end
def admin_accounts
User.admins
.includes(:account)
.where(accounts: { locked: false })
.map(&:account)
end
def local_unlocked_accounts(usernames)
Account.local
.where(username: usernames)
.where(locked: false)
end
end

View File

@@ -5,14 +5,14 @@ class FollowService < BaseService
# Follow a remote user, notify remote user about the follow # Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow # @param [Account] source_account From which to follow
# @param [String] uri User URI to follow in the form of username@domain # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
def call(source_account, uri) def call(source_account, uri)
target_account = ResolveRemoteAccountService.new.call(uri) target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri)
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
return if source_account.following?(target_account) return if source_account.following?(target_account) || source_account.requested?(target_account)
if target_account.locked? || target_account.activitypub? if target_account.locked? || target_account.activitypub?
request_follow(source_account, target_account) request_follow(source_account, target_account)

View File

@@ -27,9 +27,10 @@ class PostStatusService < BaseService
thread: in_reply_to, thread: in_reply_to,
sensitive: options[:sensitive], sensitive: options[:sensitive],
spoiler_text: options[:spoiler_text] || '', spoiler_text: options[:spoiler_text] || '',
visibility: options[:visibility], visibility: options[:visibility] || account.user&.setting_default_privacy,
language: detect_language_for(text, account), language: detect_language_for(text, account),
application: options[:application]) application: options[:application])
attach_media(status, media) attach_media(status, media)
end end

View File

@@ -41,7 +41,7 @@ class ProcessMentionsService < BaseService
NotifyService.new.call(mentioned_account, mention) NotifyService.new.call(mentioned_account, mention)
elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?) elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?)
NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id) NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
elsif mentioned_account.activitypub? && !mentioned_account.following?(status.account) elsif mentioned_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
end end
end end

View File

@@ -5,15 +5,18 @@ class UnsubscribeService < BaseService
return if account.hub_url.blank? return if account.hub_url.blank?
@account = account @account = account
begin
@response = build_request.perform @response = build_request.perform
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success? Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success?
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}"
end
@account.secret = '' @account.secret = ''
@account.subscription_expires_at = nil @account.subscription_expires_at = nil
@account.save! @account.save!
rescue HTTP::Error, OpenSSL::SSL::SSLError
Rails.logger.debug "PuSH subscription request for #{@account.acct} could not be made due to HTTP or SSL error"
end end
private private

View File

@@ -1,3 +1,4 @@
.table-wrapper
%table.table %table.table
%tbody %tbody
%tr %tr

View File

@@ -50,6 +50,7 @@
%button= t('admin.accounts.search') %button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= @account.acct = @account.acct
.table-wrapper
%table.table %table.table
%tbody %tbody
%tr %tr
@@ -81,7 +82,6 @@
%th= t('.targeted_reports') %th= t('.targeted_reports')
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id) %td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
%div{ style: 'float: right' } %div{ style: 'float: right' }
- if @account.local? - if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('admin.domain_blocks.title') = t('admin.domain_blocks.title')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('admin.instances.title') = t('admin.instances.title')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -10,6 +10,7 @@
= form_tag do = form_tag do
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -28,5 +28,10 @@
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
%hr/
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
.actions .actions
= f.button :button, t('generic.save_changes'), type: :submit = f.button :button, t('generic.save_changes'), type: :submit

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('admin.subscriptions.title') = t('admin.subscriptions.title')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -1,6 +1,7 @@
%h6= t 'sessions.title' %h6= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation' %p.muted-hint= t 'sessions.explanation'
.table-wrapper
%table.table.inline-table %table.table.inline-table
%thead %thead
%tr %tr

View File

@@ -21,13 +21,13 @@
= stylesheet_pack_tag 'common', media: 'all' = stylesheet_pack_tag 'common', media: 'all'
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
= javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/compose.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag 'emojione_picker', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' %link{ href: asset_pack_path('emojione_picker.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
= javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
= csrf_meta_tags = csrf_meta_tags

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('doorkeeper.authorized_applications.index.title') = t('doorkeeper.authorized_applications.index.title')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('doorkeeper.applications.index.title') = t('doorkeeper.applications.index.title')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -3,6 +3,7 @@
%p.hint= t('applications.warning') %p.hint= t('applications.warning')
.table-wrapper
%table.table %table.table
%tbody %tbody
%tr %tr

View File

@@ -1,6 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('settings.export') = t('settings.export')
.table-wrapper
%table.table %table.table
%tbody %tbody
%tr %tr

View File

@@ -12,6 +12,7 @@
%p= t('followers.explanation_html') %p= t('followers.explanation_html')
%p= t('followers.true_privacy_html') %p= t('followers.true_privacy_html')
.table-wrapper
%table.table %table.table
%thead %thead
%tr %tr

View File

@@ -13,7 +13,7 @@
selected: I18n.locale selected: I18n.locale
= f.input :filtered_languages, = f.input :filtered_languages,
collection: I18n.available_locales, collection: filterable_languages,
wrapper: :with_block_label, wrapper: :with_block_label,
include_blank: false, include_blank: false,
label_method: lambda { |locale| human_locale(locale) }, label_method: lambda { |locale| human_locale(locale) },

View File

@@ -1,9 +1,4 @@
.detailed-status.light .detailed-status.light
- if embedded_view?
= link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
= t('accounts.follow')
= link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do
%div %div
.avatar .avatar
@@ -12,6 +7,11 @@
%strong.p-name.emojify= display_name(status.account) %strong.p-name.emojify= display_name(status.account)
%span= acct(status.account) %span= acct(status.account)
- if embedded_view?
= link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
= render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
= t('accounts.follow')
.status__content.p-name.emojify< .status__content.p-name.emojify<
- if status.spoiler_text? - if status.spoiler_text?
%p{ style: 'margin-bottom: 0' }< %p{ style: 'margin-bottom: 0' }<

View File

@@ -1,13 +1,13 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!<p> <p>Bonjorn <%= @resource.email %>!<p>
<p>Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)</p> <p>Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)</p>
<p>Per confirmar vòstre inscripcion, mercés de clicar sul ligam seguent : <br> <p>Per confirmar vòstra inscripcion, mercés de clicar sul ligam seguent:<br>
<%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %></p> <%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
<p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.</p> <p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.</p>
<p>Pensatz tanben a gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>.</p> <p>Pensatz tanben de gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>.</p>
<p>Amistosament,</p> <p>Amistosament,</p>

View File

@@ -1,13 +1,13 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :) Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)
er confirmar vòstre inscripcion, mercés de clicar sul ligam seguent : er confirmar vòstra inscripcion, mercés de clicar sul ligam seguent:
<%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %> <%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %>
Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina. Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.
Pensatz tanben a gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>. Pensatz tanben de gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>.
Amistosament, Amistosament,

View File

@@ -1,3 +1,3 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!</p> <p>Bonjorn <%= @resource.email %>!</p>
<p>Vos contactem per vos avisar que vòstre senhal per Mastodon es ben estat cambiat.</p> <p>Vos contactem per vos avisar quavèm ben cambiat vòstre senhal Mastodon.</p>

View File

@@ -1,3 +1,3 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Vos contactem per vos avisar que vòstre senhal per Mastodon es ben estat cambiat. Vos contactem per vos avisar quavèm ben cambiat vòstre senhal Mastodon.

View File

@@ -1,8 +1,8 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!</p> <p>Bonjorn <%= @resource.email %>!</p>
<p>Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> <p>Qualquun a demandat la reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p>
<p><%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %></p> <p><%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>Savètz pas res demandat, fasquètz pas cas a aqueste corrièl.</p> <p>Savètz pas res demandat, fasquètz pas cas a aqueste corrièl.</p>
<p>Vòstre senhal cambiarà pas se clicatz pas sul ligam e que ne causissètz pas un nòu.</p> <p>Vòstre senhal cambiarà pas se clicatz pas sul ligam e que ne causissètz pas un novèl.</p>

View File

@@ -1,8 +1,8 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> Qualquun a demandat la reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p>
<%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %> <%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %>
Savètz pas res demandat, fasquètz pas cas a aqueste corrièl. Savètz pas res demandat, fasquètz pas cas a aqueste corrièl.
Vòstre senhal cambiarà pas se clicatz pas sul ligam e que ne causissètz pas un nòu. Vòstre senhal cambiarà pas se clicatz pas sul ligam e que ne causissètz pas un novèl.

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
class BootstrapTimelineWorker
include Sidekiq::Worker
def perform(account_id)
BootstrapTimelineService.new.call(Account.find(account_id))
end
end

View File

@@ -6,7 +6,7 @@ class Pubsubhubbub::DistributionWorker
sidekiq_options queue: 'push' sidekiq_options queue: 'push'
def perform(stream_entry_ids) def perform(stream_entry_ids)
stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status&.direct_visibility? } stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.direct_visibility? }
return if stream_entries.empty? return if stream_entries.empty?

View File

@@ -3,7 +3,7 @@
class Pubsubhubbub::SubscribeWorker class Pubsubhubbub::SubscribeWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false, unique_retry: true
sidekiq_retry_in do |count| sidekiq_retry_in do |count|
case count case count

View File

@@ -10,6 +10,7 @@ require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder' require_relative '../lib/paperclip/video_transcoder'
require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/version'
require_relative '../lib/mastodon/unique_retry_job_middleware'
Dotenv::Railtie.load Dotenv::Railtie.load

View File

@@ -73,7 +73,7 @@ Rails.application.configure do
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
# E-mails # E-mails
config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS') } config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost') }
config.action_mailer.smtp_settings = { config.action_mailer.smtp_settings = {
:port => ENV['SMTP_PORT'], :port => ENV['SMTP_PORT'],

View File

@@ -12,6 +12,7 @@ Rails.application.configure do
config.x.web_domain = web_host config.x.web_domain = web_host
config.x.use_https = https config.x.use_https = https
config.x.use_s3 = ENV['S3_ENABLED'] == 'true' config.x.use_s3 = ENV['S3_ENABLED'] == 'true'
config.x.use_swift = ENV['SWIFT_ENABLED'] == 'true'
config.x.alternate_domains = alternate_domains.split(/\s*,\s*/) config.x.alternate_domains = alternate_domains.split(/\s*,\s*/)

View File

@@ -40,6 +40,21 @@ if ENV['S3_ENABLED'] == 'true'
Paperclip::Attachment.default_options[:url] = ':s3_alias_url' Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST'] Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
end end
elsif ENV['SWIFT_ENABLED'] == 'true'
Paperclip::Attachment.default_options.merge!(
path: ':class/:attachment/:id_partition/:style/:filename',
storage: :fog,
fog_credentials: {
provider: 'OpenStack',
openstack_username: ENV.fetch('SWIFT_USERNAME'),
openstack_tenant: ENV.fetch('SWIFT_TENANT'),
openstack_api_key: ENV.fetch('SWIFT_PASSWORD'),
openstack_auth_url: ENV.fetch('SWIFT_AUTH_URL'),
},
fog_directory: ENV.fetch('SWIFT_CONTAINER'),
fog_host: ENV.fetch('SWIFT_OBJECT_URL'),
fog_public: true
)
else else
Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename' Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename'
Paperclip::Attachment.default_options[:url] = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename' Paperclip::Attachment.default_options[:url] = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename'

View File

@@ -9,8 +9,14 @@ end
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = redis_params config.redis = redis_params
config.client_middleware do |chain|
chain.add Mastodon::UniqueRetryJobMiddleware
end
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = redis_params config.redis = redis_params
config.client_middleware do |chain|
chain.add Mastodon::UniqueRetryJobMiddleware
end
end end

View File

@@ -50,7 +50,7 @@ ar:
about_x_months: "%{count} شهر" about_x_months: "%{count} شهر"
about_x_years: "%{count} سنة" about_x_years: "%{count} سنة"
almost_x_years: "%{count} سنوات" almost_x_years: "%{count} سنوات"
half_a_minute: Just now half_a_minute: الآن
less_than_x_minutes: "%{count} د" less_than_x_minutes: "%{count} د"
less_than_x_seconds: الآن less_than_x_seconds: الآن
over_x_years: "%{count} سنين" over_x_years: "%{count} سنين"
@@ -94,7 +94,7 @@ ar:
one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418" one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418" other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
favourite: favourite:
body: 'Your status was favourited by %{name}:' body: 'أُعجب %{name} بمنشورك'
subject: "%{name} favourited your status" subject: "%{name} favourited your status"
follow: follow:
body: "%{name} من متتبعيك الآن !" body: "%{name} من متتبعيك الآن !"
@@ -108,6 +108,17 @@ ar:
reblog: reblog:
body: 'Your status was boosted by %{name}:' body: 'Your status was boosted by %{name}:'
subject: "%{name} boosted your status" subject: "%{name} boosted your status"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: التالي next: التالي
prev: السابق prev: السابق
@@ -148,7 +159,7 @@ ar:
enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in." instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
setup: Set up setup: تنشيط
wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز. wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز.
users: users:
invalid_email: عنوان البريد الإلكتروني غير صالح invalid_email: عنوان البريد الإلكتروني غير صالح

View File

@@ -108,6 +108,17 @@ bg:
reblog: reblog:
body: 'Твоята публикация беше споделена от %{name}:' body: 'Твоята публикация беше споделена от %{name}:'
subject: "%{name} сподели публикацията ти" subject: "%{name} сподели публикацията ти"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Напред next: Напред
prev: Назад prev: Назад

View File

@@ -340,6 +340,17 @@ ca:
reblog: reblog:
body: "%{name} ha retootejat el teu estat" body: "%{name} ha retootejat el teu estat"
subject: "%{name} ha retootejat el teu estat" subject: "%{name} ha retootejat el teu estat"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Pròxim next: Pròxim
prev: Anterior prev: Anterior

View File

@@ -12,15 +12,15 @@ de:
source_code: Quellcode source_code: Quellcode
status_count_after: Beiträge verfassten status_count_after: Beiträge verfassten
status_count_before: die status_count_before: die
user_count_after: Benutzer user_count_after: Profile
user_count_before: Heimat für user_count_before: Heimat für
accounts: accounts:
follow: Folgen follow: Folgen
followers: Folgende followers: Folgende
following: Folgt following: Folgt
nothing_here: Hier gibt es nichts! nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt people_followed_by: Profile, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen people_who_follow: Profile, die %{name} folgen
posts: Beiträge posts: Beiträge
remote_follow: Folgen remote_follow: Folgen
unfollow: Entfolgen unfollow: Entfolgen
@@ -67,7 +67,7 @@ de:
title: Konten title: Konten
undo_silenced: Stummschaltung zurücknehmen undo_silenced: Stummschaltung zurücknehmen
undo_suspension: Sperre zurücknehmen undo_suspension: Sperre zurücknehmen
username: Benutzername username: Profilname
web: Web web: Web
domain_blocks: domain_blocks:
add_new: Neu hinzufügen add_new: Neu hinzufügen
@@ -124,7 +124,7 @@ de:
settings: settings:
contact_information: contact_information:
email: Eine öffentliche E-Mail-Adresse angeben email: Eine öffentliche E-Mail-Adresse angeben
username: Einen Benutzernamen angeben username: Einen Profilnamen angeben
registrations: registrations:
closed_message: closed_message:
desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen
@@ -208,7 +208,7 @@ de:
following: Folgeliste following: Folgeliste
muting: Stummschaltungsliste muting: Stummschaltungsliste
upload: Hochladen upload: Hochladen
landing_strip_html: "<strong>%{name}</strong> ist ein Benutzer auf %{link_to_root_path}. Du kannst ihm folgen oder mit ihm interagieren, sofern du ein Konto irgendwo in der Fediverse hast." landing_strip_html: "<strong>%{name}</strong> hat ein Profil auf %{link_to_root_path}. Du kannst folgen oder interagieren, sofern du ein Konto irgendwo im Fediversum hast."
landing_strip_signup_html: Wenn nicht, kannst du dich <a href="%{sign_up_path}">hier anmelden</a>. landing_strip_signup_html: Wenn nicht, kannst du dich <a href="%{sign_up_path}">hier anmelden</a>.
media_attachments: media_attachments:
validations: validations:
@@ -239,12 +239,23 @@ de:
reblog: reblog:
body: 'Dein Beitrag wurde von %{name} geteilt:' body: 'Dein Beitrag wurde von %{name} geteilt:'
subject: "%{name} teilte deinen Beitrag." subject: "%{name} teilte deinen Beitrag."
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Vorwärts next: Vorwärts
prev: Zurück prev: Zurück
truncate: "&hellip;" truncate: "&hellip;"
remote_follow: remote_follow:
acct: Dein Nutzername@Domain, von dem aus du dieser Person folgen möchtest. acct: Dein Profilname@Domain, von dem aus du dieser Person folgen möchtest.
missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden. missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
proceed: Weiter proceed: Weiter
prompt: 'Du wirst dieser Person folgen:' prompt: 'Du wirst dieser Person folgen:'

View File

@@ -77,7 +77,7 @@ de:
invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein. invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein.
invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig. invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig.
invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft. invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft.
invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieser Benutzer existiert nicht. invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieses Profil existiert nicht.
invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft. invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft.
invalid_token: invalid_token:
expired: Der Zugriffstoken ist abgelaufen expired: Der Zugriffstoken ist abgelaufen
@@ -108,6 +108,6 @@ de:
application: application:
title: OAuth-Autorisierung nötig title: OAuth-Autorisierung nötig
scopes: scopes:
follow: Nutzer folgen, blocken, entblocken und entfolgen follow: Profil folgen, blocken, entblocken und entfolgen
read: deine Daten lesen read: deine Daten lesen
write: Beiträge von deinem Konto aus veröffentlichen write: Beiträge von deinem Konto aus veröffentlichen

View File

@@ -3,8 +3,10 @@ fa:
activerecord: activerecord:
attributes: attributes:
doorkeeper/application: doorkeeper/application:
name: Name name: Application name
redirect_uri: Redirect URI redirect_uri: Redirect URI
scopes: Scopes
website: Application website
errors: errors:
models: models:
doorkeeper/application: doorkeeper/application:
@@ -33,18 +35,22 @@ fa:
redirect_uri: Use one line per URI redirect_uri: Use one line per URI
scopes: Separate scopes with spaces. Leave blank to use the default scopes. scopes: Separate scopes with spaces. Leave blank to use the default scopes.
index: index:
application: Application
callback_url: Callback URL callback_url: Callback URL
delete: Delete
name: Name name: Name
new: New Application new: New application
scopes: Scopes
show: Show
title: Your applications title: Your applications
new: new:
title: New Application title: New application
show: show:
actions: Actions actions: Actions
application_id: Application Id application_id: Client key
callback_urls: Callback urls callback_urls: Callback URLs
scopes: Scopes scopes: Scopes
secret: Secret secret: Client secret
title: 'Application: %{name}' title: 'Application: %{name}'
authorizations: authorizations:
buttons: buttons:

View File

@@ -5,6 +5,8 @@ oc:
doorkeeper/application: doorkeeper/application:
name: Nom name: Nom
redirect_uri: URL de redireccion redirect_uri: URL de redireccion
scopes: Encastres
website: Aplicacion web
errors: errors:
models: models:
doorkeeper/application: doorkeeper/application:
@@ -33,9 +35,13 @@ oc:
redirect_uri: Utilizatz una linha per URI redirect_uri: Utilizatz una linha per URI
scopes: Separatz los encastres amb despacis. Daissatz void per utilizar lencastre per defaut. scopes: Separatz los encastres amb despacis. Daissatz void per utilizar lencastre per defaut.
index: index:
application: Aplicacion
callback_url: URL de rapèl callback_url: URL de rapèl
delete: Suprimir
name: Nom name: Nom
new: Nòva aplicacion new: Nòva aplicacion
scopes: Encastres
show: Veire
title: Vòstras aplicacions title: Vòstras aplicacions
new: new:
title: Nòva aplicacion title: Nòva aplicacion

View File

@@ -167,6 +167,9 @@ en:
unresolved: Unresolved unresolved: Unresolved
view: View view: View
settings: settings:
bootstrap_timeline_accounts:
desc_html: Separate multiple usernames by comma. Only local and unlocked accounts will work. Default when empty is all local admins.
title: Default follows for new users
contact_information: contact_information:
email: Business e-mail email: Business e-mail
username: Contact username username: Contact username

View File

@@ -103,6 +103,17 @@ eo:
reblog: reblog:
body: "%{name} diskonigis vian mesaĝon:" body: "%{name} diskonigis vian mesaĝon:"
subject: "%{name} diskonigis vian mesaĝon" subject: "%{name} diskonigis vian mesaĝon"
number:
human:
decimal_units:
format: "%n%u"
units:
billion: B
million: M
quadrillion: Q
thousand: K
trillion: T
unit: ''
pagination: pagination:
next: Sekva next: Sekva
prev: Malsekva prev: Malsekva

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