Compare commits
73 Commits
compose-re
...
fix-thread
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
824a790e63 | ||
|
|
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 | ||
|
|
8d51ce4290 | ||
|
|
f41b33eb01 | ||
|
|
9fc08e4861 | ||
|
|
6236577734 | ||
|
|
06636c6eca | ||
|
|
e9822a4e4e | ||
|
|
9a61b0ef22 | ||
|
|
c69a23ae46 | ||
|
|
d872902997 | ||
|
|
5ec25ff3e1 | ||
|
|
49e296e1b0 | ||
|
|
7347d4f8bb | ||
|
|
7571c37c99 | ||
|
|
3c18964256 | ||
|
|
c61dd918a2 | ||
|
|
0f69a90588 | ||
|
|
02ba03d6db | ||
|
|
3bee0996c5 | ||
|
|
89daeb43a8 | ||
|
|
7d4f4f9aab | ||
|
|
256c2b1de0 | ||
|
|
02e3e1ec09 | ||
|
|
ff924f95bb | ||
|
|
c10f4bdb03 | ||
|
|
72b99f6ee4 | ||
|
|
4ce44ba470 | ||
|
|
0dce26b82b |
@@ -299,13 +299,11 @@ GEM
|
|||||||
sidekiq (>= 3.5.0)
|
sidekiq (>= 3.5.0)
|
||||||
statsd-ruby (~> 1.2.0)
|
statsd-ruby (~> 1.2.0)
|
||||||
oj (3.3.9)
|
oj (3.3.9)
|
||||||
openssl (2.0.6)
|
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (2.0.1)
|
ostatus2 (2.0.2)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
openssl (~> 2.0)
|
|
||||||
ox (2.8.2)
|
ox (2.8.2)
|
||||||
paperclip (5.1.0)
|
paperclip (5.1.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
class AccountsController < ApplicationController
|
class AccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -27,10 +28,11 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @account,
|
skip_session!
|
||||||
serializer: ActivityPub::ActorSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
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
|
|
||||||
@@ -21,9 +21,9 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
|||||||
|
|
||||||
weeks << {
|
weeks << {
|
||||||
week: week.to_time.to_i.to_s,
|
week: week.to_time.to_i.to_s,
|
||||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0,
|
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
||||||
logins: Redis.current.pfcount("activity:logins:#{week_id}"),
|
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
||||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0,
|
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def https_enabled?
|
def https_enabled?
|
||||||
Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'
|
Rails.env.production?
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
@@ -192,17 +192,31 @@ class ApplicationController < ActionController::Base
|
|||||||
format.any { head code }
|
format.any { head code }
|
||||||
format.html do
|
format.html do
|
||||||
set_locale
|
set_locale
|
||||||
|
use_pack 'error'
|
||||||
render "errors/#{code}", layout: 'error', status: code
|
render "errors/#{code}", layout: 'error', status: code
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_cached_json(cache_key, **options)
|
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
|
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
|
||||||
yield.to_json
|
yield.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
expires_in options[:expires_in], public: true
|
expires_in options[:expires_in], public: cache_public
|
||||||
render json: data
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
class EmojisController < ApplicationController
|
class EmojisController < ApplicationController
|
||||||
before_action :set_emoji
|
before_action :set_emoji
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @emoji,
|
skip_session!
|
||||||
serializer: ActivityPub::EmojiSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
|
|||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
before_action :check_account_suspension
|
before_action :check_account_suspension
|
||||||
before_action :redirect_to_original, only: [:show]
|
before_action :redirect_to_original, only: [:show]
|
||||||
before_action { response.headers['Vary'] = 'Accept' }
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -23,25 +23,21 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @status,
|
skip_session! unless @stream_entry.hidden?
|
||||||
serializer: ActivityPub::NoteSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
|
||||||
content_type: 'application/activity+json'
|
|
||||||
|
|
||||||
# Allow HTTP caching for 3 minutes if the status is public
|
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||||
unless @stream_entry.hidden?
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||||
request.session_options[:skip] = true
|
|
||||||
expires_in(3.minutes, public: true)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
render json: @status,
|
skip_session!
|
||||||
serializer: ActivityPub::ActivitySerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
|
|||||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||||
when 'Status'
|
when 'Status'
|
||||||
tmp_status = Status.new(attributes)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ module JsonLdHelper
|
|||||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsupported_uri_scheme?(uri)
|
||||||
|
!uri.start_with?('http://', 'https://')
|
||||||
|
end
|
||||||
|
|
||||||
def canonicalize(json)
|
def canonicalize(json)
|
||||||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
||||||
graph.dump(:normalize)
|
graph.dump(:normalize)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module RoutingHelper
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
include ActionView::Helpers::AssetTagHelper
|
include ActionView::Helpers::AssetTagHelper
|
||||||
|
include Webpacker::Helper
|
||||||
|
|
||||||
included do
|
included do
|
||||||
def default_url_options
|
def default_url_options
|
||||||
@@ -17,6 +18,10 @@ module RoutingHelper
|
|||||||
URI.join(root_url, source).to_s
|
URI.join(root_url, source).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def full_pack_url(source, **options)
|
||||||
|
full_asset_url(asset_pack_path(source, options))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def use_storage?
|
def use_storage?
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function replyCompose(status, router) {
|
|||||||
status: status,
|
status: status,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!getState().getIn(['compose', 'mounted'])) {
|
if (router && !getState().getIn(['compose', 'mounted'])) {
|
||||||
router.push('/statuses/new');
|
router.push('/statuses/new');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -118,6 +118,11 @@ export function submitCompose() {
|
|||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
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
|
// To make the app more responsive, immediately get the status into the columns
|
||||||
|
|
||||||
const insertOrRefresh = (timelineId, refreshAction) => {
|
const insertOrRefresh = (timelineId, refreshAction) => {
|
||||||
@@ -341,10 +346,11 @@ export function unmountCompose() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toggleComposeAdvancedOption(option) {
|
export function changeComposeAdvancedOption(option, value) {
|
||||||
return {
|
return {
|
||||||
|
option,
|
||||||
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
|
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
|
||||||
option: option,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(key, value) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(setAlerts(key, value));
|
||||||
|
dispatch(saveSettings());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
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 = (subscription, me) => {
|
||||||
|
const params = { subscription };
|
||||||
|
|
||||||
|
if (me) {
|
||||||
|
const data = pushNotificationsSetting.get(me);
|
||||||
|
if (data) {
|
||||||
|
params.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.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(subscription, me));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No subscription, try to subscribe
|
||||||
|
return subscribe(registration).then(
|
||||||
|
subscription => sendSubscriptionToBackend(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 };
|
||||||
|
|
||||||
|
axios.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_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||||
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
||||||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_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) {
|
export function setBrowserSupport (value) {
|
||||||
return {
|
return {
|
||||||
@@ -25,28 +23,12 @@ export function clearSubscription () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeAlerts(key, value) {
|
export function setAlerts (key, value) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ALERTS_CHANGE,
|
type: SET_ALERTS,
|
||||||
key,
|
key,
|
||||||
value,
|
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -105,10 +105,22 @@ export default class Account extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return small ? (
|
return small ? (
|
||||||
<div className='account small'>
|
<Permalink
|
||||||
<div className='account__avatar-wrapper'><Avatar account={account} size={18} /></div>
|
className='account small'
|
||||||
<DisplayName account={account} />
|
href={account.get('url')}
|
||||||
</div>
|
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'>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
|
// Package imports.
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
// The component.
|
||||||
|
export default function DisplayName ({
|
||||||
static propTypes = {
|
account,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
className,
|
||||||
className: PropTypes.string,
|
inline,
|
||||||
};
|
}) {
|
||||||
|
const computedClass = classNames('display-name', { inline }, className);
|
||||||
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 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, i) => item ? {
|
||||||
...item,
|
...item,
|
||||||
name: `${item.text}-${i}`,
|
name: `${item.text}-${i}`,
|
||||||
onClick: this.handleItemClick.bind(i),
|
onClick: this.handleItemClick.bind(this, i),
|
||||||
} : null
|
} : null
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ export default class Permalink extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { href, children, className, ...other } = this.props;
|
const {
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
to,
|
||||||
|
...other
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import {
|
import {
|
||||||
cancelReplyCompose,
|
cancelReplyCompose,
|
||||||
changeCompose,
|
changeCompose,
|
||||||
|
changeComposeAdvancedOption,
|
||||||
changeComposeSensitivity,
|
changeComposeSensitivity,
|
||||||
changeComposeSpoilerText,
|
changeComposeSpoilerText,
|
||||||
changeComposeSpoilerness,
|
changeComposeSpoilerness,
|
||||||
@@ -15,10 +16,11 @@ import {
|
|||||||
clearComposeSuggestions,
|
clearComposeSuggestions,
|
||||||
fetchComposeSuggestions,
|
fetchComposeSuggestions,
|
||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
|
mountCompose,
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
submitCompose,
|
submitCompose,
|
||||||
toggleComposeAdvancedOption,
|
|
||||||
undoUploadCompose,
|
undoUploadCompose,
|
||||||
|
unmountCompose,
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import {
|
import {
|
||||||
@@ -47,8 +49,8 @@ function mapStateToProps (state) {
|
|||||||
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
||||||
return {
|
return {
|
||||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||||
|
advancedOptions: state.getIn(['compose', 'advanced_options']),
|
||||||
amUnlocked: !state.getIn(['accounts', me, 'locked']),
|
amUnlocked: !state.getIn(['accounts', me, 'locked']),
|
||||||
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
|
|
||||||
focusDate: state.getIn(['compose', 'focusDate']),
|
focusDate: state.getIn(['compose', 'focusDate']),
|
||||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
@@ -57,7 +59,7 @@ function mapStateToProps (state) {
|
|||||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||||
privacy: state.getIn(['compose', 'privacy']),
|
privacy: state.getIn(['compose', 'privacy']),
|
||||||
progress: state.getIn(['compose', 'progress']),
|
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,
|
replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
sideArm: state.getIn(['local_settings', 'side_arm']),
|
sideArm: state.getIn(['local_settings', 'side_arm']),
|
||||||
@@ -74,6 +76,7 @@ function mapStateToProps (state) {
|
|||||||
// Dispatch mapping.
|
// Dispatch mapping.
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
onCancelReply: cancelReplyCompose,
|
onCancelReply: cancelReplyCompose,
|
||||||
|
onChangeAdvancedOption: changeComposeAdvancedOption,
|
||||||
onChangeDescription: changeUploadCompose,
|
onChangeDescription: changeUploadCompose,
|
||||||
onChangeSensitivity: changeComposeSensitivity,
|
onChangeSensitivity: changeComposeSensitivity,
|
||||||
onChangeSpoilerText: changeComposeSpoilerText,
|
onChangeSpoilerText: changeComposeSpoilerText,
|
||||||
@@ -84,12 +87,13 @@ const mapDispatchToProps = {
|
|||||||
onCloseModal: closeModal,
|
onCloseModal: closeModal,
|
||||||
onFetchSuggestions: fetchComposeSuggestions,
|
onFetchSuggestions: fetchComposeSuggestions,
|
||||||
onInsertEmoji: insertEmojiCompose,
|
onInsertEmoji: insertEmojiCompose,
|
||||||
|
onMount: mountCompose,
|
||||||
onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
|
onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
|
||||||
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
|
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
|
||||||
onSelectSuggestion: selectComposeSuggestion,
|
onSelectSuggestion: selectComposeSuggestion,
|
||||||
onSubmit: submitCompose,
|
onSubmit: submitCompose,
|
||||||
onToggleAdvancedOption: toggleComposeAdvancedOption,
|
|
||||||
onUndoUpload: undoUploadCompose,
|
onUndoUpload: undoUploadCompose,
|
||||||
|
onUnmount: unmountCompose,
|
||||||
onUpload: uploadCompose,
|
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:
|
// This statement does several things:
|
||||||
// - If we're beginning a reply, and,
|
// - If we're beginning a reply, and,
|
||||||
// - Replying to zero or one users, places the cursor at the end
|
// - Replying to zero or one users, places the cursor at the end
|
||||||
@@ -245,17 +265,17 @@ class Composer extends React.Component {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleRefTextarea,
|
handleRefTextarea,
|
||||||
} = this.handlers;
|
} = this.handlers;
|
||||||
const { history } = this.context;
|
|
||||||
const {
|
const {
|
||||||
acceptContentTypes,
|
acceptContentTypes,
|
||||||
|
advancedOptions,
|
||||||
amUnlocked,
|
amUnlocked,
|
||||||
doNotFederate,
|
|
||||||
intl,
|
intl,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
isUploading,
|
isUploading,
|
||||||
layout,
|
layout,
|
||||||
media,
|
media,
|
||||||
onCancelReply,
|
onCancelReply,
|
||||||
|
onChangeAdvancedOption,
|
||||||
onChangeDescription,
|
onChangeDescription,
|
||||||
onChangeSensitivity,
|
onChangeSensitivity,
|
||||||
onChangeSpoilerness,
|
onChangeSpoilerness,
|
||||||
@@ -266,7 +286,6 @@ class Composer extends React.Component {
|
|||||||
onFetchSuggestions,
|
onFetchSuggestions,
|
||||||
onOpenActionsModal,
|
onOpenActionsModal,
|
||||||
onOpenDoodleModal,
|
onOpenDoodleModal,
|
||||||
onToggleAdvancedOption,
|
|
||||||
onUndoUpload,
|
onUndoUpload,
|
||||||
onUpload,
|
onUpload,
|
||||||
privacy,
|
privacy,
|
||||||
@@ -297,12 +316,12 @@ class Composer extends React.Component {
|
|||||||
<ComposerReply
|
<ComposerReply
|
||||||
account={replyAccount}
|
account={replyAccount}
|
||||||
content={replyContent}
|
content={replyContent}
|
||||||
history={history}
|
|
||||||
intl={intl}
|
intl={intl}
|
||||||
onCancel={onCancelReply}
|
onCancel={onCancelReply}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<ComposerTextarea
|
<ComposerTextarea
|
||||||
|
advancedOptions={advancedOptions}
|
||||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
intl={intl}
|
intl={intl}
|
||||||
@@ -329,19 +348,19 @@ class Composer extends React.Component {
|
|||||||
) : null}
|
) : null}
|
||||||
<ComposerOptions
|
<ComposerOptions
|
||||||
acceptContentTypes={acceptContentTypes}
|
acceptContentTypes={acceptContentTypes}
|
||||||
|
advancedOptions={advancedOptions}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
doNotFederate={doNotFederate}
|
|
||||||
full={media.size >= 4 || media.some(
|
full={media.size >= 4 || media.some(
|
||||||
item => item.get('type') === 'video'
|
item => item.get('type') === 'video'
|
||||||
)}
|
)}
|
||||||
hasMedia={!!media.size}
|
hasMedia={!!media.size}
|
||||||
intl={intl}
|
intl={intl}
|
||||||
|
onChangeAdvancedOption={onChangeAdvancedOption}
|
||||||
onChangeSensitivity={onChangeSensitivity}
|
onChangeSensitivity={onChangeSensitivity}
|
||||||
onChangeVisibility={onChangeVisibility}
|
onChangeVisibility={onChangeVisibility}
|
||||||
onDoodleOpen={onOpenDoodleModal}
|
onDoodleOpen={onOpenDoodleModal}
|
||||||
onModalClose={onCloseModal}
|
onModalClose={onCloseModal}
|
||||||
onModalOpen={onOpenActionsModal}
|
onModalOpen={onOpenActionsModal}
|
||||||
onToggleAdvancedOption={onToggleAdvancedOption}
|
|
||||||
onToggleSpoiler={onChangeSpoilerness}
|
onToggleSpoiler={onChangeSpoilerness}
|
||||||
onUpload={onUpload}
|
onUpload={onUpload}
|
||||||
privacy={privacy}
|
privacy={privacy}
|
||||||
@@ -350,7 +369,7 @@ class Composer extends React.Component {
|
|||||||
spoiler={spoiler}
|
spoiler={spoiler}
|
||||||
/>
|
/>
|
||||||
<ComposerPublisher
|
<ComposerPublisher
|
||||||
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
|
countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
|
||||||
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
|
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
|
||||||
intl={intl}
|
intl={intl}
|
||||||
onSecondarySubmit={handleSecondarySubmit}
|
onSecondarySubmit={handleSecondarySubmit}
|
||||||
@@ -364,19 +383,14 @@ class Composer extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context
|
|
||||||
Composer.contextTypes = {
|
|
||||||
history: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
Composer.propTypes = {
|
Composer.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
||||||
// State props.
|
// State props.
|
||||||
acceptContentTypes: PropTypes.string,
|
acceptContentTypes: PropTypes.string,
|
||||||
|
advancedOptions: ImmutablePropTypes.map,
|
||||||
amUnlocked: PropTypes.bool,
|
amUnlocked: PropTypes.bool,
|
||||||
doNotFederate: PropTypes.bool,
|
|
||||||
focusDate: PropTypes.instanceOf(Date),
|
focusDate: PropTypes.instanceOf(Date),
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
isUploading: PropTypes.bool,
|
isUploading: PropTypes.bool,
|
||||||
@@ -385,7 +399,7 @@ Composer.propTypes = {
|
|||||||
preselectDate: PropTypes.instanceOf(Date),
|
preselectDate: PropTypes.instanceOf(Date),
|
||||||
privacy: PropTypes.string,
|
privacy: PropTypes.string,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
replyAccount: ImmutablePropTypes.map,
|
replyAccount: PropTypes.string,
|
||||||
replyContent: PropTypes.string,
|
replyContent: PropTypes.string,
|
||||||
resetFileKey: PropTypes.number,
|
resetFileKey: PropTypes.number,
|
||||||
sideArm: PropTypes.string,
|
sideArm: PropTypes.string,
|
||||||
@@ -399,6 +413,7 @@ Composer.propTypes = {
|
|||||||
|
|
||||||
// Dispatch props.
|
// Dispatch props.
|
||||||
onCancelReply: PropTypes.func,
|
onCancelReply: PropTypes.func,
|
||||||
|
onChangeAdvancedOption: PropTypes.func,
|
||||||
onChangeDescription: PropTypes.func,
|
onChangeDescription: PropTypes.func,
|
||||||
onChangeSensitivity: PropTypes.func,
|
onChangeSensitivity: PropTypes.func,
|
||||||
onChangeSpoilerText: PropTypes.func,
|
onChangeSpoilerText: PropTypes.func,
|
||||||
@@ -409,12 +424,13 @@ Composer.propTypes = {
|
|||||||
onCloseModal: PropTypes.func,
|
onCloseModal: PropTypes.func,
|
||||||
onFetchSuggestions: PropTypes.func,
|
onFetchSuggestions: PropTypes.func,
|
||||||
onInsertEmoji: PropTypes.func,
|
onInsertEmoji: PropTypes.func,
|
||||||
|
onMount: PropTypes.func,
|
||||||
onOpenActionsModal: PropTypes.func,
|
onOpenActionsModal: PropTypes.func,
|
||||||
onOpenDoodleModal: PropTypes.func,
|
onOpenDoodleModal: PropTypes.func,
|
||||||
onSelectSuggestion: PropTypes.func,
|
onSelectSuggestion: PropTypes.func,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
onToggleAdvancedOption: PropTypes.func,
|
|
||||||
onUndoUpload: PropTypes.func,
|
onUndoUpload: PropTypes.func,
|
||||||
|
onUnmount: PropTypes.func,
|
||||||
onUpload: PropTypes.func,
|
onUpload: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Package imports.
|
// Package imports.
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import {
|
import {
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
@@ -47,11 +48,11 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
local_only_long: {
|
local_only_long: {
|
||||||
defaultMessage: 'Do not post to other instances',
|
defaultMessage: 'Do not post to other instances',
|
||||||
id: 'advanced-options.local-only.long',
|
id: 'advanced_options.local-only.long',
|
||||||
},
|
},
|
||||||
local_only_short: {
|
local_only_short: {
|
||||||
defaultMessage: 'Local-only',
|
defaultMessage: 'Local-only',
|
||||||
id: 'advanced-options.local-only.short',
|
id: 'advanced_options.local-only.short',
|
||||||
},
|
},
|
||||||
private_long: {
|
private_long: {
|
||||||
defaultMessage: 'Post to followers only',
|
defaultMessage: 'Post to followers only',
|
||||||
@@ -77,6 +78,14 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Hide text behind warning',
|
defaultMessage: 'Hide text behind warning',
|
||||||
id: 'compose_form.spoiler',
|
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: {
|
unlisted_long: {
|
||||||
defaultMessage: 'Do not show in public timelines',
|
defaultMessage: 'Do not show in public timelines',
|
||||||
id: 'privacy.unlisted.long',
|
id: 'privacy.unlisted.long',
|
||||||
@@ -149,16 +158,16 @@ export default class ComposerOptions extends React.PureComponent {
|
|||||||
} = this.handlers;
|
} = this.handlers;
|
||||||
const {
|
const {
|
||||||
acceptContentTypes,
|
acceptContentTypes,
|
||||||
|
advancedOptions,
|
||||||
disabled,
|
disabled,
|
||||||
doNotFederate,
|
|
||||||
full,
|
full,
|
||||||
hasMedia,
|
hasMedia,
|
||||||
intl,
|
intl,
|
||||||
|
onChangeAdvancedOption,
|
||||||
onChangeSensitivity,
|
onChangeSensitivity,
|
||||||
onChangeVisibility,
|
onChangeVisibility,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onModalOpen,
|
onModalOpen,
|
||||||
onToggleAdvancedOption,
|
|
||||||
onToggleSpoiler,
|
onToggleSpoiler,
|
||||||
privacy,
|
privacy,
|
||||||
resetFileKey,
|
resetFileKey,
|
||||||
@@ -283,23 +292,31 @@ export default class ComposerOptions extends React.PureComponent {
|
|||||||
onClick={onToggleSpoiler}
|
onClick={onToggleSpoiler}
|
||||||
title={intl.formatMessage(messages.spoiler)}
|
title={intl.formatMessage(messages.spoiler)}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
{advancedOptions ? (
|
||||||
active={doNotFederate}
|
<Dropdown
|
||||||
disabled={disabled}
|
active={advancedOptions.some(value => !!value)}
|
||||||
icon='home'
|
disabled={disabled}
|
||||||
items={[
|
icon='ellipsis-h'
|
||||||
{
|
items={[
|
||||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
{
|
||||||
name: 'do_not_federate',
|
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||||
on: doNotFederate,
|
name: 'do_not_federate',
|
||||||
text: <FormattedMessage {...messages.local_only_short} />,
|
on: advancedOptions.get('do_not_federate'),
|
||||||
},
|
text: <FormattedMessage {...messages.local_only_short} />,
|
||||||
]}
|
},
|
||||||
onChange={onToggleAdvancedOption}
|
{
|
||||||
onModalClose={onModalClose}
|
meta: <FormattedMessage {...messages.threaded_mode_long} />,
|
||||||
onModalOpen={onModalOpen}
|
name: 'threaded_mode',
|
||||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -309,17 +326,17 @@ export default class ComposerOptions extends React.PureComponent {
|
|||||||
// Props.
|
// Props.
|
||||||
ComposerOptions.propTypes = {
|
ComposerOptions.propTypes = {
|
||||||
acceptContentTypes: PropTypes.string,
|
acceptContentTypes: PropTypes.string,
|
||||||
|
advancedOptions: ImmutablePropTypes.map,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
doNotFederate: PropTypes.bool,
|
|
||||||
full: PropTypes.bool,
|
full: PropTypes.bool,
|
||||||
hasMedia: PropTypes.bool,
|
hasMedia: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
onChangeAdvancedOption: PropTypes.func,
|
||||||
onChangeSensitivity: PropTypes.func,
|
onChangeSensitivity: PropTypes.func,
|
||||||
onChangeVisibility: PropTypes.func,
|
onChangeVisibility: PropTypes.func,
|
||||||
onDoodleOpen: PropTypes.func,
|
onDoodleOpen: PropTypes.func,
|
||||||
onModalClose: PropTypes.func,
|
onModalClose: PropTypes.func,
|
||||||
onModalOpen: PropTypes.func,
|
onModalOpen: PropTypes.func,
|
||||||
onToggleAdvancedOption: PropTypes.func,
|
|
||||||
onToggleSpoiler: PropTypes.func,
|
onToggleSpoiler: PropTypes.func,
|
||||||
onUpload: PropTypes.func,
|
onUpload: PropTypes.func,
|
||||||
privacy: PropTypes.string,
|
privacy: PropTypes.string,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default function ComposerPublisher ({
|
|||||||
unlisted: 'unlock-alt',
|
unlisted: 'unlock-alt',
|
||||||
}[privacy]}
|
}[privacy]}
|
||||||
/>
|
/>
|
||||||
|
{' '}
|
||||||
<FormattedMessage {...messages.publish} />
|
<FormattedMessage {...messages.publish} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// Package imports.
|
// Package imports.
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
// Components.
|
// Components.
|
||||||
import Avatar from 'flavours/glitch/components/avatar';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
import DisplayName from 'flavours/glitch/components/display_name';
|
|
||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
|
|
||||||
// Utils.
|
// Utils.
|
||||||
@@ -31,17 +29,6 @@ const handlers = {
|
|||||||
onCancel();
|
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.
|
// The component.
|
||||||
@@ -55,10 +42,7 @@ export default class ComposerReply extends React.PureComponent {
|
|||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
render () {
|
render () {
|
||||||
const {
|
const { handleClick } = this.handlers;
|
||||||
handleClick,
|
|
||||||
handleClickAccount,
|
|
||||||
} = this.handlers;
|
|
||||||
const {
|
const {
|
||||||
account,
|
account,
|
||||||
content,
|
content,
|
||||||
@@ -76,21 +60,10 @@ export default class ComposerReply extends React.PureComponent {
|
|||||||
title={intl.formatMessage(messages.cancel)}
|
title={intl.formatMessage(messages.cancel)}
|
||||||
/>
|
/>
|
||||||
{account ? (
|
{account ? (
|
||||||
<a
|
<AccountContainer
|
||||||
className='account'
|
id={account}
|
||||||
href={account.get('url')}
|
small
|
||||||
onClick={handleClickAccount}
|
/>
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
account={account}
|
|
||||||
className='avatar'
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
<DisplayName
|
|
||||||
account={account}
|
|
||||||
className='display_name'
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
@@ -105,9 +78,8 @@ export default class ComposerReply extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposerReply.propTypes = {
|
ComposerReply.propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: PropTypes.string,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
history: PropTypes.object,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onCancel: PropTypes.func,
|
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.
|
// Components.
|
||||||
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
|
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
|
||||||
|
import ComposerTextareaIcons from './icons';
|
||||||
import ComposerTextareaSuggestions from './suggestions';
|
import ComposerTextareaSuggestions from './suggestions';
|
||||||
|
|
||||||
// Utils.
|
// Utils.
|
||||||
@@ -32,7 +33,7 @@ const handlers = {
|
|||||||
|
|
||||||
// When blurring the textarea, suggestions are hidden.
|
// When blurring the textarea, suggestions are hidden.
|
||||||
handleBlur () {
|
handleBlur () {
|
||||||
//this.setState({ suggestionsHidden: true });
|
this.setState({ suggestionsHidden: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
// When the contents of the textarea change, we have to pull up new
|
// 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 right = value.slice(selectionStart).search(/[\s\u200B]/);
|
||||||
const token = function () {
|
const token = function () {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case left < 0 || /[@:]/.test(!value[left]):
|
case left < 0 || !/[@:]/.test(value[left]):
|
||||||
return null;
|
return null;
|
||||||
case right < 0:
|
case right < 0:
|
||||||
return value.slice(left);
|
return value.slice(left);
|
||||||
@@ -127,6 +128,11 @@ const handlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We submit the status on control/meta + enter.
|
||||||
|
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
// Switches over the pressed key.
|
// Switches over the pressed key.
|
||||||
switch(e.key) {
|
switch(e.key) {
|
||||||
|
|
||||||
@@ -156,11 +162,6 @@ const handlers = {
|
|||||||
}
|
}
|
||||||
return;
|
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
|
// When the escape key is released, we either close the suggestions
|
||||||
@@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
|
|||||||
handleRefTextarea,
|
handleRefTextarea,
|
||||||
} = this.handlers;
|
} = this.handlers;
|
||||||
const {
|
const {
|
||||||
|
advancedOptions,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
disabled,
|
disabled,
|
||||||
intl,
|
intl,
|
||||||
@@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
|
|||||||
<div className='composer--textarea'>
|
<div className='composer--textarea'>
|
||||||
<label>
|
<label>
|
||||||
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
|
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
|
||||||
|
<ComposerTextareaIcons
|
||||||
|
advancedOptions={advancedOptions}
|
||||||
|
intl={intl}
|
||||||
|
/>
|
||||||
<Textarea
|
<Textarea
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
@@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
|
|||||||
|
|
||||||
// Props.
|
// Props.
|
||||||
ComposerTextarea.propTypes = {
|
ComposerTextarea.propTypes = {
|
||||||
|
advancedOptions: ImmutablePropTypes.map,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|||||||
@@ -24,9 +24,16 @@ const handlers = {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); // Prevents following account links
|
||||||
onClick(index);
|
onClick(index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// This prevents the focus from changing, which would mess with
|
||||||
|
// our suggestion code.
|
||||||
|
handleMouseDown (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// The component.
|
// The component.
|
||||||
@@ -40,7 +47,10 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
|||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
render () {
|
render () {
|
||||||
const { handleClick } = this.handlers;
|
const {
|
||||||
|
handleMouseDown,
|
||||||
|
handleClick,
|
||||||
|
} = this.handlers;
|
||||||
const {
|
const {
|
||||||
selected,
|
selected,
|
||||||
suggestion,
|
suggestion,
|
||||||
@@ -51,7 +61,8 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={computedClass}
|
className={computedClass}
|
||||||
onMouseDown={handleClick}
|
onMouseDown={handleMouseDown}
|
||||||
|
onClickCapture={handleClick} // Jumps in front of contents
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ const handlers = {
|
|||||||
const {
|
const {
|
||||||
onClear,
|
onClear,
|
||||||
submitted,
|
submitted,
|
||||||
value: { length },
|
value,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
e.preventDefault(); // Prevents focus change ??
|
e.preventDefault(); // Prevents focus change ??
|
||||||
if (onClear && (submitted || length)) {
|
if (onClear && (submitted || value && value.length)) {
|
||||||
onClear();
|
onClear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -100,7 +100,8 @@ export default class DrawerSearch extends React.PureComponent {
|
|||||||
value,
|
value,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { expanded } = this.state;
|
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 (
|
return (
|
||||||
<div className={computedClass}>
|
<div className={computedClass}>
|
||||||
@@ -126,11 +127,11 @@ export default class DrawerSearch extends React.PureComponent {
|
|||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
<Icon icon='search' />
|
<Icon icon='search' />
|
||||||
<Icon icon='fa-times-circle' />
|
<Icon icon='times-circle' />
|
||||||
</div>
|
</div>
|
||||||
<Overlay
|
<Overlay
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
show={expanded && !(value || '').length && !submitted}
|
show={expanded && !active}
|
||||||
target={this}
|
target={this}
|
||||||
><DrawerSearchPopout /></Overlay>
|
><DrawerSearchPopout /></Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,56 +42,61 @@ export default function DrawerSearchPopout ({ style }) {
|
|||||||
|
|
||||||
// The result.
|
// The result.
|
||||||
return (
|
return (
|
||||||
<Motion
|
<div
|
||||||
defaultStyle={{
|
className='drawer--search--popout'
|
||||||
opacity: 0,
|
|
||||||
scaleX: 0.85,
|
|
||||||
scaleY: 0.75,
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
opacity: motionSpring,
|
...style,
|
||||||
scaleX: motionSpring,
|
position: 'absolute',
|
||||||
scaleY: motionSpring,
|
width: 285,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
<Motion
|
||||||
<div
|
defaultStyle={{
|
||||||
className='drawer--search--popout'
|
opacity: 0,
|
||||||
style={{
|
scaleX: 0.85,
|
||||||
...style,
|
scaleY: 0.75,
|
||||||
position: 'absolute',
|
}}
|
||||||
width: 285,
|
style={{
|
||||||
opacity: opacity,
|
opacity: motionSpring,
|
||||||
transform: `scale(${scaleX}, ${scaleY})`,
|
scaleX: motionSpring,
|
||||||
}}
|
scaleY: motionSpring,
|
||||||
>
|
}}
|
||||||
<h4><FormattedMessage {...messages.format} /></h4>
|
>
|
||||||
<ul>
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
<li>
|
<div
|
||||||
<em>#example</em>
|
style={{
|
||||||
{' '}
|
opacity: opacity,
|
||||||
<FormattedMessage {...messages.hashtag} />
|
transform: `scale(${scaleX}, ${scaleY})`,
|
||||||
</li>
|
}}
|
||||||
<li>
|
>
|
||||||
<em>@username@domain</em>
|
<h4><FormattedMessage {...messages.format} /></h4>
|
||||||
{' '}
|
<ul>
|
||||||
<FormattedMessage {...messages.user} />
|
<li>
|
||||||
</li>
|
<em>#example</em>
|
||||||
<li>
|
{' '}
|
||||||
<em>URL</em>
|
<FormattedMessage {...messages.hashtag} />
|
||||||
{' '}
|
</li>
|
||||||
<FormattedMessage {...messages.user} />
|
<li>
|
||||||
</li>
|
<em>@username@domain</em>
|
||||||
<li>
|
{' '}
|
||||||
<em>URL</em>
|
<FormattedMessage {...messages.user} />
|
||||||
{' '}
|
</li>
|
||||||
<FormattedMessage {...messages.status} />
|
<li>
|
||||||
</li>
|
<em>URL</em>
|
||||||
</ul>
|
{' '}
|
||||||
<FormattedMessage {...messages.text} />
|
<FormattedMessage {...messages.user} />
|
||||||
</div>
|
</li>
|
||||||
)}
|
<li>
|
||||||
</Motion>
|
<em>URL</em>
|
||||||
|
{' '}
|
||||||
|
<FormattedMessage {...messages.status} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<FormattedMessage {...messages.text} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' />);
|
navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||||
|
|
||||||
listItems = listItems.concat([
|
listItems = listItems.concat([
|
||||||
<div>
|
<div key='7'>
|
||||||
<ColumnLink key='7' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
<ColumnLink key='8' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||||
{lists.map(list =>
|
{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>,
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onSave: PropTypes.func.isRequired,
|
|
||||||
onClear: PropTypes.func.isRequired,
|
onClear: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ColumnSettings from '../components/column_settings';
|
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 { 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';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -26,11 +26,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSave () {
|
|
||||||
dispatch(saveSettings());
|
|
||||||
dispatch(savePushNotificationSettings());
|
|
||||||
},
|
|
||||||
|
|
||||||
onClear () {
|
onClear () {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.clearMessage),
|
message: intl.formatMessage(messages.clearMessage),
|
||||||
|
|||||||
@@ -52,9 +52,13 @@ const messages = {
|
|||||||
'compose.attach.doodle': 'Draw something',
|
'compose.attach.doodle': 'Draw something',
|
||||||
'compose.attach': 'Attach...',
|
'compose.attach': 'Attach...',
|
||||||
|
|
||||||
'advanced-options.local-only.short': 'Local-only',
|
'advanced_options.local-only.short': 'Local-only',
|
||||||
'advanced-options.local-only.long': 'Do not post to other instances',
|
'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.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);
|
export default Object.assign({}, inherited, messages);
|
||||||
|
|||||||
@@ -55,9 +55,13 @@ const messages = {
|
|||||||
'compose.attach.doodle': '落書きをする',
|
'compose.attach.doodle': '落書きをする',
|
||||||
'compose.attach': 'アタッチ...',
|
'compose.attach': 'アタッチ...',
|
||||||
|
|
||||||
'advanced-options.local-only.short': 'ローカル限定',
|
'advanced_options.local-only.short': 'ローカル限定',
|
||||||
'advanced-options.local-only.long': '他のインスタンスには投稿されません',
|
'advanced_options.local-only.long': '他のインスタンスには投稿されません',
|
||||||
|
'advanced_options.local-only.tooltip': 'この投稿はローカル限定投稿です',
|
||||||
'advanced_options.icon_title': '高度な設定',
|
'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);
|
export default Object.assign({}, inherited, messages);
|
||||||
|
|||||||
@@ -28,12 +28,16 @@ const messages = {
|
|||||||
'settings.media': 'Zawartość multimedialna',
|
'settings.media': 'Zawartość multimedialna',
|
||||||
'settings.media_letterbox': 'Letterbox media',
|
'settings.media_letterbox': 'Letterbox media',
|
||||||
'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości',
|
'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.wide_view': 'Szeroki widok (tylko w trybie desktopowym)',
|
||||||
'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
|
'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
|
||||||
'status.collapse': 'Zwiń',
|
'status.collapse': 'Zwiń',
|
||||||
'status.uncollapse': 'Rozwiń',
|
'status.uncollapse': 'Rozwiń',
|
||||||
|
|
||||||
|
'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',
|
'notification.markForDeletion': 'Oznacz do usunięcia',
|
||||||
'notifications.clear': 'Wyczyść wszystkie powiadomienia',
|
'notifications.clear': 'Wyczyść wszystkie powiadomienia',
|
||||||
'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
|
'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
|
||||||
@@ -43,6 +47,14 @@ const messages = {
|
|||||||
'notification_purge.btn_none': 'Odznacz\nwszystkie',
|
'notification_purge.btn_none': 'Odznacz\nwszystkie',
|
||||||
'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
|
'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
|
||||||
'notification_purge.btn_apply': 'Usuń\nzaznaczone',
|
'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.icon_title': 'Ustawienia zaawansowane',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Object.assign({}, inherited, messages);
|
export default Object.assign({}, inherited, messages);
|
||||||
|
|||||||
@@ -6,3 +6,10 @@ en:
|
|||||||
skins:
|
skins:
|
||||||
glitch:
|
glitch:
|
||||||
default: Default
|
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 { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
import uuid from 'flavours/glitch/util/uuid';
|
import uuid from 'flavours/glitch/util/uuid';
|
||||||
import { me } from 'flavours/glitch/util/initial_state';
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { overwrite } from 'flavours/glitch/util/js_helpers';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
mounted: false,
|
mounted: false,
|
||||||
advanced_options: ImmutableMap({
|
advanced_options: ImmutableMap({
|
||||||
do_not_federate: false,
|
do_not_federate: false,
|
||||||
|
threaded_mode: false,
|
||||||
}),
|
}),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
@@ -55,6 +57,7 @@ const initialState = ImmutableMap({
|
|||||||
suggestions: ImmutableList(),
|
suggestions: ImmutableList(),
|
||||||
default_advanced_options: ImmutableMap({
|
default_advanced_options: ImmutableMap({
|
||||||
do_not_federate: false,
|
do_not_federate: false,
|
||||||
|
threaded_mode: null, // Do not reset
|
||||||
}),
|
}),
|
||||||
default_privacy: 'public',
|
default_privacy: 'public',
|
||||||
default_sensitive: false,
|
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('');
|
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) {
|
function clearAll(state) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
@@ -90,7 +107,10 @@ function clearAll(state) {
|
|||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('in_reply_to', null);
|
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('privacy', state.get('default_privacy'));
|
||||||
map.set('sensitive', false);
|
map.set('sensitive', false);
|
||||||
map.update('media_attachments', list => list.clear());
|
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) {
|
function appendMedia(state, media) {
|
||||||
const prevSize = state.get('media_attachments').size;
|
const prevSize = state.get('media_attachments').size;
|
||||||
|
|
||||||
@@ -182,8 +227,7 @@ export default function compose(state = initialState, action) {
|
|||||||
return state.set('mounted', false);
|
return state.set('mounted', false);
|
||||||
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
|
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
|
||||||
return state
|
return state
|
||||||
.set('advanced_options',
|
.set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
|
||||||
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
|
|
||||||
.set('idempotencyKey', uuid());
|
.set('idempotencyKey', uuid());
|
||||||
case COMPOSE_SENSITIVITY_CHANGE:
|
case COMPOSE_SENSITIVITY_CHANGE:
|
||||||
return state.withMutations(map => {
|
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('in_reply_to', action.status.get('id'));
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
map.set('advanced_options', new ImmutableMap({
|
map.update(
|
||||||
do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
|
'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('focusDate', new Date());
|
||||||
map.set('preselectDate', new Date());
|
map.set('preselectDate', new Date());
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
@@ -243,14 +288,17 @@ export default function compose(state = initialState, action) {
|
|||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('privacy', state.get('default_privacy'));
|
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());
|
map.set('idempotencyKey', uuid());
|
||||||
});
|
});
|
||||||
case COMPOSE_SUBMIT_REQUEST:
|
case COMPOSE_SUBMIT_REQUEST:
|
||||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||||
return state.set('is_submitting', true);
|
return state.set('is_submitting', true);
|
||||||
case COMPOSE_SUBMIT_SUCCESS:
|
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_SUBMIT_FAIL:
|
||||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
||||||
return state.set('is_submitting', false);
|
return state.set('is_submitting', false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
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';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
@@ -43,7 +43,7 @@ export default function push_subscriptions(state = initialState, action) {
|
|||||||
return state.set('browserSupport', action.value);
|
return state.set('browserSupport', action.value);
|
||||||
case CLEAR_SUBSCRIPTION:
|
case CLEAR_SUBSCRIPTION:
|
||||||
return initialState;
|
return initialState;
|
||||||
case ALERTS_CHANGE:
|
case SET_ALERTS:
|
||||||
return state.setIn(action.key, action.value);
|
return state.setIn(action.key, action.value);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -52,22 +52,7 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > .account {
|
& > .account.small { color: $ui-base-color }
|
||||||
& > .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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .cancel {
|
& > .cancel {
|
||||||
float: right;
|
float: right;
|
||||||
@@ -87,6 +72,27 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
padding-top: 5px;
|
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 {
|
.emojione {
|
||||||
@@ -94,27 +100,6 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
margin: -5px 0 0;
|
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 {
|
.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 {
|
.composer--textarea--suggestions {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -175,6 +181,7 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
@@ -191,6 +198,12 @@
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .account.small {
|
||||||
|
.display-name {
|
||||||
|
& > span { color: lighten($ui-base-color, 36%) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer--upload_form {
|
.composer--upload_form {
|
||||||
|
|||||||
@@ -114,19 +114,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .icon {
|
& > .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 {
|
.fa {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 0;
|
||||||
right: 10px;
|
bottom: 0;
|
||||||
width: 18px;
|
left: 0;
|
||||||
height: 18px;
|
right: 0;
|
||||||
color: $ui-secondary-color;
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 2;
|
|
||||||
transition: all 100ms linear;
|
transition: all 100ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,14 +144,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fa-times-circle {
|
.fa-times-circle {
|
||||||
top: 11px;
|
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover { color: $primary-text-color }
|
&:hover { color: $primary-text-color }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
& > .icon {
|
||||||
.fa-search {
|
.fa-search {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: rotate(90deg);
|
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 {
|
.drawer--account {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
|
|||||||
@@ -745,6 +745,8 @@
|
|||||||
.account {
|
.account {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -762,27 +764,8 @@
|
|||||||
& > .account__avatar-wrapper { margin: 0 8px 0 0 }
|
& > .account__avatar-wrapper { margin: 0 8px 0 0 }
|
||||||
|
|
||||||
& > .display-name {
|
& > .display-name {
|
||||||
display: block;
|
height: 24px;
|
||||||
padding: 0;
|
line-height: 24px;
|
||||||
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: " " }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1243,6 +1226,30 @@
|
|||||||
text-decoration: underline;
|
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,
|
.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 {
|
.pseudo-drawer {
|
||||||
background: lighten($ui-base-color, 13%);
|
background: lighten($ui-base-color, 13%);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -2774,6 +2814,112 @@
|
|||||||
filter: none;
|
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 {
|
.modal-root {
|
||||||
transition: opacity 0.3s linear;
|
transition: opacity 0.3s linear;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
@@ -3911,37 +4057,6 @@
|
|||||||
border-radius: 0;
|
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 {
|
noscript {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ export const boostModal = getMeta('boost_modal');
|
|||||||
export const favouriteModal = getMeta('favourite_modal');
|
export const favouriteModal = getMeta('favourite_modal');
|
||||||
export const deleteModal = getMeta('delete_modal');
|
export const deleteModal = getMeta('delete_modal');
|
||||||
export const me = getMeta('me');
|
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;
|
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 * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
|
||||||
import Mastodon from 'flavours/glitch/containers/mastodon';
|
import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import ready from './ready';
|
import ready from './ready';
|
||||||
@@ -25,7 +25,7 @@ function main() {
|
|||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
// avoid offline in dev mode because it's harder to debug
|
// avoid offline in dev mode because it's harder to debug
|
||||||
require('offline-plugin/runtime').install();
|
require('offline-plugin/runtime').install();
|
||||||
WebPushSubscription.register();
|
store.dispatch(registerPushNotifications.register());
|
||||||
}
|
}
|
||||||
perf.stop('main()');
|
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:
|
skins:
|
||||||
vanilla:
|
vanilla:
|
||||||
default: Default
|
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
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default class Compose extends React.PureComponent {
|
|||||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
<NavigationContainer onClose={this.onBlur} />
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
<div className='mastodon' />
|
{multiColumn && <div className='mastodon' />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
<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(
|
navItems.push(
|
||||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||||
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||||
<ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (myAccount.get('locked')) {
|
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) {
|
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 (
|
return (
|
||||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
||||||
<div className='getting-started__wrapper'>
|
<div className='getting-started__wrapper'>
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
||||||
{navItems}
|
{navItems}
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
<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='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' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
|
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
|
||||||
"account.view_full_profile": "전체 프로필 보기",
|
"account.view_full_profile": "전체 프로필 보기",
|
||||||
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
|
"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.retry": "다시 시도",
|
||||||
"bundle_column_error.title": "네트워크 에러",
|
"bundle_column_error.title": "네트워크 에러",
|
||||||
"bundle_modal_error.close": "닫기",
|
"bundle_modal_error.close": "닫기",
|
||||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
"bundle_modal_error.message": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
|
||||||
"bundle_modal_error.retry": "다시 시도",
|
"bundle_modal_error.retry": "다시 시도",
|
||||||
"column.blocks": "차단 중인 사용자",
|
"column.blocks": "차단 중인 사용자",
|
||||||
"column.community": "로컬 타임라인",
|
"column.community": "로컬 타임라인",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"column_header.unpin": "고정 해제",
|
"column_header.unpin": "고정 해제",
|
||||||
"column_subheading.navigation": "내비게이션",
|
"column_subheading.navigation": "내비게이션",
|
||||||
"column_subheading.settings": "설정",
|
"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": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
|
||||||
"compose_form.lock_disclaimer.lock": "비공개",
|
"compose_form.lock_disclaimer.lock": "비공개",
|
||||||
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
|
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"lists.new.create": "리스트 추가",
|
"lists.new.create": "리스트 추가",
|
||||||
"lists.new.title_placeholder": "새 리스트의 이름",
|
"lists.new.title_placeholder": "새 리스트의 이름",
|
||||||
"lists.search": "팔로우 중인 사람들 중에서 찾기",
|
"lists.search": "팔로우 중인 사람들 중에서 찾기",
|
||||||
"lists.subheading": "Your lists",
|
"lists.subheading": "당신의 리스트",
|
||||||
"loading_indicator.label": "불러오는 중...",
|
"loading_indicator.label": "불러오는 중...",
|
||||||
"media_gallery.toggle_visible": "표시 전환",
|
"media_gallery.toggle_visible": "표시 전환",
|
||||||
"missing_indicator.label": "찾을 수 없습니다",
|
"missing_indicator.label": "찾을 수 없습니다",
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
|
"onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
|
||||||
"onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
|
"onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
|
||||||
"onboarding.page_six.almost_done": "이상입니다.",
|
"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.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
|
||||||
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
|
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
|
||||||
"onboarding.page_six.guidelines": "커뮤니티 가이드라인",
|
"onboarding.page_six.guidelines": "커뮤니티 가이드라인",
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
|
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
|
||||||
"search_popout.tips.user": "유저",
|
"search_popout.tips.user": "유저",
|
||||||
"search_results.total": "{count, number}건의 결과",
|
"search_results.total": "{count, number}건의 결과",
|
||||||
"standalone.public_title": "A look inside...",
|
"standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
|
||||||
"status.block": "@{name} 차단",
|
"status.block": "@{name} 차단",
|
||||||
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
|
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
|
||||||
"status.delete": "삭제",
|
"status.delete": "삭제",
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
|
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
|
||||||
"upload_area.title": "드래그 & 드롭으로 업로드",
|
"upload_area.title": "드래그 & 드롭으로 업로드",
|
||||||
"upload_button.label": "미디어 추가",
|
"upload_button.label": "미디어 추가",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "시각장애인을 위한 설명",
|
||||||
"upload_form.undo": "재시도",
|
"upload_form.undo": "재시도",
|
||||||
"upload_progress.label": "업로드 중...",
|
"upload_progress.label": "업로드 중...",
|
||||||
"video.close": "동영상 닫기",
|
"video.close": "동영상 닫기",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"account.media": "Mediji",
|
"account.media": "Mediji",
|
||||||
"account.mention": "Pomeni korisnika @{name}",
|
"account.mention": "Pomeni korisnika @{name}",
|
||||||
"account.moved_to": "{name} se pomerio na:",
|
"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.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
|
||||||
"account.posts": "Statusa",
|
"account.posts": "Statusa",
|
||||||
"account.report": "Prijavi @{name}",
|
"account.report": "Prijavi @{name}",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"account.unblock": "Odblokiraj korisnika @{name}",
|
"account.unblock": "Odblokiraj korisnika @{name}",
|
||||||
"account.unblock_domain": "Odblokiraj domen {domain}",
|
"account.unblock_domain": "Odblokiraj domen {domain}",
|
||||||
"account.unfollow": "Otprati",
|
"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.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
|
||||||
"account.view_full_profile": "Vidi ceo profil",
|
"account.view_full_profile": "Vidi ceo profil",
|
||||||
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
|
"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.follow_requests": "Zahtevi za praćenje",
|
||||||
"column.home": "Početna",
|
"column.home": "Početna",
|
||||||
"column.lists": "Liste",
|
"column.lists": "Liste",
|
||||||
"column.mutes": "Mutirani korisnici",
|
"column.mutes": "Ućutkani korisnici",
|
||||||
"column.notifications": "Obaveštenja",
|
"column.notifications": "Obaveštenja",
|
||||||
"column.pins": "Prikačeni tutovi",
|
"column.pins": "Prikačeni tutovi",
|
||||||
"column.public": "Združena lajna",
|
"column.public": "Federisana lajna",
|
||||||
"column_back_button.label": "Nazad",
|
"column_back_button.label": "Nazad",
|
||||||
"column_header.hide_settings": "Sakrij postavke",
|
"column_header.hide_settings": "Sakrij postavke",
|
||||||
"column_header.moveLeft_settings": "Pomeri kolonu ulevo",
|
"column_header.moveLeft_settings": "Pomeri kolonu ulevo",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"column_header.unpin": "Otkači",
|
"column_header.unpin": "Otkači",
|
||||||
"column_subheading.navigation": "Navigacija",
|
"column_subheading.navigation": "Navigacija",
|
||||||
"column_subheading.settings": "Postavke",
|
"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": "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.lock_disclaimer.lock": "zaključan",
|
||||||
"compose_form.placeholder": "Šta Vam je na umu?",
|
"compose_form.placeholder": "Šta Vam je na umu?",
|
||||||
@@ -66,9 +67,9 @@
|
|||||||
"confirmations.delete_list.confirm": "Obriši",
|
"confirmations.delete_list.confirm": "Obriši",
|
||||||
"confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?",
|
"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.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.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": "Mutiraj",
|
"confirmations.mute.confirm": "Ućutkaj",
|
||||||
"confirmations.mute.message": "Da li stvarno želite da mutirate korisnika {name}?",
|
"confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?",
|
||||||
"confirmations.unfollow.confirm": "Otprati",
|
"confirmations.unfollow.confirm": "Otprati",
|
||||||
"confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
|
"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.",
|
"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.keyboard_shortcuts": "Prečice na tastaturi",
|
||||||
"navigation_bar.lists": "Liste",
|
"navigation_bar.lists": "Liste",
|
||||||
"navigation_bar.logout": "Odjava",
|
"navigation_bar.logout": "Odjava",
|
||||||
"navigation_bar.mutes": "Mutirani korisnici",
|
"navigation_bar.mutes": "Ućutkani korisnici",
|
||||||
"navigation_bar.pins": "Prikačeni tutovi",
|
"navigation_bar.pins": "Prikačeni tutovi",
|
||||||
"navigation_bar.preferences": "Podešavanja",
|
"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.favourite": "{name} je stavio Vaš status kao omiljeni",
|
||||||
"notification.follow": "{name} Vas je zapratio",
|
"notification.follow": "{name} Vas je zapratio",
|
||||||
"notification.mention": "{name} Vas je pomenuo",
|
"notification.mention": "{name} Vas je pomenuo",
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
"notifications.column_settings.sound": "Puštaj zvuk",
|
"notifications.column_settings.sound": "Puštaj zvuk",
|
||||||
"onboarding.done": "Gotovo",
|
"onboarding.done": "Gotovo",
|
||||||
"onboarding.next": "Sledeće",
|
"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.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_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.",
|
"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_popout.tips.user": "korisnik",
|
||||||
"search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
|
"search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
|
||||||
"standalone.public_title": "Pogled iznutra...",
|
"standalone.public_title": "Pogled iznutra...",
|
||||||
|
"status.block": "Block @{name}",
|
||||||
"status.cannot_reblog": "Ovaj status ne može da se podrži",
|
"status.cannot_reblog": "Ovaj status ne može da se podrži",
|
||||||
"status.delete": "Obriši",
|
"status.delete": "Obriši",
|
||||||
"status.embed": "Ugradi na sajt",
|
"status.embed": "Ugradi na sajt",
|
||||||
@@ -221,7 +223,8 @@
|
|||||||
"status.media_hidden": "Multimedija sakrivena",
|
"status.media_hidden": "Multimedija sakrivena",
|
||||||
"status.mention": "Pomeni korisnika @{name}",
|
"status.mention": "Pomeni korisnika @{name}",
|
||||||
"status.more": "Još",
|
"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.open": "Proširi ovaj status",
|
||||||
"status.pin": "Prikači na profil",
|
"status.pin": "Prikači na profil",
|
||||||
"status.reblog": "Podrži",
|
"status.reblog": "Podrži",
|
||||||
@@ -237,7 +240,7 @@
|
|||||||
"status.unmute_conversation": "Uključi prepisku",
|
"status.unmute_conversation": "Uključi prepisku",
|
||||||
"status.unpin": "Otkači sa profila",
|
"status.unpin": "Otkači sa profila",
|
||||||
"tabs_bar.compose": "Napiši",
|
"tabs_bar.compose": "Napiši",
|
||||||
"tabs_bar.federated_timeline": "Združeno",
|
"tabs_bar.federated_timeline": "Federisano",
|
||||||
"tabs_bar.home": "Početna",
|
"tabs_bar.home": "Početna",
|
||||||
"tabs_bar.local_timeline": "Lokalno",
|
"tabs_bar.local_timeline": "Lokalno",
|
||||||
"tabs_bar.notifications": "Obaveštenja",
|
"tabs_bar.notifications": "Obaveštenja",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"account.media": "Медији",
|
"account.media": "Медији",
|
||||||
"account.mention": "Помени корисника @{name}",
|
"account.mention": "Помени корисника @{name}",
|
||||||
"account.moved_to": "{name} се померио на:",
|
"account.moved_to": "{name} се померио на:",
|
||||||
"account.mute": "Мутирај @{name}",
|
"account.mute": "Ућуткај корисника @{name}",
|
||||||
"account.mute_notifications": "Искључи обавештења од корисника @{name}",
|
"account.mute_notifications": "Искључи обавештења од корисника @{name}",
|
||||||
"account.posts": "Статуса",
|
"account.posts": "Статуса",
|
||||||
"account.report": "Пријави @{name}",
|
"account.report": "Пријави @{name}",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"account.unblock": "Одблокирај корисника @{name}",
|
"account.unblock": "Одблокирај корисника @{name}",
|
||||||
"account.unblock_domain": "Одблокирај домен {domain}",
|
"account.unblock_domain": "Одблокирај домен {domain}",
|
||||||
"account.unfollow": "Отпрати",
|
"account.unfollow": "Отпрати",
|
||||||
"account.unmute": "Одмутирај @{name}",
|
"account.unmute": "Уклони ућуткавање кориснику @{name}",
|
||||||
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
||||||
"account.view_full_profile": "Види цео профил",
|
"account.view_full_profile": "Види цео профил",
|
||||||
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
"column.follow_requests": "Захтеви за праћење",
|
"column.follow_requests": "Захтеви за праћење",
|
||||||
"column.home": "Почетна",
|
"column.home": "Почетна",
|
||||||
"column.lists": "Листе",
|
"column.lists": "Листе",
|
||||||
"column.mutes": "Мутирани корисници",
|
"column.mutes": "Ућуткани корисници",
|
||||||
"column.notifications": "Обавештења",
|
"column.notifications": "Обавештења",
|
||||||
"column.pins": "Прикачени тутови",
|
"column.pins": "Прикачени тутови",
|
||||||
"column.public": "Здружена лајна",
|
"column.public": "Федерисана лајна",
|
||||||
"column_back_button.label": "Назад",
|
"column_back_button.label": "Назад",
|
||||||
"column_header.hide_settings": "Сакриј поставке",
|
"column_header.hide_settings": "Сакриј поставке",
|
||||||
"column_header.moveLeft_settings": "Помери колону улево",
|
"column_header.moveLeft_settings": "Помери колону улево",
|
||||||
@@ -67,9 +67,9 @@
|
|||||||
"confirmations.delete_list.confirm": "Обриши",
|
"confirmations.delete_list.confirm": "Обриши",
|
||||||
"confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
|
"confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
|
||||||
"confirmations.domain_block.confirm": "Сакриј цео домен",
|
"confirmations.domain_block.confirm": "Сакриј цео домен",
|
||||||
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или мутирања су довољна и препоручљива.",
|
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или ућуткавања су довољна и препоручљива.",
|
||||||
"confirmations.mute.confirm": "Мутирај",
|
"confirmations.mute.confirm": "Ућуткај",
|
||||||
"confirmations.mute.message": "Да ли стварно желите да мутирате корисника {name}?",
|
"confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
|
||||||
"confirmations.unfollow.confirm": "Отпрати",
|
"confirmations.unfollow.confirm": "Отпрати",
|
||||||
"confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
|
"confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
|
||||||
"embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
|
"embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
|
||||||
@@ -149,10 +149,10 @@
|
|||||||
"navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
|
"navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
|
||||||
"navigation_bar.lists": "Листе",
|
"navigation_bar.lists": "Листе",
|
||||||
"navigation_bar.logout": "Одјава",
|
"navigation_bar.logout": "Одјава",
|
||||||
"navigation_bar.mutes": "Мутирани корисници",
|
"navigation_bar.mutes": "Ућуткани корисници",
|
||||||
"navigation_bar.pins": "Прикачени тутови",
|
"navigation_bar.pins": "Прикачени тутови",
|
||||||
"navigation_bar.preferences": "Подешавања",
|
"navigation_bar.preferences": "Подешавања",
|
||||||
"navigation_bar.public_timeline": "Здружена лајна",
|
"navigation_bar.public_timeline": "Федерисана лајна",
|
||||||
"notification.favourite": "{name} је ставио Ваш статус као омиљени",
|
"notification.favourite": "{name} је ставио Ваш статус као омиљени",
|
||||||
"notification.follow": "{name} Вас је запратио",
|
"notification.follow": "{name} Вас је запратио",
|
||||||
"notification.mention": "{name} Вас је поменуо",
|
"notification.mention": "{name} Вас је поменуо",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
"notifications.column_settings.sound": "Пуштај звук",
|
"notifications.column_settings.sound": "Пуштај звук",
|
||||||
"onboarding.done": "Готово",
|
"onboarding.done": "Готово",
|
||||||
"onboarding.next": "Следеће",
|
"onboarding.next": "Следеће",
|
||||||
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Здружена лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
||||||
"onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
|
"onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
|
||||||
"onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
|
"onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
|
||||||
"onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
|
"onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
"status.mention": "Помени корисника @{name}",
|
"status.mention": "Помени корисника @{name}",
|
||||||
"status.more": "Још",
|
"status.more": "Још",
|
||||||
"status.mute": "Mute @{name}",
|
"status.mute": "Mute @{name}",
|
||||||
"status.mute_conversation": "Мутирај преписку",
|
"status.mute_conversation": "Ућуткај преписку",
|
||||||
"status.open": "Прошири овај статус",
|
"status.open": "Прошири овај статус",
|
||||||
"status.pin": "Прикачи на профил",
|
"status.pin": "Прикачи на профил",
|
||||||
"status.reblog": "Подржи",
|
"status.reblog": "Подржи",
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"status.unmute_conversation": "Укључи преписку",
|
"status.unmute_conversation": "Укључи преписку",
|
||||||
"status.unpin": "Откачи са профила",
|
"status.unpin": "Откачи са профила",
|
||||||
"tabs_bar.compose": "Напиши",
|
"tabs_bar.compose": "Напиши",
|
||||||
"tabs_bar.federated_timeline": "Здружено",
|
"tabs_bar.federated_timeline": "Федерисано",
|
||||||
"tabs_bar.home": "Почетна",
|
"tabs_bar.home": "Почетна",
|
||||||
"tabs_bar.local_timeline": "Локално",
|
"tabs_bar.local_timeline": "Локално",
|
||||||
"tabs_bar.notifications": "Обавештења",
|
"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_header.unpin": "取消固定",
|
||||||
"column_subheading.navigation": "导航",
|
"column_subheading.navigation": "导航",
|
||||||
"column_subheading.settings": "设置",
|
"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": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
|
||||||
"compose_form.lock_disclaimer.lock": "开启保护",
|
"compose_form.lock_disclaimer.lock": "开启保护",
|
||||||
"compose_form.placeholder": "在想啥?",
|
"compose_form.placeholder": "在想啥?",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
"search_popout.tips.user": "用户",
|
"search_popout.tips.user": "用户",
|
||||||
"search_results.total": "共 {count, number} 个结果",
|
"search_results.total": "共 {count, number} 个结果",
|
||||||
"standalone.public_title": "大家都在干啥?",
|
"standalone.public_title": "大家都在干啥?",
|
||||||
"status.block": "Block @{name}",
|
"status.block": "屏蔽 @{name}",
|
||||||
"status.cannot_reblog": "无法转嘟这条嘟文",
|
"status.cannot_reblog": "无法转嘟这条嘟文",
|
||||||
"status.delete": "删除",
|
"status.delete": "删除",
|
||||||
"status.embed": "嵌入",
|
"status.embed": "嵌入",
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
"status.media_hidden": "隐藏媒体内容",
|
"status.media_hidden": "隐藏媒体内容",
|
||||||
"status.mention": "提及 @{name}",
|
"status.mention": "提及 @{name}",
|
||||||
"status.more": "更多",
|
"status.more": "更多",
|
||||||
"status.mute": "Mute @{name}",
|
"status.mute": "隐藏 @{name}",
|
||||||
"status.mute_conversation": "隐藏此对话",
|
"status.mute_conversation": "隐藏此对话",
|
||||||
"status.open": "展开嘟文",
|
"status.open": "展开嘟文",
|
||||||
"status.pin": "在个人资料页面置顶",
|
"status.pin": "在个人资料页面置顶",
|
||||||
|
|||||||
@@ -64,8 +64,8 @@
|
|||||||
"confirmations.block.message": "你確定要封鎖 {name} ?",
|
"confirmations.block.message": "你確定要封鎖 {name} ?",
|
||||||
"confirmations.delete.confirm": "刪除",
|
"confirmations.delete.confirm": "刪除",
|
||||||
"confirmations.delete.message": "你確定要刪除這個狀態?",
|
"confirmations.delete.message": "你確定要刪除這個狀態?",
|
||||||
"confirmations.delete_list.confirm": "Delete",
|
"confirmations.delete_list.confirm": "刪除",
|
||||||
"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.confirm": "隱藏整個網域",
|
||||||
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
||||||
"confirmations.mute.confirm": "消音",
|
"confirmations.mute.confirm": "消音",
|
||||||
@@ -128,14 +128,14 @@
|
|||||||
"lightbox.close": "關閉",
|
"lightbox.close": "關閉",
|
||||||
"lightbox.next": "繼續",
|
"lightbox.next": "繼續",
|
||||||
"lightbox.previous": "回退",
|
"lightbox.previous": "回退",
|
||||||
"lists.account.add": "Add to list",
|
"lists.account.add": "加到名單裡",
|
||||||
"lists.account.remove": "Remove from list",
|
"lists.account.remove": "從名單中移除",
|
||||||
"lists.delete": "Delete list",
|
"lists.delete": "刪除名單",
|
||||||
"lists.edit": "Edit list",
|
"lists.edit": "修改名單",
|
||||||
"lists.new.create": "Add list",
|
"lists.new.create": "新增名單",
|
||||||
"lists.new.title_placeholder": "New list title",
|
"lists.new.title_placeholder": "名單名稱",
|
||||||
"lists.search": "Search among people you follow",
|
"lists.search": "搜尋您關注的使用者",
|
||||||
"lists.subheading": "Your lists",
|
"lists.subheading": "您的名單",
|
||||||
"loading_indicator.label": "讀取中...",
|
"loading_indicator.label": "讀取中...",
|
||||||
"media_gallery.toggle_visible": "切換可見性",
|
"media_gallery.toggle_visible": "切換可見性",
|
||||||
"missing_indicator.label": "找不到",
|
"missing_indicator.label": "找不到",
|
||||||
@@ -146,8 +146,8 @@
|
|||||||
"navigation_bar.favourites": "最愛",
|
"navigation_bar.favourites": "最愛",
|
||||||
"navigation_bar.follow_requests": "關注請求",
|
"navigation_bar.follow_requests": "關注請求",
|
||||||
"navigation_bar.info": "關於本站",
|
"navigation_bar.info": "關於本站",
|
||||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
"navigation_bar.keyboard_shortcuts": "快速鍵",
|
||||||
"navigation_bar.lists": "Lists",
|
"navigation_bar.lists": "名單",
|
||||||
"navigation_bar.logout": "登出",
|
"navigation_bar.logout": "登出",
|
||||||
"navigation_bar.mutes": "消音的使用者",
|
"navigation_bar.mutes": "消音的使用者",
|
||||||
"navigation_bar.pins": "置頂貼文",
|
"navigation_bar.pins": "置頂貼文",
|
||||||
|
|||||||
@@ -398,10 +398,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
max-width: calc(100% - 90px);
|
||||||
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
overflow: hidden;
|
word-wrap: break-word;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__timestamp {
|
&__timestamp {
|
||||||
@@ -415,7 +417,7 @@
|
|||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
font-family: 'mastodon-font-monospace', monospace;
|
font-family: 'mastodon-font-monospace', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
word-wrap: break-word;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,16 @@
|
|||||||
|
|
||||||
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
if @object.respond_to?(:[]) &&
|
case @object['type']
|
||||||
@object['type'] == 'Follow' && @object['actor'].present?
|
when 'Follow'
|
||||||
accept_follow_from @object['actor']
|
accept_follow
|
||||||
else
|
|
||||||
accept_follow_object @object
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def accept_follow_from(actor)
|
def accept_follow
|
||||||
target_account = account_from_uri(value_or_id(actor))
|
target_account = account_from_uri(target_uri)
|
||||||
|
|
||||||
return if target_account.nil? || !target_account.local?
|
return if target_account.nil? || !target_account.local?
|
||||||
|
|
||||||
@@ -21,8 +19,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
|||||||
follow_request&.authorize!
|
follow_request&.authorize!
|
||||||
end
|
end
|
||||||
|
|
||||||
def accept_follow_object(object)
|
def target_uri
|
||||||
follow_request = ActivityPub::TagManager.instance.uri_to_resource(value_or_id(object), FollowRequest)
|
@target_uri ||= value_or_id(@object['actor'])
|
||||||
follow_request&.authorize!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
SUPPORTED_TYPES = %w(Article Note).freeze
|
SUPPORTED_TYPES = %w(Note).freeze
|
||||||
CONVERTED_TYPES = %w(Image Video).freeze
|
CONVERTED_TYPES = %w(Image Video Article).freeze
|
||||||
|
|
||||||
def perform
|
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|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
@@ -213,7 +213,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
|
|
||||||
def object_url
|
def object_url
|
||||||
return if @object['url'].blank?
|
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
|
end
|
||||||
|
|
||||||
def content_language_map?
|
def content_language_map?
|
||||||
@@ -245,6 +252,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||||
end
|
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?
|
def reply_to_local?
|
||||||
!replied_to_status.nil? && replied_to_status.account.local?
|
!replied_to_status.nil? && replied_to_status.account.local?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ class ActivityPub::TagManager
|
|||||||
return target.uri if target.respond_to?(:local?) && !target.local?
|
return target.uri if target.respond_to?(:local?) && !target.local?
|
||||||
|
|
||||||
case target.object_type
|
case target.object_type
|
||||||
when :follow
|
|
||||||
account_follow_url(target.account.username, target)
|
|
||||||
when :person
|
when :person
|
||||||
account_url(target)
|
account_url(target)
|
||||||
when :note, :comment, :activity
|
when :note, :comment, :activity
|
||||||
@@ -99,12 +97,6 @@ class ActivityPub::TagManager
|
|||||||
case klass.name
|
case klass.name
|
||||||
when 'Account'
|
when 'Account'
|
||||||
klass.find_local(uri_to_local_id(uri, :username))
|
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
|
else
|
||||||
StatusFinder.new(uri).status
|
StatusFinder.new(uri).status
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ class FollowRequest < ApplicationRecord
|
|||||||
|
|
||||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||||
|
|
||||||
def object_type
|
|
||||||
:follow
|
|
||||||
end
|
|
||||||
|
|
||||||
def authorize!
|
def authorize!
|
||||||
account.follow!(target_account, reblogs: show_reblogs)
|
account.follow!(target_account, reblogs: show_reblogs)
|
||||||
MergeWorker.perform_async(target_account.id, account.id)
|
MergeWorker.perform_async(target_account.id, account.id)
|
||||||
|
|||||||
@@ -126,18 +126,18 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def confirm
|
def confirm
|
||||||
return if confirmed?
|
new_user = !confirmed?
|
||||||
|
|
||||||
super
|
super
|
||||||
update_statistics!
|
update_statistics! if new_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirm!
|
def confirm!
|
||||||
return if confirmed?
|
new_user = !confirmed?
|
||||||
|
|
||||||
skip_confirmation!
|
skip_confirmation!
|
||||||
save!
|
save!
|
||||||
update_statistics!
|
update_statistics! if new_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def promote!
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::FollowSerializer < ActiveModel::Serializer
|
class ActivityPub::FollowSerializer < ActiveModel::Serializer
|
||||||
attributes :type, :actor
|
attributes :id, :type, :actor
|
||||||
attribute :id, if: :dereferencable?
|
|
||||||
attribute :virtual_object, key: :object
|
attribute :virtual_object, key: :object
|
||||||
|
|
||||||
def id
|
def id
|
||||||
ActivityPub::TagManager.instance.uri_for(object)
|
[ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join
|
||||||
end
|
end
|
||||||
|
|
||||||
def type
|
def type
|
||||||
@@ -20,8 +19,4 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer
|
|||||||
def virtual_object
|
def virtual_object
|
||||||
ActivityPub::TagManager.instance.uri_for(object.target_account)
|
ActivityPub::TagManager.instance.uri_for(object.target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def dereferencable?
|
|
||||||
object.respond_to?(:object_type)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail
|
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
|
end
|
||||||
|
|
||||||
def max_toot_chars
|
def max_toot_chars
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
# Should be called with confirmed valid JSON
|
# Should be called with confirmed valid JSON
|
||||||
# and WebFinger-resolved username and domain
|
# and WebFinger-resolved username and domain
|
||||||
def call(username, domain, json)
|
def call(username, domain, json)
|
||||||
return if json['inbox'].blank?
|
return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
|
||||||
|
|
||||||
@json = json
|
@json = json
|
||||||
@uri = @json['id']
|
@uri = @json['id']
|
||||||
@@ -107,7 +107,21 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
|
|
||||||
def url
|
def url
|
||||||
return if @json['url'].blank?
|
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
|
end
|
||||||
|
|
||||||
def outbox_total_items
|
def outbox_total_items
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
|
|
||||||
@stream_entry_batches = []
|
@stream_entry_batches = []
|
||||||
@salmon_batches = []
|
@salmon_batches = []
|
||||||
@activity_json_batches = []
|
|
||||||
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
|
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
|
||||||
@activity_json = {}
|
|
||||||
@activity_xml = {}
|
@activity_xml = {}
|
||||||
|
|
||||||
# Ensure that rendered XML reflects destroyed state
|
# Ensure that rendered XML reflects destroyed state
|
||||||
@@ -32,10 +30,7 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
unpush_from_home_timelines(account, account_statuses)
|
unpush_from_home_timelines(account, account_statuses)
|
||||||
unpush_from_list_timelines(account, account_statuses)
|
unpush_from_list_timelines(account, account_statuses)
|
||||||
|
|
||||||
if account.local?
|
batch_stream_entries(account, account_statuses) if account.local?
|
||||||
batch_stream_entries(account, account_statuses)
|
|
||||||
batch_activity_json(account, account_statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Cannot be batched
|
# Cannot be batched
|
||||||
@@ -47,7 +42,6 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
|
|
||||||
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
||||||
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
|
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
|
||||||
ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -58,22 +52,6 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch_activity_json(account, statuses)
|
|
||||||
account.followers.inboxes.each do |inbox_url|
|
|
||||||
statuses.each do |status|
|
|
||||||
@activity_json_batches << [build_json(status), account.id, inbox_url]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
statuses.each do |status|
|
|
||||||
other_recipients = (status.mentions + status.reblogs).map(&:account).reject(&:local?).select(&:activitypub?).uniq(&:id)
|
|
||||||
|
|
||||||
other_recipients.each do |target_account|
|
|
||||||
@activity_json_batches << [build_json(status), account.id, target_account.inbox_url]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def unpush_from_home_timelines(account, statuses)
|
def unpush_from_home_timelines(account, statuses)
|
||||||
recipients = account.followers.local.to_a
|
recipients = account.followers.local.to_a
|
||||||
|
|
||||||
@@ -134,23 +112,9 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
Redis.current
|
Redis.current
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_json(status)
|
|
||||||
return @activity_json[status.id] if @activity_json.key?(status.id)
|
|
||||||
|
|
||||||
@activity_json[status.id] = sign_json(status, ActiveModelSerializers::SerializableResource.new(
|
|
||||||
status,
|
|
||||||
serializer: status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
|
|
||||||
adapter: ActivityPub::Adapter
|
|
||||||
).as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_xml(stream_entry)
|
def build_xml(stream_entry)
|
||||||
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
||||||
|
|
||||||
@activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
|
@activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_json(status, json)
|
|
||||||
Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ class FetchAtomService < BaseService
|
|||||||
json = body_to_json(@response.to_s)
|
json = body_to_json(@response.to_s)
|
||||||
if supported_context?(json) && json['type'] == 'Person' && json['inbox'].present?
|
if supported_context?(json) && json['type'] == 'Person' && json['inbox'].present?
|
||||||
[json['id'], { prefetched_body: @response.to_s, id: true }, :activitypub]
|
[json['id'], { prefetched_body: @response.to_s, id: true }, :activitypub]
|
||||||
|
elsif supported_context?(json) && json['type'] == 'Note'
|
||||||
|
[json['id'], { prefetched_body: @response.to_s, id: true }, :activitypub]
|
||||||
else
|
else
|
||||||
@unsupported_activity = true
|
@unsupported_activity = true
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
elsif @response['Link'] && !terminal
|
elsif @response['Link'] && !terminal && link_header.find_link(%w(rel alternate))
|
||||||
process_headers
|
process_headers
|
||||||
elsif @response.mime_type == 'text/html' && !terminal
|
elsif @response.mime_type == 'text/html' && !terminal
|
||||||
process_html
|
process_html
|
||||||
@@ -70,8 +72,6 @@ class FetchAtomService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_headers
|
def process_headers
|
||||||
link_header = LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
|
|
||||||
|
|
||||||
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
|
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
|
||||||
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
|
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
|
||||||
|
|
||||||
@@ -80,4 +80,8 @@ class FetchAtomService < BaseService
|
|||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def link_header
|
||||||
|
@link_header ||= LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class SuspendAccountService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def purge_content!
|
def purge_content!
|
||||||
|
ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) if @account.local?
|
||||||
|
|
||||||
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
||||||
BatchedRemoveStatusService.new.call(statuses)
|
BatchedRemoveStatusService.new.call(statuses)
|
||||||
end
|
end
|
||||||
@@ -54,4 +56,14 @@ class SuspendAccountService < BaseService
|
|||||||
def destroy_all(association)
|
def destroy_all(association)
|
||||||
association.in_batches.destroy_all
|
association.in_batches.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_actor_json
|
||||||
|
payload = ActiveModelSerializers::SerializableResource.new(
|
||||||
|
@account,
|
||||||
|
serializer: ActivityPub::DeleteActorSerializer,
|
||||||
|
adapter: ActivityPub::Adapter
|
||||||
|
).as_json
|
||||||
|
|
||||||
|
Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
= fa_icon 'user-times'
|
= fa_icon 'user-times'
|
||||||
= t('accounts.unfollow')
|
= t('accounts.unfollow')
|
||||||
- else
|
- else
|
||||||
= link_to account_follows_path(account), data: { method: :post }, class: 'icon-button' do
|
= link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
|
||||||
= fa_icon 'user-plus'
|
= fa_icon 'user-plus'
|
||||||
= t('accounts.follow')
|
= t('accounts.follow')
|
||||||
- elsif !user_signed_in?
|
- elsif !user_signed_in?
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina.</p>
|
<p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina.</p>
|
||||||
|
|
||||||
<p>Pensatz tanben de gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>.</p>
|
<p>Pensatz tanben de gaitar nòstres <%= link_to 'tèrmes e condicions d\'utilizacion', terms_url %>.</p>
|
||||||
|
|
||||||
<p>Amistosament,</p>
|
<p>Amistosament,</p>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ er confirmar vòstra inscripcion, mercés de clicar sul ligam seguent :
|
|||||||
|
|
||||||
Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina.
|
Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina.
|
||||||
|
|
||||||
Pensatz tanben de gaitar nòstras <%= link_to 'conditions d\'utilisation', terms_url %>.
|
Pensatz tanben de gaitar nòstres <%= link_to 'tèrmes e condicions d\'utilizacion', terms_url %>.
|
||||||
|
|
||||||
Amistosament,
|
Amistosament,
|
||||||
|
|
||||||
|
|||||||
15
app/views/user_mailer/email_changed.oc.html.erb
Normal file
15
app/views/user_mailer/email_changed.oc.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<p>Bonjorn <%= @resource.email %> !</p>
|
||||||
|
|
||||||
|
<% if @resource&.unconfirmed_email? %>
|
||||||
|
<p>Vos contactem per vos senhalar que l’adreça qu’utilizatz per <%= @instance %> es cambiada per aquesta d’aquí <%= @resource.unconfirmed_email %>.</p>
|
||||||
|
<% else %>
|
||||||
|
<p>Vos contactem per vos senhalar que l’adreça qu’utilizatz per <%= @instance %> es cambiada per aquesta d’aquí <%= @resource.email %>.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Amistosament,<p>
|
||||||
|
|
||||||
|
<p>La còla <%= @instance %></p>
|
||||||
13
app/views/user_mailer/email_changed.oc.text.erb
Normal file
13
app/views/user_mailer/email_changed.oc.text.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Bonjorn <%= @resource.email %> !
|
||||||
|
|
||||||
|
<% if @resource&.unconfirmed_email? %>
|
||||||
|
Vos contactem per vos senhalar que l’adreça qu’utilizatz per <%= @instance %> es cambiada per aquesta d’aquí <%= @resource.unconfirmed_email %>.
|
||||||
|
<% else %>
|
||||||
|
Vos contactem per vos senhalar que l’adreça qu’utilizatz per <%= @instance %> es cambiada per aquesta d’aquí <%= @resource.email %>.
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
S’avètz pas demandat aqueste cambiament d’adreça, poiriá arribar que qualqu’un mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator d’instància se l’accès a vòstre compte vos es barrat.
|
||||||
|
|
||||||
|
Amistosament,
|
||||||
|
|
||||||
|
La còla <%= @instance %>
|
||||||
13
app/views/user_mailer/email_changed.zh-cn.html.erb
Normal file
13
app/views/user_mailer/email_changed.zh-cn.html.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<p><%= @resource.email %>,你好呀!</p>
|
||||||
|
|
||||||
|
<% if @resource&.unconfirmed_email? %>
|
||||||
|
<p>我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址即将变更为 <%= @resource.unconfirmed_email %>。</p>
|
||||||
|
<% else %>
|
||||||
|
<p>我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址已经变更为 <%= @resource.unconfirmed_email %>。</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
如果你并没有请求更改你的电子邮件地址,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系实例的管理员请求协助。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>来自 <%= @instance %> 管理团队</p>
|
||||||
11
app/views/user_mailer/email_changed.zh-cn.text.erb
Normal file
11
app/views/user_mailer/email_changed.zh-cn.text.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<%= @resource.email %>,你好呀!
|
||||||
|
|
||||||
|
<% if @resource&.unconfirmed_email? %>
|
||||||
|
我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址即将变更为 <%= @resource.unconfirmed_email %>。
|
||||||
|
<% else %>
|
||||||
|
我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址已经变更为 <%= @resource.unconfirmed_email %>。
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
如果你并没有请求更改你的电子邮件地址,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系实例的管理员请求协助。
|
||||||
|
|
||||||
|
来自 <%= @instance %> 管理团队
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<p>Bonjorn <%= @resource.unconfirmed_email %> !</p>
|
||||||
|
|
||||||
|
<p>Avètz demandat a cambiar vòstra adreça de corrièl qu’utilizatz per <%= @instance %>.</p>
|
||||||
|
|
||||||
|
<p>Per confirmar vòstra novèla adreça, mercés de clicar lo ligam seguent :<br>
|
||||||
|
<%= link_to 'Confirmar mon adreça', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||||
|
|
||||||
|
<p>Se lo ligam al dessús fonciona pas, copiatz e pegatz aquesta URL a la barra d’adreça :<br>
|
||||||
|
<span><%= confirmation_url(@resource, confirmation_token: @token) %></span>
|
||||||
|
|
||||||
|
<p>Mercés de gaitar tanben nòstres <%= link_to 'terms and conditions', terms_url %>.</p>
|
||||||
|
|
||||||
|
<p>Amistosament,<p>
|
||||||
|
|
||||||
|
<p>La còla <%= @instance %></p>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Bonjorn <%= @resource.unconfirmed_email %> !
|
||||||
|
|
||||||
|
Avètz demandat a cambiar vòstra adreça de corrièl qu’utilizatz per <%= @instance %>.
|
||||||
|
|
||||||
|
Per confirmar vòstra novèla adreça, mercés de clicar lo ligam seguent :
|
||||||
|
<%= confirmation_url(@resource, confirmation_token: @token) %>
|
||||||
|
|
||||||
|
Mercés tanben de gaitar nòstres <%= link_to 'terms and conditions', terms_url %>.
|
||||||
|
|
||||||
|
Amistosament,
|
||||||
|
|
||||||
|
La còla <%= @instance %>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<p><%= @resource.email %>,你好呀!</p>
|
||||||
|
|
||||||
|
<p>你正在更改你在 <%= @instance %> 使用的电子邮件地址。</p>
|
||||||
|
|
||||||
|
<p>点击下面的链接以确认操作:<br>
|
||||||
|
<%= link_to '确认我的电子邮件地址', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||||
|
|
||||||
|
<p>上面的链接按不动?把下面的链接复制到地址栏再试试:<br>
|
||||||
|
<span><%= confirmation_url(@resource, confirmation_token: @token) %></span>
|
||||||
|
|
||||||
|
<p>记得读一读我们的<%= link_to '使用条款', terms_url %>哦。</p>
|
||||||
|
|
||||||
|
<p>来自 <%= @instance %> 管理团队</p>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<%= @resource.email %>,你好呀!
|
||||||
|
|
||||||
|
你正在更改你在 <%= @instance %> 使用的电子邮件地址。
|
||||||
|
|
||||||
|
点击下面的链接以确认操作:
|
||||||
|
<%= confirmation_url(@resource, confirmation_token: @token) %>
|
||||||
|
|
||||||
|
记得读一读我们的使用条款哦:<%= terms_url %>
|
||||||
|
|
||||||
|
来自 <%= @instance %> 管理团队
|
||||||
@@ -20,7 +20,7 @@ class Pubsubhubbub::SubscribeWorker
|
|||||||
|
|
||||||
sidekiq_retries_exhausted do |msg, _e|
|
sidekiq_retries_exhausted do |msg, _e|
|
||||||
account = Account.find(msg['args'].first)
|
account = Account.find(msg['args'].first)
|
||||||
logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing"
|
Sidekiq.logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing"
|
||||||
::UnsubscribeService.new.call(account)
|
::UnsubscribeService.new.call(account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Rails.application.configure do
|
|||||||
'X-Frame-Options' => 'DENY',
|
'X-Frame-Options' => 'DENY',
|
||||||
'X-Content-Type-Options' => 'nosniff',
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
'X-XSS-Protection' => '1; mode=block',
|
'X-XSS-Protection' => '1; mode=block',
|
||||||
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
|
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social ; base-uri 'none';" ,
|
||||||
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
|
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
|
||||||
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
|
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
|
||||||
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
|
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
Rails.application.config.session_store :cookie_store, key: '_mastodon_session', secure: (ENV['LOCAL_HTTPS'] == 'true')
|
Rails.application.config.session_store :cookie_store, key: '_mastodon_session', secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true')
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ zh-CN:
|
|||||||
unconfirmed: 继续操作前请先确认你的帐户。
|
unconfirmed: 继续操作前请先确认你的帐户。
|
||||||
mailer:
|
mailer:
|
||||||
confirmation_instructions:
|
confirmation_instructions:
|
||||||
subject: Mastodon 帐户确认信息
|
subject: Mastodon:确认 %{instance} 帐户信息
|
||||||
email_changed:
|
email_changed:
|
||||||
subject: Mastodon 电子邮件地址已被修改
|
subject: Mastodon:电子邮件地址已被修改
|
||||||
password_change:
|
password_change:
|
||||||
subject: Mastodon 密码已被重置
|
subject: Mastodon:密码已被重置
|
||||||
|
reconfirmation_instructions:
|
||||||
|
subject: Mastodon:确认 %{instance} 电子邮件地址
|
||||||
reset_password_instructions:
|
reset_password_instructions:
|
||||||
subject: Mastodon 重置密码信息
|
subject: Mastodon:重置密码信息
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: Mastodon 帐户解锁信息
|
subject: Mastodon:帐户解锁信息
|
||||||
omniauth_callbacks:
|
omniauth_callbacks:
|
||||||
failure: 由于%{reason},无法从%{kind}获得授权。
|
failure: 由于%{reason},无法从%{kind}获得授权。
|
||||||
success: 成功地从%{kind}获得授权。
|
success: 成功地从%{kind}获得授权。
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ pl:
|
|||||||
update_status: "%{name} zaktualizował wpis użytkownika %{target}"
|
update_status: "%{name} zaktualizował wpis użytkownika %{target}"
|
||||||
title: Dziennik działań administracyjnych
|
title: Dziennik działań administracyjnych
|
||||||
custom_emojis:
|
custom_emojis:
|
||||||
|
by_domain: Według domeny
|
||||||
copied_msg: Pomyślnie utworzono lokalną kopię emoji
|
copied_msg: Pomyślnie utworzono lokalną kopię emoji
|
||||||
copy: Kopiuj
|
copy: Kopiuj
|
||||||
copy_failed_msg: Nie udało się utworzyć lokalnej kopii emoji
|
copy_failed_msg: Nie udało się utworzyć lokalnej kopii emoji
|
||||||
@@ -603,8 +604,10 @@ pl:
|
|||||||
development: Tworzenie aplikacji
|
development: Tworzenie aplikacji
|
||||||
edit_profile: Edytuj profil
|
edit_profile: Edytuj profil
|
||||||
export: Eksportowanie danych
|
export: Eksportowanie danych
|
||||||
|
flavours: Motywy
|
||||||
followers: Autoryzowani śledzący
|
followers: Autoryzowani śledzący
|
||||||
import: Importowanie danych
|
import: Importowanie danych
|
||||||
|
keyword_mutes: Wyciszone słowa
|
||||||
migrate: Migracja konta
|
migrate: Migracja konta
|
||||||
notifications: Powiadomienia
|
notifications: Powiadomienia
|
||||||
preferences: Preferencje
|
preferences: Preferencje
|
||||||
@@ -620,6 +623,7 @@ pl:
|
|||||||
private: Nie możesz przypiąć niepublicznego wpisu
|
private: Nie możesz przypiąć niepublicznego wpisu
|
||||||
reblog: Nie możesz przypiąć podbicia wpisu
|
reblog: Nie możesz przypiąć podbicia wpisu
|
||||||
show_more: Pokaż więcej
|
show_more: Pokaż więcej
|
||||||
|
title: '%{name}: "%{quote}"'
|
||||||
visibilities:
|
visibilities:
|
||||||
private: Tylko dla śledzących
|
private: Tylko dla śledzących
|
||||||
private_long: Widoczne tylko dla osób, które Cię śledzą
|
private_long: Widoczne tylko dla osób, które Cię śledzą
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ zh-TW:
|
|||||||
data: 資料
|
data: 資料
|
||||||
display_name: 顯示名稱
|
display_name: 顯示名稱
|
||||||
email: 電子信箱
|
email: 電子信箱
|
||||||
filtered_languages: 封鎖下面语言的文章
|
filtered_languages: 封鎖下面語言的文章
|
||||||
header: 個人頁面頂部
|
header: 個人頁面頂部
|
||||||
locale: 語言
|
locale: 語言
|
||||||
locked: 將帳號轉為「私密」
|
locked: 將帳號轉為「私密」
|
||||||
@@ -29,7 +29,16 @@ zh-TW:
|
|||||||
note: 簡介
|
note: 簡介
|
||||||
otp_attempt: 雙因子驗證碼
|
otp_attempt: 雙因子驗證碼
|
||||||
password: 密碼
|
password: 密碼
|
||||||
|
setting_auto_play_gif: 自動播放 GIFs
|
||||||
|
setting_boost_modal: 轉推前跳出確認視窗
|
||||||
setting_default_privacy: 文章預設隱私度
|
setting_default_privacy: 文章預設隱私度
|
||||||
|
setting_default_sensitive: 預設我的內容為敏感內容
|
||||||
|
setting_delete_modal: 刪推前跳出確認視窗
|
||||||
|
setting_noindex: 不被搜尋引擎檢索
|
||||||
|
setting_reduce_motion: 減低動畫效果
|
||||||
|
setting_system_font_ui: 使用系統預設字體
|
||||||
|
setting_theme: 網站主題
|
||||||
|
setting_unfollow_modal: 取消關注前跳出確認視窗
|
||||||
type: 匯入資料類型
|
type: 匯入資料類型
|
||||||
username: 使用者名稱
|
username: 使用者名稱
|
||||||
interactions:
|
interactions:
|
||||||
|
|||||||
@@ -409,8 +409,8 @@ sr-Latn:
|
|||||||
exports:
|
exports:
|
||||||
blocks: Blokirali ste
|
blocks: Blokirali ste
|
||||||
csv: CSV
|
csv: CSV
|
||||||
follows: PRatite
|
follows: Pratite
|
||||||
mutes: Mutirali ste
|
mutes: Ućutkali ste
|
||||||
storage: Multimedijalno skladište
|
storage: Multimedijalno skladište
|
||||||
followers:
|
followers:
|
||||||
domain: Domen
|
domain: Domen
|
||||||
@@ -441,7 +441,7 @@ sr-Latn:
|
|||||||
types:
|
types:
|
||||||
blocking: Lista blokiranja
|
blocking: Lista blokiranja
|
||||||
following: Lista pratilaca
|
following: Lista pratilaca
|
||||||
muting: Lista mutiranih
|
muting: Lista ućutkanih
|
||||||
upload: Otpremi
|
upload: Otpremi
|
||||||
in_memoriam_html: In Memoriam.
|
in_memoriam_html: In Memoriam.
|
||||||
invites:
|
invites:
|
||||||
|
|||||||
@@ -409,8 +409,8 @@ sr:
|
|||||||
exports:
|
exports:
|
||||||
blocks: Блокирали сте
|
blocks: Блокирали сте
|
||||||
csv: CSV
|
csv: CSV
|
||||||
follows: ПРатите
|
follows: Пратите
|
||||||
mutes: Мутирали сте
|
mutes: Ућуткали сте
|
||||||
storage: Мултимедијално складиште
|
storage: Мултимедијално складиште
|
||||||
followers:
|
followers:
|
||||||
domain: Домен
|
domain: Домен
|
||||||
@@ -441,7 +441,7 @@ sr:
|
|||||||
types:
|
types:
|
||||||
blocking: Листа блокирања
|
blocking: Листа блокирања
|
||||||
following: Листа пратилаца
|
following: Листа пратилаца
|
||||||
muting: Листа мутираних
|
muting: Листа ућутканих
|
||||||
upload: Отпреми
|
upload: Отпреми
|
||||||
in_memoriam_html: In Memoriam.
|
in_memoriam_html: In Memoriam.
|
||||||
invites:
|
invites:
|
||||||
|
|||||||
@@ -263,12 +263,18 @@ zh-CN:
|
|||||||
unresolved: 未处理
|
unresolved: 未处理
|
||||||
view: 查看
|
view: 查看
|
||||||
settings:
|
settings:
|
||||||
|
activity_api_enabled:
|
||||||
|
desc_html: 本站用户发布的嘟文数,以及本站的活跃用户数和一周内新用户数
|
||||||
|
title: 公开用户活跃度的统计数据
|
||||||
bootstrap_timeline_accounts:
|
bootstrap_timeline_accounts:
|
||||||
desc_html: 用半角逗号分隔多个用户名。只能添加来自本站且未开启保护的帐户。如果留空,则默认关注本站所有的管理员。
|
desc_html: 用半角逗号分隔多个用户名。只能添加来自本站且未开启保护的帐户。如果留空,则默认关注本站所有的管理员。
|
||||||
title: 新用户默认关注
|
title: 新用户默认关注
|
||||||
contact_information:
|
contact_information:
|
||||||
email: 用于联系的公开电子邮件地址
|
email: 用于联系的公开电子邮件地址
|
||||||
username: 用于联系的公开用户名
|
username: 用于联系的公开用户名
|
||||||
|
peers_api_enabled:
|
||||||
|
desc_html: 截至目前本实例在网络中已发现的域名
|
||||||
|
title: 公开已知实例的列表
|
||||||
registrations:
|
registrations:
|
||||||
closed_message:
|
closed_message:
|
||||||
desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签
|
desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ zh-TW:
|
|||||||
perform_full_suspension: 進行停權
|
perform_full_suspension: 進行停權
|
||||||
profile_url: 個人檔案網址
|
profile_url: 個人檔案網址
|
||||||
public: 公開
|
public: 公開
|
||||||
push_subscription_expires: PuSH 訂閱逾期
|
push_subscription_expires: 推播訂閱過期
|
||||||
salmon_url: Salmon URL
|
salmon_url: Salmon URL
|
||||||
silence: 靜音
|
silence: 靜音
|
||||||
statuses: 狀態
|
statuses: 狀態
|
||||||
@@ -133,12 +133,14 @@ zh-TW:
|
|||||||
forgot_password: 忘記密碼?
|
forgot_password: 忘記密碼?
|
||||||
login: 登入
|
login: 登入
|
||||||
logout: 登出
|
logout: 登出
|
||||||
|
migrate_account: 轉移到另一個帳號
|
||||||
|
migrate_account_html: 想要將這個帳號指向另一個帳號可到<a href="%{path}">到這裡設定</a>。
|
||||||
register: 註冊
|
register: 註冊
|
||||||
resend_confirmation: 重寄驗證信
|
resend_confirmation: 重寄驗證信
|
||||||
reset_password: 重設密碼
|
reset_password: 重設密碼
|
||||||
set_new_password: 設定新密碼
|
set_new_password: 設定新密碼
|
||||||
authorize_follow:
|
authorize_follow:
|
||||||
error: 對不起,尋找這個跨站使用者的過程發生錯誤
|
error: 對不起,搜尋遠端使用者出現錯誤
|
||||||
follow: 關注
|
follow: 關注
|
||||||
title: 關注 %{acct}
|
title: 關注 %{acct}
|
||||||
datetime:
|
datetime:
|
||||||
@@ -165,7 +167,16 @@ zh-TW:
|
|||||||
blocks: 您封鎖的使用者
|
blocks: 您封鎖的使用者
|
||||||
csv: CSV
|
csv: CSV
|
||||||
follows: 您關注的使用者
|
follows: 您關注的使用者
|
||||||
|
mutes: 您靜音的使用者
|
||||||
storage: 儲存空間大小
|
storage: 儲存空間大小
|
||||||
|
followers:
|
||||||
|
domain: 網域
|
||||||
|
explanation_html: 為確保個人隱私,您必須知道有哪些使用者正關注你。<strong>您的私密內容會被發送到所有您有被關注的服務站上</strong>。如果您不信任這些服務站的管理者,您可以選擇檢查或刪除您的關注者。
|
||||||
|
followers_count: 關注者數
|
||||||
|
lock_link: 鎖住你的帳號
|
||||||
|
purge: 移除關注者
|
||||||
|
unlocked_warning_html: 所有人都可以關注並檢索你的隱藏狀態。%{lock_link}以檢查或拒絕關注。
|
||||||
|
unlocked_warning_title: 你的帳號是公開的
|
||||||
generic:
|
generic:
|
||||||
changes_saved_msg: 已成功儲存修改
|
changes_saved_msg: 已成功儲存修改
|
||||||
powered_by: 網站由 %{link} 開發
|
powered_by: 網站由 %{link} 開發
|
||||||
@@ -179,6 +190,7 @@ zh-TW:
|
|||||||
types:
|
types:
|
||||||
blocking: 您封鎖的使用者名單
|
blocking: 您封鎖的使用者名單
|
||||||
following: 您關注的使用者名單
|
following: 您關注的使用者名單
|
||||||
|
muting: 您靜音的使用者名單
|
||||||
upload: 上傳
|
upload: 上傳
|
||||||
landing_strip_html: "<strong>%{name}</strong> 是一個在 %{link_to_root_path} 的使用者。只要您有任何 Mastodon 服務站、或者聯盟網站的帳號,便可以跨站關注此站使用者,或者與他們互動。"
|
landing_strip_html: "<strong>%{name}</strong> 是一個在 %{link_to_root_path} 的使用者。只要您有任何 Mastodon 服務站、或者聯盟網站的帳號,便可以跨站關注此站使用者,或者與他們互動。"
|
||||||
landing_strip_signup_html: 如果您沒有這些帳號,歡迎在<a href="%{sign_up_path}">這裡註冊</a>。
|
landing_strip_signup_html: 如果您沒有這些帳號,歡迎在<a href="%{sign_up_path}">這裡註冊</a>。
|
||||||
@@ -231,15 +243,26 @@ zh-TW:
|
|||||||
missing_resource: 無法找到資源
|
missing_resource: 無法找到資源
|
||||||
proceed: 下一步
|
proceed: 下一步
|
||||||
prompt: 您希望關注︰
|
prompt: 您希望關注︰
|
||||||
|
sessions:
|
||||||
|
activity: 最近活動
|
||||||
|
browser: 瀏覽器
|
||||||
|
current_session: 目前的 session
|
||||||
|
description: "%{platform} 上的 %{browser}"
|
||||||
|
explanation: 這些是現在正登入於你的 Mastodon 帳號的瀏覽器。
|
||||||
|
revoke: 取消
|
||||||
|
revoke_success: Session 取消成功。
|
||||||
settings:
|
settings:
|
||||||
authorized_apps: 已授權應用程式
|
authorized_apps: 已授權應用程式
|
||||||
back: 回到 Mastodon
|
back: 回到 Mastodon
|
||||||
|
development: 開發
|
||||||
edit_profile: 修改個人資料
|
edit_profile: 修改個人資料
|
||||||
export: 匯出
|
export: 匯出
|
||||||
|
followers: 授權追蹤者
|
||||||
import: 匯入
|
import: 匯入
|
||||||
|
notifications: 通知
|
||||||
preferences: 偏好設定
|
preferences: 偏好設定
|
||||||
settings: 設定
|
settings: 設定
|
||||||
two_factor_authentication: 雙因子認證
|
two_factor_authentication: 兩階段認證
|
||||||
statuses:
|
statuses:
|
||||||
open_in_web: 以網頁開啟
|
open_in_web: 以網頁開啟
|
||||||
over_character_limit: 超過了 %{max} 字的限制
|
over_character_limit: 超過了 %{max} 字的限制
|
||||||
@@ -257,14 +280,14 @@ zh-TW:
|
|||||||
default: "%Y年%-m月%d日 %H:%M"
|
default: "%Y年%-m月%d日 %H:%M"
|
||||||
two_factor_authentication:
|
two_factor_authentication:
|
||||||
code_hint: 請輸入您認證器產生的代碼,以進行認證
|
code_hint: 請輸入您認證器產生的代碼,以進行認證
|
||||||
description_html: 當您啟用<strong>雙因子認證</strong>後,您登入時將需要使您手機、或其他種類認證器產生的代碼。
|
description_html: 啟用<strong>兩階段認證</strong>後,登入時將需要使手機、或其他種類認證器產生的代碼。
|
||||||
disable: 停用
|
disable: 停用
|
||||||
enable: 啟用
|
enable: 啟用
|
||||||
enabled_success: 已成功啟用雙因子認證
|
enabled_success: 已成功啟用兩階段認證
|
||||||
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy),掃描這裡的 QR 圖形碼</strong>。在雙因子認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
|
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy),掃描這裡的 QR 圖形碼</strong>。在兩階段認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
|
||||||
manual_instructions: 如果您無法掃描 QR 圖形碼,請手動輸入︰
|
manual_instructions: 如果您無法掃描 QR 圖形碼,請手動輸入︰
|
||||||
setup: 設定
|
setup: 設定
|
||||||
wrong_code: 您輸入的認證碼並不正確!可能伺服器時間和您手機不一致,請檢查您手機的時間,或與本站管理員聯絡。
|
wrong_code: 您輸入的認證碼並不正確!可能伺服器時間和您手機不一致,請檢查您手機的時間,或與本站管理員聯絡。
|
||||||
users:
|
users:
|
||||||
invalid_email: 信箱地址格式不正確
|
invalid_email: 信箱地址格式不正確
|
||||||
invalid_otp_token: 雙因子認證碼不正確
|
invalid_otp_token: 兩階段認證碼不正確
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :followers, only: [:index], controller: :follower_accounts
|
resources :followers, only: [:index], controller: :follower_accounts
|
||||||
resources :following, only: [:index], controller: :following_accounts
|
resources :following, only: [:index], controller: :following_accounts
|
||||||
resources :follows, only: [:show], module: :activitypub
|
resource :follow, only: [:create], controller: :account_follow
|
||||||
resource :follow, only: [:create], controller: :account_follow, as: :follows
|
|
||||||
resource :unfollow, only: [:create], controller: :account_unfollow
|
resource :unfollow, only: [:create], controller: :account_unfollow
|
||||||
resource :outbox, only: [:show], module: :activitypub
|
resource :outbox, only: [:show], module: :activitypub
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { default: manageTranslations } = require('react-intl-translations-manager');
|
const { default: manageTranslations } = require('react-intl-translations-manager');
|
||||||
|
|
||||||
const RFC5646_REGEXP = /^[a-z]{2,3}(?:|-[A-Z]+)$/;
|
const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/;
|
||||||
|
|
||||||
const rootDirectory = path.resolve(__dirname, '..', '..');
|
const rootDirectory = path.resolve(__dirname, '..', '..');
|
||||||
const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales');
|
const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class AddIndexOnStreamEntries < ActiveRecord::Migration[5.1]
|
class AddIndexOnStreamEntries < ActiveRecord::Migration[5.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def change
|
def change
|
||||||
commit_db_transaction
|
|
||||||
add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently
|
add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently
|
||||||
remove_index :stream_entries, name: :index_stream_entries_on_account_id
|
remove_index :stream_entries, name: :index_stream_entries_on_account_id
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class MoreFasterIndexOnNotifications < ActiveRecord::Migration[5.1]
|
class MoreFasterIndexOnNotifications < ActiveRecord::Migration[5.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def change
|
def change
|
||||||
commit_db_transaction
|
|
||||||
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently
|
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently
|
||||||
remove_index :notifications, name: :index_notifications_on_id_and_account_id_and_activity_type
|
remove_index :notifications, name: :index_notifications_on_id_and_account_id_and_activity_type
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
0
|
2
|
||||||
end
|
end
|
||||||
|
|
||||||
def pre
|
def pre
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe ActivityPub::FollowsController, type: :controller do
|
|
||||||
let(:follow_request) { Fabricate(:follow_request, account: account) }
|
|
||||||
|
|
||||||
render_views
|
|
||||||
|
|
||||||
context 'with local account' do
|
|
||||||
let(:account) { Fabricate(:account, domain: nil) }
|
|
||||||
|
|
||||||
it 'returns follow request' do
|
|
||||||
signed_request = Request.new(:get, account_follow_url(account, follow_request))
|
|
||||||
signed_request.on_behalf_of(follow_request.target_account)
|
|
||||||
request.headers.merge! signed_request.headers
|
|
||||||
|
|
||||||
get :show, params: { id: follow_request, account_username: account.username }
|
|
||||||
|
|
||||||
expect(body_as_json[:id]).to eq ActivityPub::TagManager.instance.uri_for(follow_request)
|
|
||||||
expect(response).to have_http_status :success
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http 404 without signature' do
|
|
||||||
get :show, params: { id: follow_request, account_username: account.username }
|
|
||||||
expect(response).to have_http_status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with remote account' do
|
|
||||||
let(:account) { Fabricate(:account, domain: Faker::Internet.domain_name) }
|
|
||||||
|
|
||||||
it 'returns http 404' do
|
|
||||||
signed_request = Request.new(:get, account_follow_url(account, follow_request))
|
|
||||||
signed_request.on_behalf_of(follow_request.target_account)
|
|
||||||
request.headers.merge! signed_request.headers
|
|
||||||
|
|
||||||
get :show, params: { id: follow_request, account_username: account.username }
|
|
||||||
|
|
||||||
expect(response).to have_http_status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -47,22 +47,18 @@ describe ApplicationController, type: :controller do
|
|||||||
include_examples 'respond_with_error', 422
|
include_examples 'respond_with_error', 422
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not force ssl if LOCAL_HTTPS is not 'true'" do
|
it "does not force ssl if Rails.env.production? is not 'true'" do
|
||||||
routes.draw { get 'success' => 'anonymous#success' }
|
routes.draw { get 'success' => 'anonymous#success' }
|
||||||
ClimateControl.modify LOCAL_HTTPS: '' do
|
allow(Rails.env).to receive(:production?).and_return(false)
|
||||||
allow(Rails.env).to receive(:production?).and_return(true)
|
get 'success'
|
||||||
get 'success'
|
expect(response).to have_http_status(:success)
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "forces ssl if LOCAL_HTTPS is 'true'" do
|
it "forces ssl if Rails.env.production? is 'true'" do
|
||||||
routes.draw { get 'success' => 'anonymous#success' }
|
routes.draw { get 'success' => 'anonymous#success' }
|
||||||
ClimateControl.modify LOCAL_HTTPS: 'true' do
|
allow(Rails.env).to receive(:production?).and_return(true)
|
||||||
allow(Rails.env).to receive(:production?).and_return(true)
|
get 'success'
|
||||||
get 'success'
|
expect(response).to redirect_to('https://test.host/success')
|
||||||
expect(response).to redirect_to('https://test.host/success')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'helper_method :current_account' do
|
describe 'helper_method :current_account' do
|
||||||
|
|||||||
@@ -12,20 +12,40 @@ describe Auth::ConfirmationsController, type: :controller do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
let!(:user) { Fabricate(:user, confirmation_token: 'foobar', confirmed_at: nil) }
|
context 'when user is unconfirmed' do
|
||||||
|
let!(:user) { Fabricate(:user, confirmation_token: 'foobar', confirmed_at: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||||
get :show, params: { confirmation_token: 'foobar' }
|
get :show, params: { confirmation_token: 'foobar' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to login' do
|
||||||
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'queues up bootstrapping of home timeline' do
|
||||||
|
expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to login' do
|
context 'when user is updating email' do
|
||||||
expect(response).to redirect_to(new_user_session_path)
|
let!(:user) { Fabricate(:user, confirmation_token: 'foobar', unconfirmed_email: 'new-email@example.com') }
|
||||||
end
|
|
||||||
|
|
||||||
it 'queues up bootstrapping of home timeline' do
|
before do
|
||||||
expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id)
|
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||||
|
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||||
|
get :show, params: { confirmation_token: 'foobar' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to login' do
|
||||||
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not queue up bootstrapping of home timeline' do
|
||||||
|
expect(BootstrapTimelineWorker).to_not have_received(:perform_async)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,49 +3,36 @@ require 'rails_helper'
|
|||||||
RSpec.describe ActivityPub::Activity::Accept do
|
RSpec.describe ActivityPub::Activity::Accept do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account) }
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
let!(:follow_request) { Fabricate(:follow_request, account: recipient, target_account: sender) }
|
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Accept',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
object: {
|
||||||
|
id: 'bar',
|
||||||
|
type: 'Follow',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
},
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with concerete object representation' do
|
it 'creates a follow relationship' do
|
||||||
let(:json) do
|
expect(recipient.following?(sender)).to be true
|
||||||
{
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
id: 'foo',
|
|
||||||
type: 'Accept',
|
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
object: {
|
|
||||||
type: 'Follow',
|
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
|
||||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
},
|
|
||||||
}.with_indifferent_access
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a follow relationship' do
|
|
||||||
expect(recipient.following?(sender)).to be true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with object represented by id' do
|
it 'removes the follow request' do
|
||||||
let(:json) do
|
expect(recipient.requested?(sender)).to be false
|
||||||
{
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
id: 'foo',
|
|
||||||
type: 'Accept',
|
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
object: ActivityPub::TagManager.instance.uri_for(follow_request),
|
|
||||||
}.with_indifferent_access
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a follow relationship' do
|
|
||||||
expect(recipient.following?(sender)).to be true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
id: 'foo',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
object: object_json,
|
object: object_json,
|
||||||
@@ -16,6 +16,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||||
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
||||||
end
|
end
|
||||||
@@ -28,7 +30,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'standalone' do
|
context 'standalone' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'public' do
|
context 'public' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
@@ -70,7 +72,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'unlisted' do
|
context 'unlisted' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
@@ -88,7 +90,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'private' do
|
context 'private' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
to: 'http://example.com/followers',
|
to: 'http://example.com/followers',
|
||||||
@@ -108,7 +110,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
@@ -128,7 +130,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
||||||
@@ -151,7 +153,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -174,7 +176,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with mentions missing href' do
|
context 'with mentions missing href' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -194,7 +196,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with media attachments' do
|
context 'with media attachments' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
attachment: [
|
attachment: [
|
||||||
@@ -218,7 +220,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with media attachments missing url' do
|
context 'with media attachments missing url' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
attachment: [
|
attachment: [
|
||||||
@@ -239,7 +241,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with hashtags' do
|
context 'with hashtags' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -263,7 +265,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with hashtags missing name' do
|
context 'with hashtags missing name' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum',
|
content: 'Lorem ipsum',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -284,7 +286,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with emojis' do
|
context 'with emojis' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum :tinking:',
|
content: 'Lorem ipsum :tinking:',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -310,7 +312,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with emojis missing name' do
|
context 'with emojis missing name' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum :tinking:',
|
content: 'Lorem ipsum :tinking:',
|
||||||
tag: [
|
tag: [
|
||||||
@@ -333,7 +335,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
context 'with emojis missing icon' do
|
context 'with emojis missing icon' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: 'bar',
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
content: 'Lorem ipsum :tinking:',
|
content: 'Lorem ipsum :tinking:',
|
||||||
tag: [
|
tag: [
|
||||||
|
|||||||
@@ -34,12 +34,4 @@ RSpec.describe FollowRequest, type: :model do
|
|||||||
expect(follow_request.account.muting_reblogs?(target)).to be true
|
expect(follow_request.account.muting_reblogs?(target)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#object_type' do
|
|
||||||
let(:follow_request) { Fabricate(:follow_request) }
|
|
||||||
|
|
||||||
it 'equals to :follow' do
|
|
||||||
expect(follow_request.object_type).to eq :follow
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ RSpec.describe User, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#confirm' do
|
||||||
|
it 'sets email to unconfirmed_email' do
|
||||||
|
user = Fabricate.build(:user, confirmed_at: Time.now.utc, unconfirmed_email: 'new-email@example.com')
|
||||||
|
user.confirm
|
||||||
|
expect(user.email).to eq 'new-email@example.com'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#disable_two_factor!' do
|
describe '#disable_two_factor!' do
|
||||||
it 'saves false for otp_required_for_login' do
|
it 'saves false for otp_required_for_login' do
|
||||||
user = Fabricate.build(:user, otp_required_for_login: true)
|
user = Fabricate.build(:user, otp_required_for_login: true)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
|||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
before do
|
before do
|
||||||
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '')
|
stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '')
|
||||||
subject.call(object[:id], prefetched_body: Oj.dump(object))
|
subject.call(object[:id], prefetched_body: Oj.dump(object))
|
||||||
end
|
end
|
||||||
@@ -48,13 +50,13 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
|||||||
{
|
{
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mimeType: 'application/x-bittorrent',
|
mimeType: 'application/x-bittorrent',
|
||||||
href: 'https://example.com/12345.torrent',
|
href: "https://#{valid_domain}/12345.torrent",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mimeType: 'text/html',
|
mimeType: 'text/html',
|
||||||
href: 'https://example.com/watch?v=12345',
|
href: "https://#{valid_domain}/watch?v=12345",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -64,8 +66,8 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
|||||||
status = sender.statuses.first
|
status = sender.statuses.first
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.url).to eq 'https://example.com/watch?v=12345'
|
expect(status.url).to eq "https://#{valid_domain}/watch?v=12345"
|
||||||
expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remix https://example.com/watch?v=12345'
|
expect(strip_tags(status.text)).to eq "Nyan Cat 10 hours remix https://#{valid_domain}/watch?v=12345"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user