mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-12 23:38:20 +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)
|
||||
statsd-ruby (~> 1.2.0)
|
||||
oj (3.3.9)
|
||||
openssl (2.0.6)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (2.0.1)
|
||||
ostatus2 (2.0.2)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
openssl (~> 2.0)
|
||||
ox (2.8.2)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
class AccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -27,10 +28,11 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @account,
|
||||
serializer: ActivityPub::ActorSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,15 +4,19 @@ class ActivityPub::FollowsController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
render(
|
||||
json: FollowRequest.includes(:account).references(:account).find_by!(
|
||||
id: params.require(:id),
|
||||
accounts: { domain: nil, username: params.require(:account_username) },
|
||||
target_account: signed_request_account
|
||||
),
|
||||
serializer: ActivityPub::FollowSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
render json: follow_request,
|
||||
serializer: ActivityPub::FollowSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
@@ -21,9 +21,9 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
|
||||
weeks << {
|
||||
week: week.to_time.to_i.to_s,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0,
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}"),
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -198,11 +198,24 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
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
|
||||
yield.to_json
|
||||
end
|
||||
|
||||
expires_in options[:expires_in], public: true
|
||||
render json: data
|
||||
expires_in options[:expires_in], public: options[:public]
|
||||
render json: data, content_type: content_type
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Vary'] = 'Accept'
|
||||
end
|
||||
|
||||
def skip_session!
|
||||
request.session_options[:skip] = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
class EmojisController < ApplicationController
|
||||
before_action :set_emoji
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: @emoji,
|
||||
serializer: ActivityPub::EmojiSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
|
||||
before_action :set_link_headers
|
||||
before_action :check_account_suspension
|
||||
before_action :redirect_to_original, only: [:show]
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -23,25 +23,21 @@ class StatusesController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @status,
|
||||
serializer: ActivityPub::NoteSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session! unless @stream_entry.hidden?
|
||||
|
||||
# Allow HTTP caching for 3 minutes if the status is public
|
||||
unless @stream_entry.hidden?
|
||||
request.session_options[:skip] = true
|
||||
expires_in(3.minutes, public: true)
|
||||
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def activity
|
||||
render json: @status,
|
||||
serializer: ActivityPub::ActivitySerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
end
|
||||
|
||||
def embed
|
||||
|
||||
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes)
|
||||
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
|
||||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ module RoutingHelper
|
||||
extend ActiveSupport::Concern
|
||||
include Rails.application.routes.url_helpers
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include Webpacker::Helper
|
||||
|
||||
included do
|
||||
def default_url_options
|
||||
@@ -17,6 +18,10 @@ module RoutingHelper
|
||||
URI.join(root_url, source).to_s
|
||||
end
|
||||
|
||||
def full_pack_url(source, **options)
|
||||
full_asset_url(asset_pack_path(source, options))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def use_storage?
|
||||
|
||||
@@ -61,7 +61,7 @@ export function replyCompose(status, router) {
|
||||
status: status,
|
||||
});
|
||||
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
if (router && !getState().getIn(['compose', 'mounted'])) {
|
||||
router.push('/statuses/new');
|
||||
}
|
||||
};
|
||||
@@ -118,6 +118,11 @@ export function submitCompose() {
|
||||
}).then(function (response) {
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
// If the response has no data then we can't do anything else.
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To make the app more responsive, immediately get the status into the columns
|
||||
|
||||
const insertOrRefresh = (timelineId, refreshAction) => {
|
||||
@@ -341,10 +346,11 @@ export function unmountCompose() {
|
||||
};
|
||||
};
|
||||
|
||||
export function toggleComposeAdvancedOption(option) {
|
||||
export function changeComposeAdvancedOption(option, value) {
|
||||
return {
|
||||
option,
|
||||
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
|
||||
option: option,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
||||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
|
||||
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
|
||||
export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
|
||||
|
||||
export function setBrowserSupport (value) {
|
||||
return {
|
||||
@@ -25,28 +23,12 @@ export function clearSubscription () {
|
||||
};
|
||||
}
|
||||
|
||||
export function changeAlerts(key, value) {
|
||||
export function setAlerts (key, value) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ALERTS_CHANGE,
|
||||
type: SET_ALERTS,
|
||||
key,
|
||||
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 ? (
|
||||
<div className='account small'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
<Permalink
|
||||
className='account small'
|
||||
href={account.get('url')}
|
||||
to={`/accounts/${account.get('id')}`}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar
|
||||
account={account}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
<DisplayName
|
||||
account={account}
|
||||
inline
|
||||
/>
|
||||
</Permalink>
|
||||
) : (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
// Package imports.
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
export default class DisplayName extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
account,
|
||||
className,
|
||||
} = this.props;
|
||||
const computedClass = classNames('display-name', className);
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<span className={computedClass}>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// The component.
|
||||
export default function DisplayName ({
|
||||
account,
|
||||
className,
|
||||
inline,
|
||||
}) {
|
||||
const computedClass = classNames('display-name', { inline }, className);
|
||||
|
||||
// The result.
|
||||
return account ? (
|
||||
<span className={computedClass}>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
|
||||
{inline ? ' ' : null}
|
||||
<span className='display-name__account'>@{account.get('acct')}</span>
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Props.
|
||||
DisplayName.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
className: PropTypes.string,
|
||||
inline: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class Dropdown extends React.PureComponent {
|
||||
(item, i) => item ? {
|
||||
...item,
|
||||
name: `${item.text}-${i}`,
|
||||
onClick: this.handleItemClick.bind(i),
|
||||
onClick: this.handleItemClick.bind(this, i),
|
||||
} : null
|
||||
),
|
||||
});
|
||||
|
||||
@@ -22,7 +22,13 @@ export default class Permalink extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { href, children, className, ...other } = this.props;
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
href,
|
||||
to,
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import {
|
||||
cancelReplyCompose,
|
||||
changeCompose,
|
||||
changeComposeAdvancedOption,
|
||||
changeComposeSensitivity,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeSpoilerness,
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
insertEmojiCompose,
|
||||
mountCompose,
|
||||
selectComposeSuggestion,
|
||||
submitCompose,
|
||||
toggleComposeAdvancedOption,
|
||||
undoUploadCompose,
|
||||
unmountCompose,
|
||||
uploadCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import {
|
||||
@@ -47,8 +49,8 @@ function mapStateToProps (state) {
|
||||
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
||||
return {
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
advancedOptions: state.getIn(['compose', 'advanced_options']),
|
||||
amUnlocked: !state.getIn(['accounts', me, 'locked']),
|
||||
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
@@ -57,7 +59,7 @@ function mapStateToProps (state) {
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
progress: state.getIn(['compose', 'progress']),
|
||||
replyAccount: inReplyTo ? state.getIn(['accounts', state.getIn(['statuses', inReplyTo, 'account'])]) : null,
|
||||
replyAccount: inReplyTo ? state.getIn(['statuses', inReplyTo, 'account']) : null,
|
||||
replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
sideArm: state.getIn(['local_settings', 'side_arm']),
|
||||
@@ -74,6 +76,7 @@ function mapStateToProps (state) {
|
||||
// Dispatch mapping.
|
||||
const mapDispatchToProps = {
|
||||
onCancelReply: cancelReplyCompose,
|
||||
onChangeAdvancedOption: changeComposeAdvancedOption,
|
||||
onChangeDescription: changeUploadCompose,
|
||||
onChangeSensitivity: changeComposeSensitivity,
|
||||
onChangeSpoilerText: changeComposeSpoilerText,
|
||||
@@ -84,12 +87,13 @@ const mapDispatchToProps = {
|
||||
onCloseModal: closeModal,
|
||||
onFetchSuggestions: fetchComposeSuggestions,
|
||||
onInsertEmoji: insertEmojiCompose,
|
||||
onMount: mountCompose,
|
||||
onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
|
||||
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
|
||||
onSelectSuggestion: selectComposeSuggestion,
|
||||
onSubmit: submitCompose,
|
||||
onToggleAdvancedOption: toggleComposeAdvancedOption,
|
||||
onUndoUpload: undoUploadCompose,
|
||||
onUnmount: unmountCompose,
|
||||
onUpload: uploadCompose,
|
||||
};
|
||||
|
||||
@@ -188,6 +192,22 @@ class Composer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Tells our state the composer has been mounted.
|
||||
componentDidMount () {
|
||||
const { onMount } = this.props;
|
||||
if (onMount) {
|
||||
onMount();
|
||||
}
|
||||
}
|
||||
|
||||
// Tells our state the composer has been unmounted.
|
||||
componentWillUnmount () {
|
||||
const { onUnmount } = this.props;
|
||||
if (onUnmount) {
|
||||
onUnmount();
|
||||
}
|
||||
}
|
||||
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - Replying to zero or one users, places the cursor at the end
|
||||
@@ -245,17 +265,17 @@ class Composer extends React.Component {
|
||||
handleSubmit,
|
||||
handleRefTextarea,
|
||||
} = this.handlers;
|
||||
const { history } = this.context;
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
amUnlocked,
|
||||
doNotFederate,
|
||||
intl,
|
||||
isSubmitting,
|
||||
isUploading,
|
||||
layout,
|
||||
media,
|
||||
onCancelReply,
|
||||
onChangeAdvancedOption,
|
||||
onChangeDescription,
|
||||
onChangeSensitivity,
|
||||
onChangeSpoilerness,
|
||||
@@ -266,7 +286,6 @@ class Composer extends React.Component {
|
||||
onFetchSuggestions,
|
||||
onOpenActionsModal,
|
||||
onOpenDoodleModal,
|
||||
onToggleAdvancedOption,
|
||||
onUndoUpload,
|
||||
onUpload,
|
||||
privacy,
|
||||
@@ -297,12 +316,12 @@ class Composer extends React.Component {
|
||||
<ComposerReply
|
||||
account={replyAccount}
|
||||
content={replyContent}
|
||||
history={history}
|
||||
intl={intl}
|
||||
onCancel={onCancelReply}
|
||||
/>
|
||||
) : null}
|
||||
<ComposerTextarea
|
||||
advancedOptions={advancedOptions}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||
disabled={isSubmitting}
|
||||
intl={intl}
|
||||
@@ -329,19 +348,19 @@ class Composer extends React.Component {
|
||||
) : null}
|
||||
<ComposerOptions
|
||||
acceptContentTypes={acceptContentTypes}
|
||||
advancedOptions={advancedOptions}
|
||||
disabled={isSubmitting}
|
||||
doNotFederate={doNotFederate}
|
||||
full={media.size >= 4 || media.some(
|
||||
item => item.get('type') === 'video'
|
||||
)}
|
||||
hasMedia={!!media.size}
|
||||
intl={intl}
|
||||
onChangeAdvancedOption={onChangeAdvancedOption}
|
||||
onChangeSensitivity={onChangeSensitivity}
|
||||
onChangeVisibility={onChangeVisibility}
|
||||
onDoodleOpen={onOpenDoodleModal}
|
||||
onModalClose={onCloseModal}
|
||||
onModalOpen={onOpenActionsModal}
|
||||
onToggleAdvancedOption={onToggleAdvancedOption}
|
||||
onToggleSpoiler={onChangeSpoilerness}
|
||||
onUpload={onUpload}
|
||||
privacy={privacy}
|
||||
@@ -350,7 +369,7 @@ class Composer extends React.Component {
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
<ComposerPublisher
|
||||
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
|
||||
countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
|
||||
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
|
||||
intl={intl}
|
||||
onSecondarySubmit={handleSecondarySubmit}
|
||||
@@ -364,19 +383,14 @@ class Composer extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
// Context
|
||||
Composer.contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
// Props.
|
||||
Composer.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
||||
// State props.
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
amUnlocked: PropTypes.bool,
|
||||
doNotFederate: PropTypes.bool,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
isSubmitting: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
@@ -385,7 +399,7 @@ Composer.propTypes = {
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
privacy: PropTypes.string,
|
||||
progress: PropTypes.number,
|
||||
replyAccount: ImmutablePropTypes.map,
|
||||
replyAccount: PropTypes.string,
|
||||
replyContent: PropTypes.string,
|
||||
resetFileKey: PropTypes.number,
|
||||
sideArm: PropTypes.string,
|
||||
@@ -399,6 +413,7 @@ Composer.propTypes = {
|
||||
|
||||
// Dispatch props.
|
||||
onCancelReply: PropTypes.func,
|
||||
onChangeAdvancedOption: PropTypes.func,
|
||||
onChangeDescription: PropTypes.func,
|
||||
onChangeSensitivity: PropTypes.func,
|
||||
onChangeSpoilerText: PropTypes.func,
|
||||
@@ -409,12 +424,13 @@ Composer.propTypes = {
|
||||
onCloseModal: PropTypes.func,
|
||||
onFetchSuggestions: PropTypes.func,
|
||||
onInsertEmoji: PropTypes.func,
|
||||
onMount: PropTypes.func,
|
||||
onOpenActionsModal: PropTypes.func,
|
||||
onOpenDoodleModal: PropTypes.func,
|
||||
onSelectSuggestion: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onToggleAdvancedOption: PropTypes.func,
|
||||
onUndoUpload: PropTypes.func,
|
||||
onUnmount: PropTypes.func,
|
||||
onUpload: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import {
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
@@ -47,11 +48,11 @@ const messages = defineMessages({
|
||||
},
|
||||
local_only_long: {
|
||||
defaultMessage: 'Do not post to other instances',
|
||||
id: 'advanced-options.local-only.long',
|
||||
id: 'advanced_options.local-only.long',
|
||||
},
|
||||
local_only_short: {
|
||||
defaultMessage: 'Local-only',
|
||||
id: 'advanced-options.local-only.short',
|
||||
id: 'advanced_options.local-only.short',
|
||||
},
|
||||
private_long: {
|
||||
defaultMessage: 'Post to followers only',
|
||||
@@ -77,6 +78,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Hide text behind warning',
|
||||
id: 'compose_form.spoiler',
|
||||
},
|
||||
threaded_mode_long: {
|
||||
defaultMessage: 'Automatically opens a reply on posting',
|
||||
id: 'advanced_options.threaded_mode.long',
|
||||
},
|
||||
threaded_mode_short: {
|
||||
defaultMessage: 'Threaded mode',
|
||||
id: 'advanced_options.threaded_mode.short',
|
||||
},
|
||||
unlisted_long: {
|
||||
defaultMessage: 'Do not show in public timelines',
|
||||
id: 'privacy.unlisted.long',
|
||||
@@ -149,16 +158,16 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
} = this.handlers;
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
disabled,
|
||||
doNotFederate,
|
||||
full,
|
||||
hasMedia,
|
||||
intl,
|
||||
onChangeAdvancedOption,
|
||||
onChangeSensitivity,
|
||||
onChangeVisibility,
|
||||
onModalClose,
|
||||
onModalOpen,
|
||||
onToggleAdvancedOption,
|
||||
onToggleSpoiler,
|
||||
privacy,
|
||||
resetFileKey,
|
||||
@@ -283,23 +292,31 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
onClick={onToggleSpoiler}
|
||||
title={intl.formatMessage(messages.spoiler)}
|
||||
/>
|
||||
<Dropdown
|
||||
active={doNotFederate}
|
||||
disabled={disabled}
|
||||
icon='home'
|
||||
items={[
|
||||
{
|
||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||
name: 'do_not_federate',
|
||||
on: doNotFederate,
|
||||
text: <FormattedMessage {...messages.local_only_short} />,
|
||||
},
|
||||
]}
|
||||
onChange={onToggleAdvancedOption}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||
/>
|
||||
{advancedOptions ? (
|
||||
<Dropdown
|
||||
active={advancedOptions.some(value => !!value)}
|
||||
disabled={disabled}
|
||||
icon='ellipsis-h'
|
||||
items={[
|
||||
{
|
||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||
name: 'do_not_federate',
|
||||
on: advancedOptions.get('do_not_federate'),
|
||||
text: <FormattedMessage {...messages.local_only_short} />,
|
||||
},
|
||||
{
|
||||
meta: <FormattedMessage {...messages.threaded_mode_long} />,
|
||||
name: 'threaded_mode',
|
||||
on: advancedOptions.get('threaded_mode'),
|
||||
text: <FormattedMessage {...messages.threaded_mode_short} />,
|
||||
},
|
||||
]}
|
||||
onChange={onChangeAdvancedOption}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -309,17 +326,17 @@ export default class ComposerOptions extends React.PureComponent {
|
||||
// Props.
|
||||
ComposerOptions.propTypes = {
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
disabled: PropTypes.bool,
|
||||
doNotFederate: PropTypes.bool,
|
||||
full: PropTypes.bool,
|
||||
hasMedia: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeAdvancedOption: PropTypes.func,
|
||||
onChangeSensitivity: PropTypes.func,
|
||||
onChangeVisibility: PropTypes.func,
|
||||
onDoodleOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onToggleAdvancedOption: PropTypes.func,
|
||||
onToggleSpoiler: PropTypes.func,
|
||||
onUpload: PropTypes.func,
|
||||
privacy: PropTypes.string,
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function ComposerPublisher ({
|
||||
unlisted: 'unlock-alt',
|
||||
}[privacy]}
|
||||
/>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.publish} />
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
// Components.
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
|
||||
// Utils.
|
||||
@@ -31,17 +29,6 @@ const handlers = {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
|
||||
// Handles a click on the status's account.
|
||||
handleClickAccount () {
|
||||
const {
|
||||
account,
|
||||
history,
|
||||
} = this.props;
|
||||
if (history) {
|
||||
history.push(`/accounts/${account.get('id')}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// The component.
|
||||
@@ -55,10 +42,7 @@ export default class ComposerReply extends React.PureComponent {
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
handleClick,
|
||||
handleClickAccount,
|
||||
} = this.handlers;
|
||||
const { handleClick } = this.handlers;
|
||||
const {
|
||||
account,
|
||||
content,
|
||||
@@ -76,21 +60,10 @@ export default class ComposerReply extends React.PureComponent {
|
||||
title={intl.formatMessage(messages.cancel)}
|
||||
/>
|
||||
{account ? (
|
||||
<a
|
||||
className='account'
|
||||
href={account.get('url')}
|
||||
onClick={handleClickAccount}
|
||||
>
|
||||
<Avatar
|
||||
account={account}
|
||||
className='avatar'
|
||||
size={24}
|
||||
/>
|
||||
<DisplayName
|
||||
account={account}
|
||||
className='display_name'
|
||||
/>
|
||||
</a>
|
||||
<AccountContainer
|
||||
id={account}
|
||||
small
|
||||
/>
|
||||
) : null}
|
||||
</header>
|
||||
<div
|
||||
@@ -105,9 +78,8 @@ export default class ComposerReply extends React.PureComponent {
|
||||
}
|
||||
|
||||
ComposerReply.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
account: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
// Components.
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
// Messages.
|
||||
const messages = defineMessages({
|
||||
localOnly: {
|
||||
defaultMessage: 'This post is local-only',
|
||||
id: 'advanced_options.local-only.tooltip',
|
||||
},
|
||||
threadedMode: {
|
||||
defaultMessage: 'Threaded mode enabled',
|
||||
id: 'advanced_options.threaded_mode.tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
// We use an array of tuples here instead of an object because it
|
||||
// preserves order.
|
||||
const iconMap = [
|
||||
['do_not_federate', 'home', messages.localOnly],
|
||||
['threaded_mode', 'comments', messages.threadedMode],
|
||||
];
|
||||
|
||||
// The component.
|
||||
export default function ComposerTextareaIcons ({
|
||||
advancedOptions,
|
||||
intl,
|
||||
}) {
|
||||
|
||||
// The result. We just map every active option to its icon.
|
||||
return (
|
||||
<div className='composer--textarea--icons'>
|
||||
{advancedOptions ? iconMap.map(
|
||||
([key, icon, message]) => advancedOptions.get(key) ? (
|
||||
<span
|
||||
className='textarea_icon'
|
||||
key={key}
|
||||
title={intl.formatMessage(message)}
|
||||
>
|
||||
<Icon
|
||||
fullwidth
|
||||
icon={icon}
|
||||
/>
|
||||
</span>
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Props.
|
||||
ComposerTextareaIcons.propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||
|
||||
// Components.
|
||||
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
|
||||
import ComposerTextareaIcons from './icons';
|
||||
import ComposerTextareaSuggestions from './suggestions';
|
||||
|
||||
// Utils.
|
||||
@@ -32,7 +33,7 @@ const handlers = {
|
||||
|
||||
// When blurring the textarea, suggestions are hidden.
|
||||
handleBlur () {
|
||||
//this.setState({ suggestionsHidden: true });
|
||||
this.setState({ suggestionsHidden: true });
|
||||
},
|
||||
|
||||
// When the contents of the textarea change, we have to pull up new
|
||||
@@ -57,7 +58,7 @@ const handlers = {
|
||||
const right = value.slice(selectionStart).search(/[\s\u200B]/);
|
||||
const token = function () {
|
||||
switch (true) {
|
||||
case left < 0 || /[@:]/.test(!value[left]):
|
||||
case left < 0 || !/[@:]/.test(value[left]):
|
||||
return null;
|
||||
case right < 0:
|
||||
return value.slice(left);
|
||||
@@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
|
||||
handleRefTextarea,
|
||||
} = this.handlers;
|
||||
const {
|
||||
advancedOptions,
|
||||
autoFocus,
|
||||
disabled,
|
||||
intl,
|
||||
@@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
|
||||
<div className='composer--textarea'>
|
||||
<label>
|
||||
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
|
||||
<ComposerTextareaIcons
|
||||
advancedOptions={advancedOptions}
|
||||
intl={intl}
|
||||
/>
|
||||
<Textarea
|
||||
aria-autocomplete='list'
|
||||
autoFocus={autoFocus}
|
||||
@@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
|
||||
|
||||
// Props.
|
||||
ComposerTextarea.propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
autoFocus: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
||||
@@ -24,9 +24,16 @@ const handlers = {
|
||||
} = this.props;
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevents following account links
|
||||
onClick(index);
|
||||
}
|
||||
},
|
||||
|
||||
// This prevents the focus from changing, which would mess with
|
||||
// our suggestion code.
|
||||
handleMouseDown (e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
};
|
||||
|
||||
// The component.
|
||||
@@ -40,7 +47,10 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const { handleClick } = this.handlers;
|
||||
const {
|
||||
handleMouseDown,
|
||||
handleClick,
|
||||
} = this.handlers;
|
||||
const {
|
||||
selected,
|
||||
suggestion,
|
||||
@@ -51,7 +61,8 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
|
||||
return (
|
||||
<div
|
||||
className={computedClass}
|
||||
onMouseDown={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClickCapture={handleClick} // Jumps in front of contents
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
>
|
||||
|
||||
@@ -111,10 +111,10 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
|
||||
listItems = listItems.concat([
|
||||
<div>
|
||||
<ColumnLink key='7' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<div key='7'>
|
||||
<ColumnLink key='8' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
<ColumnLink key={(8 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
||||
@@ -11,7 +11,6 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import { clearNotifications } from 'flavours/glitch/actions/notifications';
|
||||
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications';
|
||||
import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -26,11 +26,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
dispatch(savePushNotificationSettings());
|
||||
},
|
||||
|
||||
onClear () {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
|
||||
@@ -52,9 +52,13 @@ const messages = {
|
||||
'compose.attach.doodle': 'Draw something',
|
||||
'compose.attach': 'Attach...',
|
||||
|
||||
'advanced-options.local-only.short': 'Local-only',
|
||||
'advanced-options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.short': 'Local-only',
|
||||
'advanced_options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.tooltip': 'This post is local-only',
|
||||
'advanced_options.icon_title': 'Advanced options',
|
||||
'advanced_options.threaded_mode.short': 'Threaded mode',
|
||||
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
|
||||
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
||||
|
||||
@@ -55,8 +55,8 @@ const messages = {
|
||||
'compose.attach.doodle': '落書きをする',
|
||||
'compose.attach': 'アタッチ...',
|
||||
|
||||
'advanced-options.local-only.short': 'ローカル限定',
|
||||
'advanced-options.local-only.long': '他のインスタンスには投稿されません',
|
||||
'advanced_options.local-only.short': 'ローカル限定',
|
||||
'advanced_options.local-only.long': '他のインスタンスには投稿されません',
|
||||
'advanced_options.icon_title': '高度な設定',
|
||||
};
|
||||
|
||||
|
||||
@@ -28,12 +28,16 @@ const messages = {
|
||||
'settings.media': 'Zawartość multimedialna',
|
||||
'settings.media_letterbox': 'Letterbox media',
|
||||
'settings.media_fullwidth': 'Podgląd zawartości multimedialnej o pełnej szerokości',
|
||||
'settings.preferences': 'Preferencje użyytkownika',
|
||||
'settings.preferences': 'Preferencje użytkownika',
|
||||
'settings.wide_view': 'Szeroki widok (tylko w trybie desktopowym)',
|
||||
'settings.navbar_under': 'Pasek nawigacji na dole (tylko w trybie mobilnym)',
|
||||
'status.collapse': 'Zwiń',
|
||||
'status.uncollapse': 'Rozwiń',
|
||||
|
||||
'favourite_modal.combo': 'Możesz nacisnąć {combo}, aby pominąć to następnym razem',
|
||||
|
||||
'home.column_settings.show_direct': 'Pokaż wiadomości bezpośrednie',
|
||||
|
||||
'notification.markForDeletion': 'Oznacz do usunięcia',
|
||||
'notifications.clear': 'Wyczyść wszystkie powiadomienia',
|
||||
'notifications.marked_clear_confirmation': 'Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?',
|
||||
@@ -43,6 +47,14 @@ const messages = {
|
||||
'notification_purge.btn_none': 'Odznacz\nwszystkie',
|
||||
'notification_purge.btn_invert': 'Odwróć\nzaznaczenie',
|
||||
'notification_purge.btn_apply': 'Usuń\nzaznaczone',
|
||||
|
||||
'compose.attach.upload': 'Wyślij plik',
|
||||
'compose.attach.doodle': 'Narysuj coś',
|
||||
'compose.attach': 'Załącz coś',
|
||||
|
||||
'advanced-options.local-only.short': 'Tylko lokalnie',
|
||||
'advanced-options.local-only.long': 'Nie wysyłaj na inne instancje',
|
||||
'advanced_options.icon_title': 'Ustawienia zaawansowane',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
||||
|
||||
@@ -6,3 +6,10 @@ en:
|
||||
skins:
|
||||
glitch:
|
||||
default: Default
|
||||
pl:
|
||||
flavours:
|
||||
glitch:
|
||||
description: Domyślny motyw instancji GlitchSoc.
|
||||
skins:
|
||||
glitch:
|
||||
default: Domyślny
|
||||
|
||||
@@ -33,11 +33,13 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
import uuid from 'flavours/glitch/util/uuid';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
import { overwrite } from 'flavours/glitch/util/js_helpers';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
mounted: false,
|
||||
advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
threaded_mode: false,
|
||||
}),
|
||||
sensitive: false,
|
||||
spoiler: false,
|
||||
@@ -55,6 +57,7 @@ const initialState = ImmutableMap({
|
||||
suggestions: ImmutableList(),
|
||||
default_advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
threaded_mode: null, // Do not reset
|
||||
}),
|
||||
default_privacy: 'public',
|
||||
default_sensitive: false,
|
||||
@@ -83,6 +86,20 @@ function statusToTextMentions(state, status) {
|
||||
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
|
||||
};
|
||||
|
||||
function apiStatusToTextMentions (state, status) {
|
||||
let set = ImmutableOrderedSet([]);
|
||||
|
||||
if (status.account.id !== me) {
|
||||
set = set.add(`@${status.account.acct} `);
|
||||
}
|
||||
|
||||
return set.union(status.mentions.filter(
|
||||
mention => mention.id !== me
|
||||
).map(
|
||||
mention => `@${mention.acct} `
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function clearAll(state) {
|
||||
return state.withMutations(map => {
|
||||
map.set('text', '');
|
||||
@@ -90,7 +107,10 @@ function clearAll(state) {
|
||||
map.set('spoiler_text', '');
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('advanced_options', state.get('default_advanced_options'));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||
);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
@@ -98,6 +118,31 @@ function clearAll(state) {
|
||||
});
|
||||
};
|
||||
|
||||
function continueThread (state, status) {
|
||||
return state.withMutations(function (map) {
|
||||
map.set('text', apiStatusToTextMentions(state, status));
|
||||
if (status.spoiler_text) {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', status.spoiler_text);
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', status.id);
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
|
||||
);
|
||||
map.set('privacy', 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) {
|
||||
const prevSize = state.get('media_attachments').size;
|
||||
|
||||
@@ -182,8 +227,7 @@ export default function compose(state = initialState, action) {
|
||||
return state.set('mounted', false);
|
||||
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
|
||||
return state
|
||||
.set('advanced_options',
|
||||
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
|
||||
.set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_SENSITIVITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
@@ -220,9 +264,10 @@ export default function compose(state = initialState, action) {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('advanced_options', new ImmutableMap({
|
||||
do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
|
||||
}));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
|
||||
);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('preselectDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
@@ -243,14 +288,17 @@ export default function compose(state = initialState, action) {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('advanced_options', state.get('default_advanced_options'));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||
);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
return action.status && state.get('advanced_options', 'threaded_mode') ? continueThread(state, action.status) : clearAll(state);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
||||
return state.set('is_submitting', false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications';
|
||||
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from 'flavours/glitch/actions/push_notifications';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
@@ -43,7 +43,7 @@ export default function push_subscriptions(state = initialState, action) {
|
||||
return state.set('browserSupport', action.value);
|
||||
case CLEAR_SUBSCRIPTION:
|
||||
return initialState;
|
||||
case ALERTS_CHANGE:
|
||||
case SET_ALERTS:
|
||||
return state.setIn(action.key, action.value);
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -52,22 +52,7 @@
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .account {
|
||||
& > .avatar {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
& > .display_name {
|
||||
color: $ui-base-color;
|
||||
display: block;
|
||||
padding-right: 25px;
|
||||
max-width: 100%;
|
||||
line-height: 24px;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
& > .account.small { color: $ui-base-color }
|
||||
|
||||
& > .cancel {
|
||||
float: right;
|
||||
@@ -87,6 +72,27 @@
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
padding-top: 5px;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child { margin-bottom: 0 }
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($ui-base-color, 20%);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline }
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span { text-decoration: underline }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emojione {
|
||||
@@ -94,27 +100,6 @@
|
||||
height: 20px;
|
||||
margin: -5px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child { margin-bottom: 0 }
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($ui-base-color, 20%);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover { text-decoration: underline }
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span { text-decoration: underline }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea {
|
||||
@@ -149,6 +134,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea--icons {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .textarea_icon {
|
||||
display: block;
|
||||
margin: 2px 0 0 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: darken($ui-primary-color, 24%);
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
|
||||
.composer--textarea--suggestions {
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -175,6 +181,7 @@
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
@@ -191,6 +198,12 @@
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
& > .account.small {
|
||||
.display-name {
|
||||
& > span { color: lighten($ui-base-color, 36%) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer--upload_form {
|
||||
|
||||
@@ -745,6 +745,8 @@
|
||||
.account {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
.account__display-name {
|
||||
flex: 1 1 auto;
|
||||
@@ -762,27 +764,8 @@
|
||||
& > .account__avatar-wrapper { margin: 0 8px 0 0 }
|
||||
|
||||
& > .display-name {
|
||||
display: block;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
& > strong {
|
||||
display: inline;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline;
|
||||
color: lighten($ui-base-color, 36%);
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
&::before { content: " " }
|
||||
}
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,6 +1226,30 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
padding: 0;
|
||||
height: 18px;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
strong {
|
||||
display: inline;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status__relative-time,
|
||||
|
||||
5
app/javascript/flavours/glitch/util/js_helpers.js
Normal file
5
app/javascript/flavours/glitch/util/js_helpers.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// This function returns the new value unless it is `null` or
|
||||
// `undefined`, in which case it returns the old one.
|
||||
export function overwrite (oldVal, newVal) {
|
||||
return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as WebPushSubscription from './web_push_subscription';
|
||||
import Mastodon from 'flavours/glitch/containers/mastodon';
|
||||
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
|
||||
import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ready from './ready';
|
||||
@@ -25,7 +25,7 @@ function main() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// avoid offline in dev mode because it's harder to debug
|
||||
require('offline-plugin/runtime').install();
|
||||
WebPushSubscription.register();
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
}
|
||||
perf.stop('main()');
|
||||
|
||||
|
||||
46
app/javascript/flavours/glitch/util/settings.js
Normal file
46
app/javascript/flavours/glitch/util/settings.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default class Settings {
|
||||
|
||||
constructor(keyBase = null) {
|
||||
this.keyBase = keyBase;
|
||||
}
|
||||
|
||||
generateKey(id) {
|
||||
return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
|
||||
}
|
||||
|
||||
set(id, data) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
const encodedData = JSON.stringify(data);
|
||||
localStorage.setItem(key, encodedData);
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
const rawData = localStorage.getItem(key);
|
||||
return JSON.parse(rawData);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
remove(id) {
|
||||
const data = this.get(id);
|
||||
if (data) {
|
||||
const key = this.generateKey(id);
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
||||
@@ -1,105 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { store } from 'flavours/glitch/containers/mastodon';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
const urlBase64ToUint8Array = (base64String) => {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||
|
||||
const getRegistration = () => navigator.serviceWorker.ready;
|
||||
|
||||
const getPushSubscription = (registration) =>
|
||||
registration.pushManager.getSubscription()
|
||||
.then(subscription => ({ registration, subscription }));
|
||||
|
||||
const subscribe = (registration) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
|
||||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||
|
||||
const sendSubscriptionToBackend = (subscription) =>
|
||||
axios.post('/api/web/push_subscriptions', {
|
||||
subscription,
|
||||
}).then(response => response.data);
|
||||
|
||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||
|
||||
export function register () {
|
||||
store.dispatch(setBrowserSupport(supportsPushNotifications));
|
||||
|
||||
if (supportsPushNotifications) {
|
||||
if (!getApplicationServerKey()) {
|
||||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
||||
return;
|
||||
}
|
||||
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(({ registration, subscription }) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
|
||||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
|
||||
const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']);
|
||||
|
||||
// If the VAPID public key did not change and the endpoint corresponds
|
||||
// to the endpoint saved in the backend, the subscription is valid
|
||||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
||||
return subscription;
|
||||
} else {
|
||||
// Something went wrong, try to subscribe again
|
||||
return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend);
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(registration).then(sendSubscriptionToBackend);
|
||||
})
|
||||
.then(subscription => {
|
||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||
// it means that the backend subscription is valid (and was set during hydration)
|
||||
if (!(subscription instanceof PushSubscription)) {
|
||||
store.dispatch(setSubscription(subscription));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code === 20 && error.name === 'AbortError') {
|
||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
|
||||
}
|
||||
|
||||
// Clear alerts and hide UI settings
|
||||
store.dispatch(clearSubscription());
|
||||
|
||||
try {
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(unsubscribe);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Your browser does not support Web Push Notifications.');
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,11 @@ en:
|
||||
skins:
|
||||
vanilla:
|
||||
default: Default
|
||||
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(
|
||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
||||
<ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
);
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
navItems.push(
|
||||
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
||||
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
);
|
||||
|
||||
if (multiColumn) {
|
||||
navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||
navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
|
||||
|
||||
return (
|
||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
||||
<div className='getting-started__wrapper'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
||||
{navItems}
|
||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"account.media": "Mediji",
|
||||
"account.mention": "Pomeni korisnika @{name}",
|
||||
"account.moved_to": "{name} se pomerio na:",
|
||||
"account.mute": "Mutiraj @{name}",
|
||||
"account.mute": "Ućutkaj korisnika @{name}",
|
||||
"account.mute_notifications": "Isključi obaveštenja od korisnika @{name}",
|
||||
"account.posts": "Statusa",
|
||||
"account.report": "Prijavi @{name}",
|
||||
@@ -21,7 +21,7 @@
|
||||
"account.unblock": "Odblokiraj korisnika @{name}",
|
||||
"account.unblock_domain": "Odblokiraj domen {domain}",
|
||||
"account.unfollow": "Otprati",
|
||||
"account.unmute": "Odmutiraj @{name}",
|
||||
"account.unmute": "Ukloni ućutkavanje korisniku @{name}",
|
||||
"account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
|
||||
"account.view_full_profile": "Vidi ceo profil",
|
||||
"boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
|
||||
@@ -37,10 +37,10 @@
|
||||
"column.follow_requests": "Zahtevi za praćenje",
|
||||
"column.home": "Početna",
|
||||
"column.lists": "Liste",
|
||||
"column.mutes": "Mutirani korisnici",
|
||||
"column.mutes": "Ućutkani korisnici",
|
||||
"column.notifications": "Obaveštenja",
|
||||
"column.pins": "Prikačeni tutovi",
|
||||
"column.public": "Združena lajna",
|
||||
"column.public": "Federisana lajna",
|
||||
"column_back_button.label": "Nazad",
|
||||
"column_header.hide_settings": "Sakrij postavke",
|
||||
"column_header.moveLeft_settings": "Pomeri kolonu ulevo",
|
||||
@@ -50,6 +50,7 @@
|
||||
"column_header.unpin": "Otkači",
|
||||
"column_subheading.navigation": "Navigacija",
|
||||
"column_subheading.settings": "Postavke",
|
||||
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.",
|
||||
"compose_form.lock_disclaimer.lock": "zaključan",
|
||||
"compose_form.placeholder": "Šta Vam je na umu?",
|
||||
@@ -66,9 +67,9 @@
|
||||
"confirmations.delete_list.confirm": "Obriši",
|
||||
"confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?",
|
||||
"confirmations.domain_block.confirm": "Sakrij ceo domen",
|
||||
"confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili mutiranja su dovoljna i preporučljiva.",
|
||||
"confirmations.mute.confirm": "Mutiraj",
|
||||
"confirmations.mute.message": "Da li stvarno želite da mutirate korisnika {name}?",
|
||||
"confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili ućutkavanja su dovoljna i preporučljiva.",
|
||||
"confirmations.mute.confirm": "Ućutkaj",
|
||||
"confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?",
|
||||
"confirmations.unfollow.confirm": "Otprati",
|
||||
"confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
|
||||
"embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.",
|
||||
@@ -148,10 +149,10 @@
|
||||
"navigation_bar.keyboard_shortcuts": "Prečice na tastaturi",
|
||||
"navigation_bar.lists": "Liste",
|
||||
"navigation_bar.logout": "Odjava",
|
||||
"navigation_bar.mutes": "Mutirani korisnici",
|
||||
"navigation_bar.mutes": "Ućutkani korisnici",
|
||||
"navigation_bar.pins": "Prikačeni tutovi",
|
||||
"navigation_bar.preferences": "Podešavanja",
|
||||
"navigation_bar.public_timeline": "Združena lajna",
|
||||
"navigation_bar.public_timeline": "Federisana lajna",
|
||||
"notification.favourite": "{name} je stavio Vaš status kao omiljeni",
|
||||
"notification.follow": "{name} Vas je zapratio",
|
||||
"notification.mention": "{name} Vas je pomenuo",
|
||||
@@ -169,7 +170,7 @@
|
||||
"notifications.column_settings.sound": "Puštaj zvuk",
|
||||
"onboarding.done": "Gotovo",
|
||||
"onboarding.next": "Sledeće",
|
||||
"onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Združena lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
|
||||
"onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
|
||||
"onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.",
|
||||
"onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.",
|
||||
"onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.",
|
||||
@@ -213,6 +214,7 @@
|
||||
"search_popout.tips.user": "korisnik",
|
||||
"search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
|
||||
"standalone.public_title": "Pogled iznutra...",
|
||||
"status.block": "Block @{name}",
|
||||
"status.cannot_reblog": "Ovaj status ne može da se podrži",
|
||||
"status.delete": "Obriši",
|
||||
"status.embed": "Ugradi na sajt",
|
||||
@@ -221,7 +223,8 @@
|
||||
"status.media_hidden": "Multimedija sakrivena",
|
||||
"status.mention": "Pomeni korisnika @{name}",
|
||||
"status.more": "Još",
|
||||
"status.mute_conversation": "Mutiraj prepisku",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Ućutkaj prepisku",
|
||||
"status.open": "Proširi ovaj status",
|
||||
"status.pin": "Prikači na profil",
|
||||
"status.reblog": "Podrži",
|
||||
@@ -237,7 +240,7 @@
|
||||
"status.unmute_conversation": "Uključi prepisku",
|
||||
"status.unpin": "Otkači sa profila",
|
||||
"tabs_bar.compose": "Napiši",
|
||||
"tabs_bar.federated_timeline": "Združeno",
|
||||
"tabs_bar.federated_timeline": "Federisano",
|
||||
"tabs_bar.home": "Početna",
|
||||
"tabs_bar.local_timeline": "Lokalno",
|
||||
"tabs_bar.notifications": "Obaveštenja",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"account.media": "Медији",
|
||||
"account.mention": "Помени корисника @{name}",
|
||||
"account.moved_to": "{name} се померио на:",
|
||||
"account.mute": "Мутирај @{name}",
|
||||
"account.mute": "Ућуткај корисника @{name}",
|
||||
"account.mute_notifications": "Искључи обавештења од корисника @{name}",
|
||||
"account.posts": "Статуса",
|
||||
"account.report": "Пријави @{name}",
|
||||
@@ -21,7 +21,7 @@
|
||||
"account.unblock": "Одблокирај корисника @{name}",
|
||||
"account.unblock_domain": "Одблокирај домен {domain}",
|
||||
"account.unfollow": "Отпрати",
|
||||
"account.unmute": "Одмутирај @{name}",
|
||||
"account.unmute": "Уклони ућуткавање кориснику @{name}",
|
||||
"account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
|
||||
"account.view_full_profile": "Види цео профил",
|
||||
"boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
|
||||
@@ -37,10 +37,10 @@
|
||||
"column.follow_requests": "Захтеви за праћење",
|
||||
"column.home": "Почетна",
|
||||
"column.lists": "Листе",
|
||||
"column.mutes": "Мутирани корисници",
|
||||
"column.mutes": "Ућуткани корисници",
|
||||
"column.notifications": "Обавештења",
|
||||
"column.pins": "Прикачени тутови",
|
||||
"column.public": "Здружена лајна",
|
||||
"column.public": "Федерисана лајна",
|
||||
"column_back_button.label": "Назад",
|
||||
"column_header.hide_settings": "Сакриј поставке",
|
||||
"column_header.moveLeft_settings": "Помери колону улево",
|
||||
@@ -67,9 +67,9 @@
|
||||
"confirmations.delete_list.confirm": "Обриши",
|
||||
"confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
|
||||
"confirmations.domain_block.confirm": "Сакриј цео домен",
|
||||
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или мутирања су довољна и препоручљива.",
|
||||
"confirmations.mute.confirm": "Мутирај",
|
||||
"confirmations.mute.message": "Да ли стварно желите да мутирате корисника {name}?",
|
||||
"confirmations.domain_block.message": "Да ли сте стварно, стварно сигурно да желите да блокирате цео домен {domain}? У већини случајева, пар добрих блокирања или ућуткавања су довољна и препоручљива.",
|
||||
"confirmations.mute.confirm": "Ућуткај",
|
||||
"confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
|
||||
"confirmations.unfollow.confirm": "Отпрати",
|
||||
"confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
|
||||
"embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
|
||||
@@ -149,10 +149,10 @@
|
||||
"navigation_bar.keyboard_shortcuts": "Пречице на тастатури",
|
||||
"navigation_bar.lists": "Листе",
|
||||
"navigation_bar.logout": "Одјава",
|
||||
"navigation_bar.mutes": "Мутирани корисници",
|
||||
"navigation_bar.mutes": "Ућуткани корисници",
|
||||
"navigation_bar.pins": "Прикачени тутови",
|
||||
"navigation_bar.preferences": "Подешавања",
|
||||
"navigation_bar.public_timeline": "Здружена лајна",
|
||||
"navigation_bar.public_timeline": "Федерисана лајна",
|
||||
"notification.favourite": "{name} је ставио Ваш статус као омиљени",
|
||||
"notification.follow": "{name} Вас је запратио",
|
||||
"notification.mention": "{name} Вас је поменуо",
|
||||
@@ -170,7 +170,7 @@
|
||||
"notifications.column_settings.sound": "Пуштај звук",
|
||||
"onboarding.done": "Готово",
|
||||
"onboarding.next": "Следеће",
|
||||
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Здружена лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
||||
"onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
|
||||
"onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.",
|
||||
"onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.",
|
||||
"onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.",
|
||||
@@ -224,7 +224,7 @@
|
||||
"status.mention": "Помени корисника @{name}",
|
||||
"status.more": "Још",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Мутирај преписку",
|
||||
"status.mute_conversation": "Ућуткај преписку",
|
||||
"status.open": "Прошири овај статус",
|
||||
"status.pin": "Прикачи на профил",
|
||||
"status.reblog": "Подржи",
|
||||
@@ -240,7 +240,7 @@
|
||||
"status.unmute_conversation": "Укључи преписку",
|
||||
"status.unpin": "Откачи са профила",
|
||||
"tabs_bar.compose": "Напиши",
|
||||
"tabs_bar.federated_timeline": "Здружено",
|
||||
"tabs_bar.federated_timeline": "Федерисано",
|
||||
"tabs_bar.home": "Почетна",
|
||||
"tabs_bar.local_timeline": "Локално",
|
||||
"tabs_bar.notifications": "Обавештења",
|
||||
|
||||
2
app/javascript/mastodon/locales/whitelist_sr-Latn.json
Normal file
2
app/javascript/mastodon/locales/whitelist_sr-Latn.json
Normal file
@@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
||||
@@ -64,8 +64,8 @@
|
||||
"confirmations.block.message": "你確定要封鎖 {name} ?",
|
||||
"confirmations.delete.confirm": "刪除",
|
||||
"confirmations.delete.message": "你確定要刪除這個狀態?",
|
||||
"confirmations.delete_list.confirm": "Delete",
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"confirmations.delete_list.confirm": "刪除",
|
||||
"confirmations.delete_list.message": "確定要永久性地刪除這個名單嗎?",
|
||||
"confirmations.domain_block.confirm": "隱藏整個網域",
|
||||
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
||||
"confirmations.mute.confirm": "消音",
|
||||
@@ -128,14 +128,14 @@
|
||||
"lightbox.close": "關閉",
|
||||
"lightbox.next": "繼續",
|
||||
"lightbox.previous": "回退",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.search": "Search among people you follow",
|
||||
"lists.subheading": "Your lists",
|
||||
"lists.account.add": "加到名單裡",
|
||||
"lists.account.remove": "從名單中移除",
|
||||
"lists.delete": "刪除名單",
|
||||
"lists.edit": "修改名單",
|
||||
"lists.new.create": "新增名單",
|
||||
"lists.new.title_placeholder": "名單名稱",
|
||||
"lists.search": "搜尋您關注的使用者",
|
||||
"lists.subheading": "您的名單",
|
||||
"loading_indicator.label": "讀取中...",
|
||||
"media_gallery.toggle_visible": "切換可見性",
|
||||
"missing_indicator.label": "找不到",
|
||||
@@ -146,8 +146,8 @@
|
||||
"navigation_bar.favourites": "最愛",
|
||||
"navigation_bar.follow_requests": "關注請求",
|
||||
"navigation_bar.info": "關於本站",
|
||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.keyboard_shortcuts": "快速鍵",
|
||||
"navigation_bar.lists": "名單",
|
||||
"navigation_bar.logout": "登出",
|
||||
"navigation_bar.mutes": "消音的使用者",
|
||||
"navigation_bar.pins": "置頂貼文",
|
||||
|
||||
@@ -398,10 +398,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
@@ -415,7 +417,7 @@
|
||||
color: $ui-primary-color;
|
||||
font-family: 'mastodon-font-monospace', monospace;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,18 +126,18 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def confirm
|
||||
return if confirmed?
|
||||
new_user = !confirmed?
|
||||
|
||||
super
|
||||
update_statistics!
|
||||
update_statistics! if new_user
|
||||
end
|
||||
|
||||
def confirm!
|
||||
return if confirmed?
|
||||
new_user = !confirmed?
|
||||
|
||||
skip_confirmation!
|
||||
save!
|
||||
update_statistics!
|
||||
update_statistics! if new_user
|
||||
end
|
||||
|
||||
def promote!
|
||||
|
||||
22
app/serializers/activitypub/delete_actor_serializer.rb
Normal file
22
app/serializers/activitypub/delete_actor_serializer.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
|
||||
attributes :id, :type, :actor
|
||||
attribute :virtual_object, key: :object
|
||||
|
||||
def id
|
||||
[ActivityPub::TagManager.instance.uri_for(object), '#delete'].join
|
||||
end
|
||||
|
||||
def type
|
||||
'Delete'
|
||||
end
|
||||
|
||||
def actor
|
||||
ActivityPub::TagManager.instance.uri_for(object)
|
||||
end
|
||||
|
||||
def virtual_object
|
||||
actor
|
||||
end
|
||||
end
|
||||
@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def thumbnail
|
||||
full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail
|
||||
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
|
||||
end
|
||||
|
||||
def max_toot_chars
|
||||
|
||||
@@ -17,9 +17,7 @@ class BatchedRemoveStatusService < BaseService
|
||||
|
||||
@stream_entry_batches = []
|
||||
@salmon_batches = []
|
||||
@activity_json_batches = []
|
||||
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
|
||||
@activity_json = {}
|
||||
@activity_xml = {}
|
||||
|
||||
# Ensure that rendered XML reflects destroyed state
|
||||
@@ -32,10 +30,7 @@ class BatchedRemoveStatusService < BaseService
|
||||
unpush_from_home_timelines(account, account_statuses)
|
||||
unpush_from_list_timelines(account, account_statuses)
|
||||
|
||||
if account.local?
|
||||
batch_stream_entries(account, account_statuses)
|
||||
batch_activity_json(account, account_statuses)
|
||||
end
|
||||
batch_stream_entries(account, account_statuses) if account.local?
|
||||
end
|
||||
|
||||
# Cannot be batched
|
||||
@@ -47,7 +42,6 @@ class BatchedRemoveStatusService < BaseService
|
||||
|
||||
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
||||
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
|
||||
ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
|
||||
end
|
||||
|
||||
private
|
||||
@@ -58,22 +52,6 @@ class BatchedRemoveStatusService < BaseService
|
||||
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)
|
||||
recipients = account.followers.local.to_a
|
||||
|
||||
@@ -134,23 +112,9 @@ class BatchedRemoveStatusService < BaseService
|
||||
Redis.current
|
||||
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)
|
||||
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
||||
|
||||
@activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
|
||||
end
|
||||
|
||||
def sign_json(status, json)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,11 +46,13 @@ class FetchAtomService < BaseService
|
||||
json = body_to_json(@response.to_s)
|
||||
if supported_context?(json) && json['type'] == 'Person' && json['inbox'].present?
|
||||
[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
|
||||
@unsupported_activity = true
|
||||
nil
|
||||
end
|
||||
elsif @response['Link'] && !terminal
|
||||
elsif @response['Link'] && !terminal && link_header.find_link(%w(rel alternate))
|
||||
process_headers
|
||||
elsif @response.mime_type == 'text/html' && !terminal
|
||||
process_html
|
||||
@@ -70,8 +72,6 @@ class FetchAtomService < BaseService
|
||||
end
|
||||
|
||||
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"'])
|
||||
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
|
||||
|
||||
@@ -80,4 +80,8 @@ class FetchAtomService < BaseService
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def link_header
|
||||
@link_header ||= LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,8 @@ class SuspendAccountService < BaseService
|
||||
end
|
||||
|
||||
def purge_content!
|
||||
ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) if @account.local?
|
||||
|
||||
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
||||
BatchedRemoveStatusService.new.call(statuses)
|
||||
end
|
||||
@@ -54,4 +56,14 @@ class SuspendAccountService < BaseService
|
||||
def destroy_all(association)
|
||||
association.in_batches.destroy_all
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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,
|
||||
|
||||
|
||||
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|
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# 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}"
|
||||
title: Dziennik działań administracyjnych
|
||||
custom_emojis:
|
||||
by_domain: Według domeny
|
||||
copied_msg: Pomyślnie utworzono lokalną kopię emoji
|
||||
copy: Kopiuj
|
||||
copy_failed_msg: Nie udało się utworzyć lokalnej kopii emoji
|
||||
@@ -603,8 +604,10 @@ pl:
|
||||
development: Tworzenie aplikacji
|
||||
edit_profile: Edytuj profil
|
||||
export: Eksportowanie danych
|
||||
flavours: Motywy
|
||||
followers: Autoryzowani śledzący
|
||||
import: Importowanie danych
|
||||
keyword_mutes: Wyciszone słowa
|
||||
migrate: Migracja konta
|
||||
notifications: Powiadomienia
|
||||
preferences: Preferencje
|
||||
@@ -620,6 +623,7 @@ pl:
|
||||
private: Nie możesz przypiąć niepublicznego wpisu
|
||||
reblog: Nie możesz przypiąć podbicia wpisu
|
||||
show_more: Pokaż więcej
|
||||
title: '%{name}: "%{quote}"'
|
||||
visibilities:
|
||||
private: Tylko dla śledzących
|
||||
private_long: Widoczne tylko dla osób, które Cię śledzą
|
||||
|
||||
@@ -21,7 +21,7 @@ zh-TW:
|
||||
data: 資料
|
||||
display_name: 顯示名稱
|
||||
email: 電子信箱
|
||||
filtered_languages: 封鎖下面语言的文章
|
||||
filtered_languages: 封鎖下面語言的文章
|
||||
header: 個人頁面頂部
|
||||
locale: 語言
|
||||
locked: 將帳號轉為「私密」
|
||||
@@ -29,7 +29,16 @@ zh-TW:
|
||||
note: 簡介
|
||||
otp_attempt: 雙因子驗證碼
|
||||
password: 密碼
|
||||
setting_auto_play_gif: 自動播放 GIFs
|
||||
setting_boost_modal: 轉推前跳出確認視窗
|
||||
setting_default_privacy: 文章預設隱私度
|
||||
setting_default_sensitive: 預設我的內容為敏感內容
|
||||
setting_delete_modal: 刪推前跳出確認視窗
|
||||
setting_noindex: 不被搜尋引擎檢索
|
||||
setting_reduce_motion: 減低動畫效果
|
||||
setting_system_font_ui: 使用系統預設字體
|
||||
setting_theme: 網站主題
|
||||
setting_unfollow_modal: 取消關注前跳出確認視窗
|
||||
type: 匯入資料類型
|
||||
username: 使用者名稱
|
||||
interactions:
|
||||
|
||||
@@ -409,8 +409,8 @@ sr-Latn:
|
||||
exports:
|
||||
blocks: Blokirali ste
|
||||
csv: CSV
|
||||
follows: PRatite
|
||||
mutes: Mutirali ste
|
||||
follows: Pratite
|
||||
mutes: Ućutkali ste
|
||||
storage: Multimedijalno skladište
|
||||
followers:
|
||||
domain: Domen
|
||||
@@ -441,7 +441,7 @@ sr-Latn:
|
||||
types:
|
||||
blocking: Lista blokiranja
|
||||
following: Lista pratilaca
|
||||
muting: Lista mutiranih
|
||||
muting: Lista ućutkanih
|
||||
upload: Otpremi
|
||||
in_memoriam_html: In Memoriam.
|
||||
invites:
|
||||
|
||||
@@ -409,8 +409,8 @@ sr:
|
||||
exports:
|
||||
blocks: Блокирали сте
|
||||
csv: CSV
|
||||
follows: ПРатите
|
||||
mutes: Мутирали сте
|
||||
follows: Пратите
|
||||
mutes: Ућуткали сте
|
||||
storage: Мултимедијално складиште
|
||||
followers:
|
||||
domain: Домен
|
||||
@@ -441,7 +441,7 @@ sr:
|
||||
types:
|
||||
blocking: Листа блокирања
|
||||
following: Листа пратилаца
|
||||
muting: Листа мутираних
|
||||
muting: Листа ућутканих
|
||||
upload: Отпреми
|
||||
in_memoriam_html: In Memoriam.
|
||||
invites:
|
||||
|
||||
@@ -55,7 +55,7 @@ zh-TW:
|
||||
perform_full_suspension: 進行停權
|
||||
profile_url: 個人檔案網址
|
||||
public: 公開
|
||||
push_subscription_expires: PuSH 訂閱逾期
|
||||
push_subscription_expires: 推播訂閱過期
|
||||
salmon_url: Salmon URL
|
||||
silence: 靜音
|
||||
statuses: 狀態
|
||||
@@ -133,12 +133,14 @@ zh-TW:
|
||||
forgot_password: 忘記密碼?
|
||||
login: 登入
|
||||
logout: 登出
|
||||
migrate_account: 轉移到另一個帳號
|
||||
migrate_account_html: 想要將這個帳號指向另一個帳號可到<a href="%{path}">到這裡設定</a>。
|
||||
register: 註冊
|
||||
resend_confirmation: 重寄驗證信
|
||||
reset_password: 重設密碼
|
||||
set_new_password: 設定新密碼
|
||||
authorize_follow:
|
||||
error: 對不起,尋找這個跨站使用者的過程發生錯誤
|
||||
error: 對不起,搜尋遠端使用者出現錯誤
|
||||
follow: 關注
|
||||
title: 關注 %{acct}
|
||||
datetime:
|
||||
@@ -165,7 +167,16 @@ zh-TW:
|
||||
blocks: 您封鎖的使用者
|
||||
csv: CSV
|
||||
follows: 您關注的使用者
|
||||
mutes: 您靜音的使用者
|
||||
storage: 儲存空間大小
|
||||
followers:
|
||||
domain: 網域
|
||||
explanation_html: 為確保個人隱私,您必須知道有哪些使用者正關注你。<strong>您的私密內容會被發送到所有您有被關注的服務站上</strong>。如果您不信任這些服務站的管理者,您可以選擇檢查或刪除您的關注者。
|
||||
followers_count: 關注者數
|
||||
lock_link: 鎖住你的帳號
|
||||
purge: 移除關注者
|
||||
unlocked_warning_html: 所有人都可以關注並檢索你的隱藏狀態。%{lock_link}以檢查或拒絕關注。
|
||||
unlocked_warning_title: 你的帳號是公開的
|
||||
generic:
|
||||
changes_saved_msg: 已成功儲存修改
|
||||
powered_by: 網站由 %{link} 開發
|
||||
@@ -179,6 +190,7 @@ zh-TW:
|
||||
types:
|
||||
blocking: 您封鎖的使用者名單
|
||||
following: 您關注的使用者名單
|
||||
muting: 您靜音的使用者名單
|
||||
upload: 上傳
|
||||
landing_strip_html: "<strong>%{name}</strong> 是一個在 %{link_to_root_path} 的使用者。只要您有任何 Mastodon 服務站、或者聯盟網站的帳號,便可以跨站關注此站使用者,或者與他們互動。"
|
||||
landing_strip_signup_html: 如果您沒有這些帳號,歡迎在<a href="%{sign_up_path}">這裡註冊</a>。
|
||||
@@ -231,15 +243,26 @@ zh-TW:
|
||||
missing_resource: 無法找到資源
|
||||
proceed: 下一步
|
||||
prompt: 您希望關注︰
|
||||
sessions:
|
||||
activity: 最近活動
|
||||
browser: 瀏覽器
|
||||
current_session: 目前的 session
|
||||
description: "%{platform} 上的 %{browser}"
|
||||
explanation: 這些是現在正登入於你的 Mastodon 帳號的瀏覽器。
|
||||
revoke: 取消
|
||||
revoke_success: Session 取消成功。
|
||||
settings:
|
||||
authorized_apps: 已授權應用程式
|
||||
back: 回到 Mastodon
|
||||
development: 開發
|
||||
edit_profile: 修改個人資料
|
||||
export: 匯出
|
||||
followers: 授權追蹤者
|
||||
import: 匯入
|
||||
notifications: 通知
|
||||
preferences: 偏好設定
|
||||
settings: 設定
|
||||
two_factor_authentication: 雙因子認證
|
||||
two_factor_authentication: 兩階段認證
|
||||
statuses:
|
||||
open_in_web: 以網頁開啟
|
||||
over_character_limit: 超過了 %{max} 字的限制
|
||||
@@ -257,14 +280,14 @@ zh-TW:
|
||||
default: "%Y年%-m月%d日 %H:%M"
|
||||
two_factor_authentication:
|
||||
code_hint: 請輸入您認證器產生的代碼,以進行認證
|
||||
description_html: 當您啟用<strong>雙因子認證</strong>後,您登入時將需要使您手機、或其他種類認證器產生的代碼。
|
||||
description_html: 啟用<strong>兩階段認證</strong>後,登入時將需要使手機、或其他種類認證器產生的代碼。
|
||||
disable: 停用
|
||||
enable: 啟用
|
||||
enabled_success: 已成功啟用雙因子認證
|
||||
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy),掃描這裡的 QR 圖形碼</strong>。在雙因子認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
|
||||
enabled_success: 已成功啟用兩階段認證
|
||||
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy),掃描這裡的 QR 圖形碼</strong>。在兩階段認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
|
||||
manual_instructions: 如果您無法掃描 QR 圖形碼,請手動輸入︰
|
||||
setup: 設定
|
||||
wrong_code: 您輸入的認證碼並不正確!可能伺服器時間和您手機不一致,請檢查您手機的時間,或與本站管理員聯絡。
|
||||
users:
|
||||
invalid_email: 信箱地址格式不正確
|
||||
invalid_otp_token: 雙因子認證碼不正確
|
||||
invalid_otp_token: 兩階段認證碼不正確
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
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 translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class AddIndexOnStreamEntries < ActiveRecord::Migration[5.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
commit_db_transaction
|
||||
add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently
|
||||
remove_index :stream_entries, name: :index_stream_entries_on_account_id
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class MoreFasterIndexOnNotifications < ActiveRecord::Migration[5.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
commit_db_transaction
|
||||
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
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
0
|
||||
2
|
||||
end
|
||||
|
||||
def pre
|
||||
|
||||
@@ -12,20 +12,40 @@ describe Auth::ConfirmationsController, type: :controller do
|
||||
end
|
||||
|
||||
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
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :show, params: { confirmation_token: 'foobar' }
|
||||
before do
|
||||
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 'queues up bootstrapping of home timeline' do
|
||||
expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to login' do
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
context 'when user is updating email' do
|
||||
let!(:user) { Fabricate(:user, confirmation_token: 'foobar', unconfirmed_email: 'new-email@example.com') }
|
||||
|
||||
it 'queues up bootstrapping of home timeline' do
|
||||
expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id)
|
||||
before do
|
||||
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
|
||||
|
||||
@@ -148,6 +148,14 @@ RSpec.describe User, type: :model do
|
||||
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
|
||||
it 'saves false for otp_required_for_login' do
|
||||
user = Fabricate.build(:user, otp_required_for_login: true)
|
||||
|
||||
@@ -46,7 +46,7 @@ RSpec.configure do |config|
|
||||
config.include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
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')}"
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user