Compare commits

...

73 Commits

Author SHA1 Message Date
kibigo!
35be02f21d Renamed glitch async chunks 2018-01-09 16:26:14 -08:00
beatrix
90e568413b Merge pull request #308 from KnzkDev/fix/list-editor
Fix list editor design
2018-01-08 13:08:11 -05:00
ncls7615
ef0b7d1e76 fix list editor scss 2018-01-09 02:50:24 +09:00
David Yip
65986b6f0b Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-08 09:48:42 -06:00
David Yip
2dc4fbbd1a When pulling out max_toot_chars, handle nulls
flavours/glitch/util/initial_state is used in places where we want to
exhibit different behavior based on user preferences.  This means that
it's used in places where no preference is defined, i.e. on an
unauthenticated access.  All values exported from that module must
therefore expect that case; previously, the max chars value didn't.

Addresses #306.
2018-01-08 09:45:59 -06:00
Jenkins
f839ac694c Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-08 10:17:15 +00:00
Eugen Rochko
dbda87c31f Revert #5772 (#6221) 2018-01-08 10:57:52 +01:00
Jenkins
722b3f567f Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-08 04:17:11 +00:00
Eugen Rochko
e4a241abef Fix bad URL schemes being accepted (#6219)
* Fix actors accepting invalid URI schemes or different host between URI and URL

* Fix statuses accepting invalid URI scheme or different host to actor

* Adjust tests to new requirements

* Improve readability of mismatching_origin?/invalid_origin? methods
2018-01-08 05:00:23 +01:00
Eugen Rochko
93555182c3 Do not display elephant friend in single-column layout (#6222) 2018-01-08 03:50:53 +01:00
puckipedia
0eff42d688 Move Article from supported to converted types (#6218) 2018-01-08 00:21:14 +01:00
David Yip
f7c4d4464b Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-07 13:30:52 -06:00
David Yip
70c99a9f34 Use error pack when rendering error pages. Fixes #305. 2018-01-07 13:30:17 -06:00
Jenkins
c2e1bfd9ae Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-07 15:17:13 +00:00
Yamagishi Kazutoshi
1d92b90be9 Fix force_ssl conditional (#6201) 2018-01-07 15:19:23 +01:00
Yamagishi Kazutoshi
da809f9eec Fix unintended cache (#6214) 2018-01-07 15:12:59 +01:00
SerCom_KC
c4d36d024c Update Simplified Chinese translations (#6215)
* i18n: (zh-CN) Add translations of #6125

* i18n: (zh-CN) Add translations of #6132

* i18n: (zh-CN) Add translations of #6099

* i18n: (zh-CN) Add translations of #6071

* i18n: (zh-CN) Improve translations
2018-01-07 17:32:50 +09:00
David Yip
5083311d64 Merge remote-tracking branch 'ykzts/fix-unintended-cache' into gs-master 2018-01-07 00:32:24 -06:00
Yamagishi Kazutoshi
2af307bce4 Fix unintended cache 2018-01-07 14:59:12 +09:00
Jenkins
bcbdd4f88d Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-07 02:17:10 +00:00
Jeong Arm
9e97fbf0af Translate Korean (#6212) 2018-01-07 11:13:42 +09:00
kibigo!
b5874c1428 Fixes to search dropdown 2018-01-06 15:34:01 -08:00
beatrix
61ef8d643e fix typo in vanilla names.yml 2018-01-06 16:49:53 -05:00
Ondřej Hruška
9f29fd31ba fixed ctrl enter 2018-01-06 19:58:04 +01:00
Ondřej Hruška
53caab0c0b Fix the always-threaded bug 2018-01-06 19:55:53 +01:00
beatrix-bitrot
b75a1ce326 tighten csp 2018-01-06 18:49:03 +00:00
beatrix
d442cfa65c Merge pull request #303 from KnzkDev/ja-for-thread-mode
Update ja.js for #296
2018-01-06 12:06:17 -05:00
ncls7615
f5a4201ad8 Update ja.js 2018-01-07 01:51:49 +09:00
beatrix
a251c42192 Merge pull request #296 from glitch-soc/thread-mode
Threaded mode~
2018-01-06 11:28:36 -05:00
beatrix
2ec9a75a1d Merge pull request #302 from KnzkDev/fix/search-popout
Fix search popout
2018-01-06 11:25:59 -05:00
beatrix
fa92e88fb2 appease eslint 2018-01-06 10:30:49 -05:00
ncls7615
da98c33161 Fix search popout 2018-01-06 21:50:11 +09:00
David Yip
2eed4ace11 Read max_toot_chars from root object. Fixes #297.
max_toot_chars is present in the root of the initial state object.
(Previously, we were trying to read it from the meta child object.)
2018-01-06 03:01:11 -06:00
kibigo!
c71d848855 my global .gitignore excluded this file ;_; 2018-01-05 21:40:02 -08:00
kibigo!
e4bc013d6f Threaded mode~ 2018-01-05 21:16:43 -08:00
kibigo!
6932b464e6 Fixed improper dropdown func binding for #293 + toot button spacing 2018-01-05 21:02:53 -08:00
kibigo!
ad10a80a99 Styling and autosuggest fixes for #293 2018-01-05 20:43:16 -08:00
kibigo!
8bf9d9362a Fixes composer mounting issue with #293 2018-01-05 18:30:06 -08:00
David Yip
03aeab857f Merge remote-tracking branch 'personal/merge/tootsuite/master' into gs-master 2018-01-05 17:31:56 -06:00
beatrix
f441770e50 Merge pull request #290 from chriswmartin/web-push-updates
Web push updates
2018-01-05 18:29:57 -05:00
beatrix
b4e667f86b Merge pull request #295 from chriswmartin/getting-started-key-fix
unique ColumnLink keys in getting_started
2018-01-05 18:29:40 -05:00
beatrix
faf20eeaa4 Merge pull request #293 from glitch-soc/compose-refactor
Compose refactor
2018-01-05 18:29:08 -05:00
Jenkins
f6adb409fd Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-05 22:17:12 +00:00
ThibG
10f6793fd0 Fix PuSH workers (#6200) 2018-01-05 23:04:35 +01:00
ThibG
a594139115 When fetching an ActivityPub-enabled status, do not re-request it as text/html (#6196) 2018-01-05 22:42:50 +01:00
TheKinrar
95bd85d9e8 Represent numbers by strings in instance activity API (#6198)
Fixes #6197.
2018-01-05 22:38:33 +01:00
Naoki Kosaka
8d51ce4290 Fix enforce HTTPS in production. (#6180) 2018-01-05 20:04:22 +01:00
beatrix
f41b33eb01 Merge pull request #243 from m4sk1n/glitch-pl
i18n: 🇵🇱
2018-01-05 12:36:53 -05:00
cwm
9fc08e4861 add key to lists div 2018-01-05 09:00:48 -06:00
cwm
6236577734 change how list ColumnLink keys are determined 2018-01-05 08:12:34 -06:00
Quenty31
06636c6eca l10n Occitan language: mailer update (#6193)
* Create email_changed.oc.html.erb

* Create email_changed.oc.text.erb

* Update email_changed.oc.html.erb

* Update email_changed.oc.html.erb

* Create reconfirmation_instructions.oc.html.erb

* Create reconfirmation_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update reconfirmation_instructions.oc.html.erb

* Update reconfirmation_instructions.oc.text.erb

* Update reconfirmation_instructions.oc.html.erb
2018-01-05 18:59:43 +09:00
Eugen Rochko
e9822a4e4e Bump version to 2.1.2 2018-01-05 04:52:06 +01:00
Yamagishi Kazutoshi
9a61b0ef22 Fix RFC 5646 Regular Expression (#6190) 2018-01-05 04:43:50 +01:00
Jenkins
c69a23ae46 Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master 2018-01-04 23:17:11 +00:00
Branko Kokanovic
d872902997 Small translation fixes for Serbian (and sr@Latn too) (#6188) 2018-01-05 00:16:06 +01:00
Patrick Figel
5ec25ff3e1 Fix email confirmation link not updating email (#6187)
A change introduced in #6125 prevents
`Devise::Models::Confirmable#confirm` from being called for existing
users, which in turn leads to `email` not being set to
`unconfirmed_email`, breaking email updates. This also adds a test
that would've caught this issue.
2018-01-05 00:15:35 +01:00
Lynx Kotoura
49e296e1b0 Fix overflowing audit logs (#6184) 2018-01-04 19:38:46 +01:00
unarist
7347d4f8bb Use disable_ddl_transaction! to prevent warnings on migration (#6183)
Migration is wrapped by transaction, so manual `commit_db_transaction` without transaction restarting causes "there is no transaction in progress" warnings. We should use `disable_ddl_transaction!` instead, if we can omit transaction completely.
2018-01-04 19:38:29 +01:00
Eugen Rochko
7571c37c99 Bump version to 2.1.1 (#6164) 2018-01-04 16:40:26 +01:00
Yamagishi Kazutoshi
3c18964256 Fallback default thumbnail in instance status API (#6177) 2018-01-04 15:36:55 +01:00
Marcin Mikołajczak
c61dd918a2 i18n: Update Polish translation (#6176)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-04 23:15:29 +09:00
Marcin Mikołajczak
0f69a90588 i18n: Update Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-04 14:42:58 +01:00
Eugen Rochko
02ba03d6db Send one Delete of Actor in ActivityPub when account is suspended (#6172) 2018-01-04 14:40:49 +01:00
ThibG
3bee0996c5 Make sure private toots remain private and do not end up in HTTP caches (#6175) 2018-01-04 14:39:38 +01:00
muan
89daeb43a8 Improve Traditional Chinese translation (#6166)
* Improve Traditional Chinese translations

* Sort alphabetically
2018-01-04 05:00:50 +01:00
Eugen Rochko
7d4f4f9aab Fix FetchAtomService not finding alternatives if there's a Link header (#6170)
without them, such as is the case with GNU social

Fixes the ability to find GNU social accounts via URL in search and
when using remote follow function
2018-01-04 04:56:04 +01:00
Akihiko Odaki
256c2b1de0 Rearrange items in Getting Started navigation (#6126)
Though the subsections are representing features such as navigation and
settings, they are categorized by the ways how they are implemented
(internal navigation or external links.) They are irrelevant and some
arrangements were confusing because of that. (It is nonsense that instance
information is in settings subsection, for example.)

This fixes the issue by rearranging.
2018-01-04 10:56:54 +09:00
Eugen Rochko
02e3e1ec09 Fix nil error in log_target_from_history helper (#6173) 2018-01-04 10:56:23 +09:00
Eugen Rochko
ff924f95bb Fix OpenSSL dependency in ostatus2 (#6174) 2018-01-04 10:56:00 +09:00
Eugen Rochko
c10f4bdb03 Cache JSON of immutable ActivityPub representations (#6171) 2018-01-04 01:21:38 +01:00
cwm
72b99f6ee4 bug fix (tootsuite pr #6120) 2017-12-31 08:26:50 -06:00
cwm
4ce44ba470 remove unused 'saveSettings' from column_settings_container 2017-12-30 16:42:26 -06:00
cwm
0dce26b82b web push updates (tootsuite PRs #5879, #5941, #6047) 2017-12-30 11:45:01 -06:00
101 changed files with 1361 additions and 774 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FollowsController < Api::BaseController
include SignatureVerification
def show
render(
json: FollowRequest.includes(:account).references(:account).find_by!(
id: params.require(:id),
accounts: { domain: nil, username: params.require(:account_username) },
target_account: signed_request_account
),
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
)
end
end

View File

@@ -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

View File

@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
private
def https_enabled?
Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'
Rails.env.production?
end
def store_current_location
@@ -192,17 +192,31 @@ class ApplicationController < ActionController::Base
format.any { head code }
format.html do
set_locale
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code
end
end
end
def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
cache_public = options.key?(:public) ? options.delete(:public) : true
content_type = options.delete(:content_type) || 'application/json'
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json
end
expires_in options[:expires_in], public: true
render json: data
expires_in options[:expires_in], public: cache_public
render json: data, content_type: content_type
end
def set_cache_headers
response.headers['Vary'] = 'Accept'
end
def skip_session!
request.session_options[:skip] = true
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -39,6 +39,10 @@ module JsonLdHelper
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end
def unsupported_uri_scheme?(uri)
!uri.start_with?('http://', 'https://')
end
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
graph.dump(:normalize)

View File

@@ -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?

View File

@@ -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,
};
}

View File

@@ -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());
};
}

View File

@@ -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);
}
});
};
}

View File

@@ -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,
},
});
};
}

View File

@@ -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'>

View File

@@ -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,
};

View File

@@ -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
),
});

View File

@@ -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 : ''}`}>

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -85,6 +85,7 @@ export default function ComposerPublisher ({
unlisted: 'unlock-alt',
}[privacy]}
/>
{' '}
<FormattedMessage {...messages.publish} />
</span>
);

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -10,6 +10,7 @@ import Textarea from 'react-textarea-autosize';
// Components.
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
import ComposerTextareaIcons from './icons';
import ComposerTextareaSuggestions from './suggestions';
// Utils.
@@ -32,7 +33,7 @@ const handlers = {
// When blurring the textarea, suggestions are hidden.
handleBlur () {
//this.setState({ suggestionsHidden: true });
this.setState({ suggestionsHidden: true });
},
// When the contents of the textarea change, we have to pull up new
@@ -57,7 +58,7 @@ const handlers = {
const right = value.slice(selectionStart).search(/[\s\u200B]/);
const token = function () {
switch (true) {
case left < 0 || /[@:]/.test(!value[left]):
case left < 0 || !/[@:]/.test(value[left]):
return null;
case right < 0:
return value.slice(left);
@@ -127,6 +128,11 @@ const handlers = {
return;
}
// We submit the status on control/meta + enter.
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
onSubmit();
}
// Switches over the pressed key.
switch(e.key) {
@@ -156,11 +162,6 @@ const handlers = {
}
return;
}
// We submit the status on control/meta + enter.
if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
onSubmit();
}
},
// When the escape key is released, we either close the suggestions
@@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
handleRefTextarea,
} = this.handlers;
const {
advancedOptions,
autoFocus,
disabled,
intl,
@@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
<div className='composer--textarea'>
<label>
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
<ComposerTextareaIcons
advancedOptions={advancedOptions}
intl={intl}
/>
<Textarea
aria-autocomplete='list'
autoFocus={autoFocus}
@@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
// Props.
ComposerTextarea.propTypes = {
advancedOptions: ImmutablePropTypes.map,
autoFocus: PropTypes.bool,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,

View File

@@ -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'
>

View File

@@ -45,10 +45,10 @@ const handlers = {
const {
onClear,
submitted,
value: { length },
value,
} = this.props;
e.preventDefault(); // Prevents focus change ??
if (onClear && (submitted || length)) {
if (onClear && (submitted || value && value.length)) {
onClear();
}
},
@@ -100,7 +100,8 @@ export default class DrawerSearch extends React.PureComponent {
value,
} = this.props;
const { expanded } = this.state;
const computedClass = classNames('drawer--search', { active: value.length || submitted });
const active = value && value.length || submitted;
const computedClass = classNames('drawer--search', { active });
return (
<div className={computedClass}>
@@ -126,11 +127,11 @@ export default class DrawerSearch extends React.PureComponent {
tabIndex='0'
>
<Icon icon='search' />
<Icon icon='fa-times-circle' />
<Icon icon='times-circle' />
</div>
<Overlay
placement='bottom'
show={expanded && !(value || '').length && !submitted}
show={expanded && !active}
target={this}
><DrawerSearchPopout /></Overlay>
</div>

View File

@@ -42,56 +42,61 @@ export default function DrawerSearchPopout ({ style }) {
// The result.
return (
<Motion
defaultStyle={{
opacity: 0,
scaleX: 0.85,
scaleY: 0.75,
}}
<div
className='drawer--search--popout'
style={{
opacity: motionSpring,
scaleX: motionSpring,
scaleY: motionSpring,
...style,
position: 'absolute',
width: 285,
}}
>
{({ opacity, scaleX, scaleY }) => (
<div
className='drawer--search--popout'
style={{
...style,
position: 'absolute',
width: 285,
opacity: opacity,
transform: `scale(${scaleX}, ${scaleY})`,
}}
>
<h4><FormattedMessage {...messages.format} /></h4>
<ul>
<li>
<em>#example</em>
{' '}
<FormattedMessage {...messages.hashtag} />
</li>
<li>
<em>@username@domain</em>
{' '}
<FormattedMessage {...messages.user} />
</li>
<li>
<em>URL</em>
{' '}
<FormattedMessage {...messages.user} />
</li>
<li>
<em>URL</em>
{' '}
<FormattedMessage {...messages.status} />
</li>
</ul>
<FormattedMessage {...messages.text} />
</div>
)}
</Motion>
<Motion
defaultStyle={{
opacity: 0,
scaleX: 0.85,
scaleY: 0.75,
}}
style={{
opacity: motionSpring,
scaleX: motionSpring,
scaleY: motionSpring,
}}
>
{({ opacity, scaleX, scaleY }) => (
<div
style={{
opacity: opacity,
transform: `scale(${scaleX}, ${scaleY})`,
}}
>
<h4><FormattedMessage {...messages.format} /></h4>
<ul>
<li>
<em>#example</em>
{' '}
<FormattedMessage {...messages.hashtag} />
</li>
<li>
<em>@username@domain</em>
{' '}
<FormattedMessage {...messages.user} />
</li>
<li>
<em>URL</em>
{' '}
<FormattedMessage {...messages.user} />
</li>
<li>
<em>URL</em>
{' '}
<FormattedMessage {...messages.status} />
</li>
</ul>
<FormattedMessage {...messages.text} />
</div>
)}
</Motion>
</div>
);
}

View File

@@ -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>,
]);

View File

@@ -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,
};

View File

@@ -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),

View File

@@ -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);

View File

@@ -55,9 +55,13 @@ const messages = {
'compose.attach.doodle': '落書きをする',
'compose.attach': 'アタッチ...',
'advanced-options.local-only.short': 'ローカル限定',
'advanced-options.local-only.long': '他のインスタンスには投稿されません',
'advanced_options.local-only.short': 'ローカル限定',
'advanced_options.local-only.long': '他のインスタンスには投稿されません',
'advanced_options.local-only.tooltip': 'この投稿はローカル限定投稿です',
'advanced_options.icon_title': '高度な設定',
'advanced_options.threaded_mode.short': 'スレッドモード',
'advanced_options.threaded_mode.long': '投稿時に自動的に返信するように設定します',
'advanced_options.threaded_mode.tooltip': 'スレッドモードを有効にする',
};
export default Object.assign({}, inherited, messages);

View File

@@ -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);

View File

@@ -6,3 +6,10 @@ en:
skins:
glitch:
default: Default
pl:
flavours:
glitch:
description: Domyślny motyw instancji GlitchSoc.
skins:
glitch:
default: Domyślny

View File

@@ -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.getIn(['advanced_options', 'threaded_mode']) ? continueThread(state, action.status) : clearAll(state);
case COMPOSE_SUBMIT_FAIL:
case COMPOSE_UPLOAD_CHANGE_FAIL:
return state.set('is_submitting', false);

View File

@@ -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;

View File

@@ -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 {

View File

@@ -114,19 +114,27 @@
}
& > .icon {
display: block;
position: absolute;
top: 10px;
right: 10px;
width: 18px;
height: 18px;
color: $ui-secondary-color;
font-size: 18px;
line-height: 18px;
z-index: 2;
.fa {
display: inline-block;
position: absolute;
top: 10px;
right: 10px;
width: 18px;
height: 18px;
color: $ui-secondary-color;
font-size: 18px;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: 0;
cursor: default;
pointer-events: none;
z-index: 2;
transition: all 100ms linear;
}
@@ -136,14 +144,15 @@
}
.fa-times-circle {
top: 11px;
transform: rotate(-90deg);
cursor: pointer;
&:hover { color: $primary-text-color }
}
}
&.active {
&.active {
& > .icon {
.fa-search {
opacity: 0;
transform: rotate(90deg);
@@ -158,6 +167,32 @@
}
}
.drawer--search--popout {
box-sizing: border-box;
margin-top: 10px;
border-radius: 4px;
padding: 10px 14px 14px 14px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
color: $ui-primary-color;
background: $simple-background-color;
h4 {
margin-bottom: 10px;
color: $ui-primary-color;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
}
ul { margin-bottom: 10px }
li { padding: 4px 0 }
em {
color: $ui-base-color;
font-weight: 500;
}
}
.drawer--account {
padding: 10px;
color: $ui-primary-color;

View File

@@ -745,6 +745,8 @@
.account {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
color: inherit;
text-decoration: none;
.account__display-name {
flex: 1 1 auto;
@@ -762,27 +764,8 @@
& > .account__avatar-wrapper { margin: 0 8px 0 0 }
& > .display-name {
display: block;
padding: 0;
height: auto;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
& > strong {
display: inline;
font-size: inherit;
line-height: inherit;
}
& > span {
display: inline;
color: lighten($ui-base-color, 36%);
font-size: inherit;
line-height: inherit;
&::before { content: " " }
}
height: 24px;
line-height: 24px;
}
}
}
@@ -1243,6 +1226,30 @@
text-decoration: underline;
}
}
&.inline {
padding: 0;
height: 18px;
font-size: 15px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
strong {
display: inline;
height: auto;
font-size: inherit;
line-height: inherit;
}
span {
display: inline;
height: auto;
font-size: inherit;
line-height: inherit;
}
}
}
.status__relative-time,
@@ -1561,6 +1568,39 @@
}
}
.drawer__pager {
box-sizing: border-box;
padding: 0;
flex-grow: 1;
position: relative;
overflow: hidden;
display: flex;
}
.drawer__inner {
position: absolute;
top: 0;
left: 0;
background: lighten($ui-base-color, 13%) url('~images/wave-drawer.png') no-repeat bottom / 100% auto;
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
width: 100%;
height: 100%;
&.darker {
background: $ui-base-color;
}
> .mastodon {
background: url('~images/mastodon-ui.png') no-repeat left bottom / contain;
flex: 1;
}
}
.pseudo-drawer {
background: lighten($ui-base-color, 13%);
font-size: 13px;
@@ -2774,6 +2814,112 @@
filter: none;
}
.search {
position: relative;
}
.search__input {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
padding-right: 30px;
font-family: inherit;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {
.fa {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
font-size: 18px;
width: 18px;
height: 18px;
color: $ui-secondary-color;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.fa-search {
transform: rotate(90deg);
&.active {
pointer-events: none;
transform: rotate(0deg);
}
}
.fa-times-circle {
top: 11px;
transform: rotate(0deg);
cursor: pointer;
&.active {
transform: rotate(90deg);
}
&:hover {
color: $primary-text-color;
}
}
}
.search-results__header {
color: $ui-base-lighter-color;
background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 4%);
padding: 15px 10px;
font-size: 14px;
font-weight: 500;
}
.search-results__hashtag {
display: block;
padding: 10px;
color: $ui-secondary-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: lighten($ui-secondary-color, 4%);
text-decoration: underline;
}
}
.modal-root {
transition: opacity 0.3s linear;
will-change: opacity;
@@ -3911,37 +4057,6 @@
border-radius: 0;
}
.search-popout {
background: $simple-background-color;
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: $ui-primary-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
color: $ui-primary-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: $ui-base-color;
}
}
noscript {
text-align: center;

View File

@@ -27,15 +27,15 @@ export function HashtagTimeline () {
}
export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'flavours/glitch/features/list_timeline');
return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'flavours/glitch/features/list_timeline');
}
export function Lists () {
return import(/* webpackChunkName: "features/lists" */'flavours/glitch/features/lists');
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'flavours/glitch/features/lists');
}
export function ListEditor () {
return import(/* webpackChunkName: "features/list_editor" */'flavours/glitch/features/list_editor');
return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
}
export function DirectTimeline() {
@@ -51,7 +51,7 @@ export function GettingStarted () {
}
export function KeyboardShortcuts () {
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'flavours/glitch/features/keyboard_shortcuts');
}
export function PinnedStatuses () {

View File

@@ -18,6 +18,6 @@ export const boostModal = getMeta('boost_modal');
export const favouriteModal = getMeta('favourite_modal');
export const deleteModal = getMeta('delete_modal');
export const me = getMeta('me');
export const maxChars = getMeta('max_toot_chars') || 500;
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export default initialState;

View 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;
}

View File

@@ -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()');

View 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');

View File

@@ -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.');
}
}

View File

@@ -6,3 +6,11 @@ en:
skins:
vanilla:
default: Default
pl:
flavours:
vanilla:
description: Motyw używany przez instancje czystego Mastodona. Może nie obsługiwać wszystkich funkcji GlitchSoc.
name: Mastodon Vanilla
skins:
vanilla:
default: Domyślny

View File

@@ -94,7 +94,7 @@ export default class Compose extends React.PureComponent {
<div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer />
<div className='mastodon' />
{multiColumn && <div className='mastodon' />}
</div>
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>

View File

@@ -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>

View File

@@ -25,11 +25,11 @@
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
"account.view_full_profile": "전체 프로필 보기",
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
"bundle_column_error.retry": "다시 시도",
"bundle_column_error.title": "네트워크 에러",
"bundle_modal_error.close": "닫기",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.message": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
"bundle_modal_error.retry": "다시 시도",
"column.blocks": "차단 중인 사용자",
"column.community": "로컬 타임라인",
@@ -50,7 +50,7 @@
"column_header.unpin": "고정 해제",
"column_subheading.navigation": "내비게이션",
"column_subheading.settings": "설정",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
"compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
"compose_form.lock_disclaimer.lock": "비공개",
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
@@ -135,7 +135,7 @@
"lists.new.create": "리스트 추가",
"lists.new.title_placeholder": "새 리스트의 이름",
"lists.search": "팔로우 중인 사람들 중에서 찾기",
"lists.subheading": "Your lists",
"lists.subheading": "당신의 리스트",
"loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다",
@@ -178,7 +178,7 @@
"onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
"onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
"onboarding.page_six.almost_done": "이상입니다.",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.appetoot": "본 아페툿!",
"onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
"onboarding.page_six.guidelines": "커뮤니티 가이드라인",
@@ -213,7 +213,7 @@
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
"search_popout.tips.user": "유저",
"search_results.total": "{count, number}건의 결과",
"standalone.public_title": "A look inside...",
"standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
"status.block": "@{name} 차단",
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
"status.delete": "삭제",
@@ -247,7 +247,7 @@
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
"upload_area.title": "드래그 & 드롭으로 업로드",
"upload_button.label": "미디어 추가",
"upload_form.description": "Describe for the visually impaired",
"upload_form.description": "시각장애인을 위한 설명",
"upload_form.undo": "재시도",
"upload_progress.label": "업로드 중...",
"video.close": "동영상 닫기",

View File

@@ -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",

View File

@@ -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": "Обавештења",

View File

@@ -0,0 +1,2 @@
[
]

View File

@@ -50,7 +50,7 @@
"column_header.unpin": "取消固定",
"column_subheading.navigation": "导航",
"column_subheading.settings": "设置",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
"compose_form.hashtag_warning": "这条嘟文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的嘟文才能通过话题标签进行搜索。",
"compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
"compose_form.lock_disclaimer.lock": "开启保护",
"compose_form.placeholder": "在想啥?",
@@ -214,7 +214,7 @@
"search_popout.tips.user": "用户",
"search_results.total": "共 {count, number} 个结果",
"standalone.public_title": "大家都在干啥?",
"status.block": "Block @{name}",
"status.block": "屏蔽 @{name}",
"status.cannot_reblog": "无法转嘟这条嘟文",
"status.delete": "删除",
"status.embed": "嵌入",
@@ -223,7 +223,7 @@
"status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}",
"status.more": "更多",
"status.mute": "Mute @{name}",
"status.mute": "隐藏 @{name}",
"status.mute_conversation": "隐藏此对话",
"status.open": "展开嘟文",
"status.pin": "在个人资料页面置顶",

View File

@@ -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": "置頂貼文",

View File

@@ -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;
}

View File

@@ -2,18 +2,16 @@
class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform
if @object.respond_to?(:[]) &&
@object['type'] == 'Follow' && @object['actor'].present?
accept_follow_from @object['actor']
else
accept_follow_object @object
case @object['type']
when 'Follow'
accept_follow
end
end
private
def accept_follow_from(actor)
target_account = account_from_uri(value_or_id(actor))
def accept_follow
target_account = account_from_uri(target_uri)
return if target_account.nil? || !target_account.local?
@@ -21,8 +19,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
follow_request&.authorize!
end
def accept_follow_object(object)
follow_request = ActivityPub::TagManager.instance.uri_to_resource(value_or_id(object), FollowRequest)
follow_request&.authorize!
def target_uri
@target_uri ||= value_or_id(@object['actor'])
end
end

View File

@@ -1,11 +1,11 @@
# frozen_string_literal: true
class ActivityPub::Activity::Create < ActivityPub::Activity
SUPPORTED_TYPES = %w(Article Note).freeze
CONVERTED_TYPES = %w(Image Video).freeze
SUPPORTED_TYPES = %w(Note).freeze
CONVERTED_TYPES = %w(Image Video Article).freeze
def perform
return if delete_arrived_first?(object_uri) || unsupported_object_type?
return if delete_arrived_first?(object_uri) || unsupported_object_type? || invalid_origin?(@object['id'])
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@@ -213,7 +213,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def object_url
return if @object['url'].blank?
url_to_href(@object['url'], 'text/html')
url_candidate = url_to_href(@object['url'], 'text/html')
if invalid_origin?(url_candidate)
nil
else
url_candidate
end
end
def content_language_map?
@@ -245,6 +252,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
end
def invalid_origin?(url)
return true if unsupported_uri_scheme?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@account.uri).host
!haystack.casecmp(needle).zero?
end
def reply_to_local?
!replied_to_status.nil? && replied_to_status.account.local?
end

View File

@@ -28,8 +28,6 @@ class ActivityPub::TagManager
return target.uri if target.respond_to?(:local?) && !target.local?
case target.object_type
when :follow
account_follow_url(target.account.username, target)
when :person
account_url(target)
when :note, :comment, :activity
@@ -99,12 +97,6 @@ class ActivityPub::TagManager
case klass.name
when 'Account'
klass.find_local(uri_to_local_id(uri, :username))
when 'FollowRequest'
params = Rails.application.routes.recognize_path(uri)
klass.joins(:account).find_by!(
accounts: { domain: nil, username: params[:account_username] },
id: params[:id]
)
else
StatusFinder.new(uri).status
end

View File

@@ -21,10 +21,6 @@ class FollowRequest < ApplicationRecord
validates :account_id, uniqueness: { scope: :target_account_id }
def object_type
:follow
end
def authorize!
account.follow!(target_account, reblogs: show_reblogs)
MergeWorker.perform_async(target_account.id, account.id)

View File

@@ -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!

View 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

View File

@@ -1,12 +1,11 @@
# frozen_string_literal: true
class ActivityPub::FollowSerializer < ActiveModel::Serializer
attributes :type, :actor
attribute :id, if: :dereferencable?
attributes :id, :type, :actor
attribute :virtual_object, key: :object
def id
ActivityPub::TagManager.instance.uri_for(object)
[ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join
end
def type
@@ -20,8 +19,4 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer
def virtual_object
ActivityPub::TagManager.instance.uri_for(object.target_account)
end
def dereferencable?
object.respond_to?(:object_type)
end
end

View File

@@ -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

View File

@@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
# Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain
def call(username, domain, json)
return if json['inbox'].blank?
return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
@json = json
@uri = @json['id']
@@ -107,7 +107,21 @@ class ActivityPub::ProcessAccountService < BaseService
def url
return if @json['url'].blank?
url_to_href(@json['url'], 'text/html')
url_candidate = url_to_href(@json['url'], 'text/html')
if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
nil
else
url_candidate
end
end
def mismatching_origin?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@uri).host
!haystack.casecmp(needle).zero?
end
def outbox_total_items

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -9,7 +9,7 @@
= fa_icon 'user-times'
= t('accounts.unfollow')
- else
= link_to account_follows_path(account), data: { method: :post }, class: 'icon-button' do
= link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
= fa_icon 'user-plus'
= t('accounts.follow')
- elsif !user_signed_in?

View File

@@ -7,7 +7,7 @@
<p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.</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>

View File

@@ -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 laisina.
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,

View File

@@ -0,0 +1,15 @@
<p>Bonjorn <%= @resource.email %>!</p>
<% if @resource&.unconfirmed_email? %>
<p>Vos contactem per vos senhalar que ladreça quutilizatz per <%= @instance %> es cambiada per aquesta daquí <%= @resource.unconfirmed_email %>.</p>
<% else %>
<p>Vos contactem per vos senhalar que ladreça quutilizatz per <%= @instance %> es cambiada per aquesta daquí <%= @resource.email %>.</p>
<% end %>
<p>
Savètz pas demandat aqueste cambiament dadreça, poiriá arribar que qualquun mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator dinstància se laccès a vòstre compte vos es barrat.
</p>
<p>Amistosament,<p>
<p>La còla <%= @instance %></p>

View File

@@ -0,0 +1,13 @@
Bonjorn <%= @resource.email %>!
<% if @resource&.unconfirmed_email? %>
Vos contactem per vos senhalar que ladreça quutilizatz per <%= @instance %> es cambiada per aquesta daquí <%= @resource.unconfirmed_email %>.
<% else %>
Vos contactem per vos senhalar que ladreça quutilizatz per <%= @instance %> es cambiada per aquesta daquí <%= @resource.email %>.
<% end %>
Savètz pas demandat aqueste cambiament dadreça, poiriá arribar que qualquun mai aguèsse agut accès a vòstre compte. Mercés de cambiar sulpic vòstre senhal o de contactar vòstre administrator dinstància se laccès a vòstre compte vos es barrat.
Amistosament,
La còla <%= @instance %>

View File

@@ -0,0 +1,13 @@
<p><%= @resource.email %>,你好呀!</p>
<% if @resource&.unconfirmed_email? %>
<p>我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址即将变更为 <%= @resource.unconfirmed_email %>。</p>
<% else %>
<p>我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址已经变更为 <%= @resource.unconfirmed_email %>。</p>
<% end %>
<p>
如果你并没有请求更改你的电子邮件地址,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系实例的管理员请求协助。
</p>
<p>来自 <%= @instance %> 管理团队</p>

View File

@@ -0,0 +1,11 @@
<%= @resource.email %>,你好呀!
<% if @resource&.unconfirmed_email? %>
我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址即将变更为 <%= @resource.unconfirmed_email %>。
<% else %>
我们发送这封邮件是为了提醒你,你在 <%= @instance %> 上使用的电子邮件地址已经变更为 <%= @resource.unconfirmed_email %>。
<% end %>
如果你并没有请求更改你的电子邮件地址,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系实例的管理员请求协助。
来自 <%= @instance %> 管理团队

View File

@@ -0,0 +1,15 @@
<p>Bonjorn <%= @resource.unconfirmed_email %>!</p>
<p>Avètz demandat a cambiar vòstra adreça de corrièl quutilizatz 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 dadreç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>

View File

@@ -0,0 +1,12 @@
Bonjorn <%= @resource.unconfirmed_email %>!
Avètz demandat a cambiar vòstra adreça de corrièl quutilizatz 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 %>

View File

@@ -0,0 +1,13 @@
<p><%= @resource.email %>,你好呀!</p>
<p>你正在更改你在 <%= @instance %> 使用的电子邮件地址。</p>
<p>点击下面的链接以确认操作:<br>
<%= link_to '确认我的电子邮件地址', confirmation_url(@resource, confirmation_token: @token) %></p>
<p>上面的链接按不动?把下面的链接复制到地址栏再试试:<br>
<span><%= confirmation_url(@resource, confirmation_token: @token) %></span>
<p>记得读一读我们的<%= link_to '使用条款', terms_url %>哦。</p>
<p>来自 <%= @instance %> 管理团队</p>

View File

@@ -0,0 +1,10 @@
<%= @resource.email %>,你好呀!
你正在更改你在 <%= @instance %> 使用的电子邮件地址。
点击下面的链接以确认操作:
<%= confirmation_url(@resource, confirmation_token: @token) %>
记得读一读我们的使用条款哦:<%= terms_url %>
来自 <%= @instance %> 管理团队

View File

@@ -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

View File

@@ -95,7 +95,7 @@ Rails.application.configure do
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social ; base-uri 'none';" ,
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'

View File

@@ -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')

View File

@@ -17,15 +17,17 @@ zh-CN:
unconfirmed: 继续操作前请先确认你的帐户。
mailer:
confirmation_instructions:
subject: Mastodon 帐户确认信息
subject: Mastodon:确认 %{instance} 帐户信息
email_changed:
subject: Mastodon 电子邮件地址已被修改
subject: Mastodon电子邮件地址已被修改
password_change:
subject: Mastodon 密码已被重置
subject: Mastodon密码已被重置
reconfirmation_instructions:
subject: Mastodon确认 %{instance} 电子邮件地址
reset_password_instructions:
subject: Mastodon 重置密码信息
subject: Mastodon重置密码信息
unlock_instructions:
subject: Mastodon 帐户解锁信息
subject: Mastodon帐户解锁信息
omniauth_callbacks:
failure: 由于%{reason},无法从%{kind}获得授权。
success: 成功地从%{kind}获得授权。

View File

@@ -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ą

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -263,12 +263,18 @@ zh-CN:
unresolved: 未处理
view: 查看
settings:
activity_api_enabled:
desc_html: 本站用户发布的嘟文数,以及本站的活跃用户数和一周内新用户数
title: 公开用户活跃度的统计数据
bootstrap_timeline_accounts:
desc_html: 用半角逗号分隔多个用户名。只能添加来自本站且未开启保护的帐户。如果留空,则默认关注本站所有的管理员。
title: 新用户默认关注
contact_information:
email: 用于联系的公开电子邮件地址
username: 用于联系的公开用户名
peers_api_enabled:
desc_html: 截至目前本实例在网络中已发现的域名
title: 公开已知实例的列表
registrations:
closed_message:
desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签

View File

@@ -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: 兩階段認證碼不正確

View File

@@ -54,8 +54,7 @@ Rails.application.routes.draw do
resources :followers, only: [:index], controller: :follower_accounts
resources :following, only: [:index], controller: :following_accounts
resources :follows, only: [:show], module: :activitypub
resource :follow, only: [:create], controller: :account_follow, as: :follows
resource :follow, only: [:create], controller: :account_follow
resource :unfollow, only: [:create], controller: :account_unfollow
resource :outbox, only: [:show], module: :activitypub
resource :inbox, only: [:create], module: :activitypub

View File

@@ -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');

View File

@@ -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

View File

@@ -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

View File

@@ -13,7 +13,7 @@ module Mastodon
end
def patch
0
2
end
def pre

View File

@@ -1,43 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe ActivityPub::FollowsController, type: :controller do
let(:follow_request) { Fabricate(:follow_request, account: account) }
render_views
context 'with local account' do
let(:account) { Fabricate(:account, domain: nil) }
it 'returns follow request' do
signed_request = Request.new(:get, account_follow_url(account, follow_request))
signed_request.on_behalf_of(follow_request.target_account)
request.headers.merge! signed_request.headers
get :show, params: { id: follow_request, account_username: account.username }
expect(body_as_json[:id]).to eq ActivityPub::TagManager.instance.uri_for(follow_request)
expect(response).to have_http_status :success
end
it 'returns http 404 without signature' do
get :show, params: { id: follow_request, account_username: account.username }
expect(response).to have_http_status 404
end
end
context 'with remote account' do
let(:account) { Fabricate(:account, domain: Faker::Internet.domain_name) }
it 'returns http 404' do
signed_request = Request.new(:get, account_follow_url(account, follow_request))
signed_request.on_behalf_of(follow_request.target_account)
request.headers.merge! signed_request.headers
get :show, params: { id: follow_request, account_username: account.username }
expect(response).to have_http_status 404
end
end
end

View File

@@ -47,22 +47,18 @@ describe ApplicationController, type: :controller do
include_examples 'respond_with_error', 422
end
it "does not force ssl if LOCAL_HTTPS is not 'true'" do
it "does not force ssl if Rails.env.production? is not 'true'" do
routes.draw { get 'success' => 'anonymous#success' }
ClimateControl.modify LOCAL_HTTPS: '' do
allow(Rails.env).to receive(:production?).and_return(true)
get 'success'
expect(response).to have_http_status(:success)
end
allow(Rails.env).to receive(:production?).and_return(false)
get 'success'
expect(response).to have_http_status(:success)
end
it "forces ssl if LOCAL_HTTPS is 'true'" do
it "forces ssl if Rails.env.production? is 'true'" do
routes.draw { get 'success' => 'anonymous#success' }
ClimateControl.modify LOCAL_HTTPS: 'true' do
allow(Rails.env).to receive(:production?).and_return(true)
get 'success'
expect(response).to redirect_to('https://test.host/success')
end
allow(Rails.env).to receive(:production?).and_return(true)
get 'success'
expect(response).to redirect_to('https://test.host/success')
end
describe 'helper_method :current_account' do

View File

@@ -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

View File

@@ -3,49 +3,36 @@ require 'rails_helper'
RSpec.describe ActivityPub::Activity::Accept do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let!(:follow_request) { Fabricate(:follow_request, account: recipient, target_account: sender) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: 'bar',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
Fabricate(:follow_request, account: recipient, target_account: sender)
subject.perform
end
context 'with concerete object representation' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
end
it 'creates a follow relationship' do
expect(recipient.following?(sender)).to be true
end
it 'creates a follow relationship' do
expect(recipient.following?(sender)).to be true
end
context 'with object represented by id' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(follow_request),
}.with_indifferent_access
end
it 'creates a follow relationship' do
expect(recipient.following?(sender)).to be true
end
it 'removes the follow request' do
expect(recipient.requested?(sender)).to be false
end
end
end

View File

@@ -6,7 +6,7 @@ RSpec.describe ActivityPub::Activity::Create do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
type: 'Create',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: object_json,
@@ -16,6 +16,8 @@ RSpec.describe ActivityPub::Activity::Create do
subject { described_class.new(json, sender) }
before do
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
end
@@ -28,7 +30,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'standalone' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
}
@@ -52,7 +54,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'public' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: 'https://www.w3.org/ns/activitystreams#Public',
@@ -70,7 +72,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'unlisted' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
cc: 'https://www.w3.org/ns/activitystreams#Public',
@@ -88,7 +90,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'private' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: 'http://example.com/followers',
@@ -108,7 +110,7 @@ RSpec.describe ActivityPub::Activity::Create do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
@@ -128,7 +130,7 @@ RSpec.describe ActivityPub::Activity::Create do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
@@ -151,7 +153,7 @@ RSpec.describe ActivityPub::Activity::Create do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
tag: [
@@ -174,7 +176,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with mentions missing href' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
tag: [
@@ -194,7 +196,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with media attachments' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
attachment: [
@@ -218,7 +220,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with media attachments missing url' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
attachment: [
@@ -239,7 +241,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with hashtags' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
tag: [
@@ -263,7 +265,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with hashtags missing name' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
tag: [
@@ -284,7 +286,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with emojis' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum :tinking:',
tag: [
@@ -310,7 +312,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with emojis missing name' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum :tinking:',
tag: [
@@ -333,7 +335,7 @@ RSpec.describe ActivityPub::Activity::Create do
context 'with emojis missing icon' do
let(:object_json) do
{
id: 'bar',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum :tinking:',
tag: [

View File

@@ -34,12 +34,4 @@ RSpec.describe FollowRequest, type: :model do
expect(follow_request.account.muting_reblogs?(target)).to be true
end
end
describe '#object_type' do
let(:follow_request) { Fabricate(:follow_request) }
it 'equals to :follow' do
expect(follow_request.object_type).to eq :follow
end
end
end

View File

@@ -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)

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