Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e14c20582f | ||
|
|
47eda1e5fb | ||
|
|
97dcfb0f50 | ||
|
|
79bc3d5845 | ||
|
|
106efba800 | ||
|
|
bd8d8ad894 | ||
|
|
7e07e61a30 | ||
|
|
4a974c6db1 | ||
|
|
a3c0a20373 | ||
|
|
8cd2828e91 | ||
|
|
3d881eed27 | ||
|
|
7650506b39 | ||
|
|
e6db3427b7 | ||
|
|
daefbd66a6 | ||
|
|
b1daa71da5 | ||
|
|
1cc44cba81 | ||
|
|
4ec9d8b4d9 | ||
|
|
d966878e87 | ||
|
|
2fc2725076 | ||
|
|
69f13e7bca | ||
|
|
613e7c7521 | ||
|
|
17cecd75ca | ||
|
|
8cc65cde27 | ||
|
|
b7f6ddeaf1 | ||
|
|
143fb54ab9 | ||
|
|
48cd6dc6ca | ||
|
|
cfd2b06821 | ||
|
|
d613dda91d | ||
|
|
112b1fa265 | ||
|
|
31d1485887 | ||
|
|
1287de1b83 | ||
|
|
72f9eab3d6 | ||
|
|
0b7a0d15c7 | ||
|
|
80b3ca0f6f | ||
|
|
45afdf1781 | ||
|
|
79b34a0fa2 | ||
|
|
872a0d5bd8 | ||
|
|
01421999ae | ||
|
|
0b888acfd4 | ||
|
|
238de58e65 | ||
|
|
7233ac07d2 | ||
|
|
b1e03197fa | ||
|
|
7be53a10b0 | ||
|
|
a0de3222dd | ||
|
|
540b3f37ae | ||
|
|
852b48295f | ||
|
|
9b3b40df66 | ||
|
|
d799921c75 | ||
|
|
e56404be41 | ||
|
|
7badad7797 | ||
|
|
59797ee233 | ||
|
|
fbe7756da6 | ||
|
|
0a103c7749 | ||
|
|
fb16c37d2a | ||
|
|
6f244ba82c | ||
|
|
ea75ae2d1f | ||
|
|
acb982fc66 | ||
|
|
eed7484cd6 | ||
|
|
02194838dd | ||
|
|
3323b4173e | ||
|
|
9a28052e92 | ||
|
|
e6fd4bea35 | ||
|
|
5276c0a090 | ||
|
|
7861c5f108 | ||
|
|
3987bd18a4 | ||
|
|
74c1c9ec01 | ||
|
|
537d2939b1 | ||
|
|
2091ae92be | ||
|
|
dcc614f869 | ||
|
|
ed867eca9d | ||
|
|
08e4c78e78 | ||
|
|
704053d221 | ||
|
|
35b84985a8 | ||
|
|
d41f0b66cc | ||
|
|
921b781909 | ||
|
|
6f5c0afe93 | ||
|
|
eec6095e02 | ||
|
|
9f04b0d4b1 | ||
|
|
628358aeea | ||
|
|
c235711ffe | ||
|
|
ff6ca8bdc6 | ||
|
|
dbda87c31f | ||
|
|
e4a241abef | ||
|
|
93555182c3 | ||
|
|
0eff42d688 | ||
|
|
1d92b90be9 | ||
|
|
da809f9eec | ||
|
|
c4d36d024c | ||
|
|
9e97fbf0af | ||
|
|
10f6793fd0 | ||
|
|
a594139115 | ||
|
|
95bd85d9e8 | ||
|
|
8d51ce4290 | ||
|
|
06636c6eca | ||
|
|
e9822a4e4e | ||
|
|
9a61b0ef22 | ||
|
|
d872902997 | ||
|
|
5ec25ff3e1 | ||
|
|
49e296e1b0 | ||
|
|
7347d4f8bb | ||
|
|
7571c37c99 | ||
|
|
3c18964256 | ||
|
|
c61dd918a2 | ||
|
|
02ba03d6db | ||
|
|
3bee0996c5 | ||
|
|
89daeb43a8 | ||
|
|
7d4f4f9aab | ||
|
|
256c2b1de0 | ||
|
|
02e3e1ec09 | ||
|
|
ff924f95bb | ||
|
|
c10f4bdb03 | ||
|
|
d907d4352e | ||
|
|
a8b51124ba | ||
|
|
161c72d66d | ||
|
|
53d99ebf4f | ||
|
|
1001922156 | ||
|
|
99f962ba73 | ||
|
|
2471796d75 | ||
|
|
545095b3ce | ||
|
|
d319b3dbe4 | ||
|
|
d60fd87e01 | ||
|
|
94230fe565 | ||
|
|
04ecf44c2f | ||
|
|
b6af88192f | ||
|
|
1419f656e2 | ||
|
|
3ba7cde38d | ||
|
|
ce854ed506 | ||
|
|
21b9da6418 | ||
|
|
764f876953 | ||
|
|
2c1ed5f872 | ||
|
|
7d376e41be | ||
|
|
f4b80e6511 | ||
|
|
a56c4742d3 | ||
|
|
38fc1b498d | ||
|
|
511c6f9625 | ||
|
|
868568d1c1 | ||
|
|
65f30f65a2 | ||
|
|
e0ef7f9d79 | ||
|
|
127bfda521 | ||
|
|
1494509468 | ||
|
|
1e5d1fa5c8 | ||
|
|
a3b369337f | ||
|
|
43c37a4768 | ||
|
|
cafe27fb29 | ||
|
|
7e6214b869 | ||
|
|
a8eb0bf44f | ||
|
|
35fdf561be | ||
|
|
081956742c | ||
|
|
8528fd89d2 | ||
|
|
9592b5e31e | ||
|
|
cea98e0c12 | ||
|
|
6eb60260b1 | ||
|
|
81d29e4126 | ||
|
|
c11a52d888 | ||
|
|
e52293482e | ||
|
|
f38e6a14f2 | ||
|
|
a434d9c0cc | ||
|
|
a29432f0cd |
@@ -27,6 +27,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
eslint:
|
eslint:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
channel: eslint-4
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
scss-lint:
|
scss-lint:
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ DB_PASS=
|
|||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
|
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
|
||||||
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
|
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
|
||||||
LOCAL_DOMAIN=example.com
|
LOCAL_DOMAIN=example.com
|
||||||
LOCAL_HTTPS=true
|
|
||||||
|
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
|
||||||
|
|
||||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||||
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
|
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ plugins:
|
|||||||
parserOptions:
|
parserOptions:
|
||||||
sourceType: module
|
sourceType: module
|
||||||
ecmaFeatures:
|
ecmaFeatures:
|
||||||
arrowFunctions: true
|
experimentalObjectRestSpread: true
|
||||||
jsx: true
|
jsx: true
|
||||||
destructuring: true
|
ecmaVersion: 2018
|
||||||
modules: true
|
|
||||||
spread: true
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
import/extensions:
|
import/extensions:
|
||||||
@@ -109,6 +107,7 @@ rules:
|
|||||||
react/self-closing-comp: error
|
react/self-closing-comp: error
|
||||||
|
|
||||||
jsx-a11y/accessible-emoji: warn
|
jsx-a11y/accessible-emoji: warn
|
||||||
|
jsx-a11y/alt-text: warn
|
||||||
jsx-a11y/anchor-has-content: warn
|
jsx-a11y/anchor-has-content: warn
|
||||||
jsx-a11y/aria-activedescendant-has-tabindex: warn
|
jsx-a11y/aria-activedescendant-has-tabindex: warn
|
||||||
jsx-a11y/aria-props: warn
|
jsx-a11y/aria-props: warn
|
||||||
@@ -119,16 +118,22 @@ rules:
|
|||||||
jsx-a11y/href-no-hash: warn
|
jsx-a11y/href-no-hash: warn
|
||||||
jsx-a11y/html-has-lang: warn
|
jsx-a11y/html-has-lang: warn
|
||||||
jsx-a11y/iframe-has-title: warn
|
jsx-a11y/iframe-has-title: warn
|
||||||
jsx-a11y/img-has-alt: warn
|
|
||||||
jsx-a11y/img-redundant-alt: warn
|
jsx-a11y/img-redundant-alt: warn
|
||||||
|
jsx-a11y/interactive-supports-focus: warn
|
||||||
jsx-a11y/label-has-for: off
|
jsx-a11y/label-has-for: off
|
||||||
jsx-a11y/mouse-events-have-key-events: warn
|
jsx-a11y/mouse-events-have-key-events: warn
|
||||||
jsx-a11y/no-access-key: warn
|
jsx-a11y/no-access-key: warn
|
||||||
jsx-a11y/no-distracting-elements: warn
|
jsx-a11y/no-distracting-elements: warn
|
||||||
|
jsx-a11y/no-noninteractive-element-interactions:
|
||||||
|
- warn
|
||||||
|
- handlers:
|
||||||
|
- onClick
|
||||||
jsx-a11y/no-onchange: warn
|
jsx-a11y/no-onchange: warn
|
||||||
jsx-a11y/no-redundant-roles: warn
|
jsx-a11y/no-redundant-roles: warn
|
||||||
jsx-a11y/onclick-has-focus: warn
|
jsx-a11y/no-static-element-interactions:
|
||||||
jsx-a11y/onclick-has-role: warn
|
- warn
|
||||||
|
- handlers:
|
||||||
|
- onClick
|
||||||
jsx-a11y/role-has-required-aria-props: warn
|
jsx-a11y/role-has-required-aria-props: warn
|
||||||
jsx-a11y/role-supports-aria-props: off
|
jsx-a11y/role-supports-aria-props: off
|
||||||
jsx-a11y/scope: warn
|
jsx-a11y/scope: warn
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.4.2
|
2.5.0
|
||||||
|
|||||||
10
.travis.yml
@@ -8,7 +8,10 @@ cache:
|
|||||||
- public/packs-test
|
- public/packs-test
|
||||||
- tmp/cache/babel-loader
|
- tmp/cache/babel-loader
|
||||||
dist: trusty
|
dist: trusty
|
||||||
sudo: required
|
sudo: false
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
@@ -37,8 +40,8 @@ addons:
|
|||||||
- yarn
|
- yarn
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.3.4
|
|
||||||
- 2.4.2
|
- 2.4.2
|
||||||
|
- 2.5.0
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- redis-server
|
- redis-server
|
||||||
@@ -49,8 +52,7 @@ install:
|
|||||||
- yarn install
|
- yarn install
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- bundle exec rake parallel:create parallel:load_schema parallel:prepare
|
- ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile
|
||||||
- bundle exec rails assets:precompile
|
|
||||||
- ln -s /usr/bin/x86_64-linux-gnu-g++-6 "$HOME/g++"
|
- ln -s /usr/bin/x86_64-linux-gnu-g++-6 "$HOME/g++"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:2.4.2-alpine3.6
|
FROM ruby:2.5.0-alpine3.7
|
||||||
|
|
||||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
description="A GNU Social-compatible microblogging server"
|
description="A GNU Social-compatible microblogging server"
|
||||||
@@ -40,6 +40,7 @@ RUN apk -U upgrade \
|
|||||||
protobuf \
|
protobuf \
|
||||||
su-exec \
|
su-exec \
|
||||||
tini \
|
tini \
|
||||||
|
tzdata \
|
||||||
&& update-ca-certificates \
|
&& update-ca-certificates \
|
||||||
&& mkdir -p /tmp/src /opt \
|
&& mkdir -p /tmp/src /opt \
|
||||||
&& wget -O yarn.tar.gz "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
|
&& wget -O yarn.tar.gz "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
|
||||||
|
|||||||
10
Gemfile
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 2.3.0', '< 2.5.0'
|
ruby '>= 2.3.0', '< 2.6.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.2'
|
gem 'pkg-config', '~> 1.2'
|
||||||
|
|
||||||
@@ -28,15 +28,15 @@ gem 'browser'
|
|||||||
gem 'charlock_holmes', '~> 0.7.5'
|
gem 'charlock_holmes', '~> 0.7.5'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'cld3', '~> 3.2.0'
|
gem 'cld3', '~> 3.2.0'
|
||||||
gem 'devise', '~> 4.2'
|
gem 'devise', '~> 4.4'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
gem 'doorkeeper', '~> 4.2'
|
gem 'doorkeeper', '~> 4.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'goldfinger', '~> 2.0'
|
gem 'goldfinger', '~> 2.1'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.5'
|
gem 'redis-namespace', '~> 1.5'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 2.2'
|
gem 'http', '~> 3.0'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 0.99'
|
gem 'httplog', '~> 0.99'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
@@ -49,6 +49,7 @@ gem 'oj', '~> 3.3'
|
|||||||
gem 'ostatus2', '~> 2.0'
|
gem 'ostatus2', '~> 2.0'
|
||||||
gem 'ox', '~> 2.8'
|
gem 'ox', '~> 2.8'
|
||||||
gem 'pundit', '~> 1.1'
|
gem 'pundit', '~> 1.1'
|
||||||
|
gem 'premailer-rails'
|
||||||
gem 'rack-attack', '~> 5.0'
|
gem 'rack-attack', '~> 5.0'
|
||||||
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
|
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
|
||||||
gem 'rack-timeout', '~> 0.4'
|
gem 'rack-timeout', '~> 0.4'
|
||||||
@@ -58,6 +59,7 @@ gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
|
|||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 0.10'
|
gem 'rqrcode', '~> 0.10'
|
||||||
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
|
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
|
||||||
|
gem 'ruby-progressbar', '~> 1.4'
|
||||||
gem 'sanitize', '~> 4.4'
|
gem 'sanitize', '~> 4.4'
|
||||||
gem 'sidekiq', '~> 5.0'
|
gem 'sidekiq', '~> 5.0'
|
||||||
gem 'sidekiq-scheduler', '~> 2.1'
|
gem 'sidekiq-scheduler', '~> 2.1'
|
||||||
|
|||||||
49
Gemfile.lock
@@ -70,7 +70,7 @@ GEM
|
|||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.7.3)
|
binding_of_caller (0.8.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.1.5)
|
bootsnap (1.1.5)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
@@ -110,7 +110,7 @@ GEM
|
|||||||
activesupport
|
activesupport
|
||||||
charlock_holmes (0.7.5)
|
charlock_holmes (0.7.5)
|
||||||
chunky_png (1.3.8)
|
chunky_png (1.3.8)
|
||||||
cld3 (3.2.1)
|
cld3 (3.2.2)
|
||||||
ffi (>= 1.1.0, < 1.10.0)
|
ffi (>= 1.1.0, < 1.10.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
@@ -122,8 +122,10 @@ GEM
|
|||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.3)
|
crass (1.0.3)
|
||||||
|
css_parser (1.6.0)
|
||||||
|
addressable
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
devise (4.3.0)
|
devise (4.4.0)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0, < 5.2)
|
railties (>= 4.1.0, < 5.2)
|
||||||
@@ -179,9 +181,9 @@ GEM
|
|||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.4.1)
|
globalid (0.4.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
goldfinger (2.0.1)
|
goldfinger (2.1.0)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
http (~> 2.2)
|
http (~> 3.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
oj (~> 3.0)
|
oj (~> 3.0)
|
||||||
hamlit (2.8.5)
|
hamlit (2.8.5)
|
||||||
@@ -200,14 +202,14 @@ GEM
|
|||||||
hiredis (0.6.1)
|
hiredis (0.6.1)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (2.2.2)
|
http (3.0.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 1.0.1)
|
http-form_data (>= 2.0.0.pre.pre2, < 3)
|
||||||
http_parser.rb (~> 0.6.0)
|
http_parser.rb (~> 0.6.0)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (1.0.3)
|
http-form_data (2.0.0)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httplog (0.99.7)
|
httplog (0.99.7)
|
||||||
@@ -298,14 +300,12 @@ GEM
|
|||||||
concurrent-ruby (~> 1.0.0)
|
concurrent-ruby (~> 1.0.0)
|
||||||
sidekiq (>= 3.5.0)
|
sidekiq (>= 3.5.0)
|
||||||
statsd-ruby (~> 1.2.0)
|
statsd-ruby (~> 1.2.0)
|
||||||
oj (3.3.9)
|
oj (3.3.10)
|
||||||
openssl (2.0.6)
|
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (2.0.1)
|
ostatus2 (2.0.3)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.5)
|
||||||
http (~> 2.0)
|
http (~> 3.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.8)
|
||||||
openssl (~> 2.0)
|
|
||||||
ox (2.8.2)
|
ox (2.8.2)
|
||||||
paperclip (5.1.0)
|
paperclip (5.1.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
@@ -326,6 +326,13 @@ GEM
|
|||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.2.8)
|
pkg-config (1.2.8)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
|
premailer (1.11.1)
|
||||||
|
addressable
|
||||||
|
css_parser (>= 1.6.0)
|
||||||
|
htmlentities (>= 4.0.0)
|
||||||
|
premailer-rails (1.10.1)
|
||||||
|
actionmailer (>= 3, < 6)
|
||||||
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
pry (0.11.3)
|
pry (0.11.3)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 0.9.0)
|
||||||
@@ -561,7 +568,7 @@ DEPENDENCIES
|
|||||||
charlock_holmes (~> 0.7.5)
|
charlock_holmes (~> 0.7.5)
|
||||||
cld3 (~> 3.2.0)
|
cld3 (~> 3.2.0)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
devise (~> 4.2)
|
devise (~> 4.4)
|
||||||
devise-two-factor (~> 3.0)
|
devise-two-factor (~> 3.0)
|
||||||
doorkeeper (~> 4.2)
|
doorkeeper (~> 4.2)
|
||||||
dotenv-rails (~> 2.2)
|
dotenv-rails (~> 2.2)
|
||||||
@@ -572,11 +579,11 @@ DEPENDENCIES
|
|||||||
fog-local (~> 0.4)
|
fog-local (~> 0.4)
|
||||||
fog-openstack (~> 0.1)
|
fog-openstack (~> 0.1)
|
||||||
fuubar (~> 2.2)
|
fuubar (~> 2.2)
|
||||||
goldfinger (~> 2.0)
|
goldfinger (~> 2.1)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
http (~> 2.2)
|
http (~> 3.0)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
httplog (~> 0.99)
|
httplog (~> 0.99)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 0.9)
|
||||||
@@ -602,6 +609,7 @@ DEPENDENCIES
|
|||||||
pg (~> 0.20)
|
pg (~> 0.20)
|
||||||
pghero (~> 1.7)
|
pghero (~> 1.7)
|
||||||
pkg-config (~> 1.2)
|
pkg-config (~> 1.2)
|
||||||
|
premailer-rails
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 3.10)
|
puma (~> 3.10)
|
||||||
pundit (~> 1.1)
|
pundit (~> 1.1)
|
||||||
@@ -621,6 +629,7 @@ DEPENDENCIES
|
|||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rubocop
|
rubocop
|
||||||
ruby-oembed (~> 0.12)
|
ruby-oembed (~> 0.12)
|
||||||
|
ruby-progressbar (~> 1.4)
|
||||||
sanitize (~> 4.4)
|
sanitize (~> 4.4)
|
||||||
scss_lint (~> 0.55)
|
scss_lint (~> 0.55)
|
||||||
sidekiq (~> 5.0)
|
sidekiq (~> 5.0)
|
||||||
@@ -640,7 +649,7 @@ DEPENDENCIES
|
|||||||
webpush
|
webpush
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.4.2p198
|
ruby 2.5.0p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.16.0
|
1.16.1
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
class AccountsController < ApplicationController
|
class AccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -26,10 +27,11 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @account,
|
skip_session!
|
||||||
serializer: ActivityPub::ActorSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||||||
def upgrade_account
|
def upgrade_account
|
||||||
if signed_request_account.ostatus?
|
if signed_request_account.ostatus?
|
||||||
signed_request_account.update(last_webfingered_at: nil)
|
signed_request_account.update(last_webfingered_at: nil)
|
||||||
ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
|
ResolveAccountWorker.perform_async(signed_request_account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
|
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class CustomEmojisController < BaseController
|
class CustomEmojisController < BaseController
|
||||||
before_action :set_custom_emoji, except: [:index, :new, :create]
|
before_action :set_custom_emoji, except: [:index, :new, :create]
|
||||||
|
before_action :set_filter_params
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :custom_emoji, :index?
|
authorize :custom_emoji, :index?
|
||||||
@@ -32,23 +33,26 @@ module Admin
|
|||||||
|
|
||||||
if @custom_emoji.update(resource_params)
|
if @custom_emoji.update(resource_params)
|
||||||
log_action :update, @custom_emoji
|
log_action :update, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
|
||||||
else
|
else
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
|
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
|
||||||
end
|
end
|
||||||
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @custom_emoji, :destroy?
|
authorize @custom_emoji, :destroy?
|
||||||
@custom_emoji.destroy!
|
@custom_emoji.destroy!
|
||||||
log_action :destroy, @custom_emoji
|
log_action :destroy, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
|
||||||
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy
|
def copy
|
||||||
authorize @custom_emoji, :copy?
|
authorize @custom_emoji, :copy?
|
||||||
|
|
||||||
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
|
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
|
||||||
|
shortcode: @custom_emoji.shortcode)
|
||||||
emoji.image = @custom_emoji.image
|
emoji.image = @custom_emoji.image
|
||||||
|
|
||||||
if emoji.save
|
if emoji.save
|
||||||
@@ -58,21 +62,23 @@ module Admin
|
|||||||
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to admin_custom_emojis_path(page: params[:page])
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
authorize @custom_emoji, :enable?
|
authorize @custom_emoji, :enable?
|
||||||
@custom_emoji.update!(disabled: false)
|
@custom_emoji.update!(disabled: false)
|
||||||
log_action :enable, @custom_emoji
|
log_action :enable, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
|
||||||
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable
|
def disable
|
||||||
authorize @custom_emoji, :disable?
|
authorize @custom_emoji, :disable?
|
||||||
@custom_emoji.update!(disabled: true)
|
@custom_emoji.update!(disabled: true)
|
||||||
log_action :disable, @custom_emoji
|
log_action :disable, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
|
||||||
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -81,6 +87,10 @@ module Admin
|
|||||||
@custom_emoji = CustomEmoji.find(params[:id])
|
@custom_emoji = CustomEmoji.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_filter_params
|
||||||
|
@filter_params = filter_params.to_hash.symbolize_keys
|
||||||
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
|
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ module Admin
|
|||||||
bootstrap_timeline_accounts
|
bootstrap_timeline_accounts
|
||||||
thumbnail
|
thumbnail
|
||||||
min_invite_role
|
min_invite_role
|
||||||
|
activity_api_enabled
|
||||||
|
peers_api_enabled
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_SETTINGS = %w(
|
BOOLEAN_SETTINGS = %w(
|
||||||
@@ -24,6 +26,8 @@ module Admin
|
|||||||
open_deletion
|
open_deletion
|
||||||
timeline_preview
|
timeline_preview
|
||||||
show_staff_badge
|
show_staff_badge
|
||||||
|
activity_api_enabled
|
||||||
|
peers_api_enabled
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
UPLOAD_SETTINGS = %w(
|
UPLOAD_SETTINGS = %w(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ class Api::BaseController < ApplicationController
|
|||||||
|
|
||||||
include RateLimitHeaders
|
include RateLimitHeaders
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
render json: { error: e.to_s }, status: 422
|
render json: { error: e.to_s }, status: 422
|
||||||
|
|||||||
36
app/controllers/api/v1/instances/activity_controller.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::ActivityController < Api::BaseController
|
||||||
|
before_action :require_enabled_api!
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def show
|
||||||
|
render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def activity
|
||||||
|
weeks = []
|
||||||
|
|
||||||
|
12.times do |i|
|
||||||
|
day = i.weeks.ago.to_date
|
||||||
|
week_id = day.cweek
|
||||||
|
week = Date.commercial(day.cwyear, week_id)
|
||||||
|
|
||||||
|
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}").to_s,
|
||||||
|
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
weeks
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_enabled_api!
|
||||||
|
head 404 unless Setting.activity_api_enabled
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/controllers/api/v1/instances/peers_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::PeersController < Api::BaseController
|
||||||
|
before_action :require_enabled_api!
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_enabled_api!
|
||||||
|
head 404 unless Setting.peers_api_enabled
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,7 +9,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
|
||||||
|
render json: @statuses,
|
||||||
|
each_serializer: REST::StatusSerializer,
|
||||||
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||||
|
status: regeneration_in_progress? ? 206 : 200
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -57,4 +61,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||||||
def pagination_since_id
|
def pagination_since_id
|
||||||
@statuses.first.id
|
@statuses.first.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def regeneration_in_progress?
|
||||||
|
Redis.current.exists("account:#{current_account.id}:regeneration")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
def create
|
def create
|
||||||
params.require(:subscription).require(:endpoint)
|
params.require(:subscription).require(:endpoint)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def https_enabled?
|
def https_enabled?
|
||||||
Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'
|
Rails.env.production?
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
@@ -121,4 +121,26 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
end
|
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: 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
|
end
|
||||||
|
|||||||
@@ -2,10 +2,4 @@
|
|||||||
|
|
||||||
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
def show
|
|
||||||
super do |user|
|
|
||||||
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
new_user_session_path
|
new_user_session_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_update_path_for(_resource)
|
||||||
|
edit_user_registration_path
|
||||||
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class AuthorizeFollowsController < ApplicationController
|
|||||||
layout 'modal'
|
layout 'modal'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :set_body_classes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@account = located_account || render(:error)
|
@account = located_account || render(:error)
|
||||||
@@ -40,7 +41,7 @@ class AuthorizeFollowsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_from_remote_follow
|
def account_from_remote_follow
|
||||||
ResolveRemoteAccountService.new.call(acct_without_prefix)
|
ResolveAccountService.new.call(acct_without_prefix)
|
||||||
end
|
end
|
||||||
|
|
||||||
def acct_param_is_url?
|
def acct_param_is_url?
|
||||||
@@ -58,4 +59,8 @@ class AuthorizeFollowsController < ApplicationController
|
|||||||
def acct_params
|
def acct_params
|
||||||
params.fetch(:acct, '')
|
params.fetch(:acct, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'modal-layout'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ module SignatureVerification
|
|||||||
|
|
||||||
def account_from_key_id(key_id)
|
def account_from_key_id(key_id)
|
||||||
if key_id.start_with?('acct:')
|
if key_id.start_with?('acct:')
|
||||||
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
||||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||||
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
|
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
module UserTrackingConcern
|
module UserTrackingConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
REGENERATE_FEED_DAYS = 14
|
|
||||||
UPDATE_SIGN_IN_HOURS = 24
|
UPDATE_SIGN_IN_HOURS = 24
|
||||||
|
|
||||||
included do
|
included do
|
||||||
@@ -14,24 +13,10 @@ module UserTrackingConcern
|
|||||||
|
|
||||||
def set_user_activity
|
def set_user_activity
|
||||||
return unless user_needs_sign_in_update?
|
return unless user_needs_sign_in_update?
|
||||||
|
|
||||||
# Mark as signed-in today
|
|
||||||
current_user.update_tracked_fields!(request)
|
current_user.update_tracked_fields!(request)
|
||||||
|
|
||||||
# Regenerate feed if needed
|
|
||||||
regenerate_feed! if user_needs_feed_update?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_needs_sign_in_update?
|
def user_needs_sign_in_update?
|
||||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
|
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_needs_feed_update?
|
|
||||||
current_user.last_sign_in_at < REGENERATE_FEED_DAYS.days.ago
|
|
||||||
end
|
|
||||||
|
|
||||||
def regenerate_feed!
|
|
||||||
Redis.current.setnx("account:#{current_user.account_id}:regeneration", true) == 1 && Redis.current.expire("account:#{current_user.account_id}:regeneration", 3_600 * 24)
|
|
||||||
RegenerationWorker.perform_async(current_user.account_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
class EmojisController < ApplicationController
|
class EmojisController < ApplicationController
|
||||||
before_action :set_emoji
|
before_action :set_emoji
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @emoji,
|
skip_session!
|
||||||
serializer: ActivityPub::EmojiSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,4 +38,8 @@ class RemoteFollowController < ApplicationController
|
|||||||
def suspended_account?
|
def suspended_account?
|
||||||
@account.suspended?
|
@account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'modal-layout'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module Settings
|
|||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :ensure_otp_secret
|
||||||
|
|
||||||
def new
|
def new
|
||||||
prepare_two_factor_form
|
prepare_two_factor_form
|
||||||
@@ -38,6 +39,10 @@ module Settings
|
|||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain)
|
||||||
@qrcode = RQRCode::QRCode.new(@provision_url)
|
@qrcode = RQRCode::QRCode.new(@provision_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_otp_secret
|
||||||
|
redirect_to settings_two_factor_authentication_path unless current_user.otp_secret
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ class SharesController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def initial_state_params
|
def initial_state_params
|
||||||
|
text = [params[:title], params[:text], params[:url]].compact.join(' ')
|
||||||
{
|
{
|
||||||
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||||
push_subscription: current_account.user.web_push_subscription(current_session),
|
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||||
current_account: current_account,
|
current_account: current_account,
|
||||||
token: current_session.token,
|
token: current_session.token,
|
||||||
admin: Account.find_local(Setting.site_contact_username),
|
admin: Account.find_local(Setting.site_contact_username),
|
||||||
text: params[:text],
|
text: text,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'compose-standalone'
|
@body_classes = 'modal-layout compose-standalone'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class StatusesController < ApplicationController
|
|||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
before_action :check_account_suspension
|
before_action :check_account_suspension
|
||||||
before_action :redirect_to_original, only: [:show]
|
before_action :redirect_to_original, only: [:show]
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -21,19 +22,21 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
render json: @status,
|
skip_session! unless @stream_entry.hidden?
|
||||||
serializer: ActivityPub::NoteSerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
render json: @status,
|
skip_session!
|
||||||
serializer: ActivityPub::ActivitySerializer,
|
|
||||||
adapter: ActivityPub::Adapter,
|
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||||
content_type: 'application/activity+json'
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WellKnown
|
module WellKnown
|
||||||
class HostMetaController < ApplicationController
|
class HostMetaController < ActionController::Base
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
before_action { response.headers['Vary'] = 'Accept' }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.xml { render content_type: 'application/xrd+xml' }
|
format.xml { render content_type: 'application/xrd+xml' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expires_in(3.days, public: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WellKnown
|
module WellKnown
|
||||||
class WebfingerController < ApplicationController
|
class WebfingerController < ActionController::Base
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
before_action { response.headers['Vary'] = 'Accept' }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@account = Account.find_local!(username_from_resource)
|
@account = Account.find_local!(username_from_resource)
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ module WellKnown
|
|||||||
render content_type: 'application/xrd+xml'
|
render content_type: 'application/xrd+xml'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expires_in(3.days, public: true)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
head 404
|
head 404
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
|
|||||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||||
when 'Status'
|
when 'Status'
|
||||||
tmp_status = Status.new(attributes)
|
tmp_status = Status.new(attributes)
|
||||||
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
|
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def add_rtl_body_class(other_classes)
|
def add_rtl_body_class(other_classes)
|
||||||
other_classes = "#{other_classes} rtl" if [:ar, :fa, :he].include?(I18n.locale)
|
other_classes = "#{other_classes} rtl" if locale_direction == 'rtl'
|
||||||
other_classes
|
other_classes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def locale_direction
|
||||||
|
if [:ar, :fa, :he].include?(I18n.locale)
|
||||||
|
'rtl'
|
||||||
|
else
|
||||||
|
'ltr'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def favicon_path
|
def favicon_path
|
||||||
env_suffix = Rails.env.production? ? '' : '-dev'
|
env_suffix = Rails.env.production? ? '' : '-dev'
|
||||||
"/favicon#{env_suffix}.ico"
|
"/favicon#{env_suffix}.ico"
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ module InstanceHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def site_hostname
|
def site_hostname
|
||||||
Rails.configuration.x.local_domain
|
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ module JsonLdHelper
|
|||||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsupported_uri_scheme?(uri)
|
||||||
|
!uri.start_with?('http://', 'https://')
|
||||||
|
end
|
||||||
|
|
||||||
def canonicalize(json)
|
def canonicalize(json)
|
||||||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
||||||
graph.dump(:normalize)
|
graph.dump(:normalize)
|
||||||
|
|||||||
4
app/helpers/mailer_helper.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module MailerHelper
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ module RoutingHelper
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
include ActionView::Helpers::AssetTagHelper
|
include ActionView::Helpers::AssetTagHelper
|
||||||
|
include Webpacker::Helper
|
||||||
|
|
||||||
included do
|
included do
|
||||||
def default_url_options
|
def default_url_options
|
||||||
@@ -17,6 +18,10 @@ module RoutingHelper
|
|||||||
URI.join(root_url, source).to_s
|
URI.join(root_url, source).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def full_pack_url(source, **options)
|
||||||
|
full_asset_url(asset_pack_path(source, options))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def use_storage?
|
def use_storage?
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ module SettingsHelper
|
|||||||
he: 'עברית',
|
he: 'עברית',
|
||||||
hr: 'Hrvatski',
|
hr: 'Hrvatski',
|
||||||
hu: 'Magyar',
|
hu: 'Magyar',
|
||||||
|
hy: 'Հայերեն',
|
||||||
id: 'Bahasa Indonesia',
|
id: 'Bahasa Indonesia',
|
||||||
io: 'Ido',
|
io: 'Ido',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
@@ -28,6 +29,9 @@ module SettingsHelper
|
|||||||
pt: 'Português',
|
pt: 'Português',
|
||||||
'pt-BR': 'Português do Brasil',
|
'pt-BR': 'Português do Brasil',
|
||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
|
sk: 'Slovensky',
|
||||||
|
sr: 'Српски',
|
||||||
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
sv: 'Svenska',
|
sv: 'Svenska',
|
||||||
th: 'ภาษาไทย',
|
th: 'ภาษาไทย',
|
||||||
tr: 'Türkçe',
|
tr: 'Türkçe',
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 24 KiB |
1
app/javascript/images/elephant_ui_disappointed.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
app/javascript/images/elephant_ui_greeting.svg
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
app/javascript/images/elephant_ui_plane.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
app/javascript/images/elephant_ui_working.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
2
app/javascript/images/icon_cached.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="2048" height="1792" viewBox="0 0 2048 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1344 1504q0 13-9.5 22.5t-22.5 9.5h-960q-8 0-13.5-2t-9-7-5.5-8-3-11.5-1-11.5v-600h-192q-26 0-45-19t-19-45q0-24 15-41l320-384q19-22 49-22t49 22l320 384q15 17 15 41 0 26-19 45t-45 19h-192v384h576q16 0 25 11l160 192q7 10 7 21zm640-416q0 24-15 41l-320 384q-20 23-49 23t-49-23l-320-384q-15-17-15-41 0-26 19-45t45-19h192v-384h-576q-16 0-25-12l-160-192q-7-9-7-20 0-13 9.5-22.5t22.5-9.5h960q8 0 13.5 2t9 7 5.5 8 3 11.5 1 11.5v600h192q26 0 45 19t19 45z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 604 B |
4
app/javascript/images/icon_done.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 214 B |
4
app/javascript/images/icon_email.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 273 B |
4
app/javascript/images/icon_grade.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
4
app/javascript/images/icon_lock_open.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 395 B |
4
app/javascript/images/icon_person_add.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
4
app/javascript/images/icon_reply.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
1
app/javascript/images/logo_transparent.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/javascript/images/mailer/icon_cached.png
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
app/javascript/images/mailer/icon_done.png
Normal file
|
After Width: | Height: | Size: 279 B |
BIN
app/javascript/images/mailer/icon_email.png
Normal file
|
After Width: | Height: | Size: 520 B |
BIN
app/javascript/images/mailer/icon_grade.png
Normal file
|
After Width: | Height: | Size: 541 B |
BIN
app/javascript/images/mailer/icon_lock_open.png
Normal file
|
After Width: | Height: | Size: 550 B |
BIN
app/javascript/images/mailer/icon_person_add.png
Normal file
|
After Width: | Height: | Size: 512 B |
BIN
app/javascript/images/mailer/icon_reply.png
Normal file
|
After Width: | Height: | Size: 391 B |
BIN
app/javascript/images/mailer/logo_full.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/javascript/images/mailer/logo_transparent.png
Normal file
|
After Width: | Height: | Size: 627 B |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -31,7 +31,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
|||||||
|
|
||||||
const unescapeHTML = (html) => {
|
const unescapeHTML = (html) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
html = html.replace(/<br \/>|<br>|\n/, ' ');
|
html = html.replace(/<br \/>|<br>|\n/g, ' ');
|
||||||
wrapper.innerHTML = html;
|
wrapper.innerHTML = html;
|
||||||
return wrapper.textContent;
|
return wrapper.textContent;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { pushNotificationsSetting } from '../settings';
|
|
||||||
|
|
||||||
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 function setBrowserSupport (value) {
|
|
||||||
return {
|
|
||||||
type: SET_BROWSER_SUPPORT,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSubscription (subscription) {
|
|
||||||
return {
|
|
||||||
type: SET_SUBSCRIPTION,
|
|
||||||
subscription,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSubscription () {
|
|
||||||
return {
|
|
||||||
type: CLEAR_SUBSCRIPTION,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changeAlerts(key, value) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: ALERTS_CHANGE,
|
|
||||||
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');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
23
app/javascript/mastodon/actions/push_notifications/index.js
Normal 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(path, value) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(setAlerts(path, value));
|
||||||
|
dispatch(saveSettings());
|
||||||
|
};
|
||||||
|
}
|
||||||
148
app/javascript/mastodon/actions/push_notifications/registerer.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import api from '../../api';
|
||||||
|
import { pushNotificationsSetting } from '../../settings';
|
||||||
|
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||||
|
import { me } from '../../initial_state';
|
||||||
|
|
||||||
|
// Taken from https://www.npmjs.com/package/web-push
|
||||||
|
const urlBase64ToUint8Array = (base64String) => {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||||
|
|
||||||
|
const getRegistration = () => navigator.serviceWorker.ready;
|
||||||
|
|
||||||
|
const getPushSubscription = (registration) =>
|
||||||
|
registration.pushManager.getSubscription()
|
||||||
|
.then(subscription => ({ registration, subscription }));
|
||||||
|
|
||||||
|
const subscribe = (registration) =>
|
||||||
|
registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribe = ({ registration, subscription }) =>
|
||||||
|
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||||
|
|
||||||
|
const sendSubscriptionToBackend = (getState, subscription) => {
|
||||||
|
const params = { subscription };
|
||||||
|
|
||||||
|
if (me) {
|
||||||
|
const data = pushNotificationsSetting.get(me);
|
||||||
|
if (data) {
|
||||||
|
params.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||||
|
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||||
|
|
||||||
|
export function register () {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(setBrowserSupport(supportsPushNotifications));
|
||||||
|
|
||||||
|
if (me && !pushNotificationsSetting.get(me)) {
|
||||||
|
const alerts = getState().getIn(['push_notifications', 'alerts']);
|
||||||
|
if (alerts) {
|
||||||
|
pushNotificationsSetting.set(me, { alerts: alerts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsPushNotifications) {
|
||||||
|
if (!getApplicationServerKey()) {
|
||||||
|
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRegistration()
|
||||||
|
.then(getPushSubscription)
|
||||||
|
.then(({ registration, subscription }) => {
|
||||||
|
if (subscription !== null) {
|
||||||
|
// We have a subscription, check if it is still valid
|
||||||
|
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
|
||||||
|
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
|
||||||
|
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
|
||||||
|
|
||||||
|
// If the VAPID public key did not change and the endpoint corresponds
|
||||||
|
// to the endpoint saved in the backend, the subscription is valid
|
||||||
|
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
||||||
|
return subscription;
|
||||||
|
} else {
|
||||||
|
// Something went wrong, try to subscribe again
|
||||||
|
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
||||||
|
subscription => sendSubscriptionToBackend(getState, subscription));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No subscription, try to subscribe
|
||||||
|
return subscribe(registration).then(
|
||||||
|
subscription => sendSubscriptionToBackend(getState, subscription));
|
||||||
|
})
|
||||||
|
.then(subscription => {
|
||||||
|
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||||
|
// it means that the backend subscription is valid (and was set during hydration)
|
||||||
|
if (!(subscription instanceof PushSubscription)) {
|
||||||
|
dispatch(setSubscription(subscription));
|
||||||
|
if (me) {
|
||||||
|
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.code === 20 && error.name === 'AbortError') {
|
||||||
|
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||||
|
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||||
|
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear alerts and hide UI settings
|
||||||
|
dispatch(clearSubscription());
|
||||||
|
if (me) {
|
||||||
|
pushNotificationsSetting.remove(me);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
getRegistration()
|
||||||
|
.then(getPushSubscription)
|
||||||
|
.then(unsubscribe);
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Your browser does not support Web Push Notifications.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSettings() {
|
||||||
|
return (_, getState) => {
|
||||||
|
const state = getState().get('push_notifications');
|
||||||
|
const subscription = state.get('subscription');
|
||||||
|
const alerts = state.get('alerts');
|
||||||
|
const data = { alerts };
|
||||||
|
|
||||||
|
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||||
|
data,
|
||||||
|
}).then(() => {
|
||||||
|
if (me) {
|
||||||
|
pushNotificationsSetting.set(me, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
34
app/javascript/mastodon/actions/push_notifications/setter.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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 SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
|
||||||
|
|
||||||
|
export function setBrowserSupport (value) {
|
||||||
|
return {
|
||||||
|
type: SET_BROWSER_SUPPORT,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSubscription (subscription) {
|
||||||
|
return {
|
||||||
|
type: SET_SUBSCRIPTION,
|
||||||
|
subscription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSubscription () {
|
||||||
|
return {
|
||||||
|
type: CLEAR_SUBSCRIPTION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAlerts (path, value) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_ALERTS,
|
||||||
|
path,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import axios from 'axios';
|
import api from '../api';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||||
export const SETTING_SAVE = 'SETTING_SAVE';
|
export const SETTING_SAVE = 'SETTING_SAVE';
|
||||||
|
|
||||||
export function changeSetting(key, value) {
|
export function changeSetting(path, value) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SETTING_CHANGE,
|
type: SETTING_CHANGE,
|
||||||
key,
|
path,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ const debouncedSave = debounce((dispatch, getState) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS();
|
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||||
|
|
||||||
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||||
}, 5000, { trailing: true });
|
}, 5000, { trailing: true });
|
||||||
|
|
||||||
export function saveSettings() {
|
export function saveSettings() {
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
|||||||
|
|
||||||
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
|
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
|
||||||
|
|
||||||
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
|
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next, partial) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_REFRESH_SUCCESS,
|
type: TIMELINE_REFRESH_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses,
|
statuses,
|
||||||
skipLoading,
|
skipLoading,
|
||||||
next,
|
next,
|
||||||
|
partial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
|
|||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
|
|
||||||
if (timeline.get('isLoading') || timeline.get('online')) {
|
if (timeline.get('isLoading') || (timeline.get('online') && !timeline.get('isPartial'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +105,12 @@ export function refreshTimeline(timelineId, path, params = {}) {
|
|||||||
dispatch(refreshTimelineRequest(timelineId, skipLoading));
|
dispatch(refreshTimelineRequest(timelineId, skipLoading));
|
||||||
|
|
||||||
api(getState).get(path, { params }).then(response => {
|
api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
if (response.status === 206) {
|
||||||
dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null));
|
dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true));
|
||||||
|
} else {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false));
|
||||||
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(refreshTimelineFail(timelineId, error, skipLoading));
|
dispatch(refreshTimelineFail(timelineId, error, skipLoading));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import ready from './ready';
|
||||||
import LinkHeader from './link_header';
|
import LinkHeader from './link_header';
|
||||||
|
|
||||||
export const getLinks = response => {
|
export const getLinks = response => {
|
||||||
@@ -11,10 +12,17 @@ export const getLinks = response => {
|
|||||||
return LinkHeader.parse(value);
|
return LinkHeader.parse(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let csrfHeader = {};
|
||||||
|
function setCSRFHeader() {
|
||||||
|
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
||||||
|
csrfHeader['X-CSRF-Token'] = csrfToken;
|
||||||
|
}
|
||||||
|
ready(setCSRFHeader);
|
||||||
|
|
||||||
export default getState => axios.create({
|
export default getState => axios.create({
|
||||||
headers: {
|
headers: Object.assign(csrfHeader, getState ? {
|
||||||
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
|
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`,
|
||||||
},
|
} : {}),
|
||||||
|
|
||||||
transformResponse: [function (data) {
|
transformResponse: [function (data) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ exports[`<DisplayName /> renders display name + account name 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="display-name"
|
className="display-name"
|
||||||
>
|
>
|
||||||
<strong
|
<bdi>
|
||||||
className="display-name__html"
|
<strong
|
||||||
dangerouslySetInnerHTML={
|
className="display-name__html"
|
||||||
Object {
|
dangerouslySetInnerHTML={
|
||||||
"__html": "<p>Foo</p>",
|
Object {
|
||||||
|
"__html": "<p>Foo</p>",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</bdi>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="display-name__account"
|
className="display-name__account"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default class Account extends ImmutablePureComponent {
|
|||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
|
onMuteNotifications: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@@ -92,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
|
|||||||
{hidingNotificationsButton}
|
{hidingNotificationsButton}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else if (!account.get('moved')) {
|
} else if (!account.get('moved') || following) {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className='attachment-list__list'>
|
<ul className='attachment-list__list'>
|
||||||
{media.map(attachment =>
|
{media.map(attachment => (
|
||||||
<li key={attachment.get('id')}>
|
<li key={attachment.get('id')}>
|
||||||
<a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a>
|
<a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
const Collapsable = ({ fullHeight, isVisible, children }) => (
|
const Collapsable = ({ fullHeight, isVisible, children }) => (
|
||||||
<Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
|
<Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
|
||||||
{({ opacity, height }) =>
|
{({ opacity, height }) => (
|
||||||
<div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}>
|
<div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
focusable: PropTypes.bool,
|
|
||||||
showBackButton: PropTypes.bool,
|
showBackButton: PropTypes.bool,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
pinned: PropTypes.bool,
|
pinned: PropTypes.bool,
|
||||||
@@ -32,10 +31,6 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
focusable: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
animating: false,
|
animating: false,
|
||||||
@@ -68,7 +63,7 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
|
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
|
||||||
const { collapsed, animating } = this.state;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
const wrapperClassName = classNames('column-header__wrapper', {
|
const wrapperClassName = classNames('column-header__wrapper', {
|
||||||
@@ -135,11 +130,13 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName}>
|
||||||
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
<h1 className={buttonClassName}>
|
||||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
<button onClick={this.handleTitleClick}>
|
||||||
<span className='column-header__title'>
|
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||||
{title}
|
<span className='column-header__title'>
|
||||||
</span>
|
{title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{backButton}
|
{backButton}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default class DisplayName extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||||
{({ rotate }) =>
|
{({ rotate }) => (
|
||||||
<button
|
<button
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
aria-pressed={pressed}
|
aria-pressed={pressed}
|
||||||
@@ -106,7 +106,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import React from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const MissingIndicator = () => (
|
const MissingIndicator = () => (
|
||||||
<div className='missing-indicator'>
|
<div className='regeneration-indicator missing-indicator'>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage id='missing_indicator.label' defaultMessage='Not found' />
|
<div className='regeneration-indicator__figure' />
|
||||||
|
|
||||||
|
<div className='regeneration-indicator__label'>
|
||||||
|
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
||||||
|
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,14 +178,16 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
{Component => <Component
|
{Component => (
|
||||||
preview={video.get('preview_url')}
|
<Component
|
||||||
src={video.get('url')}
|
preview={video.get('preview_url')}
|
||||||
width={239}
|
src={video.get('url')}
|
||||||
height={110}
|
width={239}
|
||||||
sensitive={status.get('sensitive')}
|
height={110}
|
||||||
onOpenVideo={this.handleOpenVideo}
|
sensitive={status.get('sensitive')}
|
||||||
/>}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import StatusContainer from '../containers/status_container';
|
import StatusContainer from '../containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class StatusList extends ImmutablePureComponent {
|
export default class StatusList extends ImmutablePureComponent {
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
trackScroll: PropTypes.bool,
|
trackScroll: PropTypes.bool,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
|
isPartial: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
@@ -48,8 +50,23 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, ...other } = this.props;
|
const { statusIds, ...other } = this.props;
|
||||||
const { isLoading } = other;
|
const { isLoading, isPartial } = other;
|
||||||
|
|
||||||
|
if (isPartial) {
|
||||||
|
return (
|
||||||
|
<div className='regeneration-indicator'>
|
||||||
|
<div>
|
||||||
|
<div className='regeneration-indicator__figure' />
|
||||||
|
|
||||||
|
<div className='regeneration-indicator__label'>
|
||||||
|
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' />
|
||||||
|
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const scrollableContent = (isLoading || statusIds.size > 0) ? (
|
const scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||||
statusIds.map((statusId) => (
|
statusIds.map((statusId) => (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Avatar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||||
{({ radius }) =>
|
{({ radius }) => (
|
||||||
<a
|
<a
|
||||||
href={account.get('url')}
|
href={account.get('url')}
|
||||||
className='account__header__avatar'
|
className='account__header__avatar'
|
||||||
@@ -56,7 +56,7 @@ class Avatar extends ImmutablePureComponent {
|
|||||||
>
|
>
|
||||||
<span style={{ display: 'none' }}>{account.get('acct')}</span>
|
<span style={{ display: 'none' }}>{account.get('acct')}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.get('moved')) {
|
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ export default class AccountGallery extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account-gallery__container'>
|
<div className='account-gallery__container'>
|
||||||
{medias.map(media =>
|
{medias.map(media => (
|
||||||
<MediaItem
|
<MediaItem
|
||||||
key={media.get('id')}
|
key={media.get('id')}
|
||||||
media={media}
|
media={media}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
{loadMore}
|
{loadMore}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default class MovedNote extends ImmutablePureComponent {
|
|||||||
<div className='account__moved-note'>
|
<div className='account__moved-note'>
|
||||||
<div className='account__moved-note__message'>
|
<div className='account__moved-note__message'>
|
||||||
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
|
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
|
||||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} />
|
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
|
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
|
||||||
{items.map(item =>
|
{items.map(item => (
|
||||||
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}>
|
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}>
|
||||||
<div className='privacy-dropdown__option__icon'>
|
<div className='privacy-dropdown__option__icon'>
|
||||||
<i className={`fa fa-fw fa-${item.icon}`} />
|
<i className={`fa fa-fw fa-${item.icon}`} />
|
||||||
@@ -83,7 +83,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||||||
{item.meta}
|
{item.meta}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||||||
count += results.get('hashtags').size;
|
count += results.get('hashtags').size;
|
||||||
hashtags = (
|
hashtags = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
{results.get('hashtags').map(hashtag =>
|
{results.get('hashtags').map(hashtag => (
|
||||||
<Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
|
<Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
|
||||||
#{hashtag}
|
#{hashtag}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,27 @@ import PropTypes from 'prop-types';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
|
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i;
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
||||||
|
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
|
||||||
});
|
});
|
||||||
|
|
||||||
const WarningWrapper = ({ needsLockWarning }) => {
|
const WarningWrapper = ({ needsLockWarning, hashtagWarning }) => {
|
||||||
if (needsLockWarning) {
|
if (needsLockWarning) {
|
||||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||||
}
|
}
|
||||||
|
if (hashtagWarning) {
|
||||||
|
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
WarningWrapper.propTypes = {
|
WarningWrapper.propTypes = {
|
||||||
needsLockWarning: PropTypes.bool,
|
needsLockWarning: PropTypes.bool,
|
||||||
|
hashtagWarning: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(WarningWrapper);
|
export default connect(mapStateToProps)(WarningWrapper);
|
||||||
|
|||||||
@@ -94,14 +94,15 @@ export default class Compose extends React.PureComponent {
|
|||||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
<NavigationContainer onClose={this.onBlur} />
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
|
{multiColumn && <div className='mastodon' />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||||
{({ x }) =>
|
{({ x }) => (
|
||||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||||
<SearchResultsContainer />
|
<SearchResultsContainer />
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,98 +40,98 @@ export const urlRegex = (function() {
|
|||||||
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
||||||
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
||||||
regexen.validGTLD = regexSupplant(RegExp(
|
regexen.validGTLD = regexSupplant(RegExp(
|
||||||
'(?:(?:' +
|
'(?:(?:' +
|
||||||
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
|
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
|
||||||
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
|
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
|
||||||
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
|
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
|
||||||
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
|
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
|
||||||
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
|
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
|
||||||
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
|
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
|
||||||
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
|
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
|
||||||
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
|
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
|
||||||
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
|
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
|
||||||
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
|
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
|
||||||
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
|
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
|
||||||
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
|
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
|
||||||
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
|
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
|
||||||
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
|
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
|
||||||
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
|
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
|
||||||
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
|
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
|
||||||
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
|
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
|
||||||
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
|
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
|
||||||
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
|
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
|
||||||
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
|
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
|
||||||
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
|
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
|
||||||
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
|
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
|
||||||
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
|
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
|
||||||
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
|
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
|
||||||
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
|
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
|
||||||
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
|
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
|
||||||
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
|
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
|
||||||
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
|
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
|
||||||
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
|
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
|
||||||
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
|
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
|
||||||
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
|
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
|
||||||
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
|
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
|
||||||
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
|
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
|
||||||
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
|
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
|
||||||
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
|
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
|
||||||
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
|
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
|
||||||
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
|
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
|
||||||
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
|
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
|
||||||
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
|
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
|
||||||
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
|
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
|
||||||
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
|
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
|
||||||
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
|
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
|
||||||
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
|
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
|
||||||
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
|
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
|
||||||
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
|
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
|
||||||
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
|
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
|
||||||
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
|
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
|
||||||
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
|
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
|
||||||
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
|
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
|
||||||
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
|
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
|
||||||
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
|
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
|
||||||
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
|
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
|
||||||
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
|
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
|
||||||
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
|
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
|
||||||
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
|
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
|
||||||
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
|
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
|
||||||
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
|
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
|
||||||
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
|
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
|
||||||
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
|
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
|
||||||
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
|
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
|
||||||
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
|
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
|
||||||
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
|
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
|
||||||
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
|
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
|
||||||
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
|
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
|
||||||
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
|
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
|
||||||
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
|
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
|
||||||
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
|
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
|
||||||
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
|
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
|
||||||
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
|
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
|
||||||
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
|
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
|
||||||
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
|
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
|
||||||
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
|
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
|
||||||
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
|
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
|
||||||
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
|
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
|
||||||
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
|
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
|
||||||
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
|
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
|
||||||
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
|
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
|
||||||
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
|
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
|
||||||
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
|
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
|
||||||
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
|
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
|
||||||
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
|
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
|
||||||
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
|
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
|
||||||
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
|
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
|
||||||
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
|
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
|
||||||
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
|
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
|
||||||
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
|
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
|
||||||
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
|
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
|
||||||
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
|
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
|
||||||
')(?=[^0-9a-zA-Z@]|$))'));
|
')(?=[^0-9a-zA-Z@]|$))'));
|
||||||
regexen.validCCTLD = regexSupplant(RegExp(
|
regexen.validCCTLD = regexSupplant(RegExp(
|
||||||
'(?:(?:' +
|
'(?:(?:' +
|
||||||
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
|
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
|
||||||
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
|
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
|
||||||
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
|
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
|
||||||
@@ -143,7 +143,7 @@ export const urlRegex = (function() {
|
|||||||
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
|
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
|
||||||
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
|
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
|
||||||
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
|
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
|
||||||
')(?=[^0-9a-zA-Z@]|$))'));
|
')(?=[^0-9a-zA-Z@]|$))'));
|
||||||
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
|
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
|
||||||
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
|
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
|
||||||
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
|
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
|
||||||
@@ -168,8 +168,8 @@ export const urlRegex = (function() {
|
|||||||
'#{validGeneralUrlPathChars}*' +
|
'#{validGeneralUrlPathChars}*' +
|
||||||
')' +
|
')' +
|
||||||
')' +
|
')' +
|
||||||
'\\)'
|
'\\)',
|
||||||
, 'i');
|
'i');
|
||||||
// Valid end-of-path chracters (so /foo. does not gobble the period).
|
// Valid end-of-path chracters (so /foo. does not gobble the period).
|
||||||
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
|
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
|
||||||
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
|
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
|
||||||
@@ -190,7 +190,7 @@ export const urlRegex = (function() {
|
|||||||
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
|
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
|
||||||
'(\\/#{validUrlPath}*)?' + // $5 URL Path
|
'(\\/#{validUrlPath}*)?' + // $5 URL Path
|
||||||
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
|
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
|
||||||
')'
|
')',
|
||||||
, 'gi');
|
'gi');
|
||||||
return regexen.validUrl;
|
return regexen.validUrl;
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ describe('emoji', () => {
|
|||||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
||||||
expect(emojify('👨👩👧👧')).toEqual(
|
expect(emojify('👨👩👧👧')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
||||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
||||||
expect(emojify('\u2757')).toEqual(
|
expect(emojify('\u2757')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does multiple unicode', () => {
|
it('does multiple unicode', () => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import PropTypes from 'prop-types';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from '../../initial_state';
|
import { me } from '../../initial_state';
|
||||||
|
import { fetchFollowRequests } from '../../actions/accounts';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
@@ -32,9 +34,25 @@ const messages = defineMessages({
|
|||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
myAccount: state.getIn(['accounts', me]),
|
myAccount: state.getIn(['accounts', me]),
|
||||||
columns: state.getIn(['settings', 'columns']),
|
columns: state.getIn(['settings', 'columns']),
|
||||||
|
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
|
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeDisplay = (number, limit) => {
|
||||||
|
if (number === 0) {
|
||||||
|
return undefined;
|
||||||
|
} else if (limit && number >= limit) {
|
||||||
|
return `${limit}+`;
|
||||||
|
} else {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@connect(mapStateToProps, mapDispatchToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class GettingStarted extends ImmutablePureComponent {
|
export default class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
@@ -43,12 +61,23 @@ export default class GettingStarted extends ImmutablePureComponent {
|
|||||||
myAccount: ImmutablePropTypes.map.isRequired,
|
myAccount: ImmutablePropTypes.map.isRequired,
|
||||||
columns: ImmutablePropTypes.list,
|
columns: ImmutablePropTypes.list,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
fetchFollowRequests: PropTypes.func.isRequired,
|
||||||
|
unreadFollowRequests: PropTypes.number,
|
||||||
|
unreadNotifications: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
componentDidMount () {
|
||||||
const { intl, myAccount, columns, multiColumn } = this.props;
|
const { myAccount, fetchFollowRequests } = this.props;
|
||||||
|
|
||||||
let navItems = [];
|
if (myAccount.get('locked')) {
|
||||||
|
fetchFollowRequests();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications } = this.props;
|
||||||
|
|
||||||
|
const navItems = [];
|
||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
if (!columns.find(item => item.get('id') === 'HOME')) {
|
if (!columns.find(item => item.get('id') === 'HOME')) {
|
||||||
@@ -56,7 +85,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
|
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
|
||||||
navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} to='/notifications' />);
|
navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
|
if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
|
||||||
@@ -68,21 +97,20 @@ export default class GettingStarted extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems = navItems.concat([
|
navItems.push(
|
||||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||||
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||||
<ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
if (myAccount.get('locked')) {
|
if (myAccount.get('locked')) {
|
||||||
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems = navItems.concat([
|
if (multiColumn) {
|
||||||
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||||
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
|
}
|
||||||
<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
|
|
||||||
]);
|
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
||||||
@@ -90,24 +118,24 @@ export default class GettingStarted extends ImmutablePureComponent {
|
|||||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
||||||
{navItems}
|
{navItems}
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
||||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
|
||||||
|
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||||
|
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='getting-started__footer scrollable optionally-scrollable'>
|
<div className='static-content getting-started'>
|
||||||
<div className='static-content getting-started'>
|
<p>
|
||||||
<p>
|
<a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
|
||||||
<a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='getting_started.open_source_notice'
|
||||||
id='getting_started.open_source_notice'
|
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
values={{ github: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> }}
|
||||||
values={{ github: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> }}
|
/>
|
||||||
/>
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline, refreshHomeTimeline } from '../../actions/timelines';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
@@ -16,6 +16,7 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
|
isPartial: state.getIn(['timelines', 'home', 'isPartial'], false),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
@@ -26,6 +27,7 @@ export default class HomeTimeline extends React.PureComponent {
|
|||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
|
isPartial: PropTypes.bool,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@@ -57,6 +59,39 @@ export default class HomeTimeline extends React.PureComponent {
|
|||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this._checkIfReloadNeeded(false, this.props.isPartial);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this._stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkIfReloadNeeded (wasPartial, isPartial) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
if (wasPartial === isPartial) {
|
||||||
|
return;
|
||||||
|
} else if (!wasPartial && isPartial) {
|
||||||
|
this.polling = setInterval(() => {
|
||||||
|
dispatch(refreshHomeTimeline());
|
||||||
|
}, 3000);
|
||||||
|
} else if (wasPartial && !isPartial) {
|
||||||
|
this._stopPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopPolling () {
|
||||||
|
if (this.polling) {
|
||||||
|
clearInterval(this.polling);
|
||||||
|
this.polling = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|||||||
@@ -33,59 +33,59 @@ export default class KeyboardShortcuts extends ImmutablePureComponent {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>r</code></td>
|
<td><kbd>r</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>m</code></td>
|
<td><kbd>m</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>f</code></td>
|
<td><kbd>f</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>b</code></td>
|
<td><kbd>b</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>enter</code></td>
|
<td><kbd>enter</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>up</code></td>
|
<td><kbd>up</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>down</code></td>
|
<td><kbd>down</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>1</code>-<code>9</code></td>
|
<td><kbd>1</kbd>-<kbd>9</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>n</code></td>
|
<td><kbd>n</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>alt</code>+<code>n</code></td>
|
<td><kbd>alt</kbd>+<kbd>n</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>backspace</code></td>
|
<td><kbd>backspace</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>s</code></td>
|
<td><kbd>s</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>esc</code></td>
|
<td><kbd>esc</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>?</code></td>
|
<td><kbd>?</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ export default class ListEditor extends ImmutablePureComponent {
|
|||||||
{showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
|
{showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||||
{({ x }) =>
|
{({ x }) => (
|
||||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,13 +120,17 @@ export default class ListTimeline extends React.PureComponent {
|
|||||||
if (typeof list === 'undefined') {
|
if (typeof list === 'undefined') {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<LoadingIndicator />
|
<div className='scrollable'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
} else if (list === false) {
|
} else if (list === false) {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<MissingIndicator />
|
<div className='scrollable'>
|
||||||
|
<MissingIndicator />
|
||||||
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onSave: PropTypes.func.isRequired,
|
|
||||||
onClear: PropTypes.func.isRequired,
|
onClear: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
onPushChange = (key, checked) => {
|
onPushChange = (path, checked) => {
|
||||||
this.props.onChange(['push', ...key], checked);
|
this.props.onChange(['push', ...path], checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -40,10 +39,10 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,10 +50,10 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,10 +61,10 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,10 +72,10 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} />;
|
const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
|
||||||
|
|
||||||
switch(notification.get('type')) {
|
switch(notification.get('type')) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
|||||||
@@ -8,23 +8,23 @@ export default class SettingToggle extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
prefix: PropTypes.string,
|
prefix: PropTypes.string,
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
settingKey: PropTypes.array.isRequired,
|
settingPath: PropTypes.array.isRequired,
|
||||||
label: PropTypes.node.isRequired,
|
label: PropTypes.node.isRequired,
|
||||||
meta: PropTypes.node,
|
meta: PropTypes.node,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = ({ target }) => {
|
onChange = ({ target }) => {
|
||||||
this.props.onChange(this.props.settingKey, target.checked);
|
this.props.onChange(this.props.settingPath, target.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { prefix, settings, settingKey, label, meta } = this.props;
|
const { prefix, settings, settingPath, label, meta } = this.props;
|
||||||
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
|
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='setting-toggle'>
|
<div className='setting-toggle'>
|
||||||
<Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||||
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
|
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
|
||||||
{meta && <span className='setting-meta__label'>{meta}</span>}
|
{meta && <span className='setting-meta__label'>{meta}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ColumnSettings from '../components/column_settings';
|
import ColumnSettings from '../components/column_settings';
|
||||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import { clearNotifications } from '../../../actions/notifications';
|
import { clearNotifications } from '../../../actions/notifications';
|
||||||
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications';
|
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -18,19 +18,14 @@ const mapStateToProps = state => ({
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onChange (key, checked) {
|
onChange (path, checked) {
|
||||||
if (key[0] === 'push') {
|
if (path[0] === 'push') {
|
||||||
dispatch(changePushNotifications(key.slice(1), checked));
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...key], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSave () {
|
|
||||||
dispatch(saveSettings());
|
|
||||||
dispatch(savePushNotificationSettings());
|
|
||||||
},
|
|
||||||
|
|
||||||
onClear () {
|
onClear () {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.clearMessage),
|
message: intl.formatMessage(messages.clearMessage),
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const messages = defineMessages({
|
|||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
|
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||||
|
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||||
|
block: { id: 'status.block', defaultMessage: 'Block @{name}' },
|
||||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||||
@@ -34,6 +38,9 @@ export default class ActionBar extends React.PureComponent {
|
|||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
onMute: PropTypes.func,
|
||||||
|
onMuteConversation: PropTypes.func,
|
||||||
|
onBlock: PropTypes.func,
|
||||||
onReport: PropTypes.func,
|
onReport: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
@@ -60,6 +67,18 @@ export default class ActionBar extends React.PureComponent {
|
|||||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMuteClick = () => {
|
||||||
|
this.props.onMute(this.props.status.get('account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConversationMuteClick = () => {
|
||||||
|
this.props.onMuteConversation(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlockClick = () => {
|
||||||
|
this.props.onBlock(this.props.status.get('account'));
|
||||||
|
}
|
||||||
|
|
||||||
handleReport = () => {
|
handleReport = () => {
|
||||||
this.props.onReport(this.props.status);
|
this.props.onReport(this.props.status);
|
||||||
}
|
}
|
||||||
@@ -83,6 +102,7 @@ export default class ActionBar extends React.PureComponent {
|
|||||||
const { status, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const mutingConversation = status.get('muted');
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
@@ -95,10 +115,15 @@ export default class ActionBar extends React.PureComponent {
|
|||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||