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
# 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=
SECRET_KEY_BASE=
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
# 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
VAPID_PRIVATE_KEY=
@@ -98,6 +98,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT=
# 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
# S3_CLOUDFRONT_HOST=

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ class AccountsController < ApplicationController
return
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 = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty?
@@ -22,7 +22,7 @@ class AccountsController < ApplicationController
format.atom do
@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
format.json do
@@ -33,6 +33,10 @@ class AccountsController < ApplicationController
private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
end
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested?

View File

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

View File

@@ -14,6 +14,16 @@ class Api::V1::AccountsController < Api::BaseController
def follow
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
end

View File

@@ -10,6 +10,12 @@ class Api::V1::FollowsController < Api::BaseController
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@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
end

View File

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

View File

@@ -12,8 +12,14 @@ module RoutingHelper
end
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
end
private
def use_storage?
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
end
end

View File

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

View File

@@ -1,6 +1,11 @@
import api from '../api';
import { updateTimeline } from './timelines';
import {
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
@@ -95,16 +100,20 @@ export function submitCompose() {
dispatch(submitComposeSuccess({ ...response.data }));
// 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 (getState().getIn(['timelines', 'community', 'loaded'])) {
dispatch(updateTimeline('community', { ...response.data }));
}
if (getState().getIn(['timelines', 'public', 'loaded'])) {
dispatch(updateTimeline('public', { ...response.data }));
}
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
}
}).catch(function (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 IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent {
@@ -26,6 +27,10 @@ export default class ScrollableList extends PureComponent {
trackScroll: true,
};
state = {
lastMouseMove: null,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
@@ -46,6 +51,14 @@ export default class ScrollableList extends PureComponent {
trailing: true,
});
handleMouseMove = throttle(() => {
this._lastMouseMove = new Date();
}, 300);
handleMouseLeave = () => {
this._lastMouseMove = null;
}
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
@@ -55,17 +68,20 @@ export default class ScrollableList extends PureComponent {
}
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
// 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 (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) {
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) {
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
@@ -95,7 +111,12 @@ export default class ScrollableList extends PureComponent {
getFirstChildKey (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;
}
@@ -108,6 +129,10 @@ export default class ScrollableList extends PureComponent {
this.props.onScrollToBottom();
}
_recentlyMoved () {
return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600);
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => {
@@ -143,7 +168,7 @@ export default class ScrollableList extends PureComponent {
if (isLoading || childrenCount > 0 || !emptyMessage) {
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}>
{prepend}

View File

@@ -134,7 +134,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
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(null);
}

View File

@@ -146,29 +146,29 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) {
if (sensitive) {
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}
<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>
</div>
</button>
);
} else {
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}
<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>
</div>
</button>
);
}
}
if (this.state.preview && !autoplay) {
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}
<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}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>
<StatusList

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
});
const mapStateToProps = state => ({
@@ -66,15 +67,16 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([
<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')) {
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([
<ColumnLink key='6' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='7' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]);
return (

View File

@@ -106,6 +106,7 @@ export default class Notifications extends React.PureComponent {
const scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading}
hasMore={hasMore}
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 ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
@@ -129,8 +130,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
);
}
renderLoading = () => {
return <ColumnLoading />;
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
}
renderError = (props) => {
@@ -158,7 +159,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
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 />}
</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.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
className='embed-modal__iframe'
scrolling='no'
frameBorder='0'
ref={this.setIframeRef}
title='preview'

View File

@@ -5,23 +5,23 @@ import spring from 'react-motion/lib/spring';
import BundleContainer from '../containers/bundle_container';
import BundleModalError from './bundle_modal_error';
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 {
MediaModal,
OnboardingModal,
VideoModal,
BoostModal,
ConfirmationModal,
ReportModal,
EmbedModal,
} from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = {
'MEDIA': MediaModal,
'MEDIA': () => Promise.resolve({ default: MediaModal }),
'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal,
'BOOST': BoostModal,
'CONFIRM': ConfirmationModal,
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
@@ -82,8 +82,8 @@ export default class ModalRoot extends React.PureComponent {
return { opacity: spring(0), scale: spring(0.98) };
}
renderLoading = () => {
return <ModalLoading />;
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
}
renderError = (props) => {
@@ -117,7 +117,7 @@ export default class ModalRoot extends React.PureComponent {
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<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})` }}>
<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} />}
</BundleContainer>
</div>

View File

@@ -35,6 +35,7 @@ import {
FavouritedStatuses,
Blocks,
Mutes,
PinnedStatuses,
} from './util/async-components';
// 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='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />

View File

@@ -34,6 +34,10 @@ export function GettingStarted () {
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
}
export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
}
export function AccountTimeline () {
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
}
@@ -78,26 +82,10 @@ export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
}
export function MediaModal () {
return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal');
}
export function OnboardingModal () {
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 () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}

View File

@@ -26,12 +26,12 @@
"bundle_modal_error.close": "Schließen",
"bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
"bundle_modal_error.retry": "Erneut versuchen",
"column.blocks": "Blockierte Benutzer",
"column.blocks": "Blockierte Profile",
"column.community": "Lokale Zeitleiste",
"column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen",
"column.home": "Startseite",
"column.mutes": "Stummgeschaltete Benutzer",
"column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz",
"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.lock": "gesperrt",
"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_loud": "{publish}!",
"compose_form.sensitive": "Medien als heikel markieren",
@@ -77,18 +77,18 @@
"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.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.public_timeline": "die öffentliche Zeitleiste",
"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.reject": "Ablehnen",
"getting_started.appsshort": "Anwendungen",
"getting_started.faq": "Häufig gestellte Fragen",
"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.userguide": "Nutzeranleitung",
"getting_started.userguide": "Bedienungsanleitung",
"home.column_settings.advanced": "Fortgeschritten",
"home.column_settings.basic": "Einfach",
"home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@@ -101,14 +101,14 @@
"loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden",
"navigation_bar.blocks": "Blockierte Benutzer",
"navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.community_timeline": "Lokale Zeitleiste",
"navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.favourites": "Favoriten",
"navigation_bar.follow_requests": "Folgeanfragen",
"navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Benutzer",
"navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste",
"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.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.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_six.admin": "Für deine Instanz ist {admin} zuständig.",
"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.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.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.skip": "Überspringen",
"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.private.long": "Beitrag nur an Folgende",
"privacy.private.short": "Privat",

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"account.block": "Blokiraj @{name}",
"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.follow": "Slijedi",
"account.followers": "Sljedbenici",
@@ -15,7 +15,7 @@
"account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile",
"account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Otkrij {domain}",
"account.unblock_domain": "Poništi sakrivanje {domain}",
"account.unfollow": "Prestani slijediti",
"account.unmute": "Poništi utišavanje @{name}",
"account.view_full_profile": "View full profile",
@@ -43,7 +43,7 @@
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigacija",
"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.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.",
@@ -54,13 +54,14 @@
"compose_form.spoiler_placeholder": "Upozorenje o sadržaju",
"confirmation_modal.cancel": "Otkaži",
"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.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.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.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.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
@@ -69,16 +70,16 @@
"emoji_button.flags": "Zastave",
"emoji_button.food": "Hrana & Piće",
"emoji_button.label": "Umetni smajlije",
"emoji_button.nature": "Nature",
"emoji_button.nature": "Priroda",
"emoji_button.objects": "Objekti",
"emoji_button.people": "Ljudi",
"emoji_button.search": "Traži...",
"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.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.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.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",
@@ -88,11 +89,11 @@
"getting_started.faq": "FAQ",
"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.userguide": "Vodič za korisnike",
"getting_started.userguide": "Upute za korištenje",
"home.column_settings.advanced": "Napredno",
"home.column_settings.basic": "Osnovno",
"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.settings": "Postavke Stupca",
"lightbox.close": "Zatvori",
@@ -113,7 +114,7 @@
"navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status",
"notification.follow": "{name} te sada slijedi",
"notification.mention": "{name} mentioned you",
"notification.mention": "{name} te je spomenuo",
"notification.reblog": "{name} je podigao tvoj status",
"notifications.clear": "Očisti notifikacije",
"notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?",
@@ -123,28 +124,28 @@
"notifications.column_settings.mention": "Spominjanja:",
"notifications.column_settings.push": "Push notifications",
"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.sound": "Sviraj zvuk",
"onboarding.done": "Učinjeno",
"onboarding.next": "Sljedeća",
"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_four.home": "The home timeline prikazuje samo postove ljudi koje slijediš.",
"onboarding.page_four.notifications": "Stupac notifikacija pokazuje kada je netko u interakciji s tobom.",
"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.handle": "Ti si na {domain}, tako da je tvoj potpuni opis {handle}",
"onboarding.page_one.welcome": "Dobro došli u Mastodon!",
"onboarding.next": "Sljedeće",
"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 postove ljudi koje slijediš.",
"onboarding.page_four.notifications": "Stupac za notifikacije pokazuje poruke drugih upućene tebi.",
"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}, i tvoja puna handle je {handle}",
"onboarding.page_one.welcome": "Dobro došli na Mastodon!",
"onboarding.page_six.admin": "Administrator tvoje instance je {admin}.",
"onboarding.page_six.almost_done": "Još malo pa gotovo...",
"onboarding.page_six.appetoot": "Živjeli!",
"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.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_three.profile": "Uredi svoj profil mijenjanjem avatara, biografije i imena koje će biti prikazano. Naći ćeš i druge korisne 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_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_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 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 sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.",
"onboarding.skip": "Preskoči",
"privacy.change": "Podesi status privatnosti",
"privacy.direct.long": "Prikaži samo spomenutim korisnicima",
@@ -162,7 +163,7 @@
"search.placeholder": "Traži",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"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.embed": "Embed",
"status.favourite": "Označi omiljenim",
@@ -196,5 +197,5 @@
"video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk",
"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.notifications": "通知",
"column.public": "連合タイムライン",
"column.pins": "固定されたトゥート",
"column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.pins": "固定されたトゥート",
"notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました",
"notification.mention": "{name}さんがあなたに返信しました",

View File

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

View File

@@ -12,7 +12,7 @@
"account.mute": "Rescondre @{name}",
"account.posts": "Estatuts",
"account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada",
"account.requested": "Invitacion mandada. Clicatz per anullar.",
"account.share": "Partejar lo perfil a @{name}",
"account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}",
@@ -34,6 +34,7 @@
"column.mutes": "Personas en silenci",
"column.notifications": "Notificacions",
"column.public": "Flux public global",
"column.pins": "Tuts penjats",
"column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres",
"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.unfollow.confirm": "Quitar de sègre",
"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.preview": "Here is what it will look like:",
"embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar",
@@ -87,7 +88,7 @@
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.heading": "Per començar",
"getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via{github} sus GitHub.",
"getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via {github} sus GitHub.",
"getting_started.userguide": "Guida dutilizacion",
"home.column_settings.advanced": "Avançat",
"home.column_settings.basic": "Basic",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global",
"navigation_bar.pins": "Tuts penjats",
"notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat:",
@@ -126,21 +128,21 @@
"notifications.column_settings.reblog": "Partatges:",
"notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach",
"onboarding.done": "Sortir",
"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.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.welcome": "Benvengut a Mastodon!",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"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.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.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_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.",
@@ -164,7 +166,7 @@
"standalone.public_title": "Una ulhada dedins…",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar",
"status.embed": "Embed",
"status.embed": "Embarcar",
"status.favourite": "Apondre als favorits",
"status.load_more": "Cargar mai",
"status.media_hidden": "Mèdia rescondut",
@@ -183,7 +185,7 @@
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat",
"status.unpin": "Despenjar del perfil",
"status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh",

View File

@@ -12,7 +12,7 @@
"account.mute": "Wycisz @{name}",
"account.posts": "Wpisy",
"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.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}",
@@ -33,6 +33,7 @@
"column.home": "Strona główna",
"column.mutes": "Wyciszeni użytkownicy",
"column.notifications": "Powiadomienia",
"column.pins": "Przypięte wpisy",
"column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia",
@@ -109,6 +110,7 @@
"navigation_bar.info": "Szczegółowe informacje",
"navigation_bar.logout": "Wyloguj",
"navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy",
"navigation_bar.preferences": "Preferencje",
"navigation_bar.public_timeline": "Oś czasu federacji",
"notification.favourite": "{name} dodał Twój status do ulubionych",
@@ -153,8 +155,8 @@
"privacy.private.short": "Tylko dla śledzących",
"privacy.public.long": "Widoczny na publicznych osiach czasu",
"privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczne",
"privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczny",
"reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze",
"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')) {
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
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'))));
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1622,6 +1622,10 @@
&:hover {
text-decoration: underline;
}
&:last-child {
padding: 0 15px 0 0;
}
}
.column-back-button__icon {
@@ -3801,6 +3805,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer;
margin-top: 8px;
position: relative;
border: 0;
display: block;
}
.media-spoiler-video-play-icon {
@@ -3966,41 +3972,10 @@ noscript {
}
}
.embed-modal__html {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
margin-bottom: 15px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.embed-modal {
max-width: 80vw;
max-height: 80vh;
h4 {
padding: 30px;
font-weight: 500;
@@ -4008,18 +3983,52 @@ noscript {
text-align: center;
}
.hint {
margin-bottom: 15px;
.embed-modal__container {
padding: 10px;
.hint {
margin-bottom: 15px;
}
.embed-modal__html {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
margin-bottom: 15px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.embed-modal__iframe {
width: 400px;
max-width: 100%;
overflow: hidden;
border: 0;
}
}
}
.embed-modal__container {
padding: 10px;
}
.embed-modal__iframe {
width: 100%;
min-width: 400px;
overflow: hidden;
border: 0;
}

View File

@@ -403,51 +403,54 @@
.embed {
.activity-stream {
border-radius: 4px;
box-shadow: none;
.entry {
&:last-child {
border-radius: 0 0 4px 4px;
}
&:first-child {
border-radius: 4px 4px 0 0;
.detailed-status.light {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
&:last-child {
border-radius: 4px;
.detailed-status__display-name {
flex: 1;
margin: 0 5px 15px 0;
}
.button.button-secondary.logo-button {
flex: 0 auto;
font-size: 14px;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
path:first-child {
fill: $ui-primary-color;
}
path:last-child {
fill: $simple-background-color;
}
}
&:active,
&:focus,
&:hover {
svg path:first-child {
fill: lighten($ui-primary-color, 4%);
}
}
}
.status__content,
.detailed-status__meta {
flex: 100%;
}
}
}
}
}
.button.button-secondary.logo-button {
position: absolute;
right: 14px;
top: 14px;
font-size: 14px;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
path:first-child {
fill: $ui-primary-color;
}
path:last-child {
fill: $simple-background-color;
}
}
&:active,
&:focus,
&:hover {
svg path:first-child {
fill: lighten($ui-primary-color, 4%);
}
}
}

View File

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

View File

@@ -33,7 +33,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def status_params
{
uri: @object['id'],
url: @object['url'] || @object['id'],
url: object_url || @object['id'],
account: @account,
text: text_from_content || '',
language: language_from_content,
@@ -147,6 +147,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@object['contentMap'].keys.first
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?
@object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
end

View File

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

View File

@@ -20,7 +20,16 @@ class LanguageDetector
private
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
def result

View File

@@ -65,7 +65,7 @@ class OStatus::AtomSerializer
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, 'updated', stream_entry.updated_at.iso8601)
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)
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, '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?

View File

@@ -49,12 +49,17 @@ class TagManager
def unique_tag_to_local_id(tag, expected_type)
return nil unless local_id?(tag)
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
return matches[1] unless matches.nil?
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)
return matches[1] unless matches.nil?
end
end
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
def web_domain?(domain)
@@ -92,7 +97,7 @@ class TagManager
when :person
account_url(target)
when :note, :comment, :activity
unique_tag(target.created_at, target.id, 'Status')
target.uri || unique_tag(target.created_at, target.id, 'Status')
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,13 +40,12 @@ class OEmbedSerializer < ActiveModel::Serializer
attributes = {
src: embed_short_account_status_url(object.account, object),
class: 'mastodon-embed',
frameborder: '0',
scrolling: 'no',
style: 'max-width: 100%; border: 0',
width: width,
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
def width

View File

@@ -6,13 +6,14 @@ class ActivityPub::ProcessAccountService < BaseService
# Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain
def call(username, domain, json)
return unless json['inbox'].present?
return if json['inbox'].blank?
@json = json
@uri = @json['id']
@username = username
@domain = domain
@account = Account.find_by(uri: @uri)
@json = json
@uri = @json['id']
@username = username
@domain = domain
@account = Account.find_by(uri: @uri)
@collections = {}
create_account if @account.nil?
upgrade_account if @account.ostatus?
@@ -42,16 +43,19 @@ class ActivityPub::ProcessAccountService < BaseService
@account.protocol = :activitypub
@account.inbox_url = @json['inbox'] || ''
@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.url = @json['url'] || @uri
@account.url = url || @uri
@account.display_name = @json['name'] || ''
@account.note = @json['summary'] || ''
@account.avatar_remote_url = image_url('icon')
@account.header_remote_url = image_url('image')
@account.avatar_remote_url = image_url('icon') unless skip_download?
@account.header_remote_url = image_url('image') unless skip_download?
@account.public_key = public_key || ''
@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
def upgrade_account
@@ -62,7 +66,7 @@ class ActivityPub::ProcessAccountService < BaseService
value = first_of_value(@json[key])
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['url'] if image
@@ -78,6 +82,43 @@ class ActivityPub::ProcessAccountService < BaseService
key['publicKeyPem'] if key
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?
domain_block && domain_block.suspend?
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
# @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)
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 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?
request_follow(source_account, target_account)

View File

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

View File

@@ -41,7 +41,7 @@ class ProcessMentionsService < BaseService
NotifyService.new.call(mentioned_account, mention)
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)
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)
end
end

View File

@@ -4,16 +4,19 @@ class UnsubscribeService < BaseService
def call(account)
return if account.hub_url.blank?
@account = account
@response = build_request.perform
@account = account
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success?
begin
@response = build_request.perform
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.subscription_expires_at = nil
@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
private

View File

@@ -1,16 +1,17 @@
%table.table
%tbody
%tr
%td= t('admin.accounts.show.created_reports')
%td= link_to pluralize(account.reports.count, t('admin.accounts.show.report')), admin_reports_path(account_id: account.id)
%tr
%td= t('admin.accounts.show.targeted_reports')
%td= link_to pluralize(account.targeted_reports.count, t('admin.accounts.show.report')), admin_reports_path(target_account_id: account.id)
- if account.silenced? || account.suspended?
.table-wrapper
%table.table
%tbody
%tr
%td= t('admin.accounts.moderation.title')
%td
- if account.silenced?
%p= t('admin.accounts.moderation.silenced')
- if account.suspended?
%p= t('admin.accounts.moderation.suspended')
%td= t('admin.accounts.show.created_reports')
%td= link_to pluralize(account.reports.count, t('admin.accounts.show.report')), admin_reports_path(account_id: account.id)
%tr
%td= t('admin.accounts.show.targeted_reports')
%td= link_to pluralize(account.targeted_reports.count, t('admin.accounts.show.report')), admin_reports_path(target_account_id: account.id)
- if account.silenced? || account.suspended?
%tr
%td= t('admin.accounts.moderation.title')
%td
- if account.silenced?
%p= t('admin.accounts.moderation.silenced')
- if account.suspended?
%p= t('admin.accounts.moderation.suspended')

View File

@@ -50,16 +50,17 @@
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
%table.table
%thead
%tr
%th= t('admin.accounts.username')
%th= t('admin.accounts.domain')
%th= t('admin.accounts.protocol')
%th= t('admin.accounts.confirmed')
%th= fa_icon 'paper-plane-o'
%th
%tbody
= render @accounts
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.accounts.username')
%th= t('admin.accounts.domain')
%th= t('admin.accounts.protocol')
%th= t('admin.accounts.confirmed')
%th= fa_icon 'paper-plane-o'
%th
%tbody
= render @accounts
= paginate @accounts

View File

@@ -1,86 +1,86 @@
- content_for :page_title do
= @account.acct
%table.table
%tbody
%tr
%th= t('admin.accounts.username')
%td= @account.username
%tr
%th= t('admin.accounts.domain')
%td= @account.domain
%tr
%th= t('admin.accounts.display_name')
%td= @account.display_name
.table-wrapper
%table.table
%tbody
%tr
%th= t('admin.accounts.username')
%td= @account.username
%tr
%th= t('admin.accounts.domain')
%td= @account.domain
%tr
%th= t('admin.accounts.display_name')
%td= @account.display_name
- if @account.local?
%tr
%th= t('admin.accounts.email')
%td= @account.user_email
%tr
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%tr
%th= t('admin.accounts.most_recent_activity')
%td
- if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }
= l @account.user_current_sign_in_at
- else
Never
- else
%tr
%th= t('admin.accounts.profile_url')
%td= link_to @account.url, @account.url
%tr
%th= t('admin.accounts.protocol')
%td= @account.protocol.humanize
- if @account.ostatus?
- if @account.local?
%tr
%th= t('admin.accounts.feed_url')
%td= link_to @account.remote_url, @account.remote_url
%th= t('admin.accounts.email')
%td= @account.user_email
%tr
%th= t('admin.accounts.push_subscription_expires')
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%tr
%th= t('admin.accounts.most_recent_activity')
%td
- if @account.subscribed?
%time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
= l @account.subscription_expires_at
- if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }
= l @account.user_current_sign_in_at
- else
= t('admin.accounts.not_subscribed')
Never
- else
%tr
%th= t('admin.accounts.salmon_url')
%td= link_to @account.salmon_url, @account.salmon_url
- elsif @account.activitypub?
%th= t('admin.accounts.profile_url')
%td= link_to @account.url, @account.url
%tr
%th= t('admin.accounts.inbox_url')
%td= link_to @account.inbox_url, @account.inbox_url
%tr
%th= t('admin.accounts.outbox_url')
%td= link_to @account.outbox_url, @account.outbox_url
%th= t('admin.accounts.protocol')
%td= @account.protocol.humanize
%tr
%th= t('admin.accounts.follows')
%td= @account.following_count
%tr
%th= t('admin.accounts.followers')
%td= @account.followers_count
%tr
%th= t('admin.accounts.statuses')
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id)
%tr
%th= t('admin.accounts.media_attachments')
%td
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true })
= surround '(', ')' do
= number_to_human_size @account.media_attachments.sum('file_file_size')
%tr
%th= t('.created_reports')
%td= link_to pluralize(@account.reports.count, t('.report')), admin_reports_path(account_id: @account.id)
%tr
%th= t('.targeted_reports')
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
- if @account.ostatus?
%tr
%th= t('admin.accounts.feed_url')
%td= link_to @account.remote_url, @account.remote_url
%tr
%th= t('admin.accounts.push_subscription_expires')
%td
- if @account.subscribed?
%time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
= l @account.subscription_expires_at
- else
= t('admin.accounts.not_subscribed')
%tr
%th= t('admin.accounts.salmon_url')
%td= link_to @account.salmon_url, @account.salmon_url
- elsif @account.activitypub?
%tr
%th= t('admin.accounts.inbox_url')
%td= link_to @account.inbox_url, @account.inbox_url
%tr
%th= t('admin.accounts.outbox_url')
%td= link_to @account.outbox_url, @account.outbox_url
%tr
%th= t('admin.accounts.follows')
%td= @account.following_count
%tr
%th= t('admin.accounts.followers')
%td= @account.followers_count
%tr
%th= t('admin.accounts.statuses')
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id)
%tr
%th= t('admin.accounts.media_attachments')
%td
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true })
= surround '(', ')' do
= number_to_human_size @account.media_attachments.sum('file_file_size')
%tr
%th= t('.created_reports')
%td= link_to pluralize(@account.reports.count, t('.report')), admin_reports_path(account_id: @account.id)
%tr
%th= t('.targeted_reports')
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
%div{ style: 'float: right' }
- if @account.local?

View File

@@ -1,15 +1,16 @@
- content_for :page_title do
= t('admin.domain_blocks.title')
%table.table
%thead
%tr
%th= t('admin.domain_blocks.domain')
%th= t('admin.domain_blocks.severity')
%th= t('admin.domain_blocks.reject_media')
%th
%tbody
= render @domain_blocks
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.domain_blocks.domain')
%th= t('admin.domain_blocks.severity')
%th= t('admin.domain_blocks.reject_media')
%th
%tbody
= render @domain_blocks
= paginate @domain_blocks
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'

View File

@@ -1,12 +1,13 @@
- content_for :page_title do
= t('admin.instances.title')
%table.table
%thead
%tr
%th= t('admin.instances.domain_name')
%th= t('admin.instances.account_count')
%tbody
= render @instances
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.instances.domain_name')
%th= t('admin.instances.account_count')
%tbody
= render @instances
= paginate paginated_instances

View File

@@ -10,17 +10,18 @@
= form_tag do
%table.table
%thead
%tr
-# %th
%th= t('admin.reports.id')
%th= t('admin.reports.target')
%th= t('admin.reports.reported_by')
%th= t('admin.reports.comment.label')
%th= t('admin.reports.report_contents')
%th
%tbody
= render @reports
.table-wrapper
%table.table
%thead
%tr
-# %th
%th= t('admin.reports.id')
%th= t('admin.reports.target')
%th= t('admin.reports.reported_by')
%th= t('admin.reports.comment.label')
%th= t('admin.reports.report_contents')
%th
%tbody
= render @reports
= paginate @reports

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_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
= f.button :button, t('generic.save_changes'), type: :submit

View File

@@ -1,15 +1,16 @@
- content_for :page_title do
= t('admin.subscriptions.title')
%table.table
%thead
%tr
%th= t('admin.subscriptions.topic')
%th= t('admin.subscriptions.callback_url')
%th= t('admin.subscriptions.confirmed')
%th= t('admin.subscriptions.expires_in')
%th= t('admin.subscriptions.last_delivery')
%tbody
= render @subscriptions
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.subscriptions.topic')
%th= t('admin.subscriptions.callback_url')
%th= t('admin.subscriptions.confirmed')
%th= t('admin.subscriptions.expires_in')
%th= t('admin.subscriptions.last_delivery')
%tbody
= render @subscriptions
= paginate @subscriptions

View File

@@ -1,28 +1,29 @@
%h6= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation'
%table.table.inline-table
%thead
%tr
%th= t 'sessions.browser'
%th= t 'sessions.ip'
%th= t 'sessions.activity'
%td
%tbody
- @sessions.each do |session|
.table-wrapper
%table.table.inline-table
%thead
%tr
%th= t 'sessions.browser'
%th= t 'sessions.ip'
%th= t 'sessions.activity'
%td
%span{ title: session.user_agent }<
= fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session)
= ' '
= t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
%td
%samp= session.ip
%td
- if current_session.session_id == session.session_id
= t 'sessions.current_session'
- else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
%td
- if current_session.session_id != session.session_id
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete
%tbody
- @sessions.each do |session|
%tr
%td
%span{ title: session.user_agent }<
= fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session)
= ' '
= t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
%td
%samp= session.ip
%td
- if current_session.session_id == session.session_id
= t 'sessions.current_session'
- else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
%td
- if current_session.session_id != session.session_id
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

View File

@@ -21,13 +21,13 @@
= stylesheet_pack_tag 'common', media: 'all'
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
= javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
= javascript_pack_tag 'emojione_picker', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
%link{ href: asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/compose.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
%link{ href: asset_pack_path('features/public_timeline.js'), 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'
= csrf_meta_tags

View File

@@ -1,23 +1,24 @@
- content_for :page_title do
= t('doorkeeper.authorized_applications.index.title')
%table.table
%thead
%tr
%th= t('doorkeeper.authorized_applications.index.application')
%th= t('doorkeeper.authorized_applications.index.scopes')
%th= t('doorkeeper.authorized_applications.index.created_at')
%th
%tbody
- @applications.each do |application|
.table-wrapper
%table.table
%thead
%tr
%td
- if application.website.blank?
= application.name
- else
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />')
%td= l application.created_at
%td
- unless application.superapp?
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }
%th= t('doorkeeper.authorized_applications.index.application')
%th= t('doorkeeper.authorized_applications.index.scopes')
%th= t('doorkeeper.authorized_applications.index.created_at')
%th
%tbody
- @applications.each do |application|
%tr
%td
- if application.website.blank?
= application.name
- else
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />')
%td= l application.created_at
%td
- unless application.superapp?
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }

View File

@@ -1,19 +1,20 @@
- content_for :page_title do
= t('doorkeeper.applications.index.title')
%table.table
%thead
%tr
%th= t('doorkeeper.applications.index.application')
%th= t('doorkeeper.applications.index.scopes')
%th
%tbody
- @applications.each do |application|
.table-wrapper
%table.table
%thead
%tr
%td= link_to application.name, settings_application_path(application)
%th= application.scopes
%td
= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') }
%th= t('doorkeeper.applications.index.application')
%th= t('doorkeeper.applications.index.scopes')
%th
%tbody
- @applications.each do |application|
%tr
%td= link_to application.name, settings_application_path(application)
%th= application.scopes
%td
= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') }
= paginate @applications
= link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button'

View File

@@ -3,22 +3,23 @@
%p.hint= t('applications.warning')
%table.table
%tbody
%tr
%th= t('doorkeeper.applications.show.application_id')
%td
%code= @application.uid
%tr
%th= t('doorkeeper.applications.show.secret')
%td
%code= @application.secret
%tr
%th{ rowspan: 2}= t('applications.your_token')
%td
%code= current_user.token_for_app(@application).token
%tr
%td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post
.table-wrapper
%table.table
%tbody
%tr
%th= t('doorkeeper.applications.show.application_id')
%td
%code= @application.uid
%tr
%th= t('doorkeeper.applications.show.secret')
%td
%code= @application.secret
%tr
%th{ rowspan: 2}= t('applications.your_token')
%td
%code= current_user.token_for_app(@application).token
%tr
%td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post
%hr/

View File

@@ -1,21 +1,22 @@
- content_for :page_title do
= t('settings.export')
%table.table
%tbody
%tr
%th= t('exports.storage')
%td= number_to_human_size @export.total_storage
%td
%tr
%th= t('exports.follows')
%td= @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
%th= t('exports.blocks')
%td= @export.total_blocks
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
%tr
%th= t('exports.mutes')
%td= @export.total_mutes
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
.table-wrapper
%table.table
%tbody
%tr
%th= t('exports.storage')
%td= number_to_human_size @export.total_storage
%td
%tr
%th= t('exports.follows')
%td= @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
%th= t('exports.blocks')
%td= @export.total_blocks
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
%tr
%th= t('exports.mutes')
%td= @export.total_mutes
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)

View File

@@ -12,20 +12,21 @@
%p= t('followers.explanation_html')
%p= t('followers.true_privacy_html')
%table.table
%thead
%tr
%th
%th= t('followers.domain')
%th= t('followers.followers_count')
%tbody
- @domains.each do |domain|
.table-wrapper
%table.table
%thead
%tr
%td
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
%td
%samp= domain.domain.presence || Rails.configuration.x.local_domain
%td= number_with_delimiter domain.accounts_from_domain
%th
%th= t('followers.domain')
%th= t('followers.followers_count')
%tbody
- @domains.each do |domain|
%tr
%td
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
%td
%samp= domain.domain.presence || Rails.configuration.x.local_domain
%td= number_with_delimiter domain.accounts_from_domain
.action-pagination
.actions

View File

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

View File

@@ -1,9 +1,4 @@
.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
%div
.avatar
@@ -12,6 +7,11 @@
%strong.p-name.emojify= display_name(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<
- if status.spoiler_text?
%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>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>
<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>

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 :)
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) %>
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,

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>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) %>
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'
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?

View File

@@ -3,7 +3,7 @@
class Pubsubhubbub::SubscribeWorker
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|
case count

View File

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

View File

@@ -73,7 +73,7 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
# 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 = {
:port => ENV['SMTP_PORT'],

View File

@@ -12,6 +12,7 @@ Rails.application.configure do
config.x.web_domain = web_host
config.x.use_https = https
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*/)

View File

@@ -40,6 +40,21 @@ if ENV['S3_ENABLED'] == 'true'
Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
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
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'

View File

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

View File

@@ -47,16 +47,16 @@ ar:
datetime:
distance_in_words:
about_x_hours: "%{count}سا"
about_x_months: "%{count}شهر"
about_x_years: "%{count}سنة"
almost_x_years: "%{count}سنوات"
half_a_minute: Just now
less_than_x_minutes: "%{count}د"
about_x_months: "%{count} شهر"
about_x_years: "%{count} سنة"
almost_x_years: "%{count} سنوات"
half_a_minute: الآن
less_than_x_minutes: "%{count} د"
less_than_x_seconds: الآن
over_x_years: "%{count}سنين"
x_days: "%{count}أيام"
over_x_years: "%{count} سنين"
x_days: "%{count} أيام"
x_minutes: "%{count}د"
x_months: "%{count}شه"
x_months: "%{count} شه"
x_seconds: "%{count}ث"
exports:
blocks: قمت بحظر
@@ -94,7 +94,7 @@ ar:
one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
favourite:
body: 'Your status was favourited by %{name}:'
body: 'أُعجب %{name} بمنشورك'
subject: "%{name} favourited your status"
follow:
body: "%{name} من متتبعيك الآن !"
@@ -108,6 +108,17 @@ ar:
reblog:
body: 'Your status was boosted by %{name}:'
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:
next: التالي
prev: السابق
@@ -148,7 +159,7 @@ ar:
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."
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: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز.
users:
invalid_email: عنوان البريد الإلكتروني غير صالح

View File

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

View File

@@ -340,6 +340,17 @@ ca:
reblog:
body: "%{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:
next: Pròxim
prev: Anterior

View File

@@ -12,15 +12,15 @@ de:
source_code: Quellcode
status_count_after: Beiträge verfassten
status_count_before: die
user_count_after: Benutzer
user_count_after: Profile
user_count_before: Heimat für
accounts:
follow: Folgen
followers: Folgende
following: Folgt
nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen
people_followed_by: Profile, denen %{name} folgt
people_who_follow: Profile, die %{name} folgen
posts: Beiträge
remote_follow: Folgen
unfollow: Entfolgen
@@ -67,7 +67,7 @@ de:
title: Konten
undo_silenced: Stummschaltung zurücknehmen
undo_suspension: Sperre zurücknehmen
username: Benutzername
username: Profilname
web: Web
domain_blocks:
add_new: Neu hinzufügen
@@ -124,7 +124,7 @@ de:
settings:
contact_information:
email: Eine öffentliche E-Mail-Adresse angeben
username: Einen Benutzernamen angeben
username: Einen Profilnamen angeben
registrations:
closed_message:
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
muting: Stummschaltungsliste
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>.
media_attachments:
validations:
@@ -239,12 +239,23 @@ de:
reblog:
body: 'Dein Beitrag wurde von %{name} geteilt:'
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:
next: Vorwärts
prev: Zurück
truncate: "&hellip;"
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.
proceed: Weiter
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_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_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_token:
expired: Der Zugriffstoken ist abgelaufen
@@ -108,6 +108,6 @@ de:
application:
title: OAuth-Autorisierung nötig
scopes:
follow: Nutzer folgen, blocken, entblocken und entfolgen
follow: Profil folgen, blocken, entblocken und entfolgen
read: deine Daten lesen
write: Beiträge von deinem Konto aus veröffentlichen

View File

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

View File

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

View File

@@ -167,6 +167,9 @@ en:
unresolved: Unresolved
view: View
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:
email: Business e-mail
username: Contact username

View File

@@ -103,6 +103,17 @@ eo:
reblog:
body: "%{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:
next: Sekva
prev: Malsekva

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