mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 16:28:59 +00:00
Compare commits
484 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1955a3f444 | ||
|
|
8ebed7fc68 | ||
|
|
8f2ed79a0b | ||
|
|
2b6b89491d | ||
|
|
f902a335f9 | ||
|
|
e4af4898de | ||
|
|
6d70a80263 | ||
|
|
0cbcc5e297 | ||
|
|
f87b51fda8 | ||
|
|
7e2e0d6dcc | ||
|
|
181115422c | ||
|
|
86d1dcc97a | ||
|
|
86eaaf0761 | ||
|
|
19f63ff801 | ||
|
|
babbb2135e | ||
|
|
99226aba93 | ||
|
|
42c9d5111a | ||
|
|
e0b5a94a4b | ||
|
|
26ec042f38 | ||
|
|
73b0af5c93 | ||
|
|
7efde22c3a | ||
|
|
90760eae4c | ||
|
|
fc34e0e191 | ||
|
|
de72db99fa | ||
|
|
77d1447ac4 | ||
|
|
2c329f2b69 | ||
|
|
c2762fa498 | ||
|
|
0611209141 | ||
|
|
da302a43cd | ||
|
|
8f8319852c | ||
|
|
75f416a492 | ||
|
|
24baaa17e8 | ||
|
|
c99fc08a0d | ||
|
|
a8f45c0838 | ||
|
|
6df63465b9 | ||
|
|
13b11ddc8c | ||
|
|
f2997c9715 | ||
|
|
e17f9d5e1a | ||
|
|
865cb39e9b | ||
|
|
5d710b1139 | ||
|
|
8a58942c80 | ||
|
|
f97272549c | ||
|
|
363de2dffd | ||
|
|
6a1ac9b31f | ||
|
|
3834e1e69b | ||
|
|
95bcbaa434 | ||
|
|
911338bdcc | ||
|
|
0f8b7d0660 | ||
|
|
e332552816 | ||
|
|
0d83569899 | ||
|
|
515434ed87 | ||
|
|
3d3e32befb | ||
|
|
fa08b5079d | ||
|
|
00392d3c63 | ||
|
|
28606d730a | ||
|
|
1c8477eab2 | ||
|
|
6670e6d33f | ||
|
|
9d2f55ecc3 | ||
|
|
b7ec2fd492 | ||
|
|
3b8908c114 | ||
|
|
7b10794afb | ||
|
|
355965c17b | ||
|
|
0f889523e4 | ||
|
|
f4045ba3d9 | ||
|
|
df4ff9a8e1 | ||
|
|
67ad84b7eb | ||
|
|
f0f6a3279a | ||
|
|
9e620ca16c | ||
|
|
bddd36f260 | ||
|
|
b857551617 | ||
|
|
e28a5aab08 | ||
|
|
f9d7ec8971 | ||
|
|
40fd1de488 | ||
|
|
31f0bcf804 | ||
|
|
09540192c9 | ||
|
|
08059ddda9 | ||
|
|
a3aa9381c4 | ||
|
|
120a37a197 | ||
|
|
ec9999cdfe | ||
|
|
e806d3c3f0 | ||
|
|
4ba6acd518 | ||
|
|
061922b38c | ||
|
|
5d8d827436 | ||
|
|
cde1f37d93 | ||
|
|
89707ad0ac | ||
|
|
4bebeb27d3 | ||
|
|
ef879a8839 | ||
|
|
9240ca6cef | ||
|
|
619817d29e | ||
|
|
0655f16cc1 | ||
|
|
ade004b5ee | ||
|
|
7609593e48 | ||
|
|
a9529d3b4b | ||
|
|
0d2910478a | ||
|
|
5ab0ffc6c8 | ||
|
|
8482f67caf | ||
|
|
fe8dd58bc1 | ||
|
|
92cd207c50 | ||
|
|
f5cd138323 | ||
|
|
66ea015a01 | ||
|
|
1616cf98a1 | ||
|
|
5aae6384ff | ||
|
|
fa89deb4eb | ||
|
|
26a892dd90 | ||
|
|
8321884eef | ||
|
|
290e8ef854 | ||
|
|
067eb220c6 | ||
|
|
1467515d3d | ||
|
|
8b74aa4217 | ||
|
|
a6807201d2 | ||
|
|
4445ebdad2 | ||
|
|
c019b0acfd | ||
|
|
7447e7a2ea | ||
|
|
c8be05a4a7 | ||
|
|
41c697fd81 | ||
|
|
c5afe573da | ||
|
|
485310a43c | ||
|
|
9aae9ae40c | ||
|
|
1fb125b630 | ||
|
|
057567d548 | ||
|
|
169c68a739 | ||
|
|
9f182346d7 | ||
|
|
a58c935c3d | ||
|
|
c0c56db0fa | ||
|
|
d9dc0fe84e | ||
|
|
55b56e3f95 | ||
|
|
c4d39b1b3d | ||
|
|
ac54da9394 | ||
|
|
043862f411 | ||
|
|
9e5c1c487e | ||
|
|
5619099564 | ||
|
|
ce80d0b0a9 | ||
|
|
6327f69cab | ||
|
|
5f8155482a | ||
|
|
efcf9448da | ||
|
|
e70b84b1dc | ||
|
|
d7a4e8739a | ||
|
|
9362700137 | ||
|
|
1206627c59 | ||
|
|
edefcfcf42 | ||
|
|
b330d1f000 | ||
|
|
1a5a54eb4b | ||
|
|
447b8bc44e | ||
|
|
093879c177 | ||
|
|
d2c20936f3 | ||
|
|
0220f3a171 | ||
|
|
905a4faa1c | ||
|
|
5355b7d930 | ||
|
|
aec2458d81 | ||
|
|
4fe5e04ea4 | ||
|
|
3f42ad7d1a | ||
|
|
f7c466c8d8 | ||
|
|
245b9cb4ba | ||
|
|
9275f92972 | ||
|
|
4a6d3bac86 | ||
|
|
00cc3066a2 | ||
|
|
a57d30c680 | ||
|
|
467d32fce3 | ||
|
|
8aadb7b0b2 | ||
|
|
79546799af | ||
|
|
90d0018fd5 | ||
|
|
1a12fd14d4 | ||
|
|
282bb55c3c | ||
|
|
0e4479bb3a | ||
|
|
af7e880df5 | ||
|
|
aa7bf1515c | ||
|
|
4f781b17cc | ||
|
|
137100dcf3 | ||
|
|
3a9eb81a80 | ||
|
|
0e39cc6a35 | ||
|
|
faefd8ec8f | ||
|
|
a18fd491b9 | ||
|
|
96715d9af5 | ||
|
|
f24daa399b | ||
|
|
af96e71883 | ||
|
|
5dc73339ae | ||
|
|
ccaf3dbc5a | ||
|
|
1ea662963f | ||
|
|
bd834add56 | ||
|
|
9966bd27c2 | ||
|
|
b0ab632531 | ||
|
|
e1264bbd92 | ||
|
|
38e24a699b | ||
|
|
bf3e56b8ad | ||
|
|
9b698bf448 | ||
|
|
0254ee9795 | ||
|
|
e32edd247f | ||
|
|
dab9b5bd3a | ||
|
|
e17b5b228d | ||
|
|
c4baa9fb6b | ||
|
|
c2a31b8032 | ||
|
|
9e63bf446e | ||
|
|
c44a700252 | ||
|
|
aa90798386 | ||
|
|
0930ce5560 | ||
|
|
7f0a865b05 | ||
|
|
08fce08217 | ||
|
|
3064ef96a1 | ||
|
|
ee69ece7b5 | ||
|
|
d90d23699c | ||
|
|
1f5ff46fd9 | ||
|
|
13528f50c3 | ||
|
|
dd1ae3b109 | ||
|
|
b352a8e5d4 | ||
|
|
fd102059aa | ||
|
|
323671a653 | ||
|
|
b155e6ccf5 | ||
|
|
f16b9a4928 | ||
|
|
24eb45425e | ||
|
|
3442bc0ea3 | ||
|
|
40bdf43297 | ||
|
|
8ead070b94 | ||
|
|
b22b2cbfac | ||
|
|
2f2b84bfbb | ||
|
|
5cdd2c2414 | ||
|
|
3ddd936b03 | ||
|
|
1921c5416b | ||
|
|
fc47c1d00e | ||
|
|
327a6e166f | ||
|
|
6f5268b02d | ||
|
|
4964433190 | ||
|
|
9e3c4fd2d7 | ||
|
|
89e8e110c8 | ||
|
|
9f7ea77d0c | ||
|
|
5f74397ef0 | ||
|
|
960181fd99 | ||
|
|
2a7602cad4 | ||
|
|
47aacb773b | ||
|
|
82d9336114 | ||
|
|
e60286a344 | ||
|
|
53850bce93 | ||
|
|
1236529e39 | ||
|
|
06444bf050 | ||
|
|
b723ee73fc | ||
|
|
c35bda0551 | ||
|
|
f53fb6aa66 | ||
|
|
a85d4473aa | ||
|
|
c9b9225951 | ||
|
|
11898a6461 | ||
|
|
01e5447e35 | ||
|
|
4ada50985a | ||
|
|
a283786463 | ||
|
|
12f72e1740 | ||
|
|
b57eed4584 | ||
|
|
3672a799d4 | ||
|
|
3fd5385e7b | ||
|
|
d439855a6d | ||
|
|
2810013b93 | ||
|
|
0687ab8ae3 | ||
|
|
64dbde0dbf | ||
|
|
ae57b3a8c5 | ||
|
|
0dbbc16c69 | ||
|
|
f690320fb9 | ||
|
|
553170b77a | ||
|
|
8a6096a3de | ||
|
|
d2f6d9b9fb | ||
|
|
dbe9f33fdc | ||
|
|
1be6aa0c7f | ||
|
|
087ca3009b | ||
|
|
db7c7d1af1 | ||
|
|
42fb4faa0f | ||
|
|
9bb398ee91 | ||
|
|
9043b32183 | ||
|
|
e30bbb1cb0 | ||
|
|
8bdf02812c | ||
|
|
93db265be7 | ||
|
|
c172919745 | ||
|
|
15d442cf9d | ||
|
|
43f955e31f | ||
|
|
4ea4ef9d0f | ||
|
|
d19ed18388 | ||
|
|
f0bd439486 | ||
|
|
b16fbd52b2 | ||
|
|
3b34c28bee | ||
|
|
8bfdbf0aa6 | ||
|
|
d4fe6cd2bf | ||
|
|
ea6c930c04 | ||
|
|
12e29c9660 | ||
|
|
082bef3027 | ||
|
|
e6b48a7048 | ||
|
|
ba2aea3a80 | ||
|
|
e5282e4ec0 | ||
|
|
53eb31f124 | ||
|
|
388ec0d5b6 | ||
|
|
71706f21c2 | ||
|
|
b1881a3d48 | ||
|
|
d5a675099a | ||
|
|
c3e7bac1cc | ||
|
|
6e3925521d | ||
|
|
b89f007862 | ||
|
|
9acdb166e8 | ||
|
|
470eb0042e | ||
|
|
fc146a19cc | ||
|
|
941a593ea8 | ||
|
|
982fef811e | ||
|
|
41f8fde83e | ||
|
|
157f0a2aa7 | ||
|
|
49043f644d | ||
|
|
c803f5b440 | ||
|
|
f860eb7d71 | ||
|
|
8f9a11b642 | ||
|
|
c6b0707cf6 | ||
|
|
b79c80b620 | ||
|
|
211920b622 | ||
|
|
01d8003867 | ||
|
|
ed3dfd0bee | ||
|
|
35eff3f2d0 | ||
|
|
ca44c13455 | ||
|
|
a345eb44fc | ||
|
|
7b814d5bcb | ||
|
|
9f9f4b248e | ||
|
|
3660f01f60 | ||
|
|
a2ec54a20b | ||
|
|
e33dcb79c6 | ||
|
|
7d5ea5c170 | ||
|
|
33849acfa7 | ||
|
|
c141f0a886 | ||
|
|
55d03da303 | ||
|
|
2c3a730eae | ||
|
|
b04cbb9f5d | ||
|
|
75aade3de2 | ||
|
|
ac0b84534e | ||
|
|
3c48c9ac2e | ||
|
|
ecf0320a78 | ||
|
|
40703b96fa | ||
|
|
1e4453405b | ||
|
|
0ad694f96b | ||
|
|
541c538f9b | ||
|
|
50910d1543 | ||
|
|
4ff8653d6c | ||
|
|
17690f51a2 | ||
|
|
cf80cb5e8b | ||
|
|
315ff648c8 | ||
|
|
41923d1c6b | ||
|
|
63686fd36f | ||
|
|
ae9d2f4a32 | ||
|
|
741bbba6ff | ||
|
|
a25a384af3 | ||
|
|
971c4de18c | ||
|
|
394c8ef680 | ||
|
|
3c5b0c55cb | ||
|
|
0dcf3c6abe | ||
|
|
b67b60fec3 | ||
|
|
2c0ef75f58 | ||
|
|
cc16fa7513 | ||
|
|
d6827e38a6 | ||
|
|
29ed448445 | ||
|
|
e7d7a99fbc | ||
|
|
8d27de32b3 | ||
|
|
abab82ec1e | ||
|
|
f0797bf8ce | ||
|
|
38c5130930 | ||
|
|
ee8af9083a | ||
|
|
c9f15f7991 | ||
|
|
91afe1f8fd | ||
|
|
9edee2e64f | ||
|
|
d7e1a282fe | ||
|
|
c890b86ef6 | ||
|
|
5b571fc434 | ||
|
|
3e4eb9c95f | ||
|
|
ceba26d527 | ||
|
|
7826b5e93d | ||
|
|
fc7b830719 | ||
|
|
c945f29e96 | ||
|
|
0d4d42dce6 | ||
|
|
cf03634e74 | ||
|
|
698d74a15f | ||
|
|
d7d165db5b | ||
|
|
d7f4300ee3 | ||
|
|
7632178300 | ||
|
|
e4e948a21b | ||
|
|
fef478781d | ||
|
|
c1a553d2c2 | ||
|
|
3e8c1a1c36 | ||
|
|
9fd8bbe15c | ||
|
|
1b42f717f2 | ||
|
|
7004c69204 | ||
|
|
9981972844 | ||
|
|
5f61ef2417 | ||
|
|
cf13c97cb2 | ||
|
|
90a408f592 | ||
|
|
451b7431c9 | ||
|
|
530725ba3c | ||
|
|
6efaee30b1 | ||
|
|
698fe3686a | ||
|
|
05be34a94b | ||
|
|
cb58694a81 | ||
|
|
e51b6bba94 | ||
|
|
7f393a0b68 | ||
|
|
9f43e3b428 | ||
|
|
4a40b40324 | ||
|
|
4c05f0e630 | ||
|
|
d438eab673 | ||
|
|
7b7bff04df | ||
|
|
e659608797 | ||
|
|
ed332693fe | ||
|
|
881e4277fd | ||
|
|
5dfc9854f1 | ||
|
|
b300bb3b4e | ||
|
|
6d519e6fd1 | ||
|
|
d2c9cc31de | ||
|
|
065defefac | ||
|
|
6bfe068904 | ||
|
|
bc237d17a7 | ||
|
|
485d75a805 | ||
|
|
4b621188ad | ||
|
|
4fb24a70d3 | ||
|
|
b2a7218ab7 | ||
|
|
a872f2f4c6 | ||
|
|
32a6f0884c | ||
|
|
a54af44975 | ||
|
|
f113af5350 | ||
|
|
f578cf8331 | ||
|
|
3cb13bdd84 | ||
|
|
0bf31f5436 | ||
|
|
a7ab2204d4 | ||
|
|
36a83cc4f9 | ||
|
|
30903d5f02 | ||
|
|
94536af96d | ||
|
|
0b32b5108e | ||
|
|
12f1cdeed1 | ||
|
|
e2f024147c | ||
|
|
1961825ff9 | ||
|
|
32748c0f71 | ||
|
|
37a36b0bec | ||
|
|
c7d9b81d41 | ||
|
|
4fdeac21f4 | ||
|
|
131f505fd0 | ||
|
|
27012aaeb6 | ||
|
|
4c751d25e5 | ||
|
|
ad5ddd5e95 | ||
|
|
13c0077003 | ||
|
|
d4c94fa004 | ||
|
|
38bec79811 | ||
|
|
0f4fa59812 | ||
|
|
41396de7a9 | ||
|
|
2ac8a590cd | ||
|
|
0c4e9fdda0 | ||
|
|
8e7d0bda40 | ||
|
|
5d43a9cae2 | ||
|
|
8a4ff30ceb | ||
|
|
56f4a94e7b | ||
|
|
4b08b7c502 | ||
|
|
cfbd90cf44 | ||
|
|
acf6436a99 | ||
|
|
a0f1f9c664 | ||
|
|
f0d1107c53 | ||
|
|
02c1ad5347 | ||
|
|
a736b28646 | ||
|
|
0d5d3c7abe | ||
|
|
c441208e29 | ||
|
|
07495cc13f | ||
|
|
6a88151eda | ||
|
|
30619a6716 | ||
|
|
eadac4e7f4 | ||
|
|
0209b7d1b5 | ||
|
|
c62696bc46 | ||
|
|
83530f0eef | ||
|
|
73b8e67f4b | ||
|
|
b5d87500d2 | ||
|
|
d025c5e593 | ||
|
|
7aede8e720 | ||
|
|
eb98c99924 | ||
|
|
6b41fb2e6f | ||
|
|
bf7cefa516 | ||
|
|
65b3a2a5a6 | ||
|
|
8fa8004a2b | ||
|
|
e5566ac6a6 | ||
|
|
ac1989d2c0 | ||
|
|
32d4b51939 | ||
|
|
b38bd58921 | ||
|
|
8989569dd4 | ||
|
|
96812a6c79 | ||
|
|
a31f5765af | ||
|
|
41a78be25e | ||
|
|
9cf0b5b255 | ||
|
|
4de3182dc8 | ||
|
|
9a534d1df6 | ||
|
|
28fb01c71a | ||
|
|
93e53a3311 | ||
|
|
b73cee9774 | ||
|
|
4512fde181 | ||
|
|
8736ef50ad | ||
|
|
0700521ef3 |
@@ -1,10 +1,6 @@
|
||||
engines:
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
enabled: false
|
||||
rubocop:
|
||||
enabled: true
|
||||
eslint:
|
||||
|
||||
@@ -6,3 +6,6 @@ node_modules
|
||||
storybook
|
||||
neo4j
|
||||
vendor/bundle
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
|
||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -1,6 +1,7 @@
|
||||
# Service dependencies
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
# REDIS_DB=0
|
||||
DB_HOST=db
|
||||
DB_USER=postgres
|
||||
DB_NAME=postgres
|
||||
@@ -11,6 +12,10 @@ DB_PORT=5432
|
||||
LOCAL_DOMAIN=example.com
|
||||
LOCAL_HTTPS=true
|
||||
|
||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||
# Do not use this unless you know exactly what you are doing.
|
||||
# WEB_DOMAIN=mastodon.example.com
|
||||
|
||||
# Application secrets
|
||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||
PAPERCLIP_SECRET=
|
||||
@@ -29,11 +34,21 @@ OTP_SECRET=
|
||||
# DEFAULT_LOCALE=de
|
||||
|
||||
# E-mail configuration
|
||||
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
||||
#SMTP_AUTH_METHOD=plain
|
||||
#SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
|
||||
|
||||
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
||||
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||
# PAPERCLIP_ROOT_URL=/system
|
||||
|
||||
# Optional asset host for multi-server setups
|
||||
# CDN_HOST=assets.example.com
|
||||
@@ -62,3 +77,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
||||
|
||||
# Streaming API integration
|
||||
# STREAMING_API_BASE_URL=
|
||||
|
||||
# Advanced settings
|
||||
# If you need to use pgBouncer, you need to disable prepared statements:
|
||||
# PREPARED_STATEMENTS=false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Federation
|
||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
LOCAL_HTTPS=true
|
||||
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||
|
||||
30
.eslintignore
Normal file
30
.eslintignore
Normal file
@@ -0,0 +1,30 @@
|
||||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore the default SQLite database.
|
||||
/db/*.sqlite3
|
||||
/db/*.sqlite3-journal
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
!/log/.keep
|
||||
/tmp
|
||||
coverage
|
||||
public/system
|
||||
public/assets
|
||||
.env
|
||||
.env.production
|
||||
node_modules/
|
||||
neo4j/
|
||||
|
||||
# Ignore Vagrant files
|
||||
.vagrant/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
config/deploy/*
|
||||
34
.eslintrc
34
.eslintrc
@@ -8,7 +8,8 @@
|
||||
"parser": "babel-eslint",
|
||||
|
||||
"plugins": [
|
||||
"react"
|
||||
"react",
|
||||
"jsx-a11y"
|
||||
],
|
||||
|
||||
"parserOptions": {
|
||||
@@ -43,9 +44,36 @@
|
||||
"no-mixed-spaces-and-tabs": 1,
|
||||
"no-nested-ternary": 1,
|
||||
"no-trailing-spaces": 1,
|
||||
"react/wrap-multilines": 2,
|
||||
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/prop-types": 2,
|
||||
"react/no-multi-comp": 0
|
||||
"react/no-multi-comp": 0,
|
||||
|
||||
"jsx-a11y/accessible-emoji": 1,
|
||||
"jsx-a11y/anchor-has-content": 1,
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||
"jsx-a11y/aria-props": 1,
|
||||
"jsx-a11y/aria-proptypes": 1,
|
||||
"jsx-a11y/aria-role": 1,
|
||||
"jsx-a11y/aria-unsupported-elements": 1,
|
||||
"jsx-a11y/heading-has-content": 1,
|
||||
"jsx-a11y/href-no-hash": 1,
|
||||
"jsx-a11y/html-has-lang": 1,
|
||||
"jsx-a11y/iframe-has-title": 1,
|
||||
"jsx-a11y/img-has-alt": 1,
|
||||
"jsx-a11y/img-redundant-alt": 1,
|
||||
"jsx-a11y/label-has-for": 1,
|
||||
"jsx-a11y/mouse-events-have-key-events": 1,
|
||||
"jsx-a11y/no-access-key": 1,
|
||||
"jsx-a11y/no-distracting-elements": 1,
|
||||
"jsx-a11y/no-onchange": 1,
|
||||
"jsx-a11y/no-redundant-roles": 1,
|
||||
"jsx-a11y/onclick-has-focus": 1,
|
||||
"jsx-a11y/onclick-has-role": 1,
|
||||
"jsx-a11y/role-has-required-aria-props": 1,
|
||||
"jsx-a11y/role-supports-aria-props": 1,
|
||||
"jsx-a11y/scope": 1,
|
||||
"jsx-a11y/tabindex-no-positive": 1
|
||||
}
|
||||
}
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -28,3 +28,17 @@ neo4j/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
config/deploy/*
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
|
||||
# Ignore postgres + redis volume optionally created by docker-compose
|
||||
postgres
|
||||
redis
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
||||
# Ignore vim files
|
||||
*~
|
||||
*.swp
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.3.1
|
||||
2.4.1
|
||||
|
||||
@@ -5,8 +5,6 @@ notifications:
|
||||
email: false
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- TRAVIS_NODE_VERSION="4"
|
||||
global:
|
||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
- LOCAL_HTTPS=true
|
||||
@@ -16,7 +14,8 @@ addons:
|
||||
postgresql: 9.4
|
||||
|
||||
rvm:
|
||||
- 2.3.1
|
||||
- 2.3.4
|
||||
- 2.4.1
|
||||
|
||||
services:
|
||||
- redis-server
|
||||
@@ -28,8 +27,7 @@ before_install:
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install g++-4.8
|
||||
install:
|
||||
- nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install -g npm@3
|
||||
- nvm install
|
||||
- npm install -g yarn
|
||||
- bundle install
|
||||
- yarn install
|
||||
@@ -40,3 +38,4 @@ before_script:
|
||||
script:
|
||||
- bundle exec rspec
|
||||
- npm test
|
||||
- i18n-tasks unused
|
||||
|
||||
@@ -7,7 +7,7 @@ There are three ways in which you can contribute to this repository:
|
||||
2. By working on the back-end application
|
||||
3. By working on the front-end application
|
||||
|
||||
Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise.
|
||||
Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation).
|
||||
|
||||
Below are the guidelines for working on pull requests:
|
||||
|
||||
@@ -41,3 +41,4 @@ It is expected that you have a working development environment set up (see back-
|
||||
* If you are introducing new strings, they must be using localization methods
|
||||
|
||||
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
|
||||
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM ruby:2.3.1-alpine
|
||||
FROM ruby:2.4.1-alpine
|
||||
|
||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||
description="A GNU Social-compatible microblogging server"
|
||||
@@ -12,24 +12,27 @@ WORKDIR /mastodon
|
||||
|
||||
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||
|
||||
RUN BUILD_DEPS=" \
|
||||
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
||||
&& BUILD_DEPS=" \
|
||||
postgresql-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
build-base" \
|
||||
&& apk -U upgrade && apk add \
|
||||
$BUILD_DEPS \
|
||||
nodejs \
|
||||
nodejs@edge \
|
||||
nodejs-npm@edge \
|
||||
libpq \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
ffmpeg \
|
||||
file \
|
||||
imagemagick \
|
||||
imagemagick@edge \
|
||||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
&& bundle install --deployment --without test development \
|
||||
&& yarn \
|
||||
&& npm cache clean \
|
||||
&& yarn --ignore-optional \
|
||||
&& yarn cache clean \
|
||||
&& npm -g cache clean \
|
||||
&& apk del $BUILD_DEPS \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
|
||||
50
Gemfile
50
Gemfile
@@ -1,12 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '2.3.1'
|
||||
ruby '>= 2.3.0', '< 2.5.0'
|
||||
|
||||
gem 'pkg-config'
|
||||
|
||||
gem 'rails', '~> 5.0.2'
|
||||
gem 'sass-rails', '~> 5.0'
|
||||
gem 'uglifier', '>= 1.3.0'
|
||||
gem 'coffee-rails', '~> 4.1.0'
|
||||
gem 'jquery-rails'
|
||||
gem 'puma'
|
||||
|
||||
@@ -21,37 +22,41 @@ gem 'paperclip', '~> 5.1'
|
||||
gem 'paperclip-av-transcoder'
|
||||
gem 'aws-sdk', '>= 2.0'
|
||||
|
||||
gem 'http'
|
||||
gem 'httplog'
|
||||
gem 'addressable'
|
||||
gem 'nokogiri'
|
||||
gem 'link_header'
|
||||
gem 'ostatus2'
|
||||
gem 'goldfinger'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'doorkeeper'
|
||||
gem 'rabl'
|
||||
gem 'rqrcode'
|
||||
gem 'twitter-text'
|
||||
gem 'ox'
|
||||
gem 'oj'
|
||||
gem 'hiredis'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'fast_blank'
|
||||
gem 'goldfinger'
|
||||
gem 'hiredis'
|
||||
gem 'htmlentities'
|
||||
gem 'simple_form'
|
||||
gem 'will_paginate'
|
||||
gem 'http'
|
||||
gem 'http_accept_language'
|
||||
gem 'httplog'
|
||||
gem 'kaminari'
|
||||
gem 'link_header'
|
||||
gem 'nokogiri'
|
||||
gem 'oj'
|
||||
gem 'ostatus2', '~> 1.1'
|
||||
gem 'ox'
|
||||
gem 'rabl'
|
||||
gem 'rack-attack'
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rack-timeout'
|
||||
gem 'rails-i18n'
|
||||
gem 'rails-settings-cached'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'rqrcode'
|
||||
gem 'ruby-oembed', require: 'oembed'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-unique-jobs'
|
||||
gem 'rails-settings-cached'
|
||||
gem 'simple-navigation'
|
||||
gem 'simple_form'
|
||||
gem 'sprockets-rails', :require => 'sprockets/railtie'
|
||||
gem 'statsd-instrument'
|
||||
gem 'ruby-oembed', require: 'oembed'
|
||||
gem 'rack-timeout'
|
||||
gem 'twitter-text'
|
||||
gem 'tzinfo-data'
|
||||
gem 'whatlanguage'
|
||||
|
||||
gem 'react-rails'
|
||||
gem 'browserify-rails'
|
||||
@@ -66,7 +71,10 @@ group :development, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara'
|
||||
gem 'faker'
|
||||
gem 'microformats2'
|
||||
gem 'rails-controller-testing'
|
||||
gem 'rspec-sidekiq'
|
||||
gem 'simplecov', require: false
|
||||
gem 'webmock'
|
||||
@@ -81,7 +89,7 @@ group :development do
|
||||
gem 'bullet'
|
||||
gem 'active_record_query_trace'
|
||||
|
||||
gem 'capistrano'
|
||||
gem 'capistrano', '3.8.0'
|
||||
gem 'capistrano-rails'
|
||||
gem 'capistrano-rbenv'
|
||||
gem 'capistrano-yarn'
|
||||
|
||||
252
Gemfile.lock
252
Gemfile.lock
@@ -24,7 +24,7 @@ GEM
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_record_query_trace (1.5.3)
|
||||
active_record_query_trace (1.5.4)
|
||||
activejob (5.0.2)
|
||||
activesupport (= 5.0.2)
|
||||
globalid (>= 0.3.6)
|
||||
@@ -39,25 +39,25 @@ GEM
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.0)
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
airbrussh (1.1.2)
|
||||
airbrussh (1.2.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
arel (7.1.4)
|
||||
ast (2.3.0)
|
||||
attr_encrypted (3.0.3)
|
||||
encryptor (~> 3.0.0)
|
||||
autoprefixer-rails (6.5.0.2)
|
||||
autoprefixer-rails (6.7.7.1)
|
||||
execjs
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-sdk (2.6.28)
|
||||
aws-sdk-resources (= 2.6.28)
|
||||
aws-sdk-core (2.6.28)
|
||||
aws-sdk (2.9.6)
|
||||
aws-sdk-resources (= 2.9.6)
|
||||
aws-sdk-core (2.9.6)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.6.28)
|
||||
aws-sdk-core (= 2.6.28)
|
||||
aws-sdk-resources (2.9.6)
|
||||
aws-sdk-core (= 2.9.6)
|
||||
aws-sigv4 (1.0.0)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
@@ -78,12 +78,11 @@ GEM
|
||||
railties (>= 4.0.0, < 5.1)
|
||||
sprockets (>= 3.6.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.3.0)
|
||||
bullet (5.5.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
capistrano (3.7.2)
|
||||
capistrano (3.8.0)
|
||||
airbrussh (>= 1.0.0)
|
||||
capistrano-harrow
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
@@ -92,8 +91,7 @@ GEM
|
||||
sshkit (~> 1.2)
|
||||
capistrano-faster-assets (1.0.2)
|
||||
capistrano (>= 3.1)
|
||||
capistrano-harrow (0.5.3)
|
||||
capistrano-rails (1.2.2)
|
||||
capistrano-rails (1.2.3)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rbenv (2.1.0)
|
||||
@@ -101,25 +99,25 @@ GEM
|
||||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (2.13.0)
|
||||
addressable
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
chunky_png (1.3.8)
|
||||
climate_control (0.1.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
coffee-rails (4.1.1)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 4.0.0, < 5.1.x)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
debug_inspector (0.0.2)
|
||||
devise (4.2.0)
|
||||
devise (4.2.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
@@ -131,16 +129,16 @@ GEM
|
||||
devise (~> 4.0)
|
||||
railties
|
||||
rotp (~> 2.0)
|
||||
diff-lcs (1.2.5)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.20161129)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.2.0)
|
||||
doorkeeper (4.2.5)
|
||||
railties (>= 4.2)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
railties (>= 4.0, < 5.1)
|
||||
dotenv (2.2.0)
|
||||
dotenv-rails (2.2.0)
|
||||
dotenv (= 2.2.0)
|
||||
railties (>= 3.2, < 5.1)
|
||||
easy_translate (0.5.0)
|
||||
json
|
||||
thread
|
||||
@@ -148,14 +146,14 @@ GEM
|
||||
encryptor (3.0.0)
|
||||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
fabrication (2.15.2)
|
||||
faker (1.6.6)
|
||||
fabrication (2.16.1)
|
||||
faker (1.7.3)
|
||||
i18n (~> 0.5)
|
||||
fast_blank (1.0.0)
|
||||
font-awesome-rails (4.6.3.1)
|
||||
font-awesome-rails (4.7.0.1)
|
||||
railties (>= 3.2, < 5.1)
|
||||
fuubar (2.1.1)
|
||||
rspec (~> 3.0)
|
||||
fuubar (2.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
@@ -163,20 +161,20 @@ GEM
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
hamlit (2.7.2)
|
||||
temple (~> 0.7.6)
|
||||
hamlit (2.8.1)
|
||||
temple (>= 0.8.0)
|
||||
thor
|
||||
tilt
|
||||
hamlit-rails (0.1.0)
|
||||
hamlit-rails (0.2.0)
|
||||
actionpack (>= 4.0.1)
|
||||
activesupport (>= 4.0.1)
|
||||
hamlit (>= 1.2.0)
|
||||
railties (>= 4.0.1)
|
||||
hashdiff (0.3.0)
|
||||
hashdiff (0.3.2)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
htmlentities (4.3.4)
|
||||
http (2.1.0)
|
||||
http (2.2.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 1.0.1)
|
||||
@@ -184,11 +182,12 @@ GEM
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (1.0.1)
|
||||
http_accept_language (2.1.0)
|
||||
http_parser.rb (0.6.0)
|
||||
httplog (0.3.2)
|
||||
httplog (0.99.2)
|
||||
colorize
|
||||
i18n (0.8.1)
|
||||
i18n-tasks (0.9.6)
|
||||
i18n-tasks (0.9.13)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
easy_translate (>= 0.5.0)
|
||||
@@ -196,19 +195,31 @@ GEM
|
||||
highline (>= 1.7.3)
|
||||
i18n
|
||||
parser (>= 2.2.3.0)
|
||||
term-ansicolor (>= 1.3.2)
|
||||
rainbow (~> 2.2)
|
||||
terminal-table (>= 1.5.1)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.1.1)
|
||||
jquery-rails (4.3.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
json (2.0.3)
|
||||
kaminari (1.0.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.0.1)
|
||||
kaminari-activerecord (= 1.0.1)
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-actionview (1.0.1)
|
||||
actionview
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-activerecord (1.0.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-core (1.0.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.4.1)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.3.0)
|
||||
letter_opener_web (1.3.1)
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
@@ -222,6 +233,10 @@ GEM
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
microformats2 (2.1.0)
|
||||
activesupport
|
||||
json
|
||||
nokogiri
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
@@ -230,16 +245,18 @@ GEM
|
||||
minitest (5.10.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (4.0.1)
|
||||
net-ssh (4.1.0)
|
||||
nio4r (2.0.0)
|
||||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
oj (2.17.3)
|
||||
oj (2.18.5)
|
||||
openssl (2.0.3)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (1.0.2)
|
||||
ostatus2 (1.1.0)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
openssl (~> 2.0)
|
||||
ox (2.4.11)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
@@ -250,26 +267,27 @@ GEM
|
||||
paperclip-av-transcoder (0.6.4)
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parser (2.3.1.2)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
pg (0.18.4)
|
||||
pghero (1.6.2)
|
||||
pg (0.20.0)
|
||||
pghero (1.6.4)
|
||||
activerecord
|
||||
pkg-config (1.1.7)
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
public_suffix (2.0.4)
|
||||
puma (3.6.0)
|
||||
pry-rails (0.3.6)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (2.0.5)
|
||||
puma (3.8.2)
|
||||
rabl (0.13.1)
|
||||
activesupport (>= 2.3.14)
|
||||
rack (2.0.1)
|
||||
rack-attack (5.0.1)
|
||||
rack
|
||||
rack-cors (0.4.0)
|
||||
rack-cors (0.4.1)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
@@ -287,11 +305,18 @@ GEM
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 5.0.2)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.1)
|
||||
actionpack (~> 5.x)
|
||||
actionview (~> 5.x)
|
||||
activesupport (~> 5.x)
|
||||
rails-dom-testing (2.0.2)
|
||||
activesupport (>= 4.2.0, < 6.0)
|
||||
nokogiri (~> 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-i18n (5.0.3)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 5.0)
|
||||
rails-settings-cached (0.6.5)
|
||||
rails (>= 4.2.0)
|
||||
rails_12factor (0.0.3)
|
||||
@@ -305,42 +330,37 @@ GEM
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
rainbow (2.2.1)
|
||||
rake (12.0.0)
|
||||
react-rails (1.10.0)
|
||||
react-rails (1.11.0)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
coffee-script-source (~> 1.8)
|
||||
connection_pool
|
||||
execjs
|
||||
railties (>= 3.2)
|
||||
tilt
|
||||
redis (3.3.2)
|
||||
redis-actionpack (5.0.0)
|
||||
actionpack (>= 4.0.0, < 6)
|
||||
redis-rack (~> 2.0.0.pre)
|
||||
redis-store (~> 1.2.0.pre)
|
||||
redis-activesupport (5.0.1)
|
||||
redis (3.3.3)
|
||||
redis-actionpack (5.0.1)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
redis-store (>= 1.1.0, < 1.4.0)
|
||||
redis-activesupport (5.0.2)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-rack (2.0.0)
|
||||
rack (~> 2.0)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-rails (5.0.1)
|
||||
redis-actionpack (~> 5.0.0)
|
||||
redis-activesupport (~> 5.0.0)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-store (1.2.0)
|
||||
redis-store (~> 1.3.0)
|
||||
redis-rack (2.0.1)
|
||||
rack (>= 2.0, < 3)
|
||||
redis-store (>= 1.2, < 1.4)
|
||||
redis-rails (5.0.2)
|
||||
redis-actionpack (>= 5.0, < 6)
|
||||
redis-activesupport (>= 5.0, < 6)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.3.0)
|
||||
redis (>= 2.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
rotp (2.1.2)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec (3.5.0)
|
||||
rspec-core (~> 3.5.0)
|
||||
rspec-expectations (~> 3.5.0)
|
||||
rspec-mocks (~> 3.5.0)
|
||||
rspec-core (3.5.2)
|
||||
rspec-core (3.5.4)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-expectations (3.5.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
@@ -348,7 +368,7 @@ GEM
|
||||
rspec-mocks (3.5.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-rails (3.5.1)
|
||||
rspec-rails (3.5.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
@@ -356,40 +376,40 @@ GEM
|
||||
rspec-expectations (~> 3.5.0)
|
||||
rspec-mocks (~> 3.5.0)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-sidekiq (2.2.0)
|
||||
rspec (~> 3.0, >= 3.0.0)
|
||||
rspec-sidekiq (3.0.0)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.5.0)
|
||||
rubocop (0.42.0)
|
||||
parser (>= 2.3.1.1, < 3.0)
|
||||
rubocop (0.48.1)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-oembed (0.10.1)
|
||||
ruby-oembed (0.12.0)
|
||||
ruby-progressbar (1.8.1)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.4.22)
|
||||
sass (3.4.23)
|
||||
sass-rails (5.0.6)
|
||||
railties (>= 4.0.0, < 6)
|
||||
sass (~> 3.1)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
sidekiq (4.2.7)
|
||||
sidekiq (4.2.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
sidekiq-unique-jobs (4.0.18)
|
||||
sidekiq (>= 2.6)
|
||||
sidekiq-unique-jobs (5.0.0)
|
||||
sidekiq (>= 4.0)
|
||||
thor
|
||||
simple-navigation (4.0.3)
|
||||
simple-navigation (4.0.5)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (3.2.1)
|
||||
simple_form (3.4.0)
|
||||
actionpack (> 4, < 5.1)
|
||||
activemodel (> 4, < 5.1)
|
||||
simplecov (0.12.0)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
@@ -402,43 +422,42 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.11.5)
|
||||
sshkit (1.13.1)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
statsd-instrument (2.1.2)
|
||||
temple (0.7.7)
|
||||
term-ansicolor (1.4.0)
|
||||
tins (~> 1.0)
|
||||
terminal-table (1.7.0)
|
||||
unicode-display_width (~> 1.1)
|
||||
temple (0.8.0)
|
||||
terminal-table (1.7.3)
|
||||
unicode-display_width (~> 1.1.1)
|
||||
thor (0.19.4)
|
||||
thread (0.2.2)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.6)
|
||||
tins (1.12.0)
|
||||
tilt (2.0.7)
|
||||
twitter-text (1.14.5)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.2)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2017.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
uglifier (3.0.1)
|
||||
uglifier (3.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.2)
|
||||
unicode-display_width (1.1.0)
|
||||
unf_ext (0.0.7.3)
|
||||
unicode-display_width (1.1.3)
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.6)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webmock (2.1.0)
|
||||
webmock (2.3.2)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
will_paginate (3.1.0)
|
||||
whatlanguage (1.0.6)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -453,12 +472,12 @@ DEPENDENCIES
|
||||
binding_of_caller
|
||||
browserify-rails
|
||||
bullet
|
||||
capistrano
|
||||
capistrano (= 3.8.0)
|
||||
capistrano-faster-assets (~> 1.0)
|
||||
capistrano-rails
|
||||
capistrano-rbenv
|
||||
capistrano-yarn
|
||||
coffee-rails (~> 4.1.0)
|
||||
capybara
|
||||
devise
|
||||
devise-two-factor
|
||||
doorkeeper
|
||||
@@ -473,21 +492,25 @@ DEPENDENCIES
|
||||
hiredis
|
||||
htmlentities
|
||||
http
|
||||
http_accept_language
|
||||
httplog
|
||||
i18n-tasks (~> 0.9.6)
|
||||
jquery-rails
|
||||
kaminari
|
||||
letter_opener
|
||||
letter_opener_web
|
||||
link_header
|
||||
lograge
|
||||
microformats2
|
||||
nokogiri
|
||||
oj
|
||||
ostatus2
|
||||
ostatus2 (~> 1.1)
|
||||
ox
|
||||
paperclip (~> 5.1)
|
||||
paperclip-av-transcoder
|
||||
pg
|
||||
pghero
|
||||
pkg-config
|
||||
pry-rails
|
||||
puma
|
||||
rabl
|
||||
@@ -495,6 +518,8 @@ DEPENDENCIES
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails (~> 5.0.2)
|
||||
rails-controller-testing
|
||||
rails-i18n
|
||||
rails-settings-cached
|
||||
rails_12factor
|
||||
react-rails
|
||||
@@ -511,15 +536,16 @@ DEPENDENCIES
|
||||
simple-navigation
|
||||
simple_form
|
||||
simplecov
|
||||
sprockets-rails
|
||||
statsd-instrument
|
||||
twitter-text
|
||||
tzinfo-data
|
||||
uglifier (>= 1.3.0)
|
||||
webmock
|
||||
will_paginate
|
||||
whatlanguage
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.3.1p112
|
||||
ruby 2.4.1p111
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.3
|
||||
1.14.6
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
* * * *
|
||||
|
||||
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
||||
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).
|
||||
|
||||
84
README.md
84
README.md
@@ -17,7 +17,7 @@ Click on the screenshot to watch a demo of the UI:
|
||||
|
||||
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
||||
|
||||
Focus of the project on a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||
|
||||
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
||||
|
||||
@@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
|
||||
|
||||
## Resources
|
||||
|
||||
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
||||
- [API overview](docs/Using-the-API/API.md)
|
||||
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
|
||||
- [List of apps](docs/Using-Mastodon/Apps.md)
|
||||
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -48,6 +48,14 @@ If you would like, you can [support the development of this project on Patreon][
|
||||
- **Deployable via Docker**
|
||||
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
||||
|
||||
## Checking out
|
||||
|
||||
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
|
||||
|
||||
git clone https://github.com/tootsuite/mastodon.git
|
||||
cd mastodon
|
||||
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
## Configuration
|
||||
|
||||
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
|
||||
@@ -67,23 +75,53 @@ Consult the example configuration file, `.env.production.sample` for the full li
|
||||
|
||||
[](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
|
||||
|
||||
The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
|
||||
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`).
|
||||
|
||||
Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
|
||||
so you may need or want to adjust the settings there.
|
||||
|
||||
Then, you need to fill in the `.env.production` file:
|
||||
|
||||
cp .env.production.sample .env.production
|
||||
nano .env.production
|
||||
|
||||
Do NOT change the `REDIS_*` or `DB_*` settings when running with the default docker configurations.
|
||||
|
||||
You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
|
||||
|
||||
Before running the first time, you need to build the images:
|
||||
|
||||
docker-compose build
|
||||
|
||||
And finally
|
||||
|
||||
docker-compose up -d
|
||||
docker-compose run --rm web rake secret
|
||||
|
||||
As usual, the first thing you would need to do would be to run migrations:
|
||||
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
|
||||
|
||||
Then you should run the `db:migrate` command to create the database, or migrate it from an older release:
|
||||
|
||||
docker-compose run --rm web rails db:migrate
|
||||
|
||||
And since the instance running in the container will be running in production mode, you need to pre-compile assets:
|
||||
Then, you will also need to precompile the assets:
|
||||
|
||||
docker-compose run --rm web rails assets:precompile
|
||||
|
||||
The container has two volumes, for the assets and for user uploads. The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
|
||||
before you can launch the docker image with:
|
||||
|
||||
docker-compose up
|
||||
|
||||
If you wish to run this as a daemon process instead of monitoring it on console, use instead:
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
Then you may login to your new Mastodon instance by browsing to http://localhost:3000/
|
||||
|
||||
Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how
|
||||
to configure Nginx to make your Mastodon instance available to the rest of the world.
|
||||
|
||||
The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases.
|
||||
|
||||
The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
|
||||
|
||||
**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
|
||||
|
||||
@@ -103,39 +141,33 @@ Running any of these tasks via docker-compose would look like this:
|
||||
|
||||
This approach makes updating to the latest version a real breeze.
|
||||
|
||||
git pull
|
||||
|
||||
To pull down the updates, re-run
|
||||
|
||||
docker-compose build
|
||||
|
||||
And finally,
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
Which will re-create the updated containers, leaving databases and data as is. Depending on what files have been updated, you might need to re-run migrations and asset compilation.
|
||||
1. `git pull` to download updates from the repository
|
||||
2. `docker-compose build` to compile the Docker image out of the changed source files
|
||||
3. (optional) `docker-compose run --rm web rails db:migrate` to perform database migrations. Does nothing if your database is up to date
|
||||
4. (optional) `docker-compose run --rm web rails assets:precompile` to compile new JS and CSS assets
|
||||
5. `docker-compose up -d` to re-create (restart) containers and pick up the changes
|
||||
|
||||
## Deployment without Docker
|
||||
|
||||
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
||||
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
||||
|
||||
## Deployment on Scalingo
|
||||
|
||||
[](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
|
||||
|
||||
[You can view a guide for deployment on Scalingo here.](docs/Running-Mastodon/Scalingo-guide.md)
|
||||
[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
|
||||
|
||||
## Deployment on Heroku (experimental)
|
||||
|
||||
[](https://heroku.com/deploy)
|
||||
|
||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
|
||||
Mastodon can run on [Heroku](https://heroku.com), but it gets expensive and impractical due to how Heroku prices resource usage. [You can view a guide for deployment on Heroku here](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md), but you have been warned.
|
||||
|
||||
## Development with Vagrant
|
||||
|
||||
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
||||
|
||||
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
|
||||
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
10
Vagrantfile
vendored
10
Vagrantfile
vendored
@@ -43,15 +43,15 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
||||
|
||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||
|
||||
export PATH="$HOME/.rbenv/bin::$PATH"
|
||||
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||
eval "$(rbenv init -)"
|
||||
|
||||
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
|
||||
rbenv install 2.3.1
|
||||
rbenv global 2.3.1
|
||||
|
||||
cd /vagrant
|
||||
|
||||
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
|
||||
rbenv install $(cat .ruby-version)
|
||||
rbenv global $(cat .ruby-version)
|
||||
|
||||
# Configure database
|
||||
sudo -u postgres createuser -U postgres vagrant -s
|
||||
sudo -u postgres createdb -U postgres mastodon_development
|
||||
|
||||
16
app.json
16
app.json
@@ -26,6 +26,10 @@
|
||||
"description": "The secret key base",
|
||||
"generator": "secret"
|
||||
},
|
||||
"OTP_SECRET": {
|
||||
"description": "One-time password secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"SINGLE_USER_MODE": {
|
||||
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
|
||||
"value": "false",
|
||||
@@ -75,6 +79,18 @@
|
||||
"SMTP_FROM_ADDRESS": {
|
||||
"description": "Address to send emails from",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_AUTH_METHOD": {
|
||||
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_OPENSSL_VERIFY_MODE": {
|
||||
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_ENABLE_STARTTLS_AUTO": {
|
||||
"description": "Enable STARTTLS if SMTP server supports it? Default is true.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"buildpacks": [
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 258 KiB |
BIN
app/assets/images/elephant-friend.png
Normal file
BIN
app/assets/images/elephant-friend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
1
app/assets/images/logo.svg
Normal file
1
app/assets/images/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><g fill="#189efc"><path d="M500 0A500 500 0 0 0 0 500a500 500 0 0 0 500 500 500 500 0 0 0 500-500A500 500 0 0 0 500 0zm-2.5 271.1h107.24c-20.56 14.471-27.24 57.064-27.24 78.927v202.145c0 43.726-35.202 78.928-80 78.928s-80-35.202-80-78.928V350.027c0-43.725 35.202-78.927 80-78.927zm-276 48.9c44.798 0 80 35.202 80 78.928v202.144c0 21.863 6.68 64.456 27.24 78.928H221.5c-44.798 0-80-35.202-80-78.928V398.928c0-43.726 35.202-78.928 80-78.928zm550.24 0c44.799 0 80 35.202 80 78.928v202.144c0 43.726-35.201 78.928-80 78.928H664.5c20.56-14.472 27.24-57.065 27.24-78.928V398.928c0-43.726 35.202-78.928 80-78.928z"/><g transform="translate(-2)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(1 0 0 -1 274 951)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(-1 0 0 1 995 0)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -2,6 +2,8 @@ import api from '../api';
|
||||
|
||||
import { updateTimeline } from './timelines';
|
||||
|
||||
import * as emojione from 'emojione';
|
||||
|
||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||
@@ -72,9 +74,8 @@ export function mentionCompose(account, router) {
|
||||
export function submitCompose() {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
api(getState).post('/api/v1/statuses', {
|
||||
status: getState().getIn(['compose', 'text'], ''),
|
||||
status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')),
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
|
||||
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import api, { getLinks } from '../api'
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
|
||||
|
||||
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
|
||||
export function fetchMutes() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchMutesRequest());
|
||||
|
||||
api(getState).get('/api/v1/mutes').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesRequest() {
|
||||
return {
|
||||
type: MUTES_FETCH_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_FETCH_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_FETCH_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutes() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'mutes', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMutesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesRequest() {
|
||||
return {
|
||||
type: MUTES_EXPAND_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_EXPAND_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
||||
@@ -50,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
};
|
||||
};
|
||||
|
||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||
|
||||
export function refreshNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(refreshNotificationsRequest());
|
||||
@@ -61,6 +63,8 @@ export function refreshNotifications() {
|
||||
params.since_id = ids.first().get('id');
|
||||
}
|
||||
|
||||
params.exclude_types = excludeTypesFromSettings(getState());
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
@@ -105,11 +109,11 @@ export function expandNotifications() {
|
||||
|
||||
dispatch(expandNotificationsRequest());
|
||||
|
||||
api(getState).get(url, {
|
||||
params: {
|
||||
limit: 5
|
||||
}
|
||||
}).then(response => {
|
||||
const params = {};
|
||||
|
||||
params.exclude_types = excludeTypesFromSettings(getState());
|
||||
|
||||
api(getState).get(url, params).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||
|
||||
14
app/assets/javascripts/components/actions/onboarding.jsx
Normal file
14
app/assets/javascripts/components/actions/onboarding.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { openModal } from './modal';
|
||||
import { changeSetting, saveSettings } from './settings';
|
||||
|
||||
export function showOnboardingOnce() {
|
||||
return (dispatch, getState) => {
|
||||
const alreadySeen = getState().getIn(['settings', 'onboarded']);
|
||||
|
||||
if (!alreadySeen) {
|
||||
dispatch(openModal('ONBOARDING'));
|
||||
dispatch(changeSetting(['onboarded'], true));
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -7,7 +7,8 @@ export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
||||
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
||||
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
||||
|
||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
||||
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
||||
|
||||
export function initReport(account, status) {
|
||||
return {
|
||||
@@ -62,3 +63,10 @@ export function submitReportFail(error) {
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export function changeReportComment(comment) {
|
||||
return {
|
||||
type: REPORT_COMMENT_CHANGE,
|
||||
comment
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import LinkHeader from './link_header';
|
||||
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
||||
@@ -10,7 +10,8 @@ const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||
});
|
||||
|
||||
const buttonsStyle = {
|
||||
@@ -25,6 +26,7 @@ const Account = React.createClass({
|
||||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
@@ -38,6 +40,10 @@ const Account = React.createClass({
|
||||
this.props.onBlock(this.props.account);
|
||||
},
|
||||
|
||||
handleMute () {
|
||||
this.props.onMute(this.props.account);
|
||||
},
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
|
||||
@@ -51,11 +57,14 @@ const Account = React.createClass({
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||
} else if (blocking) {
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||
} else {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
@@ -65,7 +74,7 @@ const Account = React.createClass({
|
||||
<div className='account'>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
||||
@@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
|
||||
|
||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
key={suggestion}
|
||||
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<AutosuggestAccountContainer id={suggestion} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,103 +1,18 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
// From: http://stackoverflow.com/a/18320662
|
||||
const resample = (canvas, width, height, resize_canvas) => {
|
||||
let width_source = canvas.width;
|
||||
let height_source = canvas.height;
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
let ratio_w = width_source / width;
|
||||
let ratio_h = height_source / height;
|
||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
||||
let img2 = ctx.createImageData(width, height);
|
||||
let data = img.data;
|
||||
let data2 = img2.data;
|
||||
|
||||
for (let j = 0; j < height; j++) {
|
||||
for (let i = 0; i < width; i++) {
|
||||
let x2 = (i + j * width) * 4;
|
||||
let weight = 0;
|
||||
let weights = 0;
|
||||
let weights_alpha = 0;
|
||||
let gx_r = 0;
|
||||
let gx_g = 0;
|
||||
let gx_b = 0;
|
||||
let gx_a = 0;
|
||||
let center_y = (j + 0.5) * ratio_h;
|
||||
let yy_start = Math.floor(j * ratio_h);
|
||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
||||
|
||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
||||
let center_x = (i + 0.5) * ratio_w;
|
||||
let w0 = dy * dy; //pre-calc part of w
|
||||
let xx_start = Math.floor(i * ratio_w);
|
||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
||||
|
||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
||||
let w = Math.sqrt(w0 + dx * dx);
|
||||
|
||||
if (w >= 1) {
|
||||
// pixel too far
|
||||
continue;
|
||||
}
|
||||
|
||||
// hermite filter
|
||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
||||
let pos_x = 4 * (xx + yy * width_source);
|
||||
|
||||
// alpha
|
||||
gx_a += weight * data[pos_x + 3];
|
||||
weights_alpha += weight;
|
||||
|
||||
// colors
|
||||
if (data[pos_x + 3] < 255)
|
||||
weight = weight * data[pos_x + 3] / 250;
|
||||
|
||||
gx_r += weight * data[pos_x];
|
||||
gx_g += weight * data[pos_x + 1];
|
||||
gx_b += weight * data[pos_x + 2];
|
||||
weights += weight;
|
||||
}
|
||||
}
|
||||
|
||||
data2[x2] = gx_r / weights;
|
||||
data2[x2 + 1] = gx_g / weights;
|
||||
data2[x2 + 2] = gx_b / weights;
|
||||
data2[x2 + 3] = gx_a / weights_alpha;
|
||||
}
|
||||
}
|
||||
|
||||
// clear and resize canvas
|
||||
if (resize_canvas === true) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
ctx.clearRect(0, 0, width_source, height_source);
|
||||
}
|
||||
|
||||
// draw
|
||||
ctx.putImageData(img2, 0, 0);
|
||||
};
|
||||
|
||||
const Avatar = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
staticSrc: React.PropTypes.string,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
animated: React.PropTypes.bool
|
||||
animate: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
animated: true
|
||||
animate: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -117,38 +32,30 @@ const Avatar = React.createClass({
|
||||
this.setState({ hovering: false });
|
||||
},
|
||||
|
||||
handleLoad () {
|
||||
this.canvas.width = this.image.naturalWidth;
|
||||
this.canvas.height = this.image.naturalHeight;
|
||||
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
|
||||
|
||||
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
|
||||
},
|
||||
|
||||
setImageRef (c) {
|
||||
this.image = c;
|
||||
},
|
||||
|
||||
setCanvasRef (c) {
|
||||
this.canvas = c;
|
||||
},
|
||||
|
||||
render () {
|
||||
const { src, size, staticSrc, animate } = this.props;
|
||||
const { hovering } = this.state;
|
||||
|
||||
if (this.props.animated) {
|
||||
return (
|
||||
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
|
||||
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
|
||||
</div>
|
||||
);
|
||||
const style = {
|
||||
...this.props.style,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundSize: `${size}px ${size}px`
|
||||
};
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
|
||||
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
|
||||
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
|
||||
</div>
|
||||
<div
|
||||
className='avatar'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const Button = React.createClass({
|
||||
block: React.PropTypes.bool,
|
||||
secondary: React.PropTypes.bool,
|
||||
size: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleClick () {
|
||||
if (window.history && window.history.length == 1) this.context.router.push("/");
|
||||
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||
else this.context.router.goBack();
|
||||
},
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
||||
render () {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ const ColumnCollapsable = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
fullHeight: React.PropTypes.number.isRequired,
|
||||
children: React.PropTypes.node,
|
||||
onCollapse: React.PropTypes.func
|
||||
@@ -39,13 +40,15 @@ const ColumnCollapsable = React.createClass({
|
||||
},
|
||||
|
||||
render () {
|
||||
const { icon, fullHeight, children } = this.props;
|
||||
const { icon, title, fullHeight, children } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<i className={`fa fa-${icon}`} />
|
||||
</div>
|
||||
|
||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||
{({ opacity, height }) =>
|
||||
|
||||
@@ -3,15 +3,43 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
const ExtendedVideoPlayer = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired
|
||||
src: React.PropTypes.string.isRequired,
|
||||
time: React.PropTypes.number,
|
||||
controls: React.PropTypes.bool.isRequired,
|
||||
muted: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleLoadedData () {
|
||||
if (this.props.time) {
|
||||
this.video.currentTime = this.props.time;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount () {
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
},
|
||||
|
||||
componentWillUnmount () {
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
},
|
||||
|
||||
setRef (c) {
|
||||
this.video = c;
|
||||
},
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<video src={this.props.src} autoPlay muted loop />
|
||||
<div className='extended-video-player'>
|
||||
<video
|
||||
ref={this.setRef}
|
||||
src={this.props.src}
|
||||
autoPlay
|
||||
muted={this.props.muted}
|
||||
controls={this.props.controls}
|
||||
loop={!this.props.controls}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -13,7 +13,8 @@ const IconButton = React.createClass({
|
||||
activeStyle: React.PropTypes.object,
|
||||
disabled: React.PropTypes.bool,
|
||||
inverted: React.PropTypes.bool,
|
||||
animate: React.PropTypes.bool
|
||||
animate: React.PropTypes.bool,
|
||||
overlay: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
@@ -21,7 +22,8 @@ const IconButton = React.createClass({
|
||||
size: 18,
|
||||
active: false,
|
||||
disabled: false,
|
||||
animate: false
|
||||
animate: false,
|
||||
overlay: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -31,7 +33,7 @@ const IconButton = React.createClass({
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.props.disabled) {
|
||||
this.props.onClick();
|
||||
this.props.onClick(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -39,7 +41,7 @@ const IconButton = React.createClass({
|
||||
let style = {
|
||||
fontSize: `${this.props.size}px`,
|
||||
width: `${this.props.size * 1.28571429}px`,
|
||||
height: `${this.props.size}px`,
|
||||
height: `${this.props.size * 1.28571429}px`,
|
||||
lineHeight: `${this.props.size}px`,
|
||||
...this.props.style
|
||||
};
|
||||
@@ -48,13 +50,31 @@ const IconButton = React.createClass({
|
||||
style = { ...style, ...this.props.activeStyle };
|
||||
}
|
||||
|
||||
const classes = ['icon-button'];
|
||||
|
||||
if (this.props.active) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
if (this.props.disabled) {
|
||||
classes.push('disabled');
|
||||
}
|
||||
|
||||
if (this.props.inverted) {
|
||||
classes.push('inverted');
|
||||
}
|
||||
|
||||
if (this.props.overlay) {
|
||||
classes.push('overlayed');
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||
{({ rotate }) =>
|
||||
<button
|
||||
aria-label={this.props.title}
|
||||
title={this.props.title}
|
||||
className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''} ${this.props.inverted ? 'inverted' : ''}`}
|
||||
className={classes.join(' ')}
|
||||
onClick={this.handleClick}
|
||||
style={style}>
|
||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const LoadMore = ({ onClick }) => (
|
||||
<a href='#' className='load-more' onClick={onClick}>
|
||||
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -39,8 +39,8 @@ const spoilerSubSpanStyle = {
|
||||
|
||||
const spoilerButtonStyle = {
|
||||
position: 'absolute',
|
||||
top: '6px',
|
||||
left: '8px',
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
zIndex: '100'
|
||||
};
|
||||
|
||||
@@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
||||
}
|
||||
|
||||
children = (
|
||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span style={spoilerSpanStyle}>{warning}</span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
@@ -232,8 +232,8 @@ const MediaGallery = React.createClass({
|
||||
|
||||
return (
|
||||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
||||
<div style={spoilerButtonStyle}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
|
||||
<div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
href: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string.isRequired
|
||||
to: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
handleClick (e) {
|
||||
|
||||
@@ -25,8 +25,10 @@ const Status = React.createClass({
|
||||
onReblog: React.PropTypes.func,
|
||||
onDelete: React.PropTypes.func,
|
||||
onOpenMedia: React.PropTypes.func,
|
||||
onOpenVideo: React.PropTypes.func,
|
||||
onBlock: React.PropTypes.func,
|
||||
me: React.PropTypes.number,
|
||||
boostModal: React.PropTypes.bool,
|
||||
muted: React.PropTypes.bool
|
||||
},
|
||||
|
||||
@@ -75,7 +77,7 @@ const Status = React.createClass({
|
||||
|
||||
if (status.get('media_attachments').size > 0 && !this.props.muted) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
|
||||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
|
||||
} else {
|
||||
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
|
||||
}
|
||||
@@ -90,7 +92,7 @@ const Status = React.createClass({
|
||||
|
||||
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
|
||||
@@ -46,8 +46,8 @@ const StatusActionBar = React.createClass({
|
||||
this.props.onFavourite(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
handleReblogClick (e) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
},
|
||||
|
||||
handleDeleteClick () {
|
||||
@@ -92,10 +92,14 @@ const StatusActionBar = React.createClass({
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||
|
||||
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||
|
||||
@@ -36,6 +36,7 @@ const StatusContent = React.createClass({
|
||||
|
||||
if (mention) {
|
||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||
link.setAttribute('title', mention.get('acct'));
|
||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||
} else if (media) {
|
||||
@@ -43,6 +44,7 @@ const StatusContent = React.createClass({
|
||||
} else {
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener');
|
||||
link.setAttribute('title', link.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -117,7 +119,7 @@ const StatusContent = React.createClass({
|
||||
return (
|
||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
</p>
|
||||
|
||||
{mentionsPlaceholder}
|
||||
@@ -125,7 +127,7 @@ const StatusContent = React.createClass({
|
||||
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
} else if (this.props.onClick) {
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
@@ -135,6 +137,14 @@ const StatusContent = React.createClass({
|
||||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
style={{ ...directionStyle }}
|
||||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import { isIOS } from '../is_mobile';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }
|
||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
||||
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
|
||||
});
|
||||
|
||||
const videoStyle = {
|
||||
@@ -21,15 +23,15 @@ const videoStyle = {
|
||||
|
||||
const muteStyle = {
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
top: '4px',
|
||||
right: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
opacity: '0.8',
|
||||
zIndex: '5'
|
||||
};
|
||||
|
||||
const spoilerStyle = {
|
||||
const coverStyle = {
|
||||
marginTop: '8px',
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
@@ -54,8 +56,17 @@ const spoilerSubSpanStyle = {
|
||||
|
||||
const spoilerButtonStyle = {
|
||||
position: 'absolute',
|
||||
top: '6px',
|
||||
left: '8px',
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
zIndex: '100'
|
||||
};
|
||||
|
||||
const expandButtonStyle = {
|
||||
position: 'absolute',
|
||||
bottom: '4px',
|
||||
right: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
zIndex: '100'
|
||||
@@ -68,7 +79,8 @@ const VideoPlayer = React.createClass({
|
||||
height: React.PropTypes.number,
|
||||
sensitive: React.PropTypes.bool,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
autoplay: React.PropTypes.bool
|
||||
autoplay: React.PropTypes.bool,
|
||||
onOpenVideo: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
@@ -83,7 +95,8 @@ const VideoPlayer = React.createClass({
|
||||
visible: !this.props.sensitive,
|
||||
preview: true,
|
||||
muted: true,
|
||||
hasAudio: true
|
||||
hasAudio: true,
|
||||
videoError: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -116,6 +129,11 @@ const VideoPlayer = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
handleExpand () {
|
||||
this.video.pause();
|
||||
this.props.onOpenVideo(this.props.media, this.video.currentTime);
|
||||
},
|
||||
|
||||
setRef (c) {
|
||||
this.video = c;
|
||||
},
|
||||
@@ -126,12 +144,17 @@ const VideoPlayer = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
handleVideoError () {
|
||||
this.setState({ videoError: true });
|
||||
},
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.video) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
componentDidUpdate () {
|
||||
@@ -140,6 +163,7 @@ const VideoPlayer = React.createClass({
|
||||
}
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
componentWillUnmount () {
|
||||
@@ -148,14 +172,21 @@ const VideoPlayer = React.createClass({
|
||||
}
|
||||
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.removeEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
render () {
|
||||
const { media, intl, width, height, sensitive, autoplay } = this.props;
|
||||
|
||||
let spoilerButton = (
|
||||
<div style={spoilerButtonStyle} >
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
||||
<div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
|
||||
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
||||
</div>
|
||||
);
|
||||
|
||||
let expandButton = (
|
||||
<div style={expandButtonStyle} >
|
||||
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -164,7 +195,7 @@ const VideoPlayer = React.createClass({
|
||||
if (this.state.hasAudio) {
|
||||
muteButton = (
|
||||
<div style={muteStyle}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
|
||||
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -172,7 +203,7 @@ const VideoPlayer = React.createClass({
|
||||
if (!this.state.visible) {
|
||||
if (sensitive) {
|
||||
return (
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
@@ -180,7 +211,7 @@ const VideoPlayer = React.createClass({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
@@ -191,18 +222,27 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
if (this.state.preview && !autoplay) {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
{spoilerButton}
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.videoError) {
|
||||
return (
|
||||
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||
{spoilerButton}
|
||||
{muteButton}
|
||||
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
{expandButton}
|
||||
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
connectTimeline,
|
||||
disconnectTimeline
|
||||
} from '../actions/timelines';
|
||||
import { showOnboardingOnce } from '../actions/onboarding';
|
||||
import { updateNotifications, refreshNotifications } from '../actions/notifications';
|
||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
||||
import {
|
||||
@@ -37,29 +38,55 @@ import FollowRequests from '../features/follow_requests';
|
||||
import GenericNotFound from '../features/generic_not_found';
|
||||
import FavouritedStatuses from '../features/favourited_statuses';
|
||||
import Blocks from '../features/blocks';
|
||||
import Mutes from '../features/mutes';
|
||||
import Report from '../features/report';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import de from 'react-intl/locale-data/de';
|
||||
import eo from 'react-intl/locale-data/eo';
|
||||
import es from 'react-intl/locale-data/es';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import pt from 'react-intl/locale-data/pt';
|
||||
import hu from 'react-intl/locale-data/hu';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import fi from 'react-intl/locale-data/fi';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import hu from 'react-intl/locale-data/hu';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
import pt from 'react-intl/locale-data/pt';
|
||||
import nl from 'react-intl/locale-data/nl';
|
||||
import no from 'react-intl/locale-data/no';
|
||||
import ru from 'react-intl/locale-data/ru';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
import bg from 'react-intl/locale-data/bg';
|
||||
import { localeData as zh_hk } from '../locales/zh-hk';
|
||||
import getMessagesForLocale from '../locales';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import createStream from '../stream';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
||||
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
|
||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||
basename: '/web'
|
||||
});
|
||||
|
||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]);
|
||||
addLocaleData([
|
||||
...en,
|
||||
...de,
|
||||
...eo,
|
||||
...es,
|
||||
...fi,
|
||||
...fr,
|
||||
...hu,
|
||||
...ja,
|
||||
...pt,
|
||||
...nl,
|
||||
...no,
|
||||
...ru,
|
||||
...uk,
|
||||
...zh,
|
||||
...zh_hk,
|
||||
...bg,
|
||||
]);
|
||||
|
||||
const Mastodon = React.createClass({
|
||||
|
||||
@@ -69,9 +96,10 @@ const Mastodon = React.createClass({
|
||||
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||
|
||||
this.subscription = createStream(accessToken, 'user', {
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||
|
||||
connected () {
|
||||
store.dispatch(connectTimeline('home'));
|
||||
@@ -107,6 +135,8 @@ const Mastodon = React.createClass({
|
||||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
store.dispatch(showOnboardingOnce());
|
||||
},
|
||||
|
||||
componentWillUnmount () {
|
||||
@@ -146,6 +176,7 @@ const Mastodon = React.createClass({
|
||||
|
||||
<Route path='follow_requests' component={FollowRequests} />
|
||||
<Route path='blocks' component={Blocks} />
|
||||
<Route path='mutes' component={Mutes} />
|
||||
<Route path='report' component={Report} />
|
||||
|
||||
<Route path='*' component={GenericNotFound} />
|
||||
|
||||
@@ -26,7 +26,8 @@ const makeMapStateToProps = () => {
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props.id),
|
||||
me: state.getIn(['meta', 'me'])
|
||||
me: state.getIn(['meta', 'me']),
|
||||
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
@@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(replyCompose(status, router));
|
||||
},
|
||||
|
||||
onReblog (status) {
|
||||
onModalReblog (status) {
|
||||
dispatch(reblog(status));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
dispatch(reblog(status));
|
||||
if (e.shiftKey || !this.boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,6 +75,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
},
|
||||
|
||||
@@ -43,7 +43,16 @@ const Avatar = React.createClass({
|
||||
return (
|
||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ radius }) =>
|
||||
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='account__header__avatar'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
onBlur={this.handleMouseOut}>
|
||||
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import createStream from '../../stream';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local' }
|
||||
title: { id: 'column.community', defaultMessage: 'Local timeline' }
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
@@ -29,6 +30,7 @@ const CommunityTimeline = React.createClass({
|
||||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
@@ -36,7 +38,7 @@ const CommunityTimeline = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, accessToken } = this.props;
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('community'));
|
||||
|
||||
@@ -44,7 +46,7 @@ const CommunityTimeline = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(accessToken, 'public:local', {
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('community'));
|
||||
|
||||
@@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const AutosuggestAccount = ({ account }) => (
|
||||
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import TextIconButton from './text_icon_button';
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
|
||||
});
|
||||
|
||||
const ComposeForm = React.createClass({
|
||||
@@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
|
||||
this.props.onChangeSpoilerText(e.target.value);
|
||||
},
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
// If this is the update where we've finished uploading,
|
||||
// save the last caret position so we can restore it below!
|
||||
if (!nextProps.is_uploading && this.props.is_uploading) {
|
||||
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.focusDate !== prevProps.focusDate) {
|
||||
// If replying to zero or one users, places the cursor at the end of the textbox.
|
||||
// If replying to more than one user, selects any usernames past the first;
|
||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||
// - Replying to more than one user, selects any usernames past the first;
|
||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||
// - If we've just finished uploading an image, and have a saved caret position,
|
||||
// restores the cursor to that position after the text changes!
|
||||
if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
|
||||
let selectionEnd, selectionStart;
|
||||
|
||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||
@@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
|
||||
|
||||
render () {
|
||||
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||
const disabled = this.props.is_submitting || this.props.is_uploading;
|
||||
const disabled = this.props.is_submitting;
|
||||
|
||||
let publishText = '';
|
||||
let privacyWarning = '';
|
||||
|
||||
@@ -46,8 +46,8 @@ const EmojiPickerDropdown = React.createClass({
|
||||
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent className='dropdown__left'>
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} />
|
||||
<DropdownContent className='dropdown__left light'>
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
|
||||
render () {
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
|
||||
|
||||
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
||||
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
||||
|
||||
@@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
|
||||
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{options.map(item =>
|
||||
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.shortText}</strong>
|
||||
|
||||
@@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
|
||||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,10 @@ const Search = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
noop () {
|
||||
|
||||
},
|
||||
|
||||
handleFocus () {
|
||||
this.props.onShow();
|
||||
},
|
||||
@@ -56,9 +60,9 @@ const Search = React.createClass({
|
||||
onFocus={this.handleFocus}
|
||||
/>
|
||||
|
||||
<div className='search__icon'>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
|
||||
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import SearchResultsContainer from './containers/search_results_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
|
||||
|
||||
@@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
||||
<div>
|
||||
<div style={outerStyle}>
|
||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const Followers = React.createClass({
|
||||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||
},
|
||||
|
||||
render () {
|
||||
|
||||
@@ -7,13 +7,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
|
||||
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
|
||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
||||
});
|
||||
|
||||
@@ -37,13 +38,14 @@ const GettingStarted = ({ intl, me }) => {
|
||||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||
{followRequests}
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
||||
<div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div className='static-content getting-started'>
|
||||
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
|
||||
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
|
||||
@@ -13,6 +13,7 @@ import createStream from '../../stream';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
@@ -21,6 +22,7 @@ const HashtagTimeline = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
@@ -28,9 +30,9 @@ const HashtagTimeline = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
_subscribe (dispatch, id) {
|
||||
const { accessToken } = this.props;
|
||||
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
|
||||
|
||||
received (data) {
|
||||
switch(data.event) {
|
||||
|
||||
@@ -6,7 +6,8 @@ import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import SettingText from './setting_text';
|
||||
|
||||
const messages = defineMessages({
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }
|
||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
@@ -39,7 +40,7 @@ const ColumnSettings = React.createClass({
|
||||
const { settings, onChange, onSave, intl } = this.props;
|
||||
|
||||
return (
|
||||
<ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
|
||||
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
|
||||
<div className='column-settings--outer' style={outerStyle}>
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
|
||||
68
app/assets/javascripts/components/features/mutes/index.jsx
Normal file
68
app/assets/javascripts/components/features/mutes/index.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' }
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
|
||||
});
|
||||
|
||||
const Mutes = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchMutes());
|
||||
},
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandMutes());
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollContainer scrollKey='mutes'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Mutes));
|
||||
@@ -1,21 +1,25 @@
|
||||
const iconStyle = {
|
||||
fontSize: '16px',
|
||||
padding: '15px',
|
||||
position: 'absolute',
|
||||
right: '48px',
|
||||
top: '0',
|
||||
cursor: 'pointer',
|
||||
zIndex: '2'
|
||||
};
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const ClearColumnButton = ({ onClick }) => (
|
||||
<div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
|
||||
<i className='fa fa-trash' />
|
||||
</div>
|
||||
);
|
||||
const messages = defineMessages({
|
||||
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
|
||||
});
|
||||
|
||||
ClearColumnButton.propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
const ClearColumnButton = React.createClass({
|
||||
|
||||
export default ClearColumnButton;
|
||||
propTypes: {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
export default injectIntl(ClearColumnButton);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnCollapsable from '../../../components/column_collapsable';
|
||||
import SettingToggle from './setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
settings: { id: 'notifications.settings', defaultMessage: 'Column settings' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
padding: '15px'
|
||||
};
|
||||
@@ -24,20 +28,23 @@ const ColumnSettings = React.createClass({
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSave: React.PropTypes.func.isRequired
|
||||
onSave: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.shape({
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
render () {
|
||||
const { settings, onChange, onSave } = this.props;
|
||||
const { settings, intl, onChange, onSave } = this.props;
|
||||
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
|
||||
return (
|
||||
<ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
|
||||
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}>
|
||||
<div className='column-settings--outer' style={outerStyle}>
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
|
||||
@@ -77,4 +84,4 @@ const ColumnSettings = React.createClass({
|
||||
|
||||
});
|
||||
|
||||
export default ColumnSettings;
|
||||
export default injectIntl(ColumnSettings);
|
||||
|
||||
@@ -21,7 +21,7 @@ const Notification = React.createClass({
|
||||
|
||||
renderFollow (account, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-follow'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-user-plus' />
|
||||
@@ -41,7 +41,7 @@ const Notification = React.createClass({
|
||||
|
||||
renderFavourite (notification, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-favourite'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
|
||||
@@ -57,7 +57,7 @@ const Notification = React.createClass({
|
||||
|
||||
renderReblog (notification, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-reblog'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-retweet' />
|
||||
@@ -71,22 +71,22 @@ const Notification = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
render () {
|
||||
render () { // eslint-disable-line consistent-return
|
||||
const { notification } = this.props;
|
||||
const account = notification.get('account');
|
||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
||||
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
||||
|
||||
switch(notification.get('type')) {
|
||||
case 'follow':
|
||||
return this.renderFollow(account, link);
|
||||
case 'mention':
|
||||
return this.renderMention(notification);
|
||||
case 'favourite':
|
||||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'follow':
|
||||
return this.renderFollow(account, link);
|
||||
case 'mention':
|
||||
return this.renderMention(notification);
|
||||
case 'favourite':
|
||||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ const labelSpanStyle = {
|
||||
marginLeft: '8px'
|
||||
};
|
||||
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
||||
<label style={labelStyle}>
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||
<label htmlFor={htmlFor} style={labelStyle}>
|
||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||
</label>
|
||||
@@ -25,7 +25,8 @@ SettingToggle.propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: React.PropTypes.array.isRequired,
|
||||
label: React.PropTypes.node.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
htmlFor: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default SettingToggle;
|
||||
|
||||
@@ -14,11 +14,12 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import createStream from '../../stream';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Whole Known Network' }
|
||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' }
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
@@ -29,6 +30,7 @@ const PublicTimeline = React.createClass({
|
||||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
@@ -36,7 +38,7 @@ const PublicTimeline = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, accessToken } = this.props;
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('public'));
|
||||
|
||||
@@ -44,7 +46,7 @@ const PublicTimeline = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(accessToken, 'public', {
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('public'));
|
||||
|
||||
@@ -47,7 +47,7 @@ const Report = React.createClass({
|
||||
propTypes: {
|
||||
isSubmitting: React.PropTypes.bool,
|
||||
account: ImmutablePropTypes.map,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
statusIds: ImmutablePropTypes.orderedSet.isRequired,
|
||||
comment: React.PropTypes.string.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
@@ -94,7 +94,8 @@ const Report = React.createClass({
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)} icon='flag'>
|
||||
<ColumnBackButtonSlim />
|
||||
<div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
|
||||
|
||||
<div className='report scrollable' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
|
||||
<div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}>
|
||||
<FormattedMessage id='report.target' defaultMessage='Reporting' />
|
||||
<strong>{account.get('acct')}</strong>
|
||||
@@ -106,7 +107,7 @@ const Report = React.createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '0 0 160px', padding: '10px' }}>
|
||||
<div style={{ flex: '0 0 100px', padding: '10px' }}>
|
||||
<textarea
|
||||
className='report__textarea'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
|
||||
@@ -37,8 +37,8 @@ const ActionBar = React.createClass({
|
||||
this.props.onReply(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
handleReblogClick (e) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
},
|
||||
|
||||
handleFavouriteClick () {
|
||||
@@ -71,10 +71,14 @@ const ActionBar = React.createClass({
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div className='detailed-status__action-bar'>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const DetailedStatus = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onOpenMedia: React.PropTypes.func.isRequired
|
||||
onOpenMedia: React.PropTypes.func.isRequired,
|
||||
onOpenVideo: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
@@ -39,7 +40,7 @@ const DetailedStatus = React.createClass({
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
|
||||
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
|
||||
} else {
|
||||
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ const DetailedStatus = React.createClass({
|
||||
return (
|
||||
<div style={{ padding: '14px 10px' }} className='detailed-status'>
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ const makeMapStateToProps = () => {
|
||||
status: getStatus(state, Number(props.params.statusId)),
|
||||
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
|
||||
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
|
||||
me: state.getIn(['meta', 'me'])
|
||||
me: state.getIn(['meta', 'me']),
|
||||
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
@@ -55,7 +56,8 @@ const Status = React.createClass({
|
||||
status: ImmutablePropTypes.map,
|
||||
ancestorsIds: ImmutablePropTypes.list,
|
||||
descendantsIds: ImmutablePropTypes.list,
|
||||
me: React.PropTypes.number
|
||||
me: React.PropTypes.number,
|
||||
boostModal: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
@@ -82,11 +84,19 @@ const Status = React.createClass({
|
||||
this.props.dispatch(replyCompose(status, this.context.router));
|
||||
},
|
||||
|
||||
handleReblogClick (status) {
|
||||
handleModalReblog (status) {
|
||||
this.props.dispatch(reblog(status));
|
||||
},
|
||||
|
||||
handleReblogClick (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
this.props.dispatch(unreblog(status));
|
||||
} else {
|
||||
this.props.dispatch(reblog(status));
|
||||
if (e.shiftKey || !this.props.boostModal) {
|
||||
this.handleModalReblog(status);
|
||||
} else {
|
||||
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -102,6 +112,10 @@ const Status = React.createClass({
|
||||
this.props.dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
handleOpenVideo (media, time) {
|
||||
this.props.dispatch(openModal('VIDEO', { media, time }));
|
||||
},
|
||||
|
||||
handleReport (status) {
|
||||
this.props.dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
@@ -141,7 +155,7 @@ const Status = React.createClass({
|
||||
<div className='scrollable'>
|
||||
{ancestors}
|
||||
|
||||
<DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
|
||||
<DetailedStatus status={status} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} />
|
||||
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
|
||||
|
||||
{descendants}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Button from '../../../components/button';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import RelativeTimestamp from '../../../components/relative_timestamp';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
|
||||
const messages = defineMessages({
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }
|
||||
});
|
||||
|
||||
const BoostModal = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReblog: React.PropTypes.func.isRequired,
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleReblog() {
|
||||
this.props.onReblog(this.props.status);
|
||||
this.props.onClose();
|
||||
},
|
||||
|
||||
handleAccountClick (e) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.props.onClose();
|
||||
this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { status, intl, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal boost-modal'>
|
||||
<div className='boost-modal__container'>
|
||||
<div className='status light'>
|
||||
<div style={{ fontSize: '15px' }}>
|
||||
<div style={{ float: 'right', fontSize: '14px' }}>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
</div>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='boost-modal__action-bar'>
|
||||
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div>
|
||||
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(BoostModal);
|
||||
@@ -41,8 +41,11 @@ const Column = React.createClass({
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleHeaderClick () {
|
||||
let node = ReactDOM.findDOMNode(this);
|
||||
this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable'));
|
||||
const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable');
|
||||
if (!scrollable) {
|
||||
return;
|
||||
}
|
||||
this._interruptScrollAnimation = scrollTop(scrollable);
|
||||
},
|
||||
|
||||
handleWheel () {
|
||||
@@ -61,7 +64,7 @@ const Column = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='column' onWheel={this.handleWheel}>
|
||||
<div role='section' className='column' onWheel={this.handleWheel}>
|
||||
{header}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
{icon}
|
||||
{type}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,8 @@ ColumnLink.propTypes = {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
text: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string,
|
||||
href: React.PropTypes.string
|
||||
href: React.PropTypes.string,
|
||||
method: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
||||
@@ -104,14 +104,14 @@ const MediaModal = React.createClass({
|
||||
leftNav = rightNav = content = '';
|
||||
|
||||
if (media.size > 1) {
|
||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
leftNav = <div role='button' tabIndex='0' style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div role='button' tabIndex='0' style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
}
|
||||
|
||||
if (attachment.get('type') === 'image') {
|
||||
content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
content = <ExtendedVideoPlayer src={url} />;
|
||||
content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import MediaModal from './media_modal';
|
||||
import OnboardingModal from './onboarding_modal';
|
||||
import VideoModal from './video_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': MediaModal
|
||||
'MEDIA': MediaModal,
|
||||
'ONBOARDING': OnboardingModal,
|
||||
'VIDEO': VideoModal,
|
||||
'BOOST': BoostModal
|
||||
};
|
||||
|
||||
const ModalRoot = React.createClass({
|
||||
@@ -62,7 +68,7 @@ const ModalRoot = React.createClass({
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<SpecificComponent {...props} onClose={onClose} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
import ComposeForm from '../../compose/components/compose_form';
|
||||
import Search from '../../compose/components/search';
|
||||
import NavigationBar from '../../compose/components/navigation_bar';
|
||||
import ColumnHeader from './column_header';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
home_title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }
|
||||
});
|
||||
|
||||
const PageOne = ({ acct, domain }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-one'>
|
||||
<div style={{ flex: '0 0 auto' }}>
|
||||
<div className='onboarding-modal__page-one__elephant-friend' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
|
||||
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a social network that belongs to everyone.' /></p>
|
||||
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, one of many independent Mastodon instances. Your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageOne.propTypes = {
|
||||
acct: React.PropTypes.string.isRequired,
|
||||
domain: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const PageTwo = () => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-two'>
|
||||
<div className='figure non-interactive'>
|
||||
<ComposeForm
|
||||
text='Awoo! #introductions'
|
||||
suggestions={Immutable.List()}
|
||||
mentionedDomains={[]}
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
onPaste={() => {}}
|
||||
onPickEmoji={() => {}}
|
||||
onChangeSpoilerText={() => {}}
|
||||
onClearSuggestions={() => {}}
|
||||
onFetchSuggestions={() => {}}
|
||||
onSuggestionSelected={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PageThree = ({ me, domain }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-three'>
|
||||
<div className='figure non-interactive'>
|
||||
<Search
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
onClear={() => {}}
|
||||
onShow={() => {}}
|
||||
/>
|
||||
|
||||
<div className='pseudo-drawer'>
|
||||
<NavigationBar account={me} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p>
|
||||
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageThree.propTypes = {
|
||||
me: ImmutablePropTypes.map.isRequired,
|
||||
domain: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const PageFour = ({ domain, intl }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-four'>
|
||||
<div className='onboarding-modal__page-four__columns'>
|
||||
<div className='row'>
|
||||
<div>
|
||||
<div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
|
||||
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='Home timeline shows posts from people you follow'/></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
|
||||
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='Notifications show when someone interacts with you' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div>
|
||||
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='Federated timeline lists public posts from everyone who people on {domain} follow. Local timeline is the same, but limited to people on {domain}.' values={{ domain }} /></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageFour.propTypes = {
|
||||
domain: React.PropTypes.string.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const PageSix = ({ admin }) => {
|
||||
let adminSection = '';
|
||||
|
||||
if (admin) {
|
||||
adminSection = (
|
||||
<p>
|
||||
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
|
||||
<br />
|
||||
<FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage='Please, do not forget to read the {guidelines}!' values={{ guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-six'>
|
||||
<h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
|
||||
{adminSection}
|
||||
<p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
|
||||
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms. And now... Bon Appetoot!' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='various mobile apps' /></a> }} /></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PageSix.propTypes = {
|
||||
admin: ImmutablePropTypes.map
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
|
||||
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
|
||||
domain: state.getIn(['meta', 'domain'])
|
||||
});
|
||||
|
||||
const OnboardingModal = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
me: ImmutablePropTypes.map.isRequired,
|
||||
domain: React.PropTypes.string.isRequired,
|
||||
admin: ImmutablePropTypes.map
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
currentIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleSkip (e) {
|
||||
e.preventDefault();
|
||||
this.props.onClose();
|
||||
},
|
||||
|
||||
handleDot (i, e) {
|
||||
e.preventDefault();
|
||||
this.setState({ currentIndex: i });
|
||||
},
|
||||
|
||||
handleNext (maxNum, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.currentIndex < maxNum - 1) {
|
||||
this.setState({ currentIndex: this.state.currentIndex + 1 });
|
||||
} else {
|
||||
this.props.onClose();
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { me, admin, domain, intl } = this.props;
|
||||
|
||||
const pages = [
|
||||
<PageOne acct={me.get('acct')} domain={domain} />,
|
||||
<PageTwo />,
|
||||
<PageThree me={me} domain={domain} />,
|
||||
<PageFour domain={domain} intl={intl} />,
|
||||
<PageSix admin={admin} />
|
||||
];
|
||||
|
||||
const { currentIndex } = this.state;
|
||||
const hasMore = currentIndex < pages.length - 1;
|
||||
|
||||
let nextOrDoneBtn;
|
||||
|
||||
if(hasMore) {
|
||||
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
|
||||
} else {
|
||||
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.next' defaultMessage='Done' /></a>;
|
||||
}
|
||||
|
||||
const styles = pages.map((page, i) => ({
|
||||
key: i,
|
||||
style: { opacity: spring(i === currentIndex ? 1 : 0) }
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal onboarding-modal'>
|
||||
<TransitionMotion styles={styles}>
|
||||
{interpolatedStyles =>
|
||||
<div className='onboarding-modal__pager'>
|
||||
{pages.map((page, i) =>
|
||||
<div key={i} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</TransitionMotion>
|
||||
|
||||
<div className='onboarding-modal__paginator'>
|
||||
<div>
|
||||
<a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
|
||||
</div>
|
||||
|
||||
<div className='onboarding-modal__dots'>
|
||||
{pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{nextOrDoneBtn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(OnboardingModal));
|
||||
@@ -0,0 +1,47 @@
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' }
|
||||
});
|
||||
|
||||
const closeStyle = {
|
||||
position: 'absolute',
|
||||
zIndex: '100',
|
||||
top: '4px',
|
||||
right: '4px'
|
||||
};
|
||||
|
||||
const VideoModal = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
time: React.PropTypes.number,
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
render () {
|
||||
const { media, intl, time, onClose } = this.props;
|
||||
|
||||
const url = media.get('url');
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal media-modal'>
|
||||
<div>
|
||||
<div style={closeStyle}><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
|
||||
<ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(VideoModal);
|
||||
@@ -24,8 +24,10 @@ const makeGetStatusIds = () => createSelector([
|
||||
|
||||
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
||||
try {
|
||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
||||
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
|
||||
if (showStatus) {
|
||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
||||
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
|
||||
}
|
||||
} catch(e) {
|
||||
// Bad regex, don't affect filters
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const UI = React.createClass({
|
||||
this.dragTargets.push(e.target);
|
||||
}
|
||||
|
||||
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
||||
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
|
||||
this.setState({ draggingOver: true });
|
||||
}
|
||||
},
|
||||
@@ -141,7 +141,7 @@ const UI = React.createClass({
|
||||
{mountedColumns}
|
||||
|
||||
<NotificationsContainer />
|
||||
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
|
||||
<LoadingBarContainer className="loading-bar" />
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} />
|
||||
</div>
|
||||
|
||||
33
app/assets/javascripts/components/link_header.jsx
Normal file
33
app/assets/javascripts/components/link_header.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from 'http-link-header';
|
||||
import querystring from 'querystring';
|
||||
|
||||
Link.parseAttrs = (link, parts) => {
|
||||
let match = null
|
||||
let attr = ''
|
||||
let value = ''
|
||||
let attrs = ''
|
||||
|
||||
let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts)
|
||||
|
||||
if(uriAttrs) {
|
||||
attrs = uriAttrs[2]
|
||||
link = Link.parseParams(link, uriAttrs[1])
|
||||
}
|
||||
|
||||
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
|
||||
attr = match[1].toLowerCase()
|
||||
value = match[4] || match[3] || match[2]
|
||||
|
||||
if( /\*$/.test(attr)) {
|
||||
Link.setAttr(link, attr, Link.parseExtendedValue(value))
|
||||
} else if(/%/.test(value)) {
|
||||
Link.setAttr(link, attr, querystring.decode(value))
|
||||
} else {
|
||||
Link.setAttr(link, attr, value)
|
||||
}
|
||||
}
|
||||
|
||||
return link
|
||||
};
|
||||
|
||||
export default Link;
|
||||
68
app/assets/javascripts/components/locales/bg.jsx
Normal file
68
app/assets/javascripts/components/locales/bg.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
const bg = {
|
||||
"column_back_button.label": "Назад",
|
||||
"lightbox.close": "Затвори",
|
||||
"loading_indicator.label": "Зареждане...",
|
||||
"status.mention": "Споменаване",
|
||||
"status.delete": "Изтриване",
|
||||
"status.reply": "Отговор",
|
||||
"status.reblog": "Споделяне",
|
||||
"status.favourite": "Предпочитани",
|
||||
"status.reblogged_by": "{name} сподели",
|
||||
"status.sensitive_warning": "Деликатно съдържание",
|
||||
"status.sensitive_toggle": "Покажи",
|
||||
"video_player.toggle_sound": "Звук",
|
||||
"account.mention": "Споменаване",
|
||||
"account.edit_profile": "Редактирай профила си",
|
||||
"account.unblock": "Не блокирай",
|
||||
"account.unfollow": "Не следвай",
|
||||
"account.block": "Блокирай",
|
||||
"account.follow": "Последвай",
|
||||
"account.posts": "Публикации",
|
||||
"account.follows": "Следвам",
|
||||
"account.followers": "Последователи",
|
||||
"account.follows_you": "Твой последовател",
|
||||
"account.requested": "В очакване на одобрение",
|
||||
"getting_started.heading": "Първи стъпки",
|
||||
"getting_started.about_addressing": "Можеш да последваш потребител, ако знаеш потребителското му име и домейна, на който се намира, като в полето за търсене ги въведеш по този начин: име@домейн",
|
||||
"getting_started.about_shortcuts": "Ако с търсения потребител се намирате на един и същ домейн, достатъчно е да въведеш само името. Същото важи и за споменаване на хора в публикации.",
|
||||
"getting_started.about_developer": "Можеш да потърсиш разработчика на този проект като: Gargron@mastodon.social",
|
||||
"getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.",
|
||||
"column.home": "Начало",
|
||||
"column.mentions": "Споменавания",
|
||||
"column.public": "Публичен канал",
|
||||
"column.notifications": "Известия",
|
||||
"tabs_bar.compose": "Съставяне",
|
||||
"tabs_bar.home": "Начало",
|
||||
"tabs_bar.mentions": "Споменавания",
|
||||
"tabs_bar.public": "Публичен канал",
|
||||
"tabs_bar.notifications": "Известия",
|
||||
"compose_form.placeholder": "Какво си мислиш?",
|
||||
"compose_form.publish": "Раздумай",
|
||||
"compose_form.sensitive": "Отбележи съдържанието като деликатно",
|
||||
"compose_form.spoiler": "Скрий текста зад предупреждение",
|
||||
"compose_form.private": "Отбележи като поверително",
|
||||
"compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?",
|
||||
"compose_form.unlisted": "Не показвай в публичния канал",
|
||||
"navigation_bar.edit_profile": "Редактирай профил",
|
||||
"navigation_bar.preferences": "Предпочитания",
|
||||
"navigation_bar.public_timeline": "Публичен канал",
|
||||
"navigation_bar.logout": "Излизане",
|
||||
"reply_indicator.cancel": "Отказ",
|
||||
"search.placeholder": "Търсене",
|
||||
"search.account": "Акаунт",
|
||||
"search.hashtag": "Хаштаг",
|
||||
"upload_button.label": "Добави медия",
|
||||
"upload_form.undo": "Отмяна",
|
||||
"notification.follow": "{name} те последва",
|
||||
"notification.favourite": "{name} хареса твоята публикация",
|
||||
"notification.reblog": "{name} сподели твоята публикация",
|
||||
"notification.mention": "{name} те спомена",
|
||||
"notifications.column_settings.alert": "Десктоп известия",
|
||||
"notifications.column_settings.show": "Покажи в колона",
|
||||
"notifications.column_settings.follow": "Нови последователи:",
|
||||
"notifications.column_settings.favourite": "Предпочитани:",
|
||||
"notifications.column_settings.mention": "Споменавания:",
|
||||
"notifications.column_settings.reblog": "Споделяния:",
|
||||
};
|
||||
|
||||
export default bg;
|
||||
@@ -1,15 +1,15 @@
|
||||
const en = {
|
||||
const de = {
|
||||
"column_back_button.label": "Zurück",
|
||||
"lightbox.close": "Schließen",
|
||||
"loading_indicator.label": "Lade...",
|
||||
"loading_indicator.label": "Lade…",
|
||||
"status.mention": "Erwähnen",
|
||||
"status.delete": "Löschen",
|
||||
"status.reply": "Antworten",
|
||||
"status.reblog": "Teilen",
|
||||
"status.favourite": "Favorisieren",
|
||||
"status.reblogged_by": "{name} teilte",
|
||||
"status.sensitive_warning": "Sensible Inhalte",
|
||||
"status.sensitive_toggle": "Klicken um zu zeigen",
|
||||
"status.sensitive_warning": "Heikle Inhalte",
|
||||
"status.sensitive_toggle": "Klicke, um sie zu sehen",
|
||||
"status.open": "Öffnen",
|
||||
"video_player.toggle_sound": "Ton umschalten",
|
||||
"account.mention": "Erwähnen",
|
||||
@@ -20,17 +20,17 @@ const en = {
|
||||
"account.follow": "Folgen",
|
||||
"account.posts": "Beiträge",
|
||||
"account.follows": "Folgt",
|
||||
"account.followers": "Folger",
|
||||
"account.followers": "Folgende",
|
||||
"account.follows_you": "Folgt dir",
|
||||
"account.requested": "Warte auf Erlaubnis",
|
||||
"getting_started.heading": "Erste Schritte",
|
||||
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
|
||||
"getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
|
||||
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben auf der Seite eingibst.",
|
||||
"getting_started.about_shortcuts": "Falls die Person auf derselben Domain ist wie du, reicht auch ihr Nutzername alleine. Das gilt auch für Erwähnungen in Beiträgen.",
|
||||
"getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
|
||||
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
|
||||
"column.home": "Home",
|
||||
"column.mentions": "Erwähnungen",
|
||||
"column.public": "Gesamtes Bekanntes Netz",
|
||||
"column.public": "Gesamtes bekanntes Netz",
|
||||
"column.notifications": "Mitteilungen",
|
||||
"column.follow_requests": "Folgeanfragen",
|
||||
"tabs_bar.compose": "Schreiben",
|
||||
@@ -38,11 +38,11 @@ const en = {
|
||||
"tabs_bar.mentions": "Erwähnungen",
|
||||
"tabs_bar.public": "Gesamtes Netz",
|
||||
"tabs_bar.notifications": "Mitteilungen",
|
||||
"compose_form.placeholder": "Worüber möchstest du schreiben?",
|
||||
"compose_form.placeholder": "Worüber möchtest du schreiben?",
|
||||
"compose_form.publish": "Tröt",
|
||||
"compose_form.sensitive": "Medien als sensitiv markieren",
|
||||
"compose_form.unlisted": "Öffentlich nicht auflisten",
|
||||
"compose_form.sensitive": "Medien als heikel markieren",
|
||||
"compose_form.private": "Als privat markieren",
|
||||
"compose_form.unlisted": "Nicht öffentlich auflisten",
|
||||
"navigation_bar.edit_profile": "Profil bearbeiten",
|
||||
"navigation_bar.preferences": "Einstellungen",
|
||||
"navigation_bar.public_timeline": "Öffentlich",
|
||||
@@ -52,15 +52,15 @@ const en = {
|
||||
"search.placeholder": "Suche",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Media-Datei anfügen",
|
||||
"upload_button.label": "Mediendatei hinzufügen",
|
||||
"upload_form.undo": "Entfernen",
|
||||
"notification.follow": "{name} folgt dir",
|
||||
"notification.favourite": "{name} favorisierte deinen Status",
|
||||
"notification.reblog": "{name} teilte deinen Status",
|
||||
"notification.mention": "{name} erwähnte dich",
|
||||
"notifications.column_settings.alert": "Desktop-Benachrichtigunen",
|
||||
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
|
||||
"notifications.column_settings.show": "In der Spalte anzeigen",
|
||||
"notifications.column_settings.follow": "Neue Folger:",
|
||||
"notifications.column_settings.follow": "Neue Folgende:",
|
||||
"notifications.column_settings.favourite": "Favorisierungen:",
|
||||
"notifications.column_settings.mention": "Erwähnungen:",
|
||||
"notifications.column_settings.reblog": "Geteilte Beiträge:",
|
||||
@@ -74,4 +74,4 @@ const en = {
|
||||
"missing_indicator.label": "Nicht gefunden"
|
||||
};
|
||||
|
||||
export default en;
|
||||
export default de;
|
||||
|
||||
@@ -1,68 +1,135 @@
|
||||
/**
|
||||
* Note for Contributors:
|
||||
* This file (en.jsx) serve as a template for other languages.
|
||||
* To make other contributors' life easier, please REMEMBER:
|
||||
* 1. to add your new string here; and
|
||||
* 2. to remove old strings that are no longer needed; and
|
||||
* 3. to sort the strings by the key.
|
||||
* 4. To rename the `en` const name and export default name to match your locale.
|
||||
* Thanks!
|
||||
*/
|
||||
const en = {
|
||||
"column_back_button.label": "Back",
|
||||
"lightbox.close": "Close",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"status.mention": "Mention @{name}",
|
||||
"status.delete": "Delete",
|
||||
"status.reply": "Reply",
|
||||
"status.reblog": "Boost",
|
||||
"status.favourite": "Favourite",
|
||||
"status.reblogged_by": "{name} boosted",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_toggle": "Click to view",
|
||||
"video_player.toggle_sound": "Toggle sound",
|
||||
"account.mention": "Mention @{name}",
|
||||
"account.edit_profile": "Edit profile",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.block": "Block @{name}",
|
||||
"account.disclaimer": "This user is from another instance. This number may be larger.",
|
||||
"account.edit_profile": "Edit profile",
|
||||
"account.follow": "Follow",
|
||||
"account.posts": "Posts",
|
||||
"account.follows": "Follows",
|
||||
"account.followers": "Followers",
|
||||
"account.follows_you": "Follows you",
|
||||
"account.follows": "Follows",
|
||||
"account.mention": "Mention @{name}",
|
||||
"account.mute": "Mute @{name}",
|
||||
"account.posts": "Posts",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Awaiting approval",
|
||||
"getting_started.heading": "Getting started",
|
||||
"getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
|
||||
"getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
|
||||
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
|
||||
"column.home": "Home",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||
"column_back_button.label": "Back",
|
||||
"column.blocks": "Blocked users",
|
||||
"column.community": "Local timeline",
|
||||
"column.public": "Federated timeline",
|
||||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.home": "Home",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
"tabs_bar.compose": "Compose",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.mentions": "Mentions",
|
||||
"tabs_bar.public": "Federated timeline",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"column.public": "Federated timeline",
|
||||
"compose_form.placeholder": "What is on your mind?",
|
||||
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.sensitive": "Mark media as sensitive",
|
||||
"compose_form.spoiler_placeholder": "Content warning",
|
||||
"compose_form.spoiler": "Hide text behind warning",
|
||||
"compose_form.private": "Mark as private",
|
||||
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
|
||||
"compose_form.unlisted": "Do not display on public timelines",
|
||||
"navigation_bar.edit_profile": "Edit profile",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"emoji_button.label": "Insert emoji",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home.public_timeline": "the public timeline",
|
||||
"empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
|
||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
|
||||
"follow_request.authorize": "Authorize",
|
||||
"follow_request.reject": "Reject",
|
||||
"getting_started.apps": "Various apps are available",
|
||||
"getting_started.heading": "Getting started",
|
||||
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
|
||||
"home.column_settings.advanced": "Advanced",
|
||||
"home.column_settings.basic": "Basic",
|
||||
"home.column_settings.filter_regex": "Filter out by regular expressions",
|
||||
"home.column_settings.show_reblogs": "Show boosts",
|
||||
"home.column_settings.show_replies": "Show replies",
|
||||
"home.settings": "Column settings",
|
||||
"lightbox.close": "Close",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"missing_indicator.label": "Not found",
|
||||
"navigation_bar.blocks": "Blocked users",
|
||||
"navigation_bar.community_timeline": "Local timeline",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"navigation_bar.edit_profile": "Edit profile",
|
||||
"navigation_bar.favourites": "Favourites",
|
||||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.info": "Extended information",
|
||||
"navigation_bar.logout": "Logout",
|
||||
"reply_indicator.cancel": "Cancel",
|
||||
"search.placeholder": "Search",
|
||||
"search.account": "Account",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Add media",
|
||||
"upload_form.undo": "Undo",
|
||||
"notification.follow": "{name} followed you",
|
||||
"navigation_bar.mutes": "Muted users",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"notification.favourite": "{name} favourited your status",
|
||||
"notification.reblog": "{name} boosted your status",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.mention": "{name} mentioned you",
|
||||
"notification.reblog": "{name} boosted your status",
|
||||
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
|
||||
"notifications.clear": "Clear notifications",
|
||||
"notifications.column_settings.alert": "Desktop notifications",
|
||||
"notifications.column_settings.show": "Show in column",
|
||||
"notifications.column_settings.follow": "New followers:",
|
||||
"notifications.column_settings.favourite": "Favourites:",
|
||||
"notifications.column_settings.follow": "New followers:",
|
||||
"notifications.column_settings.mention": "Mentions:",
|
||||
"notifications.column_settings.reblog": "Boosts:",
|
||||
"notifications.column_settings.show": "Show in column",
|
||||
"notifications.column_settings.sound": "Play sound",
|
||||
"notifications.settings": "Column settings",
|
||||
"privacy.change": "Adjust status privacy",
|
||||
"privacy.direct.long": "Post to mentioned users only",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.private.long": "Post to followers only",
|
||||
"privacy.private.short": "Private",
|
||||
"privacy.public.long": "Post to public timelines",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.unlisted.long": "Do not show in public timelines",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"reply_indicator.cancel": "Cancel",
|
||||
"report.heading": "New report",
|
||||
"report.placeholder": "Additional comments",
|
||||
"report.submit": "Submit",
|
||||
"report.target": "Reporting",
|
||||
"search_results.total": "{count} {count, plural, one {result} other {results}}",
|
||||
"search.placeholder": "Search",
|
||||
"search.status_by": "Status by {name}",
|
||||
"status.delete": "Delete",
|
||||
"status.favourite": "Favourite",
|
||||
"status.load_more": "Load more",
|
||||
"status.media_hidden": "Media hidden",
|
||||
"status.mention": "Mention @{name}",
|
||||
"status.open": "Expand this status",
|
||||
"status.reblog": "Boost",
|
||||
"status.reblogged_by": "{name} boosted",
|
||||
"status.reply": "Reply",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_toggle": "Click to view",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.show_less": "Show less",
|
||||
"status.show_more": "Show more",
|
||||
"tabs_bar.compose": "Compose",
|
||||
"tabs_bar.federated_timeline": "Federated",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.local_timeline": "Local",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"upload_area.title": "Drag & drop to upload",
|
||||
"upload_button.label": "Add media",
|
||||
"upload_form.undo": "Undo",
|
||||
"upload_progress.label": "Uploading...",
|
||||
"video_player.toggle_sound": "Toggle sound",
|
||||
"video_player.toggle_visible": "Toggle visibility",
|
||||
"video_player.expand": "Expand video",
|
||||
"video_player.video_error": "Video could not be played",
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
68
app/assets/javascripts/components/locales/eo.jsx
Normal file
68
app/assets/javascripts/components/locales/eo.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
const eo = {
|
||||
"column_back_button.label": "Reveni",
|
||||
"lightbox.close": "Fermi",
|
||||
"loading_indicator.label": "Ŝarĝanta...",
|
||||
"status.mention": "Mencii @{name}",
|
||||
"status.delete": "Forigi",
|
||||
"status.reply": "Respondi",
|
||||
"status.reblog": "Diskonigi",
|
||||
"status.favourite": "Favori",
|
||||
"status.reblogged_by": "{name} diskonigita",
|
||||
"status.sensitive_warning": "Tikla enhavo",
|
||||
"status.sensitive_toggle": "Alklaki por vidi",
|
||||
"video_player.toggle_sound": "Aktivigi sonojn",
|
||||
"account.mention": "Mencii @{name}",
|
||||
"account.edit_profile": "Redakti la profilon",
|
||||
"account.unblock": "Malbloki @{name}",
|
||||
"account.unfollow": "Malsekvi",
|
||||
"account.block": "Bloki @{name}",
|
||||
"account.follow": "Sekvi",
|
||||
"account.posts": "Mesaĝoj",
|
||||
"account.follows": "Sekvatoj",
|
||||
"account.followers": "Sekvantoj",
|
||||
"account.follows_you": "Sekvas vin",
|
||||
"account.requested": "Atendas aprobon",
|
||||
"getting_started.heading": "Por komenci",
|
||||
"getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.",
|
||||
"getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.",
|
||||
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.",
|
||||
"column.home": "Hejmo",
|
||||
"column.community": "Loka tempolinio",
|
||||
"column.public": "Fratara tempolinio",
|
||||
"column.notifications": "Sciigoj",
|
||||
"tabs_bar.compose": "Ekskribi",
|
||||
"tabs_bar.home": "Hejmo",
|
||||
"tabs_bar.mentions": "Sciigoj",
|
||||
"tabs_bar.public": "Fratara tempolinio",
|
||||
"tabs_bar.notifications": "Sciigoj",
|
||||
"compose_form.placeholder": "Pri kio vi pensas?",
|
||||
"compose_form.publish": "Hup",
|
||||
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
|
||||
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
|
||||
"compose_form.private": "Marki ke la enhavo estas privata",
|
||||
"compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.",
|
||||
"compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj",
|
||||
"navigation_bar.edit_profile": "Redakti la profilon",
|
||||
"navigation_bar.preferences": "Preferoj",
|
||||
"navigation_bar.community_timeline": "Loka tempolinio",
|
||||
"navigation_bar.public_timeline": "Fratara tempolinio",
|
||||
"navigation_bar.logout": "Elsaluti",
|
||||
"reply_indicator.cancel": "Rezigni",
|
||||
"search.placeholder": "Serĉi",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Kradvorto",
|
||||
"upload_button.label": "Aldoni enhavaĵon",
|
||||
"upload_form.undo": "Malfari",
|
||||
"notification.follow": "{name} sekvis vin",
|
||||
"notification.favourite": "{name} favoris vian mesaĝon",
|
||||
"notification.reblog": "{name} diskonigis vian mesaĝon",
|
||||
"notification.mention": "{name} menciis vin",
|
||||
"notifications.column_settings.alert": "Retumilaj atentigoj",
|
||||
"notifications.column_settings.show": "Montri en kolono",
|
||||
"notifications.column_settings.follow": "Novaj sekvantoj:",
|
||||
"notifications.column_settings.favourite": "Favoroj:",
|
||||
"notifications.column_settings.mention": "Mencioj:",
|
||||
"notifications.column_settings.reblog": "Diskonigoj:",
|
||||
};
|
||||
|
||||
export default eo;
|
||||
@@ -5,28 +5,35 @@ const es = {
|
||||
"status.mention": "Mencionar",
|
||||
"status.delete": "Borrar",
|
||||
"status.reply": "Responder",
|
||||
"status.reblog": "Republicar",
|
||||
"status.reblog": "Retoot",
|
||||
"status.favourite": "Favorito",
|
||||
"status.reblogged_by": "{name} republicado",
|
||||
"status.reblogged_by": "Retooteado por {name}",
|
||||
"status.sensitive_warning": "Contenido sensible",
|
||||
"status.sensitive_toggle": "Click para ver",
|
||||
"status.show_more": "Mostrar más",
|
||||
"status.show_less": "Mostrar menos",
|
||||
"status.open": "Expandir estado",
|
||||
"status.report": "Reportar",
|
||||
"video_player.toggle_sound": "Act/Desac. sonido",
|
||||
"account.mention": "Mención",
|
||||
"account.mention": "Mencionar",
|
||||
"account.edit_profile": "Editar perfil",
|
||||
"account.unblock": "Desbloquear",
|
||||
"account.unfollow": "Dejar de seguir",
|
||||
"account.mute": "Silenciar",
|
||||
"account.block": "Bloquear",
|
||||
"account.follow": "Seguir",
|
||||
"account.block": "Bloquear",
|
||||
"account.posts": "Publicaciones",
|
||||
"account.follows": "Seguir",
|
||||
"account.followers": "Seguidores",
|
||||
"account.follows_you": "Te sigue",
|
||||
"account.requested": "Esperando aprobación",
|
||||
"getting_started.heading": "Primeros pasos",
|
||||
"getting_started.about_addressing": "Puedes seguir a gente si conoces su nombre de usuario y el dominio en el que están registrados, introduciendo algo similar a una dirección de correo electrónico en el formulario en la parte superior de la barra lateral.",
|
||||
"getting_started.about_shortcuts": "Si el usuario que buscas está en el mismo dominio que tú, simplemente funcionará introduciendo el nombre de usuario. La misma regla se aplica para mencionar a usuarios.",
|
||||
"getting_started.about_developer": "Puedes seguir al desarrollador de este proyecto en Gargron@mastodon.social",
|
||||
"getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}. {apps}.",
|
||||
"column.home": "Inicio",
|
||||
"column.mentions": "Menciones",
|
||||
"column.public": "Historia pública",
|
||||
"column.community": "Historia local",
|
||||
"column.public": "Historia federada",
|
||||
"column.notifications": "Notificaciones",
|
||||
"tabs_bar.compose": "Redactar",
|
||||
"tabs_bar.home": "Inicio",
|
||||
@@ -34,23 +41,47 @@ const es = {
|
||||
"tabs_bar.public": "Público",
|
||||
"tabs_bar.notifications": "Notificaciones",
|
||||
"compose_form.placeholder": "¿En qué estás pensando?",
|
||||
"compose_form.publish": "Publicar",
|
||||
"compose_form.sensitive": "Marcar el contenido como sensible",
|
||||
"compose_form.unlisted": "Privado",
|
||||
"compose_form.publish": "Tootear",
|
||||
"compose_form.sensitive": "Marcar contenido como sensible",
|
||||
"compose_form.spoiler": "Ocultar texto tras advertencia",
|
||||
"compose_form.spoiler_placeholder": "Advertencia de contenido",
|
||||
"composer_form.private": "Marcar como privado",
|
||||
"composer_form.privacy_disclaimer": "Tu estado se mostrará a los usuarios mencionados en {domains}. Tu estado podrá ser visto en otras instancias, quizás no quieras que tu estado sea visto por otros usuarios.",
|
||||
"compose_form.unlisted": "No mostrar en la historia federada",
|
||||
"navigation_bar.edit_profile": "Editar perfil",
|
||||
"navigation_bar.preferences": "Preferencias",
|
||||
"navigation_bar.public_timeline": "Público",
|
||||
"navigation_bar.community_timeline": "Historia local",
|
||||
"navigation_bar.public_timeline": "Historia federada",
|
||||
"navigation_bar.favourites": "Favoritos",
|
||||
"navigation_bar.blocks": "Usuarios bloqueados",
|
||||
"navigation_bar.info": "Información adicional",
|
||||
"navigation_bar.logout": "Cerrar sesión",
|
||||
"reply_indicator.cancel": "Cancelar",
|
||||
"search.placeholder": "Buscar",
|
||||
"search.account": "Cuenta",
|
||||
"search.hashtag": "Etiqueta",
|
||||
"upload_button.label": "Añadir medio",
|
||||
"upload_button.label": "Subir multimedia",
|
||||
"upload_form.undo": "Deshacer",
|
||||
"notification.follow": "{name} le esta ahora siguiendo",
|
||||
"notification.favourite": "{name} marcó como favorito su estado",
|
||||
"notification.reblog": "{name} volvió a publicar su estado",
|
||||
"notification.mention": "Fue mencionado por {name}"
|
||||
"notification.follow": "{name} te empezó a seguir",
|
||||
"notification.favourite": "{name} marcó tu estado como favorito",
|
||||
"notification.reblog": "{name} ha retooteado tu estado",
|
||||
"notification.mention": "{name} te ha mencionado",
|
||||
"notifications.column_settings.alert": "Notificaciones de escritorio",
|
||||
"notifications.column_settings.show": "Mostrar en columna",
|
||||
"notifications.column_settings.follow": "Nuevos seguidores:",
|
||||
"notifications.column_settings.favourite": "Favoritos:",
|
||||
"notifications.column_settings.mention": "Menciones:",
|
||||
"notifications.column_settings.reblog": "Retoots:",
|
||||
"emoji_button.label": "Insertar emoji",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.long": "Mostrar en la historia federada",
|
||||
"privacy.unlisted.short": "Sin federar",
|
||||
"privacy.unlisted.long": "No mostrar en la historia federada",
|
||||
"privacy.private.short": "Privado",
|
||||
"privacy.private.long": "Sólo mostrar a seguidores",
|
||||
"privacy.direct.short": "Directo",
|
||||
"privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
|
||||
"privacy.change": "Ajustar privacidad"
|
||||
};
|
||||
|
||||
export default es;
|
||||
|
||||
@@ -10,7 +10,14 @@ const fr = {
|
||||
"status.reblogged_by": "{name} a partagé :",
|
||||
"status.sensitive_warning": "Contenu délicat",
|
||||
"status.sensitive_toggle": "Cliquer pour dévoiler",
|
||||
"status.show_more": "Déplier",
|
||||
"status.show_less": "Replier",
|
||||
"status.open": "Déplier ce statut",
|
||||
"status.report": "Signaler @{name}",
|
||||
"status.load_more": "Charger plus",
|
||||
"status.media_hidden": "Média caché",
|
||||
"video_player.toggle_sound": "Mettre/Couper le son",
|
||||
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
|
||||
"account.mention": "Mentionner",
|
||||
"account.edit_profile": "Modifier le profil",
|
||||
"account.unblock": "Débloquer",
|
||||
@@ -27,7 +34,7 @@ const fr = {
|
||||
"account.report": "Signaler",
|
||||
"account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.",
|
||||
"getting_started.heading": "Pour commencer",
|
||||
"getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
|
||||
"getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champ de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
|
||||
"getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.",
|
||||
"getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social",
|
||||
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
|
||||
@@ -35,18 +42,27 @@ const fr = {
|
||||
"column.community": "Fil public local",
|
||||
"column.public": "Fil public global",
|
||||
"column.notifications": "Notifications",
|
||||
"column.public": "Fil public",
|
||||
"column.blocks": "Utilisateurs bloqués",
|
||||
"column.favourites": "Favoris",
|
||||
"column.follow_requests": "Demandes de suivi",
|
||||
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
|
||||
"empty_column.public": "Il n'y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs d'autres instances pour remplir le fil public.",
|
||||
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d'autres utilisateurs.",
|
||||
"empty_column.home.public_timeline": "le fil public",
|
||||
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
|
||||
"empty_column.hashtag": "Il n'y a encore aucun contenu relatif à ce hashtag",
|
||||
"tabs_bar.compose": "Composer",
|
||||
"tabs_bar.home": "Accueil",
|
||||
"tabs_bar.mentions": "Mentions",
|
||||
"tabs_bar.public": "Fil public global",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"tabs_bar.local_timeline": "Fil public local",
|
||||
"tabs_bar.federated_timeline": "Fil public global",
|
||||
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
||||
"compose_form.publish": "Pouet ",
|
||||
"compose_form.publish": "Pouet",
|
||||
"compose_form.sensitive": "Marquer le média comme délicat",
|
||||
"compose_form.spoiler": "Masquer le texte par un avertissement",
|
||||
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
|
||||
"compose_form.spoiler_placeholder": "Avertissement",
|
||||
"compose_form.private": "Rendre privé",
|
||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
|
||||
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
|
||||
@@ -58,25 +74,33 @@ const fr = {
|
||||
"navigation_bar.blocks": "Utilisateurs bloqués",
|
||||
"navigation_bar.favourites": "Favoris",
|
||||
"navigation_bar.info": "Plus d'informations",
|
||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||
"navigation_bar.logout": "Déconnexion",
|
||||
"navigation_bar.mutes": "Utilisateurs muets",
|
||||
"navigation_bar.follow_requests": "Demandes de suivi",
|
||||
"reply_indicator.cancel": "Annuler",
|
||||
"search.placeholder": "Chercher",
|
||||
"search.placeholder": "Rechercher",
|
||||
"search.account": "Compte",
|
||||
"search.hashtag": "Mot-clé",
|
||||
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
|
||||
"search.status_by": "Statuts de {name}",
|
||||
"upload_button.label": "Joindre un média",
|
||||
"upload_form.undo": "Annuler",
|
||||
"upload_progress.label": "Envoi en cours…",
|
||||
"upload_area.title": "Glissez et déposez pour envoyer",
|
||||
"notification.follow": "{name} vous suit.",
|
||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||
"notification.reblog": "{name} a partagé votre statut :",
|
||||
"notification.mention": "{name} vous a mentionné⋅e :",
|
||||
"notifications.column_settings.alert": "Notifications locales",
|
||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||
"notifications.column_settings.sound": "Émettre un son",
|
||||
"notifications.column_settings.follow": "Nouveaux abonnés :",
|
||||
"notifications.column_settings.favourite": "Favoris :",
|
||||
"notifications.column_settings.mention": "Mentions :",
|
||||
"notifications.column_settings.reblog": "Partages :",
|
||||
"notifications.clear": "Nettoyer",
|
||||
"notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
|
||||
"notifications.settings": "Paramètres de la colonne",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.public.long": "Afficher dans les fils publics",
|
||||
"privacy.unlisted.short": "Non-listé",
|
||||
@@ -84,8 +108,22 @@ const fr = {
|
||||
"privacy.private.short": "Privé",
|
||||
"privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s",
|
||||
"privacy.direct.long": "N’afficher que pour les personnes mentionnées",
|
||||
"privacy.change": "Ajuster la confidentialité du message",
|
||||
"media_gallery.toggle_visible": "Modifier la visibilité",
|
||||
"missing_indicator.label": "Non trouvé",
|
||||
"follow_request.authorize": "Autoriser",
|
||||
"follow_request.reject": "Rejeter",
|
||||
"home.settings": "Paramètres de la colonne",
|
||||
"home.column_settings.basic": "Basique",
|
||||
"home.column_settings.show_reblogs": "Afficher les partages",
|
||||
"home.column_settings.show_replies": "Afficher les réponses",
|
||||
"home.column_settings.advanced": "Avancé",
|
||||
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
|
||||
"report.heading": "Nouveau signalement",
|
||||
"report.placeholder": "Commentaires additionnels",
|
||||
"report.submit": "Envoyer",
|
||||
"report.target": "Signalement"
|
||||
};
|
||||
|
||||
export default fr;
|
||||
|
||||
@@ -3,9 +3,16 @@ import de from './de';
|
||||
import es from './es';
|
||||
import hu from './hu';
|
||||
import fr from './fr';
|
||||
import nl from './nl';
|
||||
import no from './no';
|
||||
import pt from './pt';
|
||||
import uk from './uk';
|
||||
import fi from './fi';
|
||||
import eo from './eo';
|
||||
import ru from './ru';
|
||||
import ja from './ja';
|
||||
import zh_hk from './zh-hk';
|
||||
import bg from './bg';
|
||||
|
||||
const locales = {
|
||||
en,
|
||||
@@ -13,9 +20,16 @@ const locales = {
|
||||
es,
|
||||
hu,
|
||||
fr,
|
||||
nl,
|
||||
no,
|
||||
pt,
|
||||
uk,
|
||||
fi
|
||||
fi,
|
||||
eo,
|
||||
ru,
|
||||
ja,
|
||||
'zh-HK': zh_hk,
|
||||
bg,
|
||||
};
|
||||
|
||||
export default function getMessagesForLocale (locale) {
|
||||
|
||||
125
app/assets/javascripts/components/locales/ja.jsx
Normal file
125
app/assets/javascripts/components/locales/ja.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
const ja = {
|
||||
"account.block": "@{name} さんをブロック",
|
||||
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
|
||||
"account.edit_profile": "プロフィールを編集",
|
||||
"account.follow": "フォロー",
|
||||
"account.followers": "フォロワー",
|
||||
"account.follows": "フォロー",
|
||||
"account.follows_you": "フォローされています",
|
||||
"account.mention": "@{name} さんに返信",
|
||||
"account.mute": "ミュート",
|
||||
"account.posts": "投稿",
|
||||
"account.report": "@{name}を通報する",
|
||||
"account.requested": "承認待ち",
|
||||
"account.unblock": "@{name} さんのブロックを解除",
|
||||
"account.unfollow": "フォロー解除",
|
||||
"account.unmute": "ミュート解除",
|
||||
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
|
||||
"column.blocks": "ブロックしたユーザー",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.favourites": "お気に入り",
|
||||
"column.follow_requests": "フォローリクエスト",
|
||||
"column.home": "ホーム",
|
||||
"column.mutes": "ミュートしたユーザー",
|
||||
"column.notifications": "通知",
|
||||
"column.public": "連合タイムライン",
|
||||
"column_back_button.label": "戻る",
|
||||
"compose_form.placeholder": "今なにしてる?",
|
||||
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
|
||||
"compose_form.publish": "トゥート",
|
||||
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
|
||||
"compose_form.spoiler": "テキストを隠す",
|
||||
"compose_form.spoiler_placeholder": "閲覧注意",
|
||||
"emoji_button.label": "絵文字を追加",
|
||||
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
|
||||
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
||||
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
|
||||
"empty_column.home.public_timeline": "連合タイムライン",
|
||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
|
||||
"follow_request.authorize": "許可",
|
||||
"follow_request.reject": "拒否",
|
||||
"getting_started.apps": "さまざまなアプリで利用できます。",
|
||||
"getting_started.heading": "スタート",
|
||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||
"home.column_settings.advanced": "上級者向け",
|
||||
"home.column_settings.basic": "シンプル",
|
||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||
"home.column_settings.show_reblogs": "ブースト表示",
|
||||
"home.column_settings.show_replies": "返信表示",
|
||||
"home.settings": "カラム設定",
|
||||
"lightbox.close": "閉じる",
|
||||
"loading_indicator.label": "読み込み中...",
|
||||
"media_gallery.toggle_visible": "表示切り替え",
|
||||
"missing_indicator.label": "見つかりません",
|
||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"navigation_bar.edit_profile": "プロフィールを編集",
|
||||
"navigation_bar.favourites": "お気に入り",
|
||||
"navigation_bar.follow_requests": "フォローリクエスト",
|
||||
"navigation_bar.info": "サーバー情報",
|
||||
"navigation_bar.logout": "ログアウト",
|
||||
"navigation_bar.mutes": "ミュートしたユーザー",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.public_timeline": "連合タイムライン",
|
||||
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
|
||||
"notification.follow": "{name} さんにフォローされました",
|
||||
"notification.mention": "{name} さんがあなたに返信しました",
|
||||
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
|
||||
"notifications.clear": "通知を消去",
|
||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||
"notifications.column_settings.alert": "デスクトップ通知",
|
||||
"notifications.column_settings.favourite": "お気に入り",
|
||||
"notifications.column_settings.follow": "新しいフォロワー",
|
||||
"notifications.column_settings.mention": "返信",
|
||||
"notifications.column_settings.reblog": "ブースト",
|
||||
"notifications.column_settings.show": "カラムに表示",
|
||||
"notifications.column_settings.sound": "通知音を再生",
|
||||
"notifications.settings": "カラム設定",
|
||||
"privacy.change": "投稿のプライバシーを変更",
|
||||
"privacy.direct.long": "メンションしたユーザーだけに公開",
|
||||
"privacy.direct.short": "ダイレクト",
|
||||
"privacy.private.long": "フォロワーだけに公開",
|
||||
"privacy.private.short": "非公開",
|
||||
"privacy.public.long": "公開TLに投稿する",
|
||||
"privacy.public.short": "公開",
|
||||
"privacy.unlisted.long": "公開TLで表示しない",
|
||||
"privacy.unlisted.short": "未収載",
|
||||
"reply_indicator.cancel": "キャンセル",
|
||||
"report.heading": "新規通報",
|
||||
"report.placeholder": "コメント",
|
||||
"report.submit": "通報する",
|
||||
"report.target": "問題のユーザー",
|
||||
"search.placeholder": "検索",
|
||||
"search.status_by": "{name}からの投稿",
|
||||
"search_results.total": "{count} {count, plural, one {result} other {results}} 件",
|
||||
"status.delete": "削除",
|
||||
"status.favourite": "お気に入り",
|
||||
"status.load_more": "もっと見る",
|
||||
"status.media_hidden": "非表示のメデイア",
|
||||
"status.mention": "@{name} さんへの返信",
|
||||
"status.open": "詳細を表示",
|
||||
"status.reblog": "ブースト",
|
||||
"status.reblogged_by": "{name} さんにブーストされました",
|
||||
"status.reply": "返信",
|
||||
"status.report": "@{name} さんを通報",
|
||||
"status.sensitive_toggle": "クリックして表示",
|
||||
"status.sensitive_warning": "不適切なコンテンツ",
|
||||
"status.show_less": "隠す",
|
||||
"status.show_more": "もっと見る",
|
||||
"tabs_bar.compose": "投稿",
|
||||
"tabs_bar.federated_timeline": "連合",
|
||||
"tabs_bar.home": "ホーム",
|
||||
"tabs_bar.local_timeline": "ローカル",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"upload_area.title": "ドラッグ&ドロップでアップロード",
|
||||
"upload_button.label": "メディアを追加",
|
||||
"upload_form.undo": "やり直す",
|
||||
"upload_progress.label": "アップロード中…",
|
||||
"video_player.expand": "動画の詳細",
|
||||
"video_player.toggle_sound": "音の切り替え",
|
||||
"video_player.toggle_visible": "表示切り替え",
|
||||
"video_player.video_error": "動画の再生に失敗しました",
|
||||
};
|
||||
|
||||
export default ja;
|
||||
68
app/assets/javascripts/components/locales/nl.jsx
Normal file
68
app/assets/javascripts/components/locales/nl.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
const nl = {
|
||||
"column_back_button.label": "terug",
|
||||
"lightbox.close": "Sluiten",
|
||||
"loading_indicator.label": "Laden…",
|
||||
"status.mention": "@{name} vermelden",
|
||||
"status.delete": "Verwijderen",
|
||||
"status.reply": "Reageren",
|
||||
"status.reblog": "Boost",
|
||||
"status.favourite": "Favoriet",
|
||||
"status.reblogged_by": "{name} boostte",
|
||||
"status.sensitive_warning": "Gevoelige inhoud",
|
||||
"status.sensitive_toggle": "Klik om te zien",
|
||||
"video_player.toggle_sound": "Geluid in-/uitschakelen",
|
||||
"account.mention": "@{name} vermelden",
|
||||
"account.edit_profile": "Profiel bewerken",
|
||||
"account.unblock": "@{name} deblokkeren",
|
||||
"account.unfollow": "Ontvolgen",
|
||||
"account.block": "@{name} blokkeren",
|
||||
"account.follow": "Volgen",
|
||||
"account.posts": "Berichten",
|
||||
"account.follows": "Volgt",
|
||||
"account.followers": "Volgers",
|
||||
"account.follows_you": "Volgt jou",
|
||||
"account.requested": "Wacht op goedkeuring",
|
||||
"getting_started.heading": "Beginnen",
|
||||
"getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
|
||||
"getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
|
||||
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||
"column.home": "Thuis",
|
||||
"column.community": "Lokale tijdlijn",
|
||||
"column.public": "Federatietijdlijn",
|
||||
"column.notifications": "Meldingen",
|
||||
"tabs_bar.compose": "Schrijven",
|
||||
"tabs_bar.home": "Thuis",
|
||||
"tabs_bar.mentions": "Vermeldingen",
|
||||
"tabs_bar.public": "Federatietijdlijn",
|
||||
"tabs_bar.notifications": "Meldingen",
|
||||
"compose_form.placeholder": "Waar ben je mee bezig?",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.sensitive": "Media als gevoelig markeren",
|
||||
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
|
||||
"compose_form.private": "Als privé markeren",
|
||||
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
|
||||
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
|
||||
"navigation_bar.edit_profile": "Profiel bewerken",
|
||||
"navigation_bar.preferences": "Voorkeuren",
|
||||
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
||||
"navigation_bar.public_timeline": "Federatietijdlijn",
|
||||
"navigation_bar.logout": "Afmelden",
|
||||
"reply_indicator.cancel": "Annuleren",
|
||||
"search.placeholder": "Zoeken",
|
||||
"search.account": "Account",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Media toevoegen",
|
||||
"upload_form.undo": "Ongedaan maken",
|
||||
"notification.follow": "{name} volgde jou",
|
||||
"notification.favourite": "{name} markeerde je status als favoriet",
|
||||
"notification.reblog": "{name} boostte je status",
|
||||
"notification.mention": "{name} vermeldde jou",
|
||||
"notifications.column_settings.alert": "Desktopmeldingen",
|
||||
"notifications.column_settings.show": "In kolom tonen",
|
||||
"notifications.column_settings.follow": "Nieuwe volgers:",
|
||||
"notifications.column_settings.favourite": "Favorieten:",
|
||||
"notifications.column_settings.mention": "Vermeldingen:",
|
||||
"notifications.column_settings.reblog": "Boosts:",
|
||||
};
|
||||
|
||||
export default nl;
|
||||
130
app/assets/javascripts/components/locales/no.jsx
Normal file
130
app/assets/javascripts/components/locales/no.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
const no = {
|
||||
"account.block": "Blokkér @{name}",
|
||||
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
|
||||
"account.edit_profile": "Rediger profil",
|
||||
"account.follow": "Følg",
|
||||
"account.followers": "Følgere",
|
||||
"account.follows_you": "Følger deg",
|
||||
"account.follows": "Følginger",
|
||||
"account.mention": "Nevn @{name}",
|
||||
"account.mute": "Demp @{name}",
|
||||
"account.posts": "Poster",
|
||||
"account.report": "Rapportér @{name}",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"account.unblock": "Avblokker @{name}",
|
||||
"account.unfollow": "Avfølg",
|
||||
"account.unmute": "Avdemp @{name}",
|
||||
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||
"column_back_button.label": "Tilbake",
|
||||
"column.blocks": "Blokkerte brukere",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.favourites": "Likt",
|
||||
"column.follow_requests": "Følgeforespørsler",
|
||||
"column.home": "Hjem",
|
||||
"column.notifications": "Varslinger",
|
||||
"column.public": "Felles tidslinje",
|
||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
||||
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
|
||||
"compose_form.publish": "Tut",
|
||||
"compose_form.sensitive": "Merk media som følsomt",
|
||||
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
|
||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||
"emoji_button.label": "Sett inn emoji",
|
||||
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
|
||||
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
|
||||
"empty_column.home.public_timeline": "en offentlig tidslinje",
|
||||
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
|
||||
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
|
||||
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
|
||||
"follow_request.authorize": "Autorisér",
|
||||
"follow_request.reject": "Avvis",
|
||||
"getting_started.apps": "Diverse apper er tilgjengelige",
|
||||
"getting_started.heading": "Kom i gang",
|
||||
"getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
|
||||
"home.column_settings.advanced": "Advansert",
|
||||
"home.column_settings.basic": "Enkel",
|
||||
"home.column_settings.filter_regex": "Filtrér med regulære uttrykk",
|
||||
"home.column_settings.show_reblogs": "Vis fremhevinger",
|
||||
"home.column_settings.show_replies": "Vis svar",
|
||||
"home.settings": "Kolonneinnstillinger",
|
||||
"lightbox.close": "Lukk",
|
||||
"loading_indicator.label": "Laster...",
|
||||
"media_gallery.toggle_visible": "Veksle synlighet",
|
||||
"missing_indicator.label": "Ikke funnet",
|
||||
"navigation_bar.blocks": "Blokkerte brukere",
|
||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||
"navigation_bar.edit_profile": "Rediger profil",
|
||||
"navigation_bar.favourites": "Likt",
|
||||
"navigation_bar.follow_requests": "Følgeforespørsler",
|
||||
"navigation_bar.info": "Utvidet informasjon",
|
||||
"navigation_bar.logout": "Logg ut",
|
||||
"navigation_bar.preferences": "Preferanser",
|
||||
"navigation_bar.public_timeline": "Felles tidslinje",
|
||||
"notification.favourite": "{name} likte din status",
|
||||
"notification.follow": "{name} fulgte deg",
|
||||
"notification.reblog": "{name} fremhevde din status",
|
||||
"notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?",
|
||||
"notifications.clear": "Fjern varsler",
|
||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
||||
"notifications.column_settings.favourite": "Likt:",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.mention": "Nevninger:",
|
||||
"notifications.column_settings.reblog": "Fremhevinger:",
|
||||
"notifications.column_settings.show": "Vis i kolonne",
|
||||
"notifications.column_settings.sound": "Spill lyd",
|
||||
"notifications.settings": "Kolonneinstillinger",
|
||||
"privacy.change": "Justér synlighet",
|
||||
"privacy.direct.long": "Post kun til nevnte brukere",
|
||||
"privacy.direct.short": "Direkte",
|
||||
"privacy.private.long": "Post kun til følgere",
|
||||
"privacy.private.short": "Privat",
|
||||
"privacy.public.long": "Post kun til offentlige tidslinjer",
|
||||
"privacy.public.short": "Offentlig",
|
||||
"privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
|
||||
"privacy.unlisted.short": "Uoppført",
|
||||
"reply_indicator.cancel": "Avbryt",
|
||||
"report.heading": "Ny rapport",
|
||||
"report.placeholder": "Tilleggskommentarer",
|
||||
"report.submit": "Send inn",
|
||||
"report.target": "Rapporterer",
|
||||
"search_results.total": "{count} {count, plural, one {resultat} other {resultater}}",
|
||||
"search.placeholder": "Søk",
|
||||
"search.status_by": "Status fra {name}",
|
||||
"status.delete": "Slett",
|
||||
"status.favourite": "Lik",
|
||||
"status.load_more": "Last mer",
|
||||
"status.media_hidden": "Media skjult",
|
||||
"status.mention": "Nevn @{name}",
|
||||
"status.open": "Utvid denne statusen",
|
||||
"status.reblog": "Fremhev",
|
||||
"status.reblogged_by": "Fremhevd av {name}",
|
||||
"status.reply": "Svar",
|
||||
"status.report": "Rapporter @{name}",
|
||||
"status.sensitive_toggle": "Klikk for å vise",
|
||||
"status.sensitive_warning": "Følsomt innhold",
|
||||
"status.show_less": "Vis mindre",
|
||||
"status.show_more": "Vis mer",
|
||||
"tabs_bar.compose": "Komponer",
|
||||
"tabs_bar.federated_timeline": "Felles",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.local_timeline": "Lokal",
|
||||
"tabs_bar.notifications": "Varslinger",
|
||||
"upload_area.title": "Dra og slipp for å laste opp",
|
||||
"upload_button.label": "Legg til media",
|
||||
"upload_form.undo": "Angre",
|
||||
"upload_progress.label": "Laster opp...",
|
||||
"video_player.toggle_sound": "Veksle lyd",
|
||||
"video_player.toggle_visible": "Veksle synlighet",
|
||||
"video_player.expand": "Utvid video",
|
||||
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
|
||||
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
|
||||
"tabs_bar.mentions": "Nevninger",
|
||||
"tabs_bar.public": "Felles tidslinje",
|
||||
"compose_form.private": "Merk som privat",
|
||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"notification.mention": "{name} nevnte deg"
|
||||
};
|
||||
|
||||
export default no;
|
||||
@@ -2,54 +2,127 @@ const pt = {
|
||||
"column_back_button.label": "Voltar",
|
||||
"lightbox.close": "Fechar",
|
||||
"loading_indicator.label": "Carregando...",
|
||||
"status.mention": "Menção",
|
||||
"status.delete": "Deletar",
|
||||
"status.mention": "Mencionar @{name}",
|
||||
"status.delete": "Eliminar",
|
||||
"status.reply": "Responder",
|
||||
"status.reblog": "Reblogar",
|
||||
"status.favourite": "Favoritar",
|
||||
"status.reblogged_by": "{name} reblogou",
|
||||
"video_player.toggle_sound": "Alterar som",
|
||||
"account.mention": "Menção",
|
||||
"status.reblog": "Partilhar",
|
||||
"status.favourite": "Adicionar aos favoritos",
|
||||
"status.reblogged_by": "{name} partilhou",
|
||||
"status.sensitive_warning": "Conteúdo sensível",
|
||||
"status.sensitive_toggle": "Clique para ver",
|
||||
"status.show_more": "Mostrar mais",
|
||||
"status.show_less": "Mostrar menos",
|
||||
"status.open": "Expandir",
|
||||
"status.report": "Reportar @{name}",
|
||||
"status.load_more": "Carregar mais",
|
||||
"status.media_hidden": "Media escondida",
|
||||
"video_player.toggle_sound": "Ligar/Desligar som",
|
||||
"video_player.toggle_visible": "Ligar/Desligar vídeo",
|
||||
"account.mention": "Mencionar @{name}",
|
||||
"account.edit_profile": "Editar perfil",
|
||||
"account.unblock": "Desbloquear",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.block": "Bloquear",
|
||||
"account.unblock": "Não bloquear @{name}",
|
||||
"account.unfollow": "Não seguir",
|
||||
"account.block": "Bloquear @{name}",
|
||||
"account.mute": "Mute",
|
||||
"account.unmute": "Remover Mute",
|
||||
"account.follow": "Seguir",
|
||||
"account.block": "Bloquear",
|
||||
"account.posts": "Posts",
|
||||
"account.follows": "Segue",
|
||||
"account.followers": "Seguidores",
|
||||
"account.follows_you": "Segue você",
|
||||
"account.follows_you": "É teu seguidor",
|
||||
"account.requested": "A aguardar aprovação",
|
||||
"account.report": "Denunciar",
|
||||
"account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
|
||||
"getting_started.heading": "Primeiros passos",
|
||||
"getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.",
|
||||
"getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
|
||||
"getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
|
||||
"getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social",
|
||||
"getting_started.about_developer": "Pode seguir o developer deste projecto em Gargron@mastodon.social",
|
||||
"getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
|
||||
"column.home": "Home",
|
||||
"column.mentions": "Menções",
|
||||
"column.public": "Público",
|
||||
"tabs_bar.compose": "Compôr",
|
||||
"column.community": "Local",
|
||||
"column.public": "Global",
|
||||
"column.notifications": "Notificações",
|
||||
"column.blocks": "Utilizadores Bloqueados",
|
||||
"column.favourites": "Favoritos",
|
||||
"column.follow_requests": "Seguidores Pendentes",
|
||||
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
|
||||
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
|
||||
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
|
||||
"empty_column.home.public_timeline": "global",
|
||||
"empty_column.community": "Ainda não existem conteúdo local para mostrar!",
|
||||
"empty_column.hashtag": "Não existe qualquer conteúdo com essa hashtag",
|
||||
"tabs_bar.compose": "Criar",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.mentions": "Menções",
|
||||
"tabs_bar.public": "Público",
|
||||
"tabs_bar.notifications": "Notificações",
|
||||
"compose_form.placeholder": "Que estás pensando?",
|
||||
"tabs_bar.local_timeline": "Local",
|
||||
"tabs_bar.federated_timeline": "Global",
|
||||
"compose_form.placeholder": "Em que estás a pensar?",
|
||||
"compose_form.publish": "Publicar",
|
||||
"compose_form.sensitive": "Marcar conteúdo como sensível",
|
||||
"compose_form.unlisted": "Modo não-listado",
|
||||
"compose_form.sensitive": "Marcar media como conteúdo sensível",
|
||||
"compose_form.spoiler": "Esconder texto com aviso",
|
||||
"compose_form.spoiler_placeholder": "Aviso",
|
||||
"compose_form.private": "Tornar privado",
|
||||
"compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
|
||||
"compose_form.unlisted": "Não mostrar na listagem pública",
|
||||
"emoji_button.label": "Inserir Emoji",
|
||||
"navigation_bar.edit_profile": "Editar perfil",
|
||||
"navigation_bar.preferences": "Preferências",
|
||||
"navigation_bar.public_timeline": "Timeline Pública",
|
||||
"navigation_bar.logout": "Logout",
|
||||
"navigation_bar.community_timeline": "Local",
|
||||
"navigation_bar.public_timeline": "Global",
|
||||
"navigation_bar.blocks": "Utilizadores bloqueados",
|
||||
"navigation_bar.favourites": "Favoritos",
|
||||
"navigation_bar.info": "Mais informações",
|
||||
"navigation_bar.logout": "Sair",
|
||||
"navigation_bar.follow_requests": "Seguidores pendentes",
|
||||
"reply_indicator.cancel": "Cancelar",
|
||||
"search.placeholder": "Busca",
|
||||
"search.placeholder": "Pesquisar",
|
||||
"search.account": "Conta",
|
||||
"search.hashtag": "Hashtag",
|
||||
"search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
|
||||
"search.status_by": "Post de {name}",
|
||||
"upload_button.label": "Adicionar media",
|
||||
"upload_form.undo": "Desfazer",
|
||||
"notification.follow": "{name} seguiu você",
|
||||
"notification.favourite": "{name} favoritou seu post",
|
||||
"notification.reblog": "{name} reblogou o seu post",
|
||||
"notification.mention": "{name} mecionou você"
|
||||
"upload_form.undo": "Anular",
|
||||
"upload_progress.label": "A gravar…",
|
||||
"upload_area.title": "Arraste e solte para enviar",
|
||||
"notification.follow": "{name} seguiu-te",
|
||||
"notification.favourite": "{name} adicionou o teu post aos favoritos",
|
||||
"notification.reblog": "{name} partilhou o teu post",
|
||||
"notification.mention": "{name} mencionou-te",
|
||||
"notifications.column_settings.alert": "Notificações no computador",
|
||||
"notifications.column_settings.show": "Mostrar nas colunas",
|
||||
"notifications.column_settings.sound": "Reproduzir som",
|
||||
"notifications.column_settings.follow": "Novos seguidores:",
|
||||
"notifications.column_settings.favourite": "Favoritos:",
|
||||
"notifications.column_settings.mention": "Menções:",
|
||||
"notifications.column_settings.reblog": "Partilhas:",
|
||||
"notifications.clear": "Limpar notificações",
|
||||
"notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
|
||||
"notifications.settings": "Parâmetros da lista de Notificações",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.long": "Publicar em todos os feeds",
|
||||
"privacy.unlisted.short": "Não listar",
|
||||
"privacy.unlisted.long": "Não publicar nos feeds públicos",
|
||||
"privacy.private.short": "Privado",
|
||||
"privacy.private.long": "Apenas para os seguidores",
|
||||
"privacy.direct.short": "Directo",
|
||||
"privacy.direct.long": "Apenas para utilizadores mencionados",
|
||||
"privacy.change": "Ajustar a privacidade da mensagem",
|
||||
"media_gallery.toggle_visible": "Modificar a visibilidade",
|
||||
"missing_indicator.label": "Não encontrado",
|
||||
"follow_request.authorize": "Autorizar",
|
||||
"follow_request.reject": "Rejeitar",
|
||||
"home.settings": "Parâmetros da coluna Home",
|
||||
"home.column_settings.basic": "Básico",
|
||||
"home.column_settings.show_reblogs": "Mostrar as partilhas",
|
||||
"home.column_settings.show_replies": "Mostrar as respostas",
|
||||
"home.column_settings.advanced": "Avançadas",
|
||||
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
|
||||
"report.heading": "Nova denuncia",
|
||||
"report.placeholder": "Comentários adicionais",
|
||||
"report.submit": "Enviar",
|
||||
"report.target": "Denunciar"
|
||||
};
|
||||
|
||||
export default pt;
|
||||
|
||||
127
app/assets/javascripts/components/locales/ru.jsx
Normal file
127
app/assets/javascripts/components/locales/ru.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
const ru = {
|
||||
"column_back_button.label": "Назад",
|
||||
"lightbox.close": "Закрыть",
|
||||
"loading_indicator.label": "Загрузка...",
|
||||
"missing_indicator.label": "Не найдено",
|
||||
"status.mention": "Упомянуть @{name}",
|
||||
"status.media_hidden": "Медиаконтент скрыт",
|
||||
"status.delete": "Удалить",
|
||||
"status.reply": "Ответить",
|
||||
"status.reblog": "Продвинуть",
|
||||
"status.favourite": "Нравится",
|
||||
"status.reblogged_by": "{name} продвинул(а)",
|
||||
"status.sensitive_warning": "Чувствительный контент",
|
||||
"status.sensitive_toggle": "Нажмите для просмотра",
|
||||
"status.show_more": "Развернуть",
|
||||
"status.show_less": "Свернуть",
|
||||
"status.open": "Развернуть статус",
|
||||
"status.report": "Пожаловаться",
|
||||
"status.load_more": "Показать еще",
|
||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||
"video_player.toggle_visible": "Показать/скрыть",
|
||||
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||
"account.mention": "Упомянуть",
|
||||
"account.edit_profile": "Изменить профиль",
|
||||
"account.unblock": "Разблокировать",
|
||||
"account.unfollow": "Отписаться",
|
||||
"account.block": "Блокировать",
|
||||
"account.mute": "Заглушить",
|
||||
"account.report": "Пожаловаться",
|
||||
"account.unmute": "Снять глушение",
|
||||
"account.follow": "Подписаться",
|
||||
"account.posts": "Посты",
|
||||
"account.follows": "Подписки",
|
||||
"account.followers": "Подписаны",
|
||||
"account.follows_you": "Подписан(а) на Вас",
|
||||
"account.requested": "Ожидает подтверждения",
|
||||
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||
"getting_started.heading": "Добро пожаловать",
|
||||
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
||||
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
||||
"getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.",
|
||||
"getting_started.apps": "Доступны различные приложения.",
|
||||
"column.home": "Главная",
|
||||
"column.community": "Локальная лента",
|
||||
"column.public": "Глобальная лента",
|
||||
"column.notifications": "Уведомления",
|
||||
"column.favourites": "Понравившееся",
|
||||
"column.blocks": "Список блокировки",
|
||||
"column.follow_requests": "Запросы на подписку",
|
||||
"tabs_bar.compose": "Написать",
|
||||
"tabs_bar.home": "Главная",
|
||||
"tabs_bar.mentions": "Упоминания",
|
||||
"tabs_bar.public": "Глобальная лента",
|
||||
"tabs_bar.notifications": "Уведомления",
|
||||
"tabs_bar.local_timeline": "Локальная",
|
||||
"tabs_bar.federated_timeline": "Глобальная",
|
||||
"compose_form.placeholder": "О чем Вы думаете?",
|
||||
"compose_form.publish": "Трубить",
|
||||
"compose_form.sensitive": "Отметить как чувствительный контент",
|
||||
"compose_form.spoiler": "Скрыть текст за предупреждением",
|
||||
"compose_form.private": "Отметить как приватное",
|
||||
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
||||
"compose_form.unlisted": "Не отображать в публичных лентах",
|
||||
"compose_form.spoiler_placeholder": "Не для всех",
|
||||
"navigation_bar.edit_profile": "Изменить профиль",
|
||||
"navigation_bar.preferences": "Опции",
|
||||
"navigation_bar.community_timeline": "Локальная лента",
|
||||
"navigation_bar.public_timeline": "Глобальная лента",
|
||||
"navigation_bar.logout": "Выйти",
|
||||
"navigation_bar.info": "Об узле",
|
||||
"navigation_bar.favourites": "Понравившееся",
|
||||
"navigation_bar.blocks": "Список блокировки",
|
||||
"navigation_bar.follow_requests": "Запросы на подписку",
|
||||
"reply_indicator.cancel": "Отмена",
|
||||
"report.target": "Жалуемся на",
|
||||
"report.heading": "Новая жалоба",
|
||||
"report.placeholder": "Комментарий",
|
||||
"report.submit": "Отправить",
|
||||
"search.placeholder": "Поиск",
|
||||
"search.account": "Аккаунт",
|
||||
"search.hashtag": "Хэштег",
|
||||
"search.status_by": "Статус от {name}",
|
||||
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||
"upload_button.label": "Добавить медиаконтент",
|
||||
"upload_form.undo": "Отменить",
|
||||
"upload_progress.label": "Загрузка...",
|
||||
"notification.follow": "{name} подписался(-лась) на Вас",
|
||||
"notification.favourite": "{name} понравился Ваш статус",
|
||||
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
||||
"notification.mention": "{name} упомянул(а) Вас",
|
||||
"home.settings": "Настройки колонки",
|
||||
"home.column_settings.basic": "Основные",
|
||||
"home.column_settings.advanced": "Дополнительные",
|
||||
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
||||
"home.column_settings.show_reblogs": "Показывать продвижения",
|
||||
"home.column_settings.show_replies": "Показывать ответы",
|
||||
"notifications.clear": "Очистить уведомления",
|
||||
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
|
||||
"notifications.settings": "Настройки колонки",
|
||||
"notifications.column_settings.alert": "Десктопные уведомления",
|
||||
"notifications.column_settings.show": "Показывать в колонке",
|
||||
"notifications.column_settings.follow": "Новые подписчики:",
|
||||
"notifications.column_settings.favourite": "Нравится:",
|
||||
"notifications.column_settings.mention": "Упоминания:",
|
||||
"notifications.column_settings.reblog": "Продвижения:",
|
||||
"notifications.column_settings.sound": "Проигрывать звук",
|
||||
"empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
|
||||
"empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
|
||||
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
|
||||
"empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
|
||||
"empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
|
||||
"empty_column.home.public_timeline": "публичные ленты",
|
||||
"privacy.public.short": "Публичный",
|
||||
"privacy.public.long": "Показать в публичных лентах",
|
||||
"privacy.unlisted.short": "Скрытый",
|
||||
"privacy.unlisted.long": "Не показывать в лентах",
|
||||
"privacy.private.short": "Приватный",
|
||||
"privacy.private.long": "Показать только подписчикам",
|
||||
"privacy.direct.short": "Направленный",
|
||||
"privacy.direct.long": "Показать только упомянутым",
|
||||
"emoji_button.label": "Вставить эмодзи",
|
||||
"follow_request.authorize": "Авторизовать",
|
||||
"follow_request.reject": "Отказать",
|
||||
"media_gallery.toggle_visible": "Показать/скрыть",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
150
app/assets/javascripts/components/locales/zh-hk.jsx
Normal file
150
app/assets/javascripts/components/locales/zh-hk.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
|
||||
const localeData = zh.reduce(function (acc, localeData) {
|
||||
if (localeData.locale === "zh-Hant-HK") {
|
||||
// rename the locale "zh-Hant-HK" as "zh-HK"
|
||||
// (match the code usually used in Accepted-Language header)
|
||||
acc.push(Object.assign({},
|
||||
localeData,
|
||||
{
|
||||
"locale": "zh-HK",
|
||||
"parentLocale": "zh-Hant-HK",
|
||||
}
|
||||
));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
export { localeData as localeData };
|
||||
|
||||
const zh_hk = {
|
||||
"account.block": "封鎖 @{name}",
|
||||
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
|
||||
"account.edit_profile": "修改個人資料",
|
||||
"account.follow": "關注",
|
||||
"account.followers": "關注的人",
|
||||
"account.follows_you": "關注你",
|
||||
"account.follows": "正在關注",
|
||||
"account.mention": "提及 @{name}",
|
||||
"account.mute": "將 @{name} 靜音",
|
||||
"account.posts": "文章",
|
||||
"account.report": "舉報 @{name}",
|
||||
"account.requested": "等候審批",
|
||||
"account.unblock": "解除對 @{name} 的封鎖",
|
||||
"account.unfollow": "取消關注",
|
||||
"account.unmute": "取消 @{name} 的靜音",
|
||||
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||
"column_back_button.label": "返回",
|
||||
"column.blocks": "封鎖用戶",
|
||||
"column.community": "本站時間軸",
|
||||
"column.favourites": "喜歡的文章",
|
||||
"column.follow_requests": "關注請求",
|
||||
"column.home": "主頁",
|
||||
"column.notifications": "通知",
|
||||
"column.public": "跨站公共時間軸",
|
||||
"compose_form.placeholder": "你在想甚麼?",
|
||||
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至你所提及的 {domains} 用戶。你是否信任 {domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於各 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將無法收到這篇文章的私隱設定,然後可能被轉推給不能預知的用戶閱讀。",
|
||||
"compose_form.private": "標示為「只有關注你的人能看」",
|
||||
"compose_form.publish": "發文",
|
||||
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
||||
"compose_form.spoiler_placeholder": "敏感內容",
|
||||
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
||||
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
||||
"emoji_button.label": "加入表情符號",
|
||||
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
||||
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.home.public_timeline": "公共時間軸",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
|
||||
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
|
||||
"follow_request.authorize": "批准",
|
||||
"follow_request.reject": "拒絕",
|
||||
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
||||
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
||||
"getting_started.apps": "手機或桌面應用程式",
|
||||
"getting_started.heading": "開始使用",
|
||||
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
||||
"home.column_settings.advanced": "進階",
|
||||
"home.column_settings.basic": "基本",
|
||||
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
|
||||
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
||||
"home.column_settings.show_replies": "顯示回應文章",
|
||||
"home.settings": "欄位設定",
|
||||
"lightbox.close": "Close",
|
||||
"loading_indicator.label": "載入中...",
|
||||
"media_gallery.toggle_visible": "打開或關上",
|
||||
"missing_indicator.label": "找不到內容",
|
||||
"navigation_bar.blocks": "被封鎖的用戶",
|
||||
"navigation_bar.community_timeline": "本站時間軸",
|
||||
"navigation_bar.edit_profile": "修改個人資料",
|
||||
"navigation_bar.favourites": "喜歡的內容",
|
||||
"navigation_bar.follow_requests": "關注請求",
|
||||
"navigation_bar.info": "關於本服務站",
|
||||
"navigation_bar.logout": "登出",
|
||||
"navigation_bar.preferences": "偏好設定",
|
||||
"navigation_bar.public_timeline": "跨站公共時間軸",
|
||||
"notification.favourite": "{name} 喜歡你的文章",
|
||||
"notification.follow": "{name} 開始關注你",
|
||||
"notification.mention": "{name} 提及你",
|
||||
"notification.reblog": "{name} 轉推你的文章",
|
||||
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
|
||||
"notifications.clear": "清空通知紀錄",
|
||||
"notifications.column_settings.alert": "顯示桌面通知",
|
||||
"notifications.column_settings.favourite": "喜歡你的文章:",
|
||||
"notifications.column_settings.follow": "關注你:",
|
||||
"notifications.column_settings.mention": "提及你:",
|
||||
"notifications.column_settings.reblog": "轉推你的文章:",
|
||||
"notifications.column_settings.show": "在通知欄顯示",
|
||||
"notifications.column_settings.sound": "播放音效",
|
||||
"notifications.settings": "欄位設定",
|
||||
"privacy.change": "調整私隱設定",
|
||||
"privacy.direct.long": "只有提及的用戶能看到",
|
||||
"privacy.direct.short": "私人訊息",
|
||||
"privacy.private.long": "只有關注你用戶能看到",
|
||||
"privacy.private.short": "關注者",
|
||||
"privacy.public.long": "在公共時間軸顯示",
|
||||
"privacy.public.short": "公共",
|
||||
"privacy.unlisted.long": "公開,但不在公共時間軸顯示",
|
||||
"privacy.unlisted.short": "公開",
|
||||
"reply_indicator.cancel": "取消",
|
||||
"report.heading": "舉報",
|
||||
"report.placeholder": "額外訊息",
|
||||
"report.submit": "提交",
|
||||
"report.target": "Reporting",
|
||||
"search_results.total": "{count} 項結果",
|
||||
"search.account": "用戶",
|
||||
"search.hashtag": "標籤",
|
||||
"search.placeholder": "搜尋",
|
||||
"search.status_by": "按{name}搜尋文章",
|
||||
"status.delete": "刪除",
|
||||
"status.favourite": "喜歡",
|
||||
"status.load_more": "載入更多",
|
||||
"status.media_hidden": "隱藏媒體內容",
|
||||
"status.mention": "提及 @{name}",
|
||||
"status.open": "展開文章",
|
||||
"status.reblog": "轉推",
|
||||
"status.reblogged_by": "{name} 轉推",
|
||||
"status.reply": "回應",
|
||||
"status.report": "舉報 @{name}",
|
||||
"status.sensitive_toggle": "點擊顯示",
|
||||
"status.sensitive_warning": "敏感內容",
|
||||
"status.show_less": "減少顯示",
|
||||
"status.show_more": "顯示更多",
|
||||
"tabs_bar.compose": "撰寫",
|
||||
"tabs_bar.federated_timeline": "跨站",
|
||||
"tabs_bar.home": "主頁",
|
||||
"tabs_bar.local_timeline": "本站",
|
||||
"tabs_bar.mentions": "提及",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"tabs_bar.public": "跨站公共時間軸",
|
||||
"upload_area.title": "將檔案拖放至此上載",
|
||||
"upload_button.label": "上載媒體檔案",
|
||||
"upload_form.undo": "還原",
|
||||
"upload_progress.label": "上載中……",
|
||||
"video_player.expand": "展開影片",
|
||||
"video_player.toggle_sound": "開關音效",
|
||||
"video_player.toggle_visible": "打開或關上",
|
||||
};
|
||||
|
||||
export default zh_hk;
|
||||
@@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
||||
|
||||
dispatch(showAlert(title, message));
|
||||
} else {
|
||||
console.error(action.error);
|
||||
console.error(action.error); // eslint-disable-line no-console
|
||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_EXPAND_SUCCESS
|
||||
} from '../actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS
|
||||
} from '../actions/mutes';
|
||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
||||
import {
|
||||
REBLOG_SUCCESS,
|
||||
@@ -94,6 +98,8 @@ export default function accounts(state = initialState, action) {
|
||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||
case BLOCKS_FETCH_SUCCESS:
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return normalizeAccounts(state, action.accounts);
|
||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||
|
||||
@@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
||||
|
||||
export default function alerts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ function clearAll(state) {
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
});
|
||||
};
|
||||
@@ -76,7 +77,8 @@ function appendMedia(state, media) {
|
||||
map.update('media_attachments', list => list.push(media));
|
||||
map.set('is_uploading', false);
|
||||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||
map.update('text', oldText => `${oldText} ${media.get('text_url')}`.trim());
|
||||
map.set('focusDate', new Date());
|
||||
map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`.trim() + ' ');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -156,6 +158,9 @@ export default function compose(state = initialState, action) {
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', action.status.get('spoiler_text'));
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
});
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
|
||||
@@ -2,6 +2,7 @@ import { STORE_HYDRATE } from '../actions/store';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
streaming_api_base_url: null,
|
||||
access_token: null,
|
||||
me: null
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user