mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 07:49:29 +00:00
Compare commits
123 Commits
compose-re
...
load-publi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a0b47982d | ||
|
|
9b9b7fa005 | ||
|
|
0210e59759 | ||
|
|
4773481a90 | ||
|
|
ee7217bc94 | ||
|
|
e2ce628724 | ||
|
|
cf5789146b | ||
|
|
9fa79bc317 | ||
|
|
704053d221 | ||
|
|
8c08c852bc | ||
|
|
f13ebd02c9 | ||
|
|
26f054253c | ||
|
|
395e64e858 | ||
|
|
514db316f7 | ||
|
|
6fcb870d96 | ||
|
|
3ce1385b25 | ||
|
|
095a00ef3d | ||
|
|
35be02f21d | ||
|
|
622c8fdb75 | ||
|
|
991371af5f | ||
|
|
35b84985a8 | ||
|
|
d41f0b66cc | ||
|
|
aef4b1af66 | ||
|
|
f43c2f12f3 | ||
|
|
9bdbe66316 | ||
|
|
b535f24fe5 | ||
|
|
921b781909 | ||
|
|
0d4dcb5fb2 | ||
|
|
6d1c325167 | ||
|
|
7f4374d97d | ||
|
|
8a0e4bb9a4 | ||
|
|
5963630c63 | ||
|
|
6f5c0afe93 | ||
|
|
eec6095e02 | ||
|
|
e780d5951b | ||
|
|
9f04b0d4b1 | ||
|
|
ce7f4aef16 | ||
|
|
ec0bdd6c1a | ||
|
|
df04da098a | ||
|
|
7c719c567c | ||
|
|
488381ae2f | ||
|
|
60433d03f5 | ||
|
|
5d2ef7a616 | ||
|
|
44792de49a | ||
|
|
824a790e63 | ||
|
|
628358aeea | ||
|
|
c235711ffe | ||
|
|
ff6ca8bdc6 | ||
|
|
90e568413b | ||
|
|
ef0b7d1e76 | ||
|
|
65986b6f0b | ||
|
|
2dc4fbbd1a | ||
|
|
f839ac694c | ||
|
|
dbda87c31f | ||
|
|
722b3f567f | ||
|
|
e4a241abef | ||
|
|
93555182c3 | ||
|
|
0eff42d688 | ||
|
|
f7c4d4464b | ||
|
|
70c99a9f34 | ||
|
|
c2e1bfd9ae | ||
|
|
1d92b90be9 | ||
|
|
da809f9eec | ||
|
|
c4d36d024c | ||
|
|
5083311d64 | ||
|
|
2af307bce4 | ||
|
|
bcbdd4f88d | ||
|
|
9e97fbf0af | ||
|
|
b5874c1428 | ||
|
|
61ef8d643e | ||
|
|
9f29fd31ba | ||
|
|
53caab0c0b | ||
|
|
b75a1ce326 | ||
|
|
d442cfa65c | ||
|
|
f5a4201ad8 | ||
|
|
a251c42192 | ||
|
|
2ec9a75a1d | ||
|
|
fa92e88fb2 | ||
|
|
da98c33161 | ||
|
|
2eed4ace11 | ||
|
|
c71d848855 | ||
|
|
e4bc013d6f | ||
|
|
6932b464e6 | ||
|
|
ad10a80a99 | ||
|
|
8bf9d9362a | ||
|
|
03aeab857f | ||
|
|
f441770e50 | ||
|
|
b4e667f86b | ||
|
|
faf20eeaa4 | ||
|
|
f6adb409fd | ||
|
|
10f6793fd0 | ||
|
|
a594139115 | ||
|
|
95bd85d9e8 | ||
|
|
ac686d5a5d | ||
|
|
ec620ae486 | ||
|
|
8d51ce4290 | ||
|
|
f41b33eb01 | ||
|
|
9fc08e4861 | ||
|
|
6236577734 | ||
|
|
06636c6eca | ||
|
|
e9822a4e4e | ||
|
|
9a61b0ef22 | ||
|
|
c69a23ae46 | ||
|
|
d872902997 | ||
|
|
5ec25ff3e1 | ||
|
|
49e296e1b0 | ||
|
|
7347d4f8bb | ||
|
|
3b016342c6 | ||
|
|
7571c37c99 | ||
|
|
3c18964256 | ||
|
|
c61dd918a2 | ||
|
|
0f69a90588 | ||
|
|
02ba03d6db | ||
|
|
3bee0996c5 | ||
|
|
89daeb43a8 | ||
|
|
7d4f4f9aab | ||
|
|
256c2b1de0 | ||
|
|
02e3e1ec09 | ||
|
|
ff924f95bb | ||
|
|
c10f4bdb03 | ||
|
|
72b99f6ee4 | ||
|
|
4ce44ba470 | ||
|
|
0dce26b82b |
@@ -61,6 +61,9 @@ RUN apk -U upgrade \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
|
||||
COPY stack-fix.c /lib
|
||||
RUN gcc -shared -fPIC /lib/stack-fix.c -o /lib/stack-fix.so
|
||||
RUN rm /lib/stack-fix.c
|
||||
|
||||
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
|
||||
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
|
||||
|
||||
@@ -299,13 +299,11 @@ GEM
|
||||
sidekiq (>= 3.5.0)
|
||||
statsd-ruby (~> 1.2.0)
|
||||
oj (3.3.9)
|
||||
openssl (2.0.6)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (2.0.1)
|
||||
ostatus2 (2.0.2)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
openssl (~> 2.0)
|
||||
ox (2.8.2)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
class AccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -27,10 +28,11 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @account,
|
||||
serializer: ActivityPub::ActorSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FollowsController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
render(
|
||||
json: FollowRequest.includes(:account).references(:account).find_by!(
|
||||
id: params.require(:id),
|
||||
accounts: { domain: nil, username: params.require(:account_username) },
|
||||
target_account: signed_request_account
|
||||
),
|
||||
serializer: ActivityPub::FollowSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -6,8 +6,8 @@ class Api::BaseController < ApplicationController
|
||||
|
||||
include RateLimitHeaders
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_before_action :store_current_location
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||
render json: { error: e.to_s }, status: 422
|
||||
|
||||
@@ -21,9 +21,9 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
|
||||
weeks << {
|
||||
week: week.to_time.to_i.to_s,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0,
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}"),
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
|
||||
private
|
||||
|
||||
def https_enabled?
|
||||
Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'
|
||||
Rails.env.production?
|
||||
end
|
||||
|
||||
def store_current_location
|
||||
@@ -192,17 +192,31 @@ class ApplicationController < ActionController::Base
|
||||
format.any { head code }
|
||||
format.html do
|
||||
set_locale
|
||||
use_pack 'error'
|
||||
render "errors/#{code}", layout: 'error', status: code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_cached_json(cache_key, **options)
|
||||
options[:expires_in] ||= 3.minutes
|
||||
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
|
||||
cache_public = options.key?(:public) ? options.delete(:public) : true
|
||||
content_type = options.delete(:content_type) || 'application/json'
|
||||
|
||||
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
|
||||
yield.to_json
|
||||
end
|
||||
|
||||
expires_in options[:expires_in], public: true
|
||||
render json: data
|
||||
expires_in options[:expires_in], public: cache_public
|
||||
render json: data, content_type: content_type
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Vary'] = 'Accept'
|
||||
end
|
||||
|
||||
def skip_session!
|
||||
request.session_options[:skip] = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
class EmojisController < ApplicationController
|
||||
before_action :set_emoji
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: @emoji,
|
||||
serializer: ActivityPub::EmojiSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::FlavoursController < Settings::BaseController
|
||||
|
||||
def index
|
||||
redirect_to action: 'show', flavour: current_flavour
|
||||
end
|
||||
|
||||
def show
|
||||
unless Themes.instance.flavours.include?(params[:flavour]) or params[:flavour] == current_flavour
|
||||
unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour)
|
||||
redirect_to action: 'show', flavour: current_flavour
|
||||
end
|
||||
|
||||
@@ -16,7 +15,7 @@ class Settings::FlavoursController < Settings::BaseController
|
||||
end
|
||||
|
||||
def update
|
||||
user_settings.update(user_settings_params(params[:flavour]).to_h)
|
||||
user_settings.update(user_settings_params)
|
||||
redirect_to action: 'show', flavour: params[:flavour]
|
||||
end
|
||||
|
||||
@@ -26,10 +25,8 @@ class Settings::FlavoursController < Settings::BaseController
|
||||
UserSettingsDecorator.new(current_user)
|
||||
end
|
||||
|
||||
def user_settings_params(flavour)
|
||||
params.require(:user).merge({ setting_flavour: flavour }).permit(
|
||||
:setting_flavour,
|
||||
:setting_skin
|
||||
)
|
||||
def user_settings_params
|
||||
{ setting_flavour: params.require(:flavour),
|
||||
setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
|
||||
before_action :set_link_headers
|
||||
before_action :check_account_suspension
|
||||
before_action :redirect_to_original, only: [:show]
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -23,25 +23,21 @@ class StatusesController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @status,
|
||||
serializer: ActivityPub::NoteSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session! unless @stream_entry.hidden?
|
||||
|
||||
# Allow HTTP caching for 3 minutes if the status is public
|
||||
unless @stream_entry.hidden?
|
||||
request.session_options[:skip] = true
|
||||
expires_in(3.minutes, public: true)
|
||||
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def activity
|
||||
render json: @status,
|
||||
serializer: ActivityPub::ActivitySerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
|
||||
def embed
|
||||
|
||||
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes)
|
||||
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
|
||||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ module JsonLdHelper
|
||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||
end
|
||||
|
||||
def unsupported_uri_scheme?(uri)
|
||||
!uri.start_with?('http://', 'https://')
|
||||
end
|
||||
|
||||
def canonicalize(json)
|
||||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
||||
graph.dump(:normalize)
|
||||
|
||||
@@ -4,6 +4,7 @@ module RoutingHelper
|
||||
extend ActiveSupport::Concern
|
||||
include Rails.application.routes.url_helpers
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include Webpacker::Helper
|
||||
|
||||
included do
|
||||
def default_url_options
|
||||
@@ -17,6 +18,10 @@ module RoutingHelper
|
||||
URI.join(root_url, source).to_s
|
||||
end
|
||||
|
||||
def full_pack_url(source, **options)
|
||||
full_asset_url(asset_pack_path(source, options))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def use_storage?
|
||||
|
||||
@@ -61,7 +61,7 @@ export function replyCompose(status, router) {
|
||||
status: status,
|
||||
});
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
if (router && !getState().getIn(['compose', 'mounted'])) {
|
||||
router.push('/statuses/new');
|
||||
}
|
||||
};
|
||||
@@ -118,6 +118,11 @@ export function submitCompose() {
|
||||
}).then(function (response) {
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
// If the response has no data then we can't do anything else.
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To make the app more responsive, immediately get the status into the columns
|
||||
|
||||
const insertOrRefresh = (timelineId, refreshAction) => {
|
||||
@@ -341,10 +346,11 @@ export function unmountCompose() {
|
||||
};
|
||||
};
|
||||
|
||||
export function toggleComposeAdvancedOption(option) {
|
||||
export function changeComposeAdvancedOption(option, value) {
|
||||
return {
|
||||
option,
|
||||
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
|
||||
option: option,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA
|
||||
|
||||
export function fetchFavouritedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/favourites').then(response => {
|
||||
@@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
|
||||
|
||||
if (url === null) {
|
||||
if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||
|
||||
const unescapeHTML = (html) => {
|
||||
const wrapper = document.createElement('div');
|
||||
html = html.replace(/<br \/>|<br>|\n/, ' ');
|
||||
html = html.replace(/<br \/>|<br>|\n/g, ' ');
|
||||
wrapper.innerHTML = html;
|
||||
return wrapper.textContent;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
setAlerts,
|
||||
} from './setter';
|
||||
import { register, saveSettings } from './registerer';
|
||||
|
||||
export {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
register,
|
||||
};
|
||||
|
||||
export function changeAlerts(path, value) {
|
||||
return dispatch => {
|
||||
dispatch(setAlerts(path, value));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { pushNotificationsSetting } from 'flavours/glitch/util/settings';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
const urlBase64ToUint8Array = (base64String) => {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||
|
||||
const getRegistration = () => navigator.serviceWorker.ready;
|
||||
|
||||
const getPushSubscription = (registration) =>
|
||||
registration.pushManager.getSubscription()
|
||||
.then(subscription => ({ registration, subscription }));
|
||||
|
||||
const subscribe = (registration) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
|
||||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||
|
||||
const sendSubscriptionToBackend = (getState, subscription, me) => {
|
||||
const params = { subscription };
|
||||
|
||||
if (me) {
|
||||
const data = pushNotificationsSetting.get(me);
|
||||
if (data) {
|
||||
params.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||
};
|
||||
|
||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||
|
||||
export function register () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setBrowserSupport(supportsPushNotifications));
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
|
||||
if (me && !pushNotificationsSetting.get(me)) {
|
||||
const alerts = getState().getIn(['push_notifications', 'alerts']);
|
||||
if (alerts) {
|
||||
pushNotificationsSetting.set(me, { alerts: alerts });
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsPushNotifications) {
|
||||
if (!getApplicationServerKey()) {
|
||||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
||||
return;
|
||||
}
|
||||
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(({ registration, subscription }) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
|
||||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
|
||||
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
|
||||
|
||||
// If the VAPID public key did not change and the endpoint corresponds
|
||||
// to the endpoint saved in the backend, the subscription is valid
|
||||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
||||
return subscription;
|
||||
} else {
|
||||
// Something went wrong, try to subscribe again
|
||||
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(registration).then(
|
||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
||||
})
|
||||
.then(subscription => {
|
||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||
// it means that the backend subscription is valid (and was set during hydration)
|
||||
if (!(subscription instanceof PushSubscription)) {
|
||||
dispatch(setSubscription(subscription));
|
||||
if (me) {
|
||||
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code === 20 && error.name === 'AbortError') {
|
||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
|
||||
}
|
||||
|
||||
// Clear alerts and hide UI settings
|
||||
dispatch(clearSubscription());
|
||||
if (me) {
|
||||
pushNotificationsSetting.remove(me);
|
||||
}
|
||||
|
||||
try {
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(unsubscribe);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Your browser does not support Web Push Notifications.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveSettings() {
|
||||
return (_, getState) => {
|
||||
const state = getState().get('push_notifications');
|
||||
const subscription = state.get('subscription');
|
||||
const alerts = state.get('alerts');
|
||||
const data = { alerts };
|
||||
|
||||
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
data,
|
||||
}).then(() => {
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
if (me) {
|
||||
pushNotificationsSetting.set(me, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
||||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
|
||||
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
|
||||
export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
|
||||
|
||||
export function setBrowserSupport (value) {
|
||||
return {
|
||||
@@ -25,28 +23,12 @@ export function clearSubscription () {
|
||||
};
|
||||
}
|
||||
|
||||
export function changeAlerts(key, value) {
|
||||
export function setAlerts (path, value) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ALERTS_CHANGE,
|
||||
key,
|
||||
type: SET_ALERTS,
|
||||
path,
|
||||
value,
|
||||
});
|
||||
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
|
||||
export function saveSettings() {
|
||||
return (_, getState) => {
|
||||
const state = getState().get('push_notifications');
|
||||
const subscription = state.get('subscription');
|
||||
const alerts = state.get('alerts');
|
||||
|
||||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
data: {
|
||||
alerts,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import axios from 'axios';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||
export const SETTING_SAVE = 'SETTING_SAVE';
|
||||
|
||||
export function changeSetting(key, value) {
|
||||
export function changeSetting(path, value) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: SETTING_CHANGE,
|
||||
key,
|
||||
path,
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -21,9 +21,9 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS();
|
||||
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||
|
||||
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||
api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||
}, 5000, { trailing: true });
|
||||
|
||||
export function saveSettings() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from './avatar';
|
||||
@@ -94,21 +94,33 @@ export default class Account extends ImmutablePureComponent {
|
||||
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
|
||||
}
|
||||
buttons = (
|
||||
<div>
|
||||
<Fragment>
|
||||
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
|
||||
{hidingNotificationsButton}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
} else if (!account.get('moved')) {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
}
|
||||
|
||||
return small ? (
|
||||
<div className='account small'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
<Permalink
|
||||
className='account small'
|
||||
href={account.get('url')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar
|
||||
account={account}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
<DisplayName
|
||||
account={account}
|
||||
inline
|
||||
/>
|
||||
</Permalink>
|
||||
) : (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
// Package imports.
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
export default class DisplayName extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
account,
|
||||
className,
|
||||
} = this.props;
|
||||
const computedClass = classNames('display-name', className);
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<span className={computedClass}>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// The component.
|
||||
export default function DisplayName ({
|
||||
account,
|
||||
className,
|
||||
inline,
|
||||
}) {
|
||||
const computedClass = classNames('display-name', { inline }, className);
|
||||
|
||||
// The result.
|
||||
return account ? (
|
||||
<span className={computedClass}>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
|
||||
{inline ? ' ' : null}
|
||||
<span className='display-name__account'>@{account.get('acct')}</span>
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Props.
|
||||
DisplayName.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
className: PropTypes.string,
|
||||
inline: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class Dropdown extends React.PureComponent {
|
||||
(item, i) => item ? {
|
||||
...item,
|
||||
name: `${item.text}-${i}`,
|
||||
onClick: this.handleItemClick.bind(i),
|
||||
onClick: this.handleItemClick.bind(this, i),
|
||||
} : null
|
||||
),
|
||||
});
|
||||
|
||||
@@ -9,7 +9,26 @@ import classNames from 'classnames';
|
||||
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
hidden: {
|
||||
defaultMessage: 'Media hidden',
|
||||
id: 'status.media_hidden',
|
||||
},
|
||||
sensitive: {
|
||||
defaultMessage: 'Sensitive',
|
||||
id: 'media_gallery.sensitive',
|
||||
},
|
||||
toggle: {
|
||||
defaultMessage: 'Click to view',
|
||||
id: 'status.sensitive_toggle',
|
||||
},
|
||||
toggle_visible: {
|
||||
defaultMessage: 'Toggle visibility',
|
||||
id: 'media_gallery.toggle_visible',
|
||||
},
|
||||
warning: {
|
||||
defaultMessage: 'Sensitive content',
|
||||
id: 'status.sensitive_warning',
|
||||
},
|
||||
});
|
||||
|
||||
class Item extends React.PureComponent {
|
||||
@@ -206,48 +225,79 @@ export default class MediaGallery extends React.PureComponent {
|
||||
this.props.onOpenMedia(this.props.media, index);
|
||||
}
|
||||
|
||||
isStandaloneEligible() {
|
||||
const { media, standalone } = this.props;
|
||||
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive, letterbox, fullwidth } = this.props;
|
||||
const {
|
||||
handleClick,
|
||||
handleOpen,
|
||||
} = this;
|
||||
const {
|
||||
fullwidth,
|
||||
intl,
|
||||
letterbox,
|
||||
media,
|
||||
sensitive,
|
||||
standalone,
|
||||
} = this.props;
|
||||
const { visible } = this.state;
|
||||
const size = media.take(4).size;
|
||||
|
||||
let children;
|
||||
|
||||
if (!visible) {
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
|
||||
children = (
|
||||
<button className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span className='media-spoiler__warning'>{warning}</span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />);
|
||||
}
|
||||
}
|
||||
const computedClass = classNames('media-gallery', `size-${size}`, { 'full-width': fullwidth });
|
||||
|
||||
return (
|
||||
<div className={`media-gallery size-${size} ${fullwidth ? 'full-width' : ''}`}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
<div className={computedClass}>
|
||||
{visible ? (
|
||||
<div className='sensitive-info'>
|
||||
<IconButton
|
||||
icon='eye'
|
||||
onClick={handleOpen}
|
||||
overlay
|
||||
title={intl.formatMessage(messages.toggle_visible)}
|
||||
/>
|
||||
{sensitive ? (
|
||||
<span className='sensitive-marker'>
|
||||
<FormattedMessage {...messages.sensitive} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{function () {
|
||||
switch (true) {
|
||||
case !visible:
|
||||
return (
|
||||
<button
|
||||
className='media-spoiler'
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<span className='media-spoiler__warning'>
|
||||
<FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />
|
||||
</span>
|
||||
<span className='media-spoiler__trigger'>
|
||||
<FormattedMessage {...messages.toggle} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
case standalone && media.size === 1 && !!media.getIn([0, 'meta', 'small', 'aspect']):
|
||||
return (
|
||||
<Item
|
||||
attachment={media.get(0)}
|
||||
onClick={handleClick}
|
||||
standalone
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return media.take(4).map(
|
||||
(attachment, i) => (
|
||||
<Item
|
||||
attachment={attachment}
|
||||
index={i}
|
||||
key={attachment.get('id')}
|
||||
letterbox={letterbox}
|
||||
onClick={handleClick}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ export default class Permalink extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { href, children, className, ...other } = this.props;
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
href,
|
||||
to,
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||
|
||||
@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
onChange (path, checked) {
|
||||
dispatch(changeSetting(['community', ...path], checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import {
|
||||
cancelReplyCompose,
|
||||
changeCompose,
|
||||
changeComposeAdvancedOption,
|
||||
changeComposeSensitivity,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeSpoilerness,
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
insertEmojiCompose,
|
||||
mountCompose,
|
||||
selectComposeSuggestion,
|
||||
submitCompose,
|
||||
toggleComposeAdvancedOption,
|
||||
undoUploadCompose,
|
||||
unmountCompose,
|
||||
uploadCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import {
|
||||
@@ -47,8 +49,8 @@ function mapStateToProps (state) {
|
||||
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
||||
return {
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
advancedOptions: state.getIn(['compose', 'advanced_options']),
|
||||
amUnlocked: !state.getIn(['accounts', me, 'locked']),
|
||||
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
@@ -57,7 +59,7 @@ function mapStateToProps (state) {
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
progress: state.getIn(['compose', 'progress']),
|
||||
replyAccount: inReplyTo ? state.getIn(['accounts', state.getIn(['statuses', inReplyTo, 'account'])]) : null,
|
||||
replyAccount: inReplyTo ? state.getIn(['statuses', inReplyTo, 'account']) : null,
|
||||
replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
sideArm: state.getIn(['local_settings', 'side_arm']),
|
||||
@@ -74,6 +76,7 @@ function mapStateToProps (state) {
|
||||
// Dispatch mapping.
|
||||
const mapDispatchToProps = {
|
||||
onCancelReply: cancelReplyCompose,
|
||||
onChangeAdvancedOption: changeComposeAdvancedOption,
|
||||
onChangeDescription: changeUploadCompose,
|
||||
onChangeSensitivity: changeComposeSensitivity,
|
||||
onChangeSpoilerText: changeComposeSpoilerText,
|
||||
@@ -84,12 +87,13 @@ const mapDispatchToProps = {
|
||||
onCloseModal: closeModal,
|
||||
onFetchSuggestions: fetchComposeSuggestions,
|
||||
onInsertEmoji: insertEmojiCompose,
|
||||
onMount: mountCompose,
|
||||
onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
|
||||
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
|
||||
onSelectSuggestion: selectComposeSuggestion,
|
||||
onSubmit: submitCompose,
|
||||
onToggleAdvancedOption: toggleComposeAdvancedOption,
|
||||
onUndoUpload: undoUploadCompose,
|
||||
onUnmount: unmountCompose,
|
||||
onUpload: uploadCompose,
|
||||
};
|
||||
|
||||
@@ -188,6 +192,22 @@ class Composer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Tells our state the composer has been mounted.
|
||||
componentDidMount () {
|
||||
const { onMount } = this.props;
|
||||
if (onMount) {
|
||||
onMount();
|
||||
}
|
||||
}
|
||||
|
||||
// Tells our state the composer has been unmounted.
|
||||
componentWillUnmount () {
|
||||
const { onUnmount } = this.props;
|
||||
if (onUnmount) {
|
||||
onUnmount();
|
||||
}
|
||||
}
|
||||
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - Replying to zero or one users, places the cursor at the end
|
||||
@@ -245,17 +265,17 @@ class Composer extends React.Component {
|
||||
handleSubmit,
|
||||
handleRefTextarea,
|
||||
} = this.handlers;
|
||||
const { history } = this.context;
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
amUnlocked,
|
||||
doNotFederate,
|
||||
intl,
|
||||
isSubmitting,
|
||||
isUploading,
|
||||
layout,
|
||||
media,
|
||||
onCancelReply,
|
||||
onChangeAdvancedOption,
|
||||
onChangeDescription,
|
||||
onChangeSensitivity,
|
||||
onChangeSpoilerness,
|
||||
@@ -266,7 +286,6 @@ class Composer extends React.Component {
|
||||
onFetchSuggestions,
|
||||
onOpenActionsModal,
|
||||
onOpenDoodleModal,
|
||||
onToggleAdvancedOption,
|
||||
onUndoUpload,
|
||||
onUpload,
|
||||
privacy,
|
||||
@@ -297,12 +316,12 @@ class Composer extends React.Component {
|
||||
<ComposerReply
|
||||
account={replyAccount}
|
||||
content={replyContent}
|
||||
history={history}
|
||||
intl={intl}
|
||||
onCancel={onCancelReply}
|
||||
/>
|
||||
) : null}
|
||||
<ComposerTextarea
|
||||
advancedOptions={advancedOptions}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||
disabled={isSubmitting}
|
||||
intl={intl}
|
||||
@@ -329,19 +348,19 @@ class Composer extends React.Component {
|
||||
) : null}
|
||||
<ComposerOptions
|
||||
acceptContentTypes={acceptContentTypes}
|
||||
advancedOptions={advancedOptions}
|
||||
disabled={isSubmitting}
|
||||
doNotFederate={doNotFederate}
|
||||
full={media.size >= 4 || media.some(
|
||||
item => item.get('type') === 'video'
|
||||
)}
|
||||
hasMedia={!!media.size}
|
||||
intl={intl}
|
||||
onChangeAdvancedOption={onChangeAdvancedOption}
|
||||
onChangeSensitivity={onChangeSensitivity}
|
||||
onChangeVisibility={onChangeVisibility}
|
||||
onDoodleOpen={onOpenDoodleModal}
|
||||
onModalClose={onCloseModal}
|
||||
onModalOpen={onOpenActionsModal}
|
||||
onToggleAdvancedOption={onToggleAdvancedOption}
|
||||
onToggleSpoiler={onChangeSpoilerness}
|
||||
onUpload={onUpload}
|
||||
privacy={privacy}
|
||||
@@ -350,7 +369,7 @@ class Composer extends React.Component {
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
<ComposerPublisher
|
||||
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
|
||||
countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
|
||||
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
|
||||
intl={intl}
|
||||
onSecondarySubmit={handleSecondarySubmit}
|
||||
@@ -364,19 +383,14 @@ class Composer extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
// Context
|
||||
Composer.contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
// Props.
|
||||
Composer.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
||||
// State props.
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
amUnlocked: PropTypes.bool,
|
||||
doNotFederate: PropTypes.bool,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
isSubmitting: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
@@ -385,7 +399,7 @@ Composer.propTypes = {
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
privacy: PropTypes.string,
|
||||
progress: PropTypes.number,
|
||||
replyAccount: ImmutablePropTypes.map,
|
||||
replyAccount: PropTypes.string,
|
||||
replyContent: PropTypes.string,
|
||||
resetFileKey: PropTypes.number,
|
||||
sideArm: PropTypes.string,
|
||||
@@ -399,6 +413,7 @@ Composer.propTypes = {
|
||||
|
||||
// Dispatch props.
|
||||
onCancelReply: PropTypes.func,
|
||||
onChangeAdvancedOption: PropTypes.func,
|
||||
onChangeDescription: PropTypes.func,
|
||||
onChangeSensitivity: PropTypes.func,
|
||||
onChangeSpoilerText: PropTypes.func,
|
||||
@@ -409,12 +424,13 @@ Composer.propTypes = {
|
||||
onCloseModal: PropTypes.func,
|
||||
onFetchSuggestions: PropTypes.func,
|
||||
onInsertEmoji: PropTypes.func,
|
||||
onMount: PropTypes.func,
|
||||
onOpenActionsModal: PropTypes.func,
|
||||
onOpenDoodleModal: PropTypes.func,
|
||||
onSelectSuggestion: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onToggleAdvancedOption: PropTypes.func,
|
||||
onUndoUpload: PropTypes.func,
|
||||
onUnmount: PropTypes.func,
|
||||
onUpload: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -104,7 +104,10 @@ export default class ComposerOptionsDropdownContentItem extends React.PureCompon
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
) : <div className='content'>{text}</div>}
|
||||
) :
|
||||
<div className='content'>
|
||||
<strong>{text}</strong>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import {
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
@@ -47,11 +48,11 @@ const messages = defineMessages({
|
||||
},
|
||||
local_only_long: {
|
||||
defaultMessage: 'Do not post to other instances',
|
||||
id: 'advanced-options.local-only.long',
|
||||
id: 'advanced_options.local-only.long',
|
||||
},
|
||||
local_only_short: {
|
||||
defaultMessage: 'Local-only',
|
||||
id: 'advanced-options.local-only.short',
|
||||
id: 'advanced_options.local-only.short',
|
||||
},
|
||||
private_long: {
|
||||
defaultMessage: 'Post to followers only',
|
||||
@@ -77,6 +78,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Hide text behind warning',
|
||||
id: 'compose_form.spoiler',
|
||||
},
|
||||
threaded_mode_long: {
|
||||
defaultMessage: 'Automatically opens a reply on posting',
|
||||
id: 'advanced_options.threaded_mode.long',
|
||||
},
|
||||
threaded_mode_short: {
|
||||
defaultMessage: 'Threaded mode',
|
||||
id: 'advanced_options.threaded_mode.short',
|
||||
},
|
||||
unlisted_long: {
|
||||
defaultMessage: 'Do not show in public timelines',
|
||||
id: 'privacy.unlisted.long',
|
||||
@@ -149,16 +158,16 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
} = this.handlers;
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
disabled,
|
||||
doNotFederate,
|
||||
full,
|
||||
hasMedia,
|
||||
intl,
|
||||
onChangeAdvancedOption,
|
||||
onChangeSensitivity,
|
||||
onChangeVisibility,
|
||||
onModalClose,
|
||||
onModalOpen,
|
||||
onToggleAdvancedOption,
|
||||
onToggleSpoiler,
|
||||
privacy,
|
||||
resetFileKey,
|
||||
@@ -283,23 +292,31 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
onClick={onToggleSpoiler}
|
||||
title={intl.formatMessage(messages.spoiler)}
|
||||
/>
|
||||
<Dropdown
|
||||
active={doNotFederate}
|
||||
disabled={disabled}
|
||||
icon='home'
|
||||
items={[
|
||||
{
|
||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||
name: 'do_not_federate',
|
||||
on: doNotFederate,
|
||||
text: <FormattedMessage {...messages.local_only_short} />,
|
||||
},
|
||||
]}
|
||||
onChange={onToggleAdvancedOption}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||
/>
|
||||
{advancedOptions ? (
|
||||
<Dropdown
|
||||
active={advancedOptions.some(value => !!value)}
|
||||
disabled={disabled}
|
||||
icon='ellipsis-h'
|
||||
items={[
|
||||
{
|
||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||
name: 'do_not_federate',
|
||||
on: advancedOptions.get('do_not_federate'),
|
||||
text: <FormattedMessage {...messages.local_only_short} />,
|
||||
},
|
||||
{
|
||||
meta: <FormattedMessage {...messages.threaded_mode_long} />,
|
||||
name: 'threaded_mode',
|
||||
on: advancedOptions.get('threaded_mode'),
|
||||
text: <FormattedMessage {...messages.threaded_mode_short} />,
|
||||
},
|
||||
]}
|
||||
onChange={onChangeAdvancedOption}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -309,17 +326,17 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
// Props.
|
||||
ComposerOptions.propTypes = {
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
disabled: PropTypes.bool,
|
||||
doNotFederate: PropTypes.bool,
|
||||
full: PropTypes.bool,
|
||||
hasMedia: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeAdvancedOption: PropTypes.func,
|
||||
onChangeSensitivity: PropTypes.func,
|
||||
onChangeVisibility: PropTypes.func,
|
||||
onDoodleOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onToggleAdvancedOption: PropTypes.func,
|
||||
onToggleSpoiler: PropTypes.func,
|
||||
onUpload: PropTypes.func,
|
||||
privacy: PropTypes.string,
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function ComposerPublisher ({
|
||||
unlisted: 'unlock-alt',
|
||||
}[privacy]}
|
||||
/>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.publish} />
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
// Components.
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
|
||||
// Utils.
|
||||
@@ -31,17 +29,6 @@ const handlers = {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
|
||||
// Handles a click on the status's account.
|
||||
handleClickAccount () {
|
||||
const {
|
||||
account,
|
||||
history,
|
||||
} = this.props;
|
||||
if (history) {
|
||||
history.push(`/accounts/${account.get('id')}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// The component.
|
||||
@@ -55,10 +42,7 @@ export default class ComposerReply extends React.PureComponent {
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
handleClick,
|
||||
handleClickAccount,
|
||||
} = this.handlers;
|
||||
const { handleClick } = this.handlers;
|
||||
const {
|
||||
account,
|
||||
content,
|
||||
@@ -76,21 +60,10 @@ export default class ComposerReply extends React.PureComponent {
|
||||
title={intl.formatMessage(messages.cancel)}
|
||||
/>
|
||||
{account ? (
|
||||
<a
|
||||
className='account'
|
||||
href={account.get('url')}
|
||||
onClick={handleClickAccount}
|
||||
>
|
||||
<Avatar
|
||||
account={account}
|
||||
className='avatar'
|
||||
size={24}
|
||||
/>
|
||||
<DisplayName
|
||||
account={account}
|
||||
className='display_name'
|
||||
/>
|
||||
</a>
|
||||
<AccountContainer
|
||||
id={account}
|
||||
small
|
||||
/>
|
||||
) : null}
|
||||
</header>
|
||||
<div
|
||||
@@ -105,9 +78,8 @@ export default class ComposerReply extends React.PureComponent {
|
||||
}
|
||||
|
||||
ComposerReply.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
account: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
// Components.
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
// Messages.
|
||||
const messages = defineMessages({
|
||||
localOnly: {
|
||||
defaultMessage: 'This post is local-only',
|
||||
id: 'advanced_options.local-only.tooltip',
|
||||
},
|
||||
threadedMode: {
|
||||
defaultMessage: 'Threaded mode enabled',
|
||||
id: 'advanced_options.threaded_mode.tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
// We use an array of tuples here instead of an object because it
|
||||
// preserves order.
|
||||
const iconMap = [
|
||||
['do_not_federate', 'home', messages.localOnly],
|
||||
['threaded_mode', 'comments', messages.threadedMode],
|
||||
];
|
||||
|
||||
// The component.
|
||||
export default function ComposerTextareaIcons ({
|
||||
advancedOptions,
|
||||
intl,
|
||||
}) {
|
||||
|
||||
// The result. We just map every active option to its icon.
|
||||
return (
|
||||
<div className='composer--textarea--icons'>
|
||||
{advancedOptions ? iconMap.map(
|
||||
([key, icon, message]) => advancedOptions.get(key) ? (
|
||||
<span
|
||||
className='textarea_icon'
|
||||
key={key}
|
||||
title={intl.formatMessage(message)}
|
||||
>
|
||||
<Icon
|
||||
fullwidth
|
||||
icon={icon}
|
||||
/>
|
||||
</span>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Props.
|
||||
ComposerTextareaIcons.propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||
|
||||
// Components.
|
||||
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
|
||||
import ComposerTextareaIcons from './icons';
|
||||
import ComposerTextareaSuggestions from './suggestions';
|
||||
|
||||
// Utils.
|
||||
@@ -32,7 +33,7 @@ const handlers = {
|
||||
|
||||
// When blurring the textarea, suggestions are hidden.
|
||||
handleBlur () {
|
||||
//this.setState({ suggestionsHidden: true });
|
||||
this.setState({ suggestionsHidden: true });
|
||||
},
|
||||
|
||||
// When the contents of the textarea change, we have to pull up new
|
||||
@@ -57,7 +58,7 @@ const handlers = {
|
||||
const right = value.slice(selectionStart).search(/[\s\u200B]/);
|
||||
const token = function () {
|
||||
switch (true) {
|
||||
case left < 0 || /[@:]/.test(!value[left]):
|
||||
case left < 0 || !/[@:]/.test(value[left]):
|
||||
return null;
|
||||
case right < 0:
|
||||
return value.slice(left);
|
||||
@@ -127,6 +128,11 @@ const handlers = {
|
||||
return;
|
||||
}
|
||||
|
||||
// We submit the status on control/meta + enter.
|
||||
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
onSubmit();
|
||||
}
|
||||
|
||||
// Switches over the pressed key.
|
||||
switch(e.key) {
|
||||
|
||||
@@ -156,11 +162,6 @@ const handlers = {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We submit the status on control/meta + enter.
|
||||
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
// When the escape key is released, we either close the suggestions
|
||||
@@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
|
||||
handleRefTextarea,
|
||||
} = this.handlers;
|
||||
const {
|
||||
advancedOptions,
|
||||
autoFocus,
|
||||
disabled,
|
||||
intl,
|
||||
@@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
|
||||
<div className='composer--textarea'>
|
||||
<label>
|
||||
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
|
||||
<ComposerTextareaIcons
|
||||
advancedOptions={advancedOptions}
|
||||
intl={intl}
|
||||
/>
|
||||
<Textarea
|
||||
aria-autocomplete='list'
|
||||
autoFocus={autoFocus}
|
||||
@@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
|
||||
|
||||
// Props.
|
||||
ComposerTextarea.propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
autoFocus: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
||||
@@ -24,9 +24,16 @@ const handlers = {
|
||||
} = this.props;
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevents following account links
|
||||
onClick(index);
|
||||
}
|
||||
},
|
||||
|
||||
// This prevents the focus from changing, which would mess with
|
||||
// our suggestion code.
|
||||
handleMouseDown (e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
};
|
||||
|
||||
// The component.
|
||||
@@ -40,7 +47,10 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const { handleClick } = this.handlers;
|
||||
const {
|
||||
handleMouseDown,
|
||||
handleClick,
|
||||
} = this.handlers;
|
||||
const {
|
||||
selected,
|
||||
suggestion,
|
||||
@@ -51,7 +61,8 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
||||
return (
|
||||
<div
|
||||
className={computedClass}
|
||||
onMouseDown={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClickCapture={handleClick} // Jumps in front of contents
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
>
|
||||
|
||||
@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['direct', ...key], checked));
|
||||
onChange (path, checked) {
|
||||
dispatch(changeSetting(['direct', ...path], checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -45,10 +45,10 @@ const handlers = {
|
||||
const {
|
||||
onClear,
|
||||
submitted,
|
||||
value: { length },
|
||||
value,
|
||||
} = this.props;
|
||||
e.preventDefault(); // Prevents focus change ??
|
||||
if (onClear && (submitted || length)) {
|
||||
if (onClear && (submitted || value && value.length)) {
|
||||
onClear();
|
||||
}
|
||||
},
|
||||
@@ -100,7 +100,8 @@ export default class DrawerSearch extends React.PureComponent {
|
||||
value,
|
||||
} = this.props;
|
||||
const { expanded } = this.state;
|
||||
const computedClass = classNames('drawer--search', { active: value.length || submitted });
|
||||
const active = value && value.length || submitted;
|
||||
const computedClass = classNames('drawer--search', { active });
|
||||
|
||||
return (
|
||||
<div className={computedClass}>
|
||||
@@ -126,11 +127,11 @@ export default class DrawerSearch extends React.PureComponent {
|
||||
tabIndex='0'
|
||||
>
|
||||
<Icon icon='search' />
|
||||
<Icon icon='fa-times-circle' />
|
||||
<Icon icon='times-circle' />
|
||||
</div>
|
||||
<Overlay
|
||||
placement='bottom'
|
||||
show={expanded && !(value || '').length && !submitted}
|
||||
show={expanded && !active}
|
||||
target={this}
|
||||
><DrawerSearchPopout /></Overlay>
|
||||
</div>
|
||||
|
||||
@@ -42,56 +42,61 @@ export default function DrawerSearchPopout ({ style }) {
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<Motion
|
||||
defaultStyle={{
|
||||
opacity: 0,
|
||||
scaleX: 0.85,
|
||||
scaleY: 0.75,
|
||||
}}
|
||||
<div
|
||||
className='drawer--search--popout'
|
||||
style={{
|
||||
opacity: motionSpring,
|
||||
scaleX: motionSpring,
|
||||
scaleY: motionSpring,
|
||||
...style,
|
||||
position: 'absolute',
|
||||
width: 285,
|
||||
}}
|
||||
>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div
|
||||
className='drawer--search--popout'
|
||||
style={{
|
||||
...style,
|
||||
position: 'absolute',
|
||||
width: 285,
|
||||
opacity: opacity,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
}}
|
||||
>
|
||||
<h4><FormattedMessage {...messages.format} /></h4>
|
||||
<ul>
|
||||
<li>
|
||||
<em>#example</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.hashtag} />
|
||||
</li>
|
||||
<li>
|
||||
<em>@username@domain</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.user} />
|
||||
</li>
|
||||
<li>
|
||||
<em>URL</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.user} />
|
||||
</li>
|
||||
<li>
|
||||
<em>URL</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.status} />
|
||||
</li>
|
||||
</ul>
|
||||
<FormattedMessage {...messages.text} />
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<Motion
|
||||
defaultStyle={{
|
||||
opacity: 0,
|
||||
scaleX: 0.85,
|
||||
scaleY: 0.75,
|
||||
}}
|
||||
style={{
|
||||
opacity: motionSpring,
|
||||
scaleX: motionSpring,
|
||||
scaleY: motionSpring,
|
||||
}}
|
||||
>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div
|
||||
style={{
|
||||
opacity: opacity,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
}}
|
||||
>
|
||||
<h4><FormattedMessage {...messages.format} /></h4>
|
||||
<ul>
|
||||
<li>
|
||||
<em>#example</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.hashtag} />
|
||||
</li>
|
||||
<li>
|
||||
<em>@username@domain</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.user} />
|
||||
</li>
|
||||
<li>
|
||||
<em>URL</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.user} />
|
||||
</li>
|
||||
<li>
|
||||
<em>URL</em>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.status} />
|
||||
</li>
|
||||
</ul>
|
||||
<FormattedMessage {...messages.text} />
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
|
||||
import StatusList from 'flavours/glitch/components/status_list';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||
@@ -16,6 +17,7 @@ const messages = defineMessages({
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
});
|
||||
|
||||
@@ -30,6 +32,7 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@@ -59,12 +62,12 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
handleScrollToBottom = debounce(() => {
|
||||
this.props.dispatch(expandFavouritedStatuses());
|
||||
}
|
||||
}, 300, { leading: true })
|
||||
|
||||
render () {
|
||||
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
|
||||
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
@@ -85,6 +88,7 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
statusIds={statusIds}
|
||||
scrollKey={`favourited_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onScrollToBottom={this.handleScrollToBottom}
|
||||
/>
|
||||
</Column>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { intl, myAccount, columns, multiColumn, lists } = this.props;
|
||||
|
||||
let navItems = [];
|
||||
const navItems = [];
|
||||
let listItems = [];
|
||||
|
||||
if (multiColumn) {
|
||||
@@ -111,10 +111,10 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
|
||||
listItems = listItems.concat([
|
||||
<div>
|
||||
<ColumnLink key='7' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<div key='7'>
|
||||
<ColumnLink key='8' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
<ColumnLink key={(8 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
||||
@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
onChange (path, checked) {
|
||||
dispatch(changeSetting(['home', ...path], checked));
|
||||
},
|
||||
|
||||
onSave () {
|
||||
|
||||
@@ -11,12 +11,11 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onPushChange = (key, checked) => {
|
||||
this.props.onChange(['push', ...key], checked);
|
||||
onPushChange = (path, checked) => {
|
||||
this.props.onChange(['push', ...path], checked);
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import { clearNotifications } from 'flavours/glitch/actions/notifications';
|
||||
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications';
|
||||
import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -18,19 +18,14 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
if (key[0] === 'push') {
|
||||
dispatch(changePushNotifications(key.slice(1), checked));
|
||||
onChange (path, checked) {
|
||||
if (path[0] === 'push') {
|
||||
dispatch(changePushNotifications(path.slice(1), checked));
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...key], checked));
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
dispatch(savePushNotificationSettings());
|
||||
},
|
||||
|
||||
onClear () {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
|
||||
@@ -8,8 +8,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['public', ...key], checked));
|
||||
onChange (path, checked) {
|
||||
dispatch(changeSetting(['public', ...path], checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import axios from 'axios';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
|
||||
@injectIntl
|
||||
export default class EmbedModal extends ImmutablePureComponent {
|
||||
@@ -23,7 +23,7 @@ export default class EmbedModal extends ImmutablePureComponent {
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
axios.post('/api/web/embed', { url }).then(res => {
|
||||
api().post('/api/web/embed', { url }).then(res => {
|
||||
this.setState({ loading: false, oembed: res.data });
|
||||
|
||||
const iframeDocument = this.iframe.contentWindow.document;
|
||||
|
||||
@@ -34,6 +34,8 @@ const messages = {
|
||||
'status.collapse': 'Collapse',
|
||||
'status.uncollapse': 'Uncollapse',
|
||||
|
||||
'media_gallery.sensitive': 'Sensitive',
|
||||
|
||||
'favourite_modal.combo': 'You can press {combo} to skip this next time',
|
||||
|
||||
'home.column_settings.show_direct': 'Show DMs',
|
||||
@@ -52,9 +54,13 @@ const messages = {
|
||||
'compose.attach.doodle': 'Draw something',
|
||||
'compose.attach': 'Attach...',
|
||||
|
||||
'advanced-options.local-only.short': 'Local-only',
|
||||
'advanced-options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.short': 'Local-only',
|
||||
'advanced_options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.tooltip': 'This post is local-only',
|
||||
'advanced_options.icon_title': 'Advanced options',
|
||||
'advanced_options.threaded_mode.short': 'Threaded mode',
|
||||
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
|
||||
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
||||
|
||||
@@ -55,9 +55,13 @@ const messages = {
|
||||
'compose.attach.doodle': '落書きをする',
|
||||
'compose.attach': 'アタッチ...',
|
||||
|
||||
'advanced-options.local-only.short': 'ローカル限定',
|
||||
'advanced-options.local-only.long': '他のインスタンスには投稿されません',
|
||||
'advanced_options.local-only.short': 'ローカル限定',
|
||||
'advanced_options.local-only.long': '他のインスタンスには投稿されません',
|
||||
'advanced_options.local-only.tooltip': 'この投稿はローカル限定投稿です',
|
||||
'advanced_options.icon_title': '高度な設定',
|
||||
'advanced_options.threaded_mode.short': 'スレッドモード',
|
||||
'advanced_options.threaded_mode.long': '投稿時に自動的に返信するように設定します',
|
||||
'advanced_options.threaded_mode.tooltip': 'スレッドモードを有効にする',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
||||
|
||||
@@ -28,12 +28,18 @@ const messages = {
|
||||
'settings.media': 'Zawartość multimedialna',
|
||||
'settings.media_letterbox': 'Letterbox media',
|
||||
'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości',
|
||||
'settings.preferences': 'Preferencje użyytkownika',
|
||||
'settings.preferences': 'Preferencje użytkownika',
|
||||
'settings.wide_view': 'Szeroki widok (tylko w trybie desktopowym)',
|
||||
'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
|
||||
'status.collapse': 'Zwiń',
|
||||
'status.uncollapse': 'Rozwiń',
|
||||
|
||||
'media_gallery.sensitive': 'Zawartość wrażliwa',
|
||||
|
||||
'favourite_modal.combo': 'Możesz nacisnąć {combo}, aby pominąć to następnym razem',
|
||||
|
||||
'home.column_settings.show_direct': 'Pokaż wiadomości bezpośrednie',
|
||||
|
||||
'notification.markForDeletion': 'Oznacz do usunięcia',
|
||||
'notifications.clear': 'Wyczyść wszystkie powiadomienia',
|
||||
'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
|
||||
@@ -43,6 +49,18 @@ const messages = {
|
||||
'notification_purge.btn_none': 'Odznacz\nwszystkie',
|
||||
'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
|
||||
'notification_purge.btn_apply': 'Usuń\nzaznaczone',
|
||||
|
||||
'compose.attach.upload': 'Wyślij plik',
|
||||
'compose.attach.doodle': 'Narysuj coś',
|
||||
'compose.attach': 'Załącz coś',
|
||||
|
||||
'advanced_options.local-only.short': 'Tylko lokalnie',
|
||||
'advanced_options.local-only.long': 'Nie wysyłaj na inne instancje',
|
||||
'advanced_options.local-only.tooltip': 'Ten wpis jest widoczny tylko lokalnie',
|
||||
'advanced_options.icon_title': 'Ustawienia zaawansowane',
|
||||
'advanced_options.threaded_mode.short': 'Tryb wątków',
|
||||
'advanced_options.threaded_mode.long': 'Przechodzi do tworzenia odpowiedzi po publikacji wpisu',
|
||||
'advanced_options.threaded_mode.tooltip': 'Włączono tryb wątków',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
||||
|
||||
@@ -6,3 +6,10 @@ en:
|
||||
skins:
|
||||
glitch:
|
||||
default: Default
|
||||
pl:
|
||||
flavours:
|
||||
glitch:
|
||||
description: Domyślny motyw instancji GlitchSoc.
|
||||
skins:
|
||||
glitch:
|
||||
default: Domyślny
|
||||
|
||||
@@ -33,11 +33,13 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
import uuid from 'flavours/glitch/util/uuid';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
import { overwrite } from 'flavours/glitch/util/js_helpers';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
mounted: false,
|
||||
advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
threaded_mode: false,
|
||||
}),
|
||||
sensitive: false,
|
||||
spoiler: false,
|
||||
@@ -55,6 +57,7 @@ const initialState = ImmutableMap({
|
||||
suggestions: ImmutableList(),
|
||||
default_advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
threaded_mode: null, // Do not reset
|
||||
}),
|
||||
default_privacy: 'public',
|
||||
default_sensitive: false,
|
||||
@@ -83,6 +86,20 @@ function statusToTextMentions(state, status) {
|
||||
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
|
||||
};
|
||||
|
||||
function apiStatusToTextMentions (state, status) {
|
||||
let set = ImmutableOrderedSet([]);
|
||||
|
||||
if (status.account.id !== me) {
|
||||
set = set.add(`@${status.account.acct} `);
|
||||
}
|
||||
|
||||
return set.union(status.mentions.filter(
|
||||
mention => mention.id !== me
|
||||
).map(
|
||||
mention => `@${mention.acct} `
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function clearAll(state) {
|
||||
return state.withMutations(map => {
|
||||
map.set('text', '');
|
||||
@@ -90,7 +107,10 @@ function clearAll(state) {
|
||||
map.set('spoiler_text', '');
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('advanced_options', state.get('default_advanced_options'));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||
);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
@@ -98,6 +118,31 @@ function clearAll(state) {
|
||||
});
|
||||
};
|
||||
|
||||
function continueThread (state, status) {
|
||||
return state.withMutations(function (map) {
|
||||
map.set('text', apiStatusToTextMentions(state, status));
|
||||
if (status.spoiler_text) {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', status.spoiler_text);
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', status.id);
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
|
||||
);
|
||||
map.set('privacy', status.visibility);
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('focusDate', new Date());
|
||||
map.set('preselectDate', new Date());
|
||||
});
|
||||
}
|
||||
|
||||
function appendMedia(state, media) {
|
||||
const prevSize = state.get('media_attachments').size;
|
||||
|
||||
@@ -182,8 +227,7 @@ export default function compose(state = initialState, action) {
|
||||
return state.set('mounted', false);
|
||||
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
|
||||
return state
|
||||
.set('advanced_options',
|
||||
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
|
||||
.set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_SENSITIVITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
@@ -220,9 +264,10 @@ export default function compose(state = initialState, action) {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('advanced_options', new ImmutableMap({
|
||||
do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
|
||||
}));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
|
||||
);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('preselectDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
@@ -243,14 +288,17 @@ export default function compose(state = initialState, action) {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('advanced_options', state.get('default_advanced_options'));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||
);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
return action.status && state.getIn(['advanced_options', 'threaded_mode']) ? continueThread(state, action.status) : clearAll(state);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
||||
return state.set('is_submitting', false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications';
|
||||
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from 'flavours/glitch/actions/push_notifications';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
@@ -43,8 +43,8 @@ export default function push_subscriptions(state = initialState, action) {
|
||||
return state.set('browserSupport', action.value);
|
||||
case CLEAR_SUBSCRIPTION:
|
||||
return initialState;
|
||||
case ALERTS_CHANGE:
|
||||
return state.setIn(action.key, action.value);
|
||||
case SET_ALERTS:
|
||||
return state.setIn(action.path, action.value);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function settings(state = initialState, action) {
|
||||
return hydrate(state, action.state.get('settings'));
|
||||
case SETTING_CHANGE:
|
||||
return state
|
||||
.setIn(action.key, action.value)
|
||||
.setIn(action.path, action.value)
|
||||
.set('saved', false);
|
||||
case COLUMN_ADD:
|
||||
return state
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {
|
||||
FAVOURITED_STATUSES_FETCH_REQUEST,
|
||||
FAVOURITED_STATUSES_FETCH_SUCCESS,
|
||||
FAVOURITED_STATUSES_FETCH_FAIL,
|
||||
FAVOURITED_STATUSES_EXPAND_REQUEST,
|
||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
||||
FAVOURITED_STATUSES_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/favourites';
|
||||
import {
|
||||
PINNED_STATUSES_FETCH_SUCCESS,
|
||||
@@ -30,6 +34,7 @@ const normalizeList = (state, listType, statuses, next) => {
|
||||
return state.update(listType, listMap => listMap.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('loaded', true);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', ImmutableList(statuses.map(item => item.id)));
|
||||
}));
|
||||
};
|
||||
@@ -37,6 +42,7 @@ const normalizeList = (state, listType, statuses, next) => {
|
||||
const appendToList = (state, listType, statuses, next) => {
|
||||
return state.update(listType, listMap => listMap.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', map.get('items').concat(statuses.map(item => item.id)));
|
||||
}));
|
||||
};
|
||||
@@ -55,6 +61,12 @@ const removeOneFromList = (state, listType, status) => {
|
||||
|
||||
export default function statusLists(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case FAVOURITED_STATUSES_FETCH_REQUEST:
|
||||
case FAVOURITED_STATUSES_EXPAND_REQUEST:
|
||||
return state.setIn(['favourites', 'isLoading'], true);
|
||||
case FAVOURITED_STATUSES_FETCH_FAIL:
|
||||
case FAVOURITED_STATUSES_EXPAND_FAIL:
|
||||
return state.setIn(['favourites', 'isLoading'], false);
|
||||
case FAVOURITED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'favourites', action.statuses, action.next);
|
||||
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
|
||||
|
||||
@@ -396,10 +396,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
@@ -413,7 +415,7 @@
|
||||
color: $ui-primary-color;
|
||||
font-family: 'mastodon-font-monospace', monospace;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,22 +52,7 @@
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .account {
|
||||
& > .avatar {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
& > .display_name {
|
||||
color: $ui-base-color;
|
||||
display: block;
|
||||
padding-right: 25px;
|
||||
max-width: 100%;
|
||||
line-height: 24px;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
& > .account.small { color: $ui-base-color }
|
||||
|
||||
& > .cancel {
|
||||
float: right;
|
||||
@@ -87,6 +72,27 @@
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
padding-top: 5px;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child { margin-bottom: 0 }
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($ui-base-color, 20%);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline }
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span { text-decoration: underline }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emojione {
|
||||
@@ -94,27 +100,6 @@
|
||||
height: 20px;
|
||||
margin: -5px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child { margin-bottom: 0 }
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($ui-base-color, 20%);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline }
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span { text-decoration: underline }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea {
|
||||
@@ -149,6 +134,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea--icons {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .textarea_icon {
|
||||
display: block;
|
||||
margin: 2px 0 0 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: darken($ui-primary-color, 24%);
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea--suggestions {
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -175,6 +181,7 @@
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
@@ -191,6 +198,12 @@
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
& > .account.small {
|
||||
.display-name {
|
||||
& > span { color: lighten($ui-base-color, 36%) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer--upload_form {
|
||||
|
||||
@@ -114,19 +114,27 @@
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $ui-secondary-color;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
z-index: 2;
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $ui-secondary-color;
|
||||
font-size: 18px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
transition: all 100ms linear;
|
||||
}
|
||||
|
||||
@@ -136,14 +144,15 @@
|
||||
}
|
||||
|
||||
.fa-times-circle {
|
||||
top: 11px;
|
||||
transform: rotate(-90deg);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { color: $primary-text-color }
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&.active {
|
||||
& > .icon {
|
||||
.fa-search {
|
||||
opacity: 0;
|
||||
transform: rotate(90deg);
|
||||
@@ -158,6 +167,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.drawer--search--popout {
|
||||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px 14px 14px;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
color: $ui-primary-color;
|
||||
background: $simple-background-color;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
color: $ui-primary-color;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
ul { margin-bottom: 10px }
|
||||
li { padding: 4px 0 }
|
||||
|
||||
em {
|
||||
color: $ui-base-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer--account {
|
||||
padding: 10px;
|
||||
color: $ui-primary-color;
|
||||
|
||||
@@ -745,6 +745,8 @@
|
||||
.account {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
.account__display-name {
|
||||
flex: 1 1 auto;
|
||||
@@ -762,27 +764,8 @@
|
||||
& > .account__avatar-wrapper { margin: 0 8px 0 0 }
|
||||
|
||||
& > .display-name {
|
||||
display: block;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
& > strong {
|
||||
display: inline;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline;
|
||||
color: lighten($ui-base-color, 36%);
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
&::before { content: " " }
|
||||
}
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,6 +1226,30 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
strong {
|
||||
display: inline;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status__relative-time,
|
||||
@@ -1561,6 +1568,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drawer__inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: lighten($ui-base-color, 13%) url('~images/wave-drawer.png') no-repeat bottom / 100% auto;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.darker {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
|
||||
> .mastodon {
|
||||
background: url('~images/mastodon-ui.png') no-repeat left bottom / contain;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pseudo-drawer {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
font-size: 13px;
|
||||
@@ -1841,6 +1881,11 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.getting-started__wrapper,
|
||||
.getting_started {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
|
||||
.getting-started__wrapper {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
@@ -2447,17 +2492,29 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spoiler-button {
|
||||
display: none;
|
||||
left: 4px;
|
||||
.sensitive-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&.spoiler-button--visible {
|
||||
display: block;
|
||||
}
|
||||
.sensitive-marker {
|
||||
margin: 0 3px;
|
||||
border-radius: 2px;
|
||||
padding: 2px 6px;
|
||||
color: rgba($primary-text-color, 0.8);
|
||||
background: rgba($base-overlay-background, 0.5);
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
text-transform: uppercase;
|
||||
opacity: .9;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
.media-gallery:hover & { opacity: 1 }
|
||||
}
|
||||
|
||||
.modal-container--preloader {
|
||||
@@ -2774,6 +2831,112 @@
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search__input {
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
padding-right: 30px;
|
||||
font-family: inherit;
|
||||
background: $ui-base-color;
|
||||
color: $ui-primary-color;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
|
||||
.search__icon {
|
||||
.fa {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
transition: all 100ms linear;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $ui-secondary-color;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
|
||||
&.active {
|
||||
pointer-events: auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-search {
|
||||
transform: rotate(90deg);
|
||||
|
||||
&.active {
|
||||
pointer-events: none;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fa-times-circle {
|
||||
top: 11px;
|
||||
transform: rotate(0deg);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results__header {
|
||||
color: $ui-base-lighter-color;
|
||||
background: lighten($ui-base-color, 2%);
|
||||
border-bottom: 1px solid darken($ui-base-color, 4%);
|
||||
padding: 15px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-results__hashtag {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
color: $ui-secondary-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: lighten($ui-secondary-color, 4%);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-root {
|
||||
transition: opacity 0.3s linear;
|
||||
will-change: opacity;
|
||||
@@ -3911,37 +4074,6 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.search-popout {
|
||||
background: $simple-background-color;
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
padding-bottom: 14px;
|
||||
margin-top: 10px;
|
||||
color: $ui-primary-color;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
color: $ui-primary-color;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
em {
|
||||
font-weight: 500;
|
||||
color: $ui-base-color;
|
||||
}
|
||||
}
|
||||
|
||||
noscript {
|
||||
text-align: center;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# (REQUIRED) The location of the pack files.
|
||||
pack:
|
||||
about: packs/about.js
|
||||
admin:
|
||||
admin: packs/public.js
|
||||
auth:
|
||||
common:
|
||||
filename: packs/common.js
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import ready from './ready';
|
||||
import LinkHeader from './link_header';
|
||||
|
||||
export const getLinks = response => {
|
||||
@@ -11,10 +12,17 @@ export const getLinks = response => {
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
let csrfHeader = {};
|
||||
function setCSRFHeader() {
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
ready(setCSRFHeader);
|
||||
|
||||
export default getState => axios.create({
|
||||
headers: {
|
||||
headers: Object.assign(csrfHeader, getState ? {
|
||||
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
|
||||
},
|
||||
} : {}),
|
||||
|
||||
transformResponse: [function (data) {
|
||||
try {
|
||||
|
||||
@@ -27,15 +27,15 @@ export function HashtagTimeline () {
|
||||
}
|
||||
|
||||
export function ListTimeline () {
|
||||
return import(/* webpackChunkName: "features/list_timeline" */'flavours/glitch/features/list_timeline');
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'flavours/glitch/features/list_timeline');
|
||||
}
|
||||
|
||||
export function Lists () {
|
||||
return import(/* webpackChunkName: "features/lists" */'flavours/glitch/features/lists');
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'flavours/glitch/features/lists');
|
||||
}
|
||||
|
||||
export function ListEditor () {
|
||||
return import(/* webpackChunkName: "features/list_editor" */'flavours/glitch/features/list_editor');
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
|
||||
}
|
||||
|
||||
export function DirectTimeline() {
|
||||
@@ -51,7 +51,7 @@ export function GettingStarted () {
|
||||
}
|
||||
|
||||
export function KeyboardShortcuts () {
|
||||
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
|
||||
}
|
||||
|
||||
export function PinnedStatuses () {
|
||||
|
||||
@@ -18,6 +18,6 @@ export const boostModal = getMeta('boost_modal');
|
||||
export const favouriteModal = getMeta('favourite_modal');
|
||||
export const deleteModal = getMeta('delete_modal');
|
||||
export const me = getMeta('me');
|
||||
export const maxChars = getMeta('max_toot_chars') || 500;
|
||||
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
|
||||
|
||||
export default initialState;
|
||||
|
||||
5
app/javascript/flavours/glitch/util/js_helpers.js
Normal file
5
app/javascript/flavours/glitch/util/js_helpers.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// This function returns the new value unless it is `null` or
|
||||
// `undefined`, in which case it returns the old one.
|
||||
export function overwrite (oldVal, newVal) {
|
||||
return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as WebPushSubscription from './web_push_subscription';
|
||||
import Mastodon from 'flavours/glitch/containers/mastodon';
|
||||
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
|
||||
import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ready from './ready';
|
||||
@@ -25,7 +25,7 @@ function main() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// avoid offline in dev mode because it's harder to debug
|
||||
require('offline-plugin/runtime').install();
|
||||
WebPushSubscription.register();
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
}
|
||||
perf.stop('main()');
|
||||
|
||||
|
||||
46
app/javascript/flavours/glitch/util/settings.js
Normal file
46
app/javascript/flavours/glitch/util/settings.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default class Settings {
|
||||
|
||||
constructor(keyBase = null) {
|
||||
this.keyBase = keyBase;
|
||||
}
|
||||
|
||||
generateKey(id) {
|
||||
return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
|
||||
}
|
||||
|
||||
set(id, data) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
const encodedData = JSON.stringify(data);
|
||||
localStorage.setItem(key, encodedData);
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
const rawData = localStorage.getItem(key);
|
||||
return JSON.parse(rawData);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
remove(id) {
|
||||
const data = this.get(id);
|
||||
if (data) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
||||
@@ -1,105 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { store } from 'flavours/glitch/containers/mastodon';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
const urlBase64ToUint8Array = (base64String) => {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||
|
||||
const getRegistration = () => navigator.serviceWorker.ready;
|
||||
|
||||
const getPushSubscription = (registration) =>
|
||||
registration.pushManager.getSubscription()
|
||||
.then(subscription => ({ registration, subscription }));
|
||||
|
||||
const subscribe = (registration) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
|
||||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||
|
||||
const sendSubscriptionToBackend = (subscription) =>
|
||||
axios.post('/api/web/push_subscriptions', {
|
||||
subscription,
|
||||
}).then(response => response.data);
|
||||
|
||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||
|
||||
export function register () {
|
||||
store.dispatch(setBrowserSupport(supportsPushNotifications));
|
||||
|
||||
if (supportsPushNotifications) {
|
||||
if (!getApplicationServerKey()) {
|
||||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
||||
return;
|
||||
}
|
||||
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(({ registration, subscription }) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
|
||||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
|
||||
const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']);
|
||||
|
||||
// If the VAPID public key did not change and the endpoint corresponds
|
||||
// to the endpoint saved in the backend, the subscription is valid
|
||||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
||||
return subscription;
|
||||
} else {
|
||||
// Something went wrong, try to subscribe again
|
||||
return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend);
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(registration).then(sendSubscriptionToBackend);
|
||||
})
|
||||
.then(subscription => {
|
||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||
// it means that the backend subscription is valid (and was set during hydration)
|
||||
if (!(subscription instanceof PushSubscription)) {
|
||||
store.dispatch(setSubscription(subscription));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code === 20 && error.name === 'AbortError') {
|
||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
|
||||
}
|
||||
|
||||
// Clear alerts and hide UI settings
|
||||
store.dispatch(clearSubscription());
|
||||
|
||||
try {
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(unsubscribe);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Your browser does not support Web Push Notifications.');
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,11 @@ en:
|
||||
skins:
|
||||
vanilla:
|
||||
default: Default
|
||||
pl:
|
||||
flavours:
|
||||
vanilla:
|
||||
description: Motyw używany przez instancje czystego Mastodona. Może nie obsługiwać wszystkich funkcji GlitchSoc.
|
||||
name: Mastodon Vanilla
|
||||
skins:
|
||||
vanilla:
|
||||
default: Domyślny
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# (REQUIRED) The location of the pack files inside `pack_directory`.
|
||||
pack:
|
||||
about: about.js
|
||||
admin:
|
||||
admin: public.js
|
||||
auth:
|
||||
common:
|
||||
filename: common.js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import api from '../../api';
|
||||
import { pushNotificationsSetting } from '../../settings';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||
|
||||
@@ -35,7 +35,7 @@ const subscribe = (registration) =>
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||
|
||||
const sendSubscriptionToBackend = (subscription, me) => {
|
||||
const sendSubscriptionToBackend = (getState, subscription, me) => {
|
||||
const params = { subscription };
|
||||
|
||||
if (me) {
|
||||
@@ -45,7 +45,7 @@ const sendSubscriptionToBackend = (subscription, me) => {
|
||||
}
|
||||
}
|
||||
|
||||
return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||
};
|
||||
|
||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||
@@ -85,13 +85,13 @@ export function register () {
|
||||
} else {
|
||||
// Something went wrong, try to subscribe again
|
||||
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
||||
subscription => sendSubscriptionToBackend(subscription, me));
|
||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(registration).then(
|
||||
subscription => sendSubscriptionToBackend(subscription, me));
|
||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
||||
})
|
||||
.then(subscription => {
|
||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||
@@ -137,7 +137,7 @@ export function saveSettings() {
|
||||
const alerts = state.get('alerts');
|
||||
const data = { alerts };
|
||||
|
||||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
data,
|
||||
}).then(() => {
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import api from '../api';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||
@@ -23,7 +23,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||
|
||||
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||
|
||||
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||
api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||
}, 5000, { trailing: true });
|
||||
|
||||
export function saveSettings() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import ready from './ready';
|
||||
import LinkHeader from './link_header';
|
||||
|
||||
export const getLinks = response => {
|
||||
@@ -11,10 +12,17 @@ export const getLinks = response => {
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
let csrfHeader = {};
|
||||
function setCSRFHeader() {
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
ready(setCSRFHeader);
|
||||
|
||||
export default getState => axios.create({
|
||||
headers: {
|
||||
headers: Object.assign(csrfHeader, getState ? {
|
||||
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
|
||||
},
|
||||
} : {}),
|
||||
|
||||
transformResponse: [function (data) {
|
||||
try {
|
||||
|
||||
@@ -23,7 +23,6 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
icon: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
focusable: PropTypes.bool,
|
||||
showBackButton: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
@@ -32,10 +31,6 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focusable: true,
|
||||
}
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
@@ -68,7 +63,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
@@ -135,11 +130,13 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
<span className='column-header__title'>
|
||||
{title}
|
||||
</span>
|
||||
<h1 className={buttonClassName}>
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
<span className='column-header__title'>
|
||||
{title}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{backButton}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class Compose extends React.PureComponent {
|
||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer />
|
||||
<div className='mastodon' />
|
||||
{multiColumn && <div className='mastodon' />}
|
||||
</div>
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
|
||||
@@ -70,30 +70,28 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
navItems.push(
|
||||
<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' />,
|
||||
<ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
);
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key='7' 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.push(
|
||||
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
||||
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
);
|
||||
|
||||
if (multiColumn) {
|
||||
navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||
navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
|
||||
|
||||
return (
|
||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
||||
<div className='getting-started__wrapper'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
||||
{navItems}
|
||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import axios from 'axios';
|
||||
import api from '../../../api';
|
||||
|
||||
@injectIntl
|
||||
export default class EmbedModal extends ImmutablePureComponent {
|
||||
@@ -23,7 +23,7 @@ export default class EmbedModal extends ImmutablePureComponent {
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
axios.post('/api/web/embed', { url }).then(res => {
|
||||
api().post('/api/web/embed', { url }).then(res => {
|
||||
this.setState({ loading: false, oembed: res.data });
|
||||
|
||||
const iframeDocument = this.iframe.contentWindow.document;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
|
||||
"confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||
"confirmations.domain_block.message": "متأكد من أنك تود حظر إسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.",
|
||||
"confirmations.mute.confirm": "أكتم",
|
||||
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
|
||||
"confirmations.unfollow.confirm": "إلغاء المتابعة",
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
|
||||
"empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
|
||||
"empty_column.home.public_timeline": "الخيط العام",
|
||||
"empty_column.list": "هذه القائمة فارغة.",
|
||||
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر تبويقات.",
|
||||
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
|
||||
"empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
|
||||
"follow_request.authorize": "ترخيص",
|
||||
@@ -123,7 +123,7 @@
|
||||
"keyboard_shortcuts.reply": "للردّ",
|
||||
"keyboard_shortcuts.search": "للتركيز على البحث",
|
||||
"keyboard_shortcuts.toot": "لتحرير تبويق جديد",
|
||||
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
|
||||
"keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
|
||||
"keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
|
||||
"lightbox.close": "إغلاق",
|
||||
"lightbox.next": "التالي",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
|
||||
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
|
||||
"empty_column.home.public_timeline": "la línia de temps pública",
|
||||
"empty_column.list": "Encara no hi ha res en aquesta llista.",
|
||||
"empty_column.list": "Encara no hi ha res en aquesta llista. Quan els membres d'aquesta llista publiquin nous estats, apareixeran aquí.",
|
||||
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
|
||||
"empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
|
||||
"follow_request.authorize": "Autoritzar",
|
||||
|
||||
@@ -7,22 +7,22 @@
|
||||
"account.followers": "پیگیران",
|
||||
"account.follows": "پی میگیرد",
|
||||
"account.follows_you": "پیگیر شماست",
|
||||
"account.hide_reblogs": "Hide boosts from @{name}",
|
||||
"account.hide_reblogs": "پنهان کردن بازبوقهای @{name}",
|
||||
"account.media": "رسانه",
|
||||
"account.mention": "نامبردن از @{name}",
|
||||
"account.moved_to": "{name} has moved to:",
|
||||
"account.moved_to": "{name} منتقل شده است به:",
|
||||
"account.mute": "بیصدا کردن @{name}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||
"account.mute_notifications": "بیصداکردن اعلانها از طرف @{name}",
|
||||
"account.posts": "نوشتهها",
|
||||
"account.report": "گزارش @{name}",
|
||||
"account.requested": "در انتظار پذیرش",
|
||||
"account.share": "همرسانی نمایهٔ @{name}",
|
||||
"account.show_reblogs": "Show boosts from @{name}",
|
||||
"account.show_reblogs": "نشاندادن بازبوقهای @{name}",
|
||||
"account.unblock": "رفع انسداد @{name}",
|
||||
"account.unblock_domain": "رفع پنهانسازی از {domain}",
|
||||
"account.unfollow": "پایان پیگیری",
|
||||
"account.unmute": "باصدا کردن @{name}",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account.unmute_notifications": "باصداکردن اعلانها از طرف @{name}",
|
||||
"account.view_full_profile": "نمایش نمایهٔ کامل",
|
||||
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
|
||||
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
|
||||
@@ -36,7 +36,7 @@
|
||||
"column.favourites": "پسندیدهها",
|
||||
"column.follow_requests": "درخواستهای پیگیری",
|
||||
"column.home": "خانه",
|
||||
"column.lists": "Lists",
|
||||
"column.lists": "فهرستها",
|
||||
"column.mutes": "کاربران بیصداشده",
|
||||
"column.notifications": "اعلانها",
|
||||
"column.pins": "نوشتههای ثابت",
|
||||
@@ -65,7 +65,7 @@
|
||||
"confirmations.delete.confirm": "پاک کن",
|
||||
"confirmations.delete.message": "آیا واقعاً میخواهید این نوشته را پاک کنید؟",
|
||||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"confirmations.delete_list.message": "آیا واقعاً میخواهید این فهرست را برای همیشه پاک کنید؟",
|
||||
"confirmations.domain_block.confirm": "پنهانسازی کل دامین",
|
||||
"confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.",
|
||||
"confirmations.mute.confirm": "بیصدا کن",
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
|
||||
"empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
|
||||
"empty_column.home.public_timeline": "فهرست نوشتههای همهجا",
|
||||
"empty_column.list": "There is nothing in this list yet.",
|
||||
"empty_column.list": "در این فهرست هنوز چیزی نیست. وقتی اعضای این فهرست چیزی بنویسند، اینجا ظاهر خواهد شد.",
|
||||
"empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشتههای دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
|
||||
"empty_column.public": "اینجا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا اینجا پر شود",
|
||||
"follow_request.authorize": "اجازه دهید",
|
||||
@@ -108,46 +108,46 @@
|
||||
"home.column_settings.show_reblogs": "نمایش بازبوقها",
|
||||
"home.column_settings.show_replies": "نمایش پاسخها",
|
||||
"home.settings": "تنظیمات ستون",
|
||||
"keyboard_shortcuts.back": "to navigate back",
|
||||
"keyboard_shortcuts.boost": "to boost",
|
||||
"keyboard_shortcuts.column": "to focus a status in one of the columns",
|
||||
"keyboard_shortcuts.compose": "to focus the compose textarea",
|
||||
"keyboard_shortcuts.back": "برای بازگشت",
|
||||
"keyboard_shortcuts.boost": "برای بازبوقیدن",
|
||||
"keyboard_shortcuts.column": "برای برجستهکردن یک نوشته در یکی از ستونها",
|
||||
"keyboard_shortcuts.compose": "برای فعالکردن کادر نوشتهٔ تازه",
|
||||
"keyboard_shortcuts.description": "Description",
|
||||
"keyboard_shortcuts.down": "to move down in the list",
|
||||
"keyboard_shortcuts.down": "برای پایینرفتن در فهرست",
|
||||
"keyboard_shortcuts.enter": "to open status",
|
||||
"keyboard_shortcuts.favourite": "to favourite",
|
||||
"keyboard_shortcuts.favourite": "برای پسندیدن",
|
||||
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
|
||||
"keyboard_shortcuts.hotkey": "Hotkey",
|
||||
"keyboard_shortcuts.legend": "to display this legend",
|
||||
"keyboard_shortcuts.mention": "to mention author",
|
||||
"keyboard_shortcuts.reply": "to reply",
|
||||
"keyboard_shortcuts.search": "to focus search",
|
||||
"keyboard_shortcuts.toot": "to start a brand new toot",
|
||||
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
|
||||
"keyboard_shortcuts.up": "to move up in the list",
|
||||
"keyboard_shortcuts.hotkey": "میانبر",
|
||||
"keyboard_shortcuts.legend": "برای نمایش این راهنما",
|
||||
"keyboard_shortcuts.mention": "برای نامبردن از نویسنده",
|
||||
"keyboard_shortcuts.reply": "برای پاسخدادن",
|
||||
"keyboard_shortcuts.search": "برای فعالکردن جستجو",
|
||||
"keyboard_shortcuts.toot": "برای آغاز یک بوق تازه",
|
||||
"keyboard_shortcuts.unfocus": "برای برداشتن توجه از نوشتن/جستجو",
|
||||
"keyboard_shortcuts.up": "برای بالا رفتن در فهرست",
|
||||
"lightbox.close": "بستن",
|
||||
"lightbox.next": "بعدی",
|
||||
"lightbox.previous": "قبلی",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.search": "Search among people you follow",
|
||||
"lists.subheading": "Your lists",
|
||||
"lists.account.add": "افزودن به فهرست",
|
||||
"lists.account.remove": "پاککردن از فهرست",
|
||||
"lists.delete": "حذف فهرست",
|
||||
"lists.edit": "ویرایش فهرست",
|
||||
"lists.new.create": "افزودن فهرست",
|
||||
"lists.new.title_placeholder": "نام فهرست تازه",
|
||||
"lists.search": "بین کسانی که پی میگیرید بگردید",
|
||||
"lists.subheading": "فهرستهای شما",
|
||||
"loading_indicator.label": "بارگیری...",
|
||||
"media_gallery.toggle_visible": "تغییر پیدایی",
|
||||
"missing_indicator.label": "پیدا نشد",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
"mute_modal.hide_notifications": "اعلانهای این کاربر پنهان شود؟",
|
||||
"navigation_bar.blocks": "کاربران مسدودشده",
|
||||
"navigation_bar.community_timeline": "نوشتههای محلی",
|
||||
"navigation_bar.edit_profile": "ویرایش نمایه",
|
||||
"navigation_bar.favourites": "پسندیدهها",
|
||||
"navigation_bar.follow_requests": "درخواستهای پیگیری",
|
||||
"navigation_bar.info": "اطلاعات تکمیلی",
|
||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.keyboard_shortcuts": "میانبرهای صفحهکلید",
|
||||
"navigation_bar.lists": "فهرستها",
|
||||
"navigation_bar.logout": "خروج",
|
||||
"navigation_bar.mutes": "کاربران بیصداشده",
|
||||
"navigation_bar.pins": "نوشتههای ثابت",
|
||||
@@ -174,7 +174,7 @@
|
||||
"onboarding.page_four.home": "ستون «خانه» نوشتههای کسانی را نشان میدهد که شما پی میگیرید.",
|
||||
"onboarding.page_four.notifications": "ستون «اعلانها» ارتباطهای شما با دیگران را نشان میدهد.",
|
||||
"onboarding.page_one.federation": "ماستدون شبکهای از سرورهای مستقل است که با پیوستن به یکدیگر یک شبکهٔ اجتماعی بزرگ را تشکیل میدهند.",
|
||||
"onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است.",
|
||||
"onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است",
|
||||
"onboarding.page_one.welcome": "به ماستدون خوش آمدید!",
|
||||
"onboarding.page_six.admin": "نشانی مسئول سرور شما {admin} است.",
|
||||
"onboarding.page_six.almost_done": "الان تقریباً آمادهاید...",
|
||||
@@ -199,7 +199,7 @@
|
||||
"privacy.unlisted.short": "فهرستنشده",
|
||||
"relative_time.days": "{number}d",
|
||||
"relative_time.hours": "{number}h",
|
||||
"relative_time.just_now": "now",
|
||||
"relative_time.just_now": "الان",
|
||||
"relative_time.minutes": "{number}m",
|
||||
"relative_time.seconds": "{number}s",
|
||||
"reply_indicator.cancel": "لغو",
|
||||
@@ -222,7 +222,7 @@
|
||||
"status.load_more": "بیشتر نشان بده",
|
||||
"status.media_hidden": "تصویر پنهان شده",
|
||||
"status.mention": "نامبردن از @{name}",
|
||||
"status.more": "More",
|
||||
"status.more": "بیشتر",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "بیصداکردن گفتگو",
|
||||
"status.open": "این نوشته را باز کن",
|
||||
@@ -244,7 +244,7 @@
|
||||
"tabs_bar.home": "خانه",
|
||||
"tabs_bar.local_timeline": "محلی",
|
||||
"tabs_bar.notifications": "اعلانها",
|
||||
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
|
||||
"ui.beforeunload": "اگر از ماستدون خارج شوید پیشنویس شما پاک خواهد شد.",
|
||||
"upload_area.title": "برای بارگذاری به اینجا بکشید",
|
||||
"upload_button.label": "افزودن تصویر",
|
||||
"upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
|
||||
"empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
|
||||
"empty_column.home.public_timeline": "a liña temporal pública",
|
||||
"empty_column.list": "Aínda non hai nada en esta lista.",
|
||||
"empty_column.list": "Aínda non hai nada en esta lista. Cando as usuarias incluídas na lista publiquen mensaxes, aparecerán aquí.",
|
||||
"empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.",
|
||||
"empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa",
|
||||
"follow_request.authorize": "Autorizar",
|
||||
@@ -109,7 +109,7 @@
|
||||
"home.column_settings.show_replies": "Mostrar respostas",
|
||||
"home.settings": "Axustes da columna",
|
||||
"keyboard_shortcuts.back": "voltar atrás",
|
||||
"keyboard_shortcuts.boost": "repetir",
|
||||
"keyboard_shortcuts.boost": "promover",
|
||||
"keyboard_shortcuts.column": "destacar un estado en unha das columnas",
|
||||
"keyboard_shortcuts.compose": "Foco no área de escritura",
|
||||
"keyboard_shortcuts.description": "Descrición",
|
||||
@@ -227,8 +227,8 @@
|
||||
"status.mute_conversation": "Acalar conversa",
|
||||
"status.open": "Expandir este estado",
|
||||
"status.pin": "Fixar no perfil",
|
||||
"status.reblog": "Promocionar",
|
||||
"status.reblogged_by": "{name} promocionado",
|
||||
"status.reblog": "Promover",
|
||||
"status.reblogged_by": "{name} promoveu",
|
||||
"status.reply": "Resposta",
|
||||
"status.replyAll": "Resposta a conversa",
|
||||
"status.report": "Informar @{name}",
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
|
||||
"account.view_full_profile": "전체 프로필 보기",
|
||||
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
|
||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||
"bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
|
||||
"bundle_column_error.retry": "다시 시도",
|
||||
"bundle_column_error.title": "네트워크 에러",
|
||||
"bundle_modal_error.close": "닫기",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||
"bundle_modal_error.message": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
|
||||
"bundle_modal_error.retry": "다시 시도",
|
||||
"column.blocks": "차단 중인 사용자",
|
||||
"column.community": "로컬 타임라인",
|
||||
@@ -50,7 +50,7 @@
|
||||
"column_header.unpin": "고정 해제",
|
||||
"column_subheading.navigation": "내비게이션",
|
||||
"column_subheading.settings": "설정",
|
||||
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
|
||||
"compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
|
||||
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
|
||||
"compose_form.lock_disclaimer.lock": "비공개",
|
||||
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
|
||||
@@ -135,7 +135,7 @@
|
||||
"lists.new.create": "리스트 추가",
|
||||
"lists.new.title_placeholder": "새 리스트의 이름",
|
||||
"lists.search": "팔로우 중인 사람들 중에서 찾기",
|
||||
"lists.subheading": "Your lists",
|
||||
"lists.subheading": "당신의 리스트",
|
||||
"loading_indicator.label": "불러오는 중...",
|
||||
"media_gallery.toggle_visible": "표시 전환",
|
||||
"missing_indicator.label": "찾을 수 없습니다",
|
||||
@@ -178,7 +178,7 @@
|
||||
"onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
|
||||
"onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
|
||||
"onboarding.page_six.almost_done": "이상입니다.",
|
||||
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
||||
"onboarding.page_six.appetoot": "본 아페툿!",
|
||||
"onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
|
||||
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
|
||||
"onboarding.page_six.guidelines": "커뮤니티 가이드라인",
|
||||
@@ -213,7 +213,7 @@
|
||||
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
|
||||
"search_popout.tips.user": "유저",
|
||||
"search_results.total": "{count, number}건의 결과",
|
||||
"standalone.public_title": "A look inside...",
|
||||
"standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
|
||||
"status.block": "@{name} 차단",
|
||||
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
|
||||
"status.delete": "삭제",
|
||||
@@ -247,7 +247,7 @@
|
||||
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
|
||||
"upload_area.title": "드래그 & 드롭으로 업로드",
|
||||
"upload_button.label": "미디어 추가",
|
||||
"upload_form.description": "Describe for the visually impaired",
|
||||
"upload_form.description": "시각장애인을 위한 설명",
|
||||
"upload_form.undo": "재시도",
|
||||
"upload_progress.label": "업로드 중...",
|
||||
"video.close": "동영상 닫기",
|
||||
|
||||
@@ -100,10 +100,10 @@
|
||||
"getting_started.appsshort": "Apps",
|
||||
"getting_started.faq": "FAQ",
|
||||
"getting_started.heading": "Beginnen",
|
||||
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
|
||||
"getting_started.open_source_notice": "Mastodon is vrije software. Je kunt bijdragen of problemen melden op GitHub via {github}.",
|
||||
"getting_started.userguide": "Gebruikersgids",
|
||||
"home.column_settings.advanced": "Geavanceerd",
|
||||
"home.column_settings.basic": "Basis",
|
||||
"home.column_settings.basic": "Algemeen",
|
||||
"home.column_settings.filter_regex": "Wegfilteren met reguliere expressies",
|
||||
"home.column_settings.show_reblogs": "Boosts tonen",
|
||||
"home.column_settings.show_replies": "Reacties tonen",
|
||||
@@ -146,7 +146,7 @@
|
||||
"navigation_bar.favourites": "Favorieten",
|
||||
"navigation_bar.follow_requests": "Volgverzoeken",
|
||||
"navigation_bar.info": "Uitgebreide informatie",
|
||||
"navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
|
||||
"navigation_bar.keyboard_shortcuts": "Sneltoetsen",
|
||||
"navigation_bar.lists": "Lijsten",
|
||||
"navigation_bar.logout": "Afmelden",
|
||||
"navigation_bar.mutes": "Genegeerde gebruikers",
|
||||
@@ -180,7 +180,7 @@
|
||||
"onboarding.page_six.almost_done": "Bijna klaar...",
|
||||
"onboarding.page_six.appetoot": "Veel succes!",
|
||||
"onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.",
|
||||
"onboarding.page_six.github": "Mastodon kost niets, en is open-source- en vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.",
|
||||
"onboarding.page_six.github": "Mastodon kost niets en is vrije software. Je kan bugs melden, nieuwe mogelijkheden aanvragen en als ontwikkelaar meewerken op {github}.",
|
||||
"onboarding.page_six.guidelines": "communityrichtlijnen",
|
||||
"onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!",
|
||||
"onboarding.page_six.various_app": "mobiele apps",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"bundle_modal_error.retry": "Spróbuj ponownie",
|
||||
"column.blocks": "Zablokowani użytkownicy",
|
||||
"column.community": "Lokalna oś czasu",
|
||||
"column.direct": "Wiadomości bezpośrednie",
|
||||
"column.favourites": "Ulubione",
|
||||
"column.follow_requests": "Prośby o śledzenie",
|
||||
"column.home": "Strona główna",
|
||||
@@ -48,6 +49,9 @@
|
||||
"column_header.pin": "Przypnij",
|
||||
"column_header.show_settings": "Pokaż ustawienia",
|
||||
"column_header.unpin": "Cofnij przypięcie",
|
||||
"column.heading": "Różne",
|
||||
"column.subheading": "Różne opcje",
|
||||
"column_subheading.lists": "Listy",
|
||||
"column_subheading.navigation": "Nawigacja",
|
||||
"column_subheading.settings": "Ustawienia",
|
||||
"compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
|
||||
@@ -89,10 +93,11 @@
|
||||
"emoji_button.symbols": "Symbole",
|
||||
"emoji_button.travel": "Podróże i miejsca",
|
||||
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
|
||||
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Jeżeli wyślesz lub otrzymasz jakąś, będzie tu widoczna.",
|
||||
"empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
|
||||
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
|
||||
"empty_column.home.public_timeline": "publiczna oś czasu",
|
||||
"empty_column.list": "Nie ma nic na tej liście.",
|
||||
"empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.",
|
||||
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
|
||||
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
|
||||
"follow_request.authorize": "Autoryzuj",
|
||||
@@ -142,6 +147,7 @@
|
||||
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
|
||||
"navigation_bar.blocks": "Zablokowani użytkownicy",
|
||||
"navigation_bar.community_timeline": "Lokalna oś czasu",
|
||||
"navigation_bar.direct": "Wiadomości bezpośrednie",
|
||||
"navigation_bar.edit_profile": "Edytuj profil",
|
||||
"navigation_bar.favourites": "Ulubione",
|
||||
"navigation_bar.follow_requests": "Prośby o śledzenie",
|
||||
@@ -149,6 +155,7 @@
|
||||
"navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
|
||||
"navigation_bar.lists": "Listy",
|
||||
"navigation_bar.logout": "Wyloguj",
|
||||
"navigation_bar.misc": "Różne",
|
||||
"navigation_bar.mutes": "Wyciszeni użytkownicy",
|
||||
"navigation_bar.pins": "Przypięte wpisy",
|
||||
"navigation_bar.preferences": "Preferencje",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
|
||||
"empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
|
||||
"empty_column.home.public_timeline": "global",
|
||||
"empty_column.list": "Ainda não há nada nesta lista.",
|
||||
"empty_column.list": "Ainda não há nada nesta lista. Quando membros dessa lista fizerem novas postagens, elas aparecerão aqui.",
|
||||
"empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.",
|
||||
"empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias",
|
||||
"follow_request.authorize": "Autorizar",
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
"column.community": "Local",
|
||||
"column.favourites": "Favoritos",
|
||||
"column.follow_requests": "Seguidores Pendentes",
|
||||
"column.home": "Home",
|
||||
"column.home": "Início",
|
||||
"column.lists": "Listas",
|
||||
"column.mutes": "Utilizadores silenciados",
|
||||
"column.notifications": "Notificações",
|
||||
"column.pins": "Pinned toot",
|
||||
"column.pins": "Posts fixos",
|
||||
"column.public": "Global",
|
||||
"column_back_button.label": "Voltar",
|
||||
"column_header.hide_settings": "Esconder preferências",
|
||||
@@ -47,7 +47,7 @@
|
||||
"column_header.moveRight_settings": "Mover coluna para a direita",
|
||||
"column_header.pin": "Fixar",
|
||||
"column_header.show_settings": "Mostrar preferências",
|
||||
"column_header.unpin": "Remover fixar",
|
||||
"column_header.unpin": "Desafixar",
|
||||
"column_subheading.navigation": "Navegação",
|
||||
"column_subheading.settings": "Preferências",
|
||||
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
|
||||
@@ -92,7 +92,7 @@
|
||||
"empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
|
||||
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
|
||||
"empty_column.home.public_timeline": "global",
|
||||
"empty_column.list": "Ainda não existem publicações nesta lista.",
|
||||
"empty_column.list": "Ainda não existem publicações nesta lista. Quando membros desta lista fizerem novas publicações, elas aparecerão aqui.",
|
||||
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
|
||||
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
|
||||
"follow_request.authorize": "Autorizar",
|
||||
@@ -226,7 +226,7 @@
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Silenciar conversa",
|
||||
"status.open": "Expandir",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pin": "Fixar no perfil",
|
||||
"status.reblog": "Partilhar",
|
||||
"status.reblogged_by": "{name} partilhou",
|
||||
"status.reply": "Responder",
|
||||
@@ -234,7 +234,7 @@
|
||||
"status.report": "Denunciar @{name}",
|
||||
"status.sensitive_toggle": "Clique para ver",
|
||||
"status.sensitive_warning": "Conteúdo sensível",
|
||||
"status.share": "Share",
|
||||
"status.share": "Compartilhar",
|
||||
"status.show_less": "Mostrar menos",
|
||||
"status.show_more": "Mostrar mais",
|
||||
"status.unmute_conversation": "Deixar de silenciar esta conversa",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"account.media": "Mediji",
|
||||
"account.mention": "Pomeni korisnika @{name}",
|
||||
"account.moved_to": "{name} se pomerio na:",
|
||||
"account.mute": "Mutiraj @{name}",
|
||||
"account.mute": "Ućutkaj korisnika @{name}",
|
||||
"account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
|
||||
"account.posts": "Statusa",
|
||||
"account.report": "Prijavi @{name}",
|
||||
@@ -21,7 +21,7 @@
|
||||
"account.unblock": "Odblokiraj korisnika @{name}",
|
||||
"account.unblock_domain": "Odblokiraj domen {domain}",
|
||||
"account.unfollow": "Otprati",
|
||||
"account.unmute": "Odmutiraj @{name}",
|
||||
"account.unmute": "Ukloni ućutkavanje korisniku @{name}",
|
||||
"account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
|
||||
"account.view_full_profile": "Vidi ceo profil",
|
||||
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
|
||||
@@ -37,10 +37,10 @@
|
||||
"column.follow_requests": "Zahtevi za praćenje",
|
||||
"column.home": "Početna",
|
||||
"column.lists": "Liste",
|
||||
"column.mutes": "Mutirani korisnici",
|
||||
"column.mutes": "Ućutkani korisnici",
|
||||
"column.notifications": "Obaveštenja",
|
||||
"column.pins": "Prikačeni tutovi",
|
||||
"column.public": "Združena lajna",
|
||||
"column.public": "Federisana lajna",
|
||||
"column_back_button.label": "Nazad",
|
||||
"column_header.hide_settings": "Sakrij postavke",
|
||||
"column_header.moveLeft_settings": "Pomeri kolonu ulevo",
|
||||
@@ -50,6 +50,7 @@
|
||||
"column_header.unpin": "Otkači",
|
||||
"column_subheading.navigation": "Navigacija",
|
||||
"column_subheading.settings": "Postavke",
|
||||
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.",
|
||||
"compose_form.lock_disclaimer.lock": "zaključan",
|
||||
"compose_form.placeholder": "Šta Vam je na umu?",
|
||||
@@ -66,9 +67,9 @@
|
||||
"confirmations.delete_list.confirm": "Obriši",
|
||||
"confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?",
|
||||
"confirmations.domain_block.confirm": "Sakrij ceo domen",
|
||||
"confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili mutiranja su dovoljna i preporučljiva.",
|
||||
"confirmations.mute.confirm": "Mutiraj",
|
||||
"confirmations.mute.message": "Da li stvarno želite da mutirate korisnika {name}?",
|
||||
"confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili ućutkavanja su dovoljna i preporučljiva.",
|
||||
"confirmations.mute.confirm": "Ućutkaj",
|
||||
"confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?",
|
||||
"confirmations.unfollow.confirm": "Otprati",
|
||||
"confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
|
||||
"embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.",
|
||||
@@ -148,10 +149,10 @@
|
||||
"navigation_bar.keyboard_shortcuts": "Prečice na tastaturi",
|
||||
"navigation_bar.lists": "Liste",
|
||||
"navigation_bar.logout": "Odjava",
|
||||
"navigation_bar.mutes": "Mutirani korisnici",
|
||||
"navigation_bar.mutes": "Ućutkani korisnici",
|
||||
"navigation_bar.pins": "Prikačeni tutovi",
|
||||
"navigation_bar.preferences": "Podešavanja",
|
||||
"navigation_bar.public_timeline": "Združena lajna",
|
||||
"navigation_bar.public_timeline": "Federisana lajna",
|
||||
"notification.favourite": "{name} je stavio Vaš status kao omiljeni",
|
||||
"notification.follow": "{name} Vas je zapratio",
|
||||
"notification.mention": "{name} Vas je pomenuo",
|
||||
@@ -169,7 +170,7 @@
|
||||
"notifications.column_settings.sound": "Puštaj zvuk",
|
||||
"onboarding.done": "Gotovo",
|
||||
"onboarding.next": "Sledeće",
|
||||
"onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Združena lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
|
||||
"onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
|
||||
"onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.",
|
||||
"onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.",
|
||||
"onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.",
|
||||
@@ -213,6 +214,7 @@
|
||||
"search_popout.tips.user": "korisnik",
|
||||
"search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
|
||||
"standalone.public_title": "Pogled iznutra...",
|
||||
"status.block": "Block @{name}",
|
||||
"status.cannot_reblog": "Ovaj status ne može da se podrži",
|
||||
"status.delete": "Obriši",
|
||||
"status.embed": "Ugradi na sajt",
|
||||
@@ -221,7 +223,8 @@
|
||||
"status.media_hidden": "Multimedija sakrivena",
|
||||
"status.mention": "Pomeni korisnika @{name}",
|
||||
"status.more": "Još",
|
||||
"status.mute_conversation": "Mutiraj prepisku",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Ućutkaj prepisku",
|
||||
"status.open": "Proširi ovaj status",
|
||||
"status.pin": "Prikači na profil",
|
||||
"status.reblog": "Podrži",
|
||||
@@ -237,7 +240,7 @@
|
||||
"status.unmute_conversation": "Uključi prepisku",
|
||||
"status.unpin": "Otkači sa profila",
|
||||
"tabs_bar.compose": "Napiši",
|
||||
"tabs_bar.federated_timeline": "Združeno",
|
||||
"tabs_bar.federated_timeline": "Federisano",
|
||||
"tabs_bar.home": "Početna",
|
||||
"tabs_bar.local_timeline": "Lokalno",
|
||||
"tabs_bar.notifications": "Obaveštenja",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"account.media": "Медији",
|
||||
"account.mention": "Помени корисника @{name}",
|
||||
"account.moved_to": "{name} се померио на:",
|
||||
"account.mute": "Мутирај @{name}",
|
||||
"account.mute": "Ућуткај корисника @{name}",
|
||||
"account.mute_notifications": "Искључи обавештења од корисника @{name}",
|
||||
"account.posts": "Статуса",
|
||||
"account.report": "Пријави @{name}",
|
||||
@@ -21,7 +21,7 @@
|
||||
"account.unblock": "Одблокирај корисника @{name}",
|
||||
"account.unblock_domain": "Одблокирај домен {domain}",
|
||||
"account.unfollow": "Отпрати",
|
||||
"account.unmute": "Одмутирај @{name}",
|
||||
"account.unmute": "Уклони ућуткавање кориснику @{name}",
|
||||
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
||||
"account.view_full_profile": "Види цео профил",
|
||||
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
||||
@@ -37,10 +37,10 @@
|
||||
"column.follow_requests": "Захтеви за праћење",
|
||||
"column.home": "Почетна",
|
||||
"column.lists": "Листе",
|
||||
"column.mutes": "Мутирани корисници",
|
||||
"column.mutes": "Ућуткани корисници",
|
||||
"column.notifications": "Обавештења",
|
||||
"column.pins": "Прикачени тутови",
|
||||
"column.public": "Здружена лајна",
|
||||
"column.public": "Федерисана лајна",
|
||||
"column_back_button.label": "Назад",
|
||||
"column_header.hide_settings": "Сакриј поставке",
|
||||
"column_header.moveLeft_settings": "Помери колону улево",
|
||||
@@ -67,9 +67,9 @@
|
||||
"confirmations.delete_list.confirm": "Обриши",
|
||||
"confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
|
||||
"confirmations.domain_block.confirm": "Сакриј цео домен",
|
||||
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или мутирања су довољна и препоручљива.",
|
||||
"confirmations.mute.confirm": "Мутирај",
|
||||
"confirmations.mute.message": "Да ли стварно желите да мутирате корисника {name}?",
|
||||
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или ућуткавања су довољна и препоручљива.",
|
||||
"confirmations.mute.confirm": "Ућуткај",
|
||||
"confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
|
||||
"confirmations.unfollow.confirm": "Отпрати",
|
||||
"confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
|
||||
"embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
|
||||
@@ -149,10 +149,10 @@
|
||||
"navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
|
||||
"navigation_bar.lists": "Листе",
|
||||
"navigation_bar.logout": "Одјава",
|
||||
"navigation_bar.mutes": "Мутирани корисници",
|
||||
"navigation_bar.mutes": "Ућуткани корисници",
|
||||
"navigation_bar.pins": "Прикачени тутови",
|
||||
"navigation_bar.preferences": "Подешавања",
|
||||
"navigation_bar.public_timeline": "Здружена лајна",
|
||||
"navigation_bar.public_timeline": "Федерисана лајна",
|
||||
"notification.favourite": "{name} је ставио Ваш статус као омиљени",
|
||||
"notification.follow": "{name} Вас је запратио",
|
||||
"notification.mention": "{name} Вас је поменуо",
|
||||
@@ -170,7 +170,7 @@
|
||||
"notifications.column_settings.sound": "Пуштај звук",
|
||||
"onboarding.done": "Готово",
|
||||
"onboarding.next": "Следеће",
|
||||
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Здружена лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
||||
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
||||
"onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
|
||||
"onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
|
||||
"onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
|
||||
@@ -224,7 +224,7 @@
|
||||
"status.mention": "Помени корисника @{name}",
|
||||
"status.more": "Још",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Мутирај преписку",
|
||||
"status.mute_conversation": "Ућуткај преписку",
|
||||
"status.open": "Прошири овај статус",
|
||||
"status.pin": "Прикачи на профил",
|
||||
"status.reblog": "Подржи",
|
||||
@@ -240,7 +240,7 @@
|
||||
"status.unmute_conversation": "Укључи преписку",
|
||||
"status.unpin": "Откачи са профила",
|
||||
"tabs_bar.compose": "Напиши",
|
||||
"tabs_bar.federated_timeline": "Здружено",
|
||||
"tabs_bar.federated_timeline": "Федерисано",
|
||||
"tabs_bar.home": "Почетна",
|
||||
"tabs_bar.local_timeline": "Локално",
|
||||
"tabs_bar.notifications": "Обавештења",
|
||||
|
||||
2
app/javascript/mastodon/locales/whitelist_sr-Latn.json
Normal file
2
app/javascript/mastodon/locales/whitelist_sr-Latn.json
Normal file
@@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
||||
@@ -50,7 +50,7 @@
|
||||
"column_header.unpin": "取消固定",
|
||||
"column_subheading.navigation": "导航",
|
||||
"column_subheading.settings": "设置",
|
||||
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
|
||||
"compose_form.hashtag_warning": "这条嘟文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的嘟文才能通过话题标签进行搜索。",
|
||||
"compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
|
||||
"compose_form.lock_disclaimer.lock": "开启保护",
|
||||
"compose_form.placeholder": "在想啥?",
|
||||
@@ -214,7 +214,7 @@
|
||||
"search_popout.tips.user": "用户",
|
||||
"search_results.total": "共 {count, number} 个结果",
|
||||
"standalone.public_title": "大家都在干啥?",
|
||||
"status.block": "Block @{name}",
|
||||
"status.block": "屏蔽 @{name}",
|
||||
"status.cannot_reblog": "无法转嘟这条嘟文",
|
||||
"status.delete": "删除",
|
||||
"status.embed": "嵌入",
|
||||
@@ -223,7 +223,7 @@
|
||||
"status.media_hidden": "隐藏媒体内容",
|
||||
"status.mention": "提及 @{name}",
|
||||
"status.more": "更多",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute": "隐藏 @{name}",
|
||||
"status.mute_conversation": "隐藏此对话",
|
||||
"status.open": "展开嘟文",
|
||||
"status.pin": "在个人资料页面置顶",
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
"confirmations.block.message": "你確定要封鎖 {name} ?",
|
||||
"confirmations.delete.confirm": "刪除",
|
||||
"confirmations.delete.message": "你確定要刪除這個狀態?",
|
||||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"confirmations.delete_list.confirm": "刪除",
|
||||
"confirmations.delete_list.message": "確定要永久性地刪除這個名單嗎?",
|
||||
"confirmations.domain_block.confirm": "隱藏整個網域",
|
||||
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
||||
"confirmations.mute.confirm": "消音",
|
||||
@@ -128,14 +128,14 @@
|
||||
"lightbox.close": "關閉",
|
||||
"lightbox.next": "繼續",
|
||||
"lightbox.previous": "回退",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.search": "Search among people you follow",
|
||||
"lists.subheading": "Your lists",
|
||||
"lists.account.add": "加到名單裡",
|
||||
"lists.account.remove": "從名單中移除",
|
||||
"lists.delete": "刪除名單",
|
||||
"lists.edit": "修改名單",
|
||||
"lists.new.create": "新增名單",
|
||||
"lists.new.title_placeholder": "名單名稱",
|
||||
"lists.search": "搜尋您關注的使用者",
|
||||
"lists.subheading": "您的名單",
|
||||
"loading_indicator.label": "讀取中...",
|
||||
"media_gallery.toggle_visible": "切換可見性",
|
||||
"missing_indicator.label": "找不到",
|
||||
@@ -146,8 +146,8 @@
|
||||
"navigation_bar.favourites": "最愛",
|
||||
"navigation_bar.follow_requests": "關注請求",
|
||||
"navigation_bar.info": "關於本站",
|
||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.keyboard_shortcuts": "快速鍵",
|
||||
"navigation_bar.lists": "名單",
|
||||
"navigation_bar.logout": "登出",
|
||||
"navigation_bar.mutes": "消音的使用者",
|
||||
"navigation_bar.pins": "置頂貼文",
|
||||
|
||||
@@ -398,10 +398,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
@@ -415,7 +417,7 @@
|
||||
color: $ui-primary-color;
|
||||
font-family: 'mastodon-font-monospace', monospace;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -2350,6 +2350,19 @@
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
|
||||
& > button {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3);
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
def perform
|
||||
if @object.respond_to?(:[]) &&
|
||||
@object['type'] == 'Follow' && @object['actor'].present?
|
||||
accept_follow_from @object['actor']
|
||||
else
|
||||
accept_follow_object @object
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
accept_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accept_follow_from(actor)
|
||||
target_account = account_from_uri(value_or_id(actor))
|
||||
def accept_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
||||
@@ -21,8 +19,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
follow_request&.authorize!
|
||||
end
|
||||
|
||||
def accept_follow_object(object)
|
||||
follow_request = ActivityPub::TagManager.instance.uri_to_resource(value_or_id(object), FollowRequest)
|
||||
follow_request&.authorize!
|
||||
def target_uri
|
||||
@target_uri ||= value_or_id(@object['actor'])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||
original_status = status_from_uri(object_uri)
|
||||
original_status ||= fetch_remote_original_status
|
||||
|
||||
return if original_status.nil? || delete_arrived_first?(@json['id'])
|
||||
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
|
||||
|
||||
status = Status.find_by(account: @account, reblog: original_status)
|
||||
|
||||
@@ -33,4 +33,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||
::FetchRemoteStatusService.new.call(@object['url'])
|
||||
end
|
||||
end
|
||||
|
||||
def announceable?(status)
|
||||
status.public_visibility? || status.unlisted_visibility?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
SUPPORTED_TYPES = %w(Article Note).freeze
|
||||
CONVERTED_TYPES = %w(Image Video).freeze
|
||||
SUPPORTED_TYPES = %w(Note).freeze
|
||||
CONVERTED_TYPES = %w(Image Video Article).freeze
|
||||
|
||||
def perform
|
||||
return if delete_arrived_first?(object_uri) || unsupported_object_type?
|
||||
return if delete_arrived_first?(object_uri) || unsupported_object_type? || invalid_origin?(@object['id'])
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@@ -213,7 +213,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
|
||||
def object_url
|
||||
return if @object['url'].blank?
|
||||
url_to_href(@object['url'], 'text/html')
|
||||
|
||||
url_candidate = url_to_href(@object['url'], 'text/html')
|
||||
|
||||
if invalid_origin?(url_candidate)
|
||||
nil
|
||||
else
|
||||
url_candidate
|
||||
end
|
||||
end
|
||||
|
||||
def content_language_map?
|
||||
@@ -245,6 +252,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||
end
|
||||
|
||||
def invalid_origin?(url)
|
||||
return true if unsupported_uri_scheme?(url)
|
||||
|
||||
needle = Addressable::URI.parse(url).host
|
||||
haystack = Addressable::URI.parse(@account.uri).host
|
||||
|
||||
!haystack.casecmp(needle).zero?
|
||||
end
|
||||
|
||||
def reply_to_local?
|
||||
!replied_to_status.nil? && replied_to_status.account.local?
|
||||
end
|
||||
|
||||
@@ -28,8 +28,6 @@ class ActivityPub::TagManager
|
||||
return target.uri if target.respond_to?(:local?) && !target.local?
|
||||
|
||||
case target.object_type
|
||||
when :follow
|
||||
account_follow_url(target.account.username, target)
|
||||
when :person
|
||||
account_url(target)
|
||||
when :note, :comment, :activity
|
||||
@@ -69,6 +67,8 @@ class ActivityPub::TagManager
|
||||
def cc(status)
|
||||
cc = []
|
||||
|
||||
cc << uri_for(status.reblog.account) if status.reblog?
|
||||
|
||||
case status.visibility
|
||||
when 'public'
|
||||
cc << account_followers_url(status.account)
|
||||
@@ -99,12 +99,6 @@ class ActivityPub::TagManager
|
||||
case klass.name
|
||||
when 'Account'
|
||||
klass.find_local(uri_to_local_id(uri, :username))
|
||||
when 'FollowRequest'
|
||||
params = Rails.application.routes.recognize_path(uri)
|
||||
klass.joins(:account).find_by!(
|
||||
accounts: { domain: nil, username: params[:account_username] },
|
||||
id: params[:id]
|
||||
)
|
||||
else
|
||||
StatusFinder.new(uri).status
|
||||
end
|
||||
|
||||
@@ -26,6 +26,9 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
|
||||
cached_reblog = reblog
|
||||
status = nil
|
||||
|
||||
# Skip if the reblogged status is not public
|
||||
return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?)
|
||||
|
||||
media_attachments = save_media
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
|
||||
@@ -21,10 +21,6 @@ class FollowRequest < ApplicationRecord
|
||||
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
|
||||
def object_type
|
||||
:follow
|
||||
end
|
||||
|
||||
def authorize!
|
||||
account.follow!(target_account, reblogs: show_reblogs)
|
||||
MergeWorker.perform_async(target_account.id, account.id)
|
||||
|
||||
@@ -126,18 +126,18 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def confirm
|
||||
return if confirmed?
|
||||
new_user = !confirmed?
|
||||
|
||||
super
|
||||
update_statistics!
|
||||
update_statistics! if new_user
|
||||
end
|
||||
|
||||
def confirm!
|
||||
return if confirmed?
|
||||
new_user = !confirmed?
|
||||
|
||||
skip_confirmation!
|
||||
save!
|
||||
update_statistics!
|
||||
update_statistics! if new_user
|
||||
end
|
||||
|
||||
def promote!
|
||||
|
||||
22
app/serializers/activitypub/delete_actor_serializer.rb
Normal file
22
app/serializers/activitypub/delete_actor_serializer.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
|
||||
attributes :id, :type, :actor
|
||||
attribute :virtual_object, key: :object
|
||||
|
||||
def id
|
||||
[ActivityPub::TagManager.instance.uri_for(object), '#delete'].join
|
||||
end
|
||||
|
||||
def type
|
||||
'Delete'
|
||||
end
|
||||
|
||||
def actor
|
||||
ActivityPub::TagManager.instance.uri_for(object)
|
||||
end
|
||||
|
||||
def virtual_object
|
||||
actor
|
||||
end
|
||||
end
|
||||
@@ -1,12 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FollowSerializer < ActiveModel::Serializer
|
||||
attributes :type, :actor
|
||||
attribute :id, if: :dereferencable?
|
||||
attributes :id, :type, :actor
|
||||
attribute :virtual_object, key: :object
|
||||
|
||||
def id
|
||||
ActivityPub::TagManager.instance.uri_for(object)
|
||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join
|
||||
end
|
||||
|
||||
def type
|
||||
@@ -20,8 +19,4 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer
|
||||
def virtual_object
|
||||
ActivityPub::TagManager.instance.uri_for(object.target_account)
|
||||
end
|
||||
|
||||
def dereferencable?
|
||||
object.respond_to?(:object_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def thumbnail
|
||||
full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail
|
||||
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
|
||||
end
|
||||
|
||||
def max_toot_chars
|
||||
|
||||
@@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
# Should be called with confirmed valid JSON
|
||||
# and WebFinger-resolved username and domain
|
||||
def call(username, domain, json)
|
||||
return if json['inbox'].blank?
|
||||
return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
|
||||
|
||||
@json = json
|
||||
@uri = @json['id']
|
||||
@@ -107,7 +107,21 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
|
||||
def url
|
||||
return if @json['url'].blank?
|
||||
url_to_href(@json['url'], 'text/html')
|
||||
|
||||
url_candidate = url_to_href(@json['url'], 'text/html')
|
||||
|
||||
if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
|
||||
nil
|
||||
else
|
||||
url_candidate
|
||||
end
|
||||
end
|
||||
|
||||
def mismatching_origin?(url)
|
||||
needle = Addressable::URI.parse(url).host
|
||||
haystack = Addressable::URI.parse(@uri).host
|
||||
|
||||
!haystack.casecmp(needle).zero?
|
||||
end
|
||||
|
||||
def outbox_total_items
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user