mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-16 09:18:46 +00:00
Compare commits
40 Commits
compose-re
...
thread-mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ class ActivityPub::FollowsController < Api::BaseController
|
|||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render(
|
render json: follow_request,
|
||||||
json: FollowRequest.includes(:account).references(:account).find_by!(
|
serializer: ActivityPub::FollowSerializer,
|
||||||
id: params.require(:id),
|
adapter: ActivityPub::Adapter,
|
||||||
accounts: { domain: nil, username: params.require(:account_username) },
|
content_type: 'application/activity+json'
|
||||||
target_account: signed_request_account
|
end
|
||||||
),
|
|
||||||
serializer: ActivityPub::FollowSerializer,
|
private
|
||||||
adapter: ActivityPub::Adapter,
|
|
||||||
content_type: 'application/activity+json'
|
def follow_request
|
||||||
|
FollowRequest.includes(:account).references(:account).find_by!(
|
||||||
|
id: params.require(:id),
|
||||||
|
accounts: { domain: nil, username: params.require(:account_username) },
|
||||||
|
target_account: signed_request_account
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -198,11 +198,24 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_cached_json(cache_key, **options)
|
def render_cached_json(cache_key, **options)
|
||||||
|
options[:expires_in] ||= 3.minutes
|
||||||
|
options[:public] ||= true
|
||||||
|
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
|
||||||
|
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: options[: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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,8 +55,8 @@ 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.icon_title': '高度な設定',
|
'advanced_options.icon_title': '高度な設定',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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', privacyPreference(status.visibility, state.get('default_privacy')));
|
||||||
|
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.get('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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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
|
||||||
|
en:
|
||||||
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
[
|
||||||
|
]
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %>
|
||||||
@@ -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 %>
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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: 兩階段認證碼不正確
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ RSpec.configure do |config|
|
|||||||
config.include ActiveSupport::Testing::TimeHelpers
|
config.include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
config.before :each, type: :feature do
|
config.before :each, type: :feature do
|
||||||
https = ENV['LOCAL_HTTPS'] == 'true'
|
https = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'
|
||||||
Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
|
Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user