Compare commits
545 Commits
Author | SHA1 | Date |
---|---|---|
Blackle Mori | 0f84a7174c | |
multiple creatures | f9eaffc790 | |
multiple creatures | d2eb644d45 | |
multiple creatures | f40c6dbc93 | |
multiple creatures | 14cf223041 | |
multiple creatures | 7bbcf793bc | |
multiple creatures | f783ec279d | |
multiple creatures | f54329f9d6 | |
multiple creatures | 7ce8751692 | |
multiple creatures | e8a5c9a972 | |
multiple creatures | 18e727efdf | |
multiple creatures | bbb025be69 | |
multiple creatures | 5ae918d968 | |
multiple creatures | ad81a82ea4 | |
multiple creatures | c8a9f38bcd | |
multiple creatures | b4b6f39c87 | |
multiple creatures | 903cabc4b2 | |
multiple creatures | cc3cf7b606 | |
multiple creatures | e2e0eddda7 | |
multiple creatures | b6b5bae72c | |
Lumb | c19fc4cf9b | |
multiple creatures | a06f8140d9 | |
multiple creatures | 6d026c5007 | |
Lumb | c9dc797ef2 | |
multiple creatures | 8a7605e502 | |
multiple creatures | 2a41140dc7 | |
multiple creatures | 1cdf37da83 | |
multiple creatures | 0ea96118af | |
multiple creatures | eac4369868 | |
multiple creatures | 5f08b96cbe | |
multiple creatures | b2d0389fea | |
multiple creatures | 8c196e70b1 | |
multiple creatures | e466f9c2ce | |
multiple creatures | d8156acb06 | |
multiple creatures | a4b7b5c132 | |
multiple creatures | e496fd473f | |
multiple creatures | ecd461aa78 | |
multiple creatures | 4283f15493 | |
multiple creatures | 4dfc40324b | |
multiple creatures | d019e55b7b | |
multiple creatures | d8a1e472c2 | |
multiple creatures | bcfa50f5f5 | |
multiple creatures | 72592b3c9c | |
multiple creatures | ef04f3879a | |
multiple creatures | a8475313b8 | |
multiple creatures | d9a8c50f92 | |
multiple creatures | ff22f11aae | |
multiple creatures | 2329043e7b | |
multiple creatures | b564aac6f3 | |
multiple creatures | da389a664b | |
multiple creatures | 647ac0f86a | |
multiple creatures | 5e3ab78fa4 | |
multiple creatures | 2ff40d3788 | |
multiple creatures | 39a58f4061 | |
multiple creatures | d9cb0d32ed | |
multiple creatures | 1cd9fea3b5 | |
multiple creatures | 879166633c | |
multiple creatures | f86a3314f7 | |
multiple creatures | 9a3c4bc051 | |
multiple creatures | 9ba2081720 | |
multiple creatures | 4e3d546f61 | |
multiple creatures | f46293f6d9 | |
multiple creatures | 2be5c8a55c | |
multiple creatures | 4801d5ac84 | |
multiple creatures | 0a646efd48 | |
multiple creatures | d69ee097dd | |
multiple creatures | 4427480356 | |
multiple creatures | 53a1f9d634 | |
multiple creatures | 3e6831d7d6 | |
multiple creatures | e1bdc82d07 | |
multiple creatures | 1785c93da7 | |
multiple creatures | b644f1c505 | |
multiple creatures | cd52f75006 | |
multiple creatures | a2b9ac9a48 | |
multiple creatures | 60179e53ea | |
multiple creatures | ceaf900dfc | |
multiple creatures | a96d89ac56 | |
multiple creatures | 6613005ae6 | |
multiple creatures | 99d1b1ff6f | |
multiple creatures | f0094fd143 | |
multiple creatures | cf333d3699 | |
multiple creatures | fc2e81c93f | |
multiple creatures | dbcc560826 | |
multiple creatures | b1d125d704 | |
multiple creatures | e11196775f | |
multiple creatures | 7cfc0f0dce | |
multiple creatures | 65c42e5398 | |
multiple creatures | 3813810cac | |
multiple creatures | 06fb561bd6 | |
multiple creatures | a6e34404a2 | |
multiple creatures | 0e87431d61 | |
multiple creatures | c4005a0b25 | |
multiple creatures | 720207cf4b | |
multiple creatures | c4bf59ed9c | |
multiple creatures | 6bc75ce03b | |
multiple creatures | 80a81fe223 | |
multiple creatures | 964054b6db | |
multiple creatures | 9e9a593f5a | |
multiple creatures | a5ce8eddb4 | |
multiple creatures | 85aec06845 | |
multiple creatures | ccb84572d6 | |
multiple creatures | 3f327a3ea7 | |
multiple creatures | 2ca0b8ce62 | |
multiple creatures | 96770151ef | |
multiple creatures | d51a846d3a | |
multiple creatures | d9758157b9 | |
multiple creatures | 90130014dd | |
multiple creatures | 0fb1e7888e | |
multiple creatures | 9d55cfc6ad | |
multiple creatures | 6fa955e8a1 | |
multiple creatures | 42bf20d22f | |
multiple creatures | 896817421a | |
multiple creatures | 74290d4eb3 | |
multiple creatures | 02729ab3ae | |
multiple creatures | feeb789ecd | |
multiple creatures | 863c101e0a | |
multiple creatures | 90d72f19ba | |
multiple creatures | 965b713ac2 | |
multiple creatures | 4fc97d77a9 | |
multiple creatures | b93bf4b271 | |
multiple creatures | 30d3b9a6f7 | |
multiple creatures | 9e841ece20 | |
multiple creatures | 28b2a700f0 | |
multiple creatures | 0f18a0ad00 | |
multiple creatures | c4f5de4e06 | |
multiple creatures | 712137fda9 | |
multiple creatures | 31d2b16e43 | |
multiple creatures | 9febf12029 | |
multiple creatures | aaa207284a | |
multiple creatures | cfb28743fa | |
multiple creatures | 1aba334730 | |
multiple creatures | f9e382b9a6 | |
multiple creatures | 78dd3d0e92 | |
multiple creatures | 0151f14dbc | |
multiple creatures | e0b257d512 | |
multiple creatures | 5c27502afa | |
multiple creatures | 0d17c2bf2e | |
multiple creatures | 234fae09ad | |
multiple creatures | bf27f256c5 | |
multiple creatures | cf28bbd9fa | |
multiple creatures | ab43f884e3 | |
multiple creatures | 8945a3b534 | |
multiple creatures | 5ec6e9c1e2 | |
multiple creatures | c0d23aa032 | |
multiple creatures | 88c23ae912 | |
multiple creatures | 9cd09d4a70 | |
multiple creatures | 1f7a5bb57e | |
multiple creatures | cefcad1130 | |
multiple creatures | 8f6e737f38 | |
multiple creatures | b75f7be799 | |
multiple creatures | 4415e8b047 | |
multiple creatures | 25d628fca3 | |
multiple creatures | d83fcfd1f1 | |
multiple creatures | aaae5aee52 | |
multiple creatures | de542eca57 | |
multiple creatures | 913ef775ab | |
multiple creatures | a73ec02673 | |
multiple creatures | 3862f48c34 | |
multiple creatures | 2a6ccce070 | |
multiple creatures | d377c828ef | |
multiple creatures | 4836e1f5df | |
multiple creatures | 6a2b323006 | |
multiple creatures | 7df4d0e132 | |
multiple creatures | 54bc08a8a3 | |
multiple creatures | c2e47f5871 | |
multiple creatures | 2822fbc443 | |
multiple creatures | 86f29a68fb | |
multiple creatures | d82d7e0b2b | |
multiple creatures | 155c324a7b | |
multiple creatures | e14d543edd | |
multiple creatures | e3ecc0871c | |
multiple creatures | b0eade5ad6 | |
multiple creatures | acc1fb81fe | |
multiple creatures | 47d9a34401 | |
multiple creatures | 084b950401 | |
multiple creatures | bca5a3073f | |
multiple creatures | d9073f132b | |
multiple creatures | 61461a5323 | |
multiple creatures | 3582566a52 | |
multiple creatures | 6de7b8e021 | |
multiple creatures | 1cf4d5a83d | |
multiple creatures | c4600411f7 | |
multiple creatures | 19fc6952b2 | |
multiple creatures | 70080ce6e6 | |
multiple creatures | c4718cd2be | |
multiple creatures | 7a37731210 | |
multiple creatures | 0dabcbfb02 | |
multiple creatures | 483f550f9c | |
multiple creatures | 1edc2f1aeb | |
multiple creatures | f0506110c4 | |
multiple creatures | 4cfff5b001 | |
multiple creatures | ed50fee09f | |
multiple creatures | 3ff2871b27 | |
multiple creatures | 243cbb2861 | |
multiple creatures | c864465e71 | |
multiple creatures | 74e81d4ef7 | |
multiple creatures | be251c1eb4 | |
multiple creatures | 2d99300a6d | |
multiple creatures | 29cdfc36fc | |
multiple creatures | d9d2c9a77e | |
multiple creatures | 4e28528888 | |
multiple creatures | 07794055f9 | |
multiple creatures | 348dd5aa35 | |
multiple creatures | 2f8ac8838d | |
multiple creatures | 9b7e4018b0 | |
multiple creatures | dc32d286bd | |
multiple creatures | 6d07ba50f3 | |
multiple creatures | 3fda862ea0 | |
multiple creatures | bc22ab034b | |
multiple creatures | 23c36c2d7c | |
multiple creatures | ff75f5ea4b | |
multiple creatures | cfd314432d | |
multiple creatures | 7c60955f06 | |
multiple creatures | 44e204613d | |
multiple creatures | e80921bf83 | |
multiple creatures | 6578d02a0a | |
multiple creatures | 66286178ad | |
multiple creatures | 40debd9f80 | |
multiple creatures | 879a4a8029 | |
multiple creatures | d620d1749d | |
multiple creatures | d219ecded6 | |
multiple creatures | b233e1eddf | |
multiple creatures | afa8bb3892 | |
multiple creatures | 24c40ef9b9 | |
multiple creatures | 881ccb2de1 | |
multiple creatures | 42618190b1 | |
multiple creatures | 96050ff1d9 | |
multiple creatures | 7f19514527 | |
multiple creatures | b28fae301a | |
multiple creatures | f927cb47b4 | |
multiple creatures | 40d4eccb00 | |
multiple creatures | ddd84a97ad | |
multiple creatures | 54c3ac4aba | |
multiple creatures | 3f1e5d2f87 | |
multiple creatures | 6bffa56473 | |
multiple creatures | 65b79ae188 | |
multiple creatures | 83cb62809b | |
multiple creatures | 9f2d158864 | |
multiple creatures | 6a5b0b65bb | |
multiple creatures | 6cb00bc91d | |
multiple creatures | d3357a90fe | |
Lumb | 0189e487f8 | |
Lumb | 5f03b404c4 | |
Lumb | 7629b2c22b | |
Lumb | 2e4bc1a64d | |
Lumb | 6d8357a6f0 | |
Lumb | 05980a56d2 | |
Lumb | 2597f31daf | |
Lumb | 6181c72ff5 | |
Lumb | b052644d2e | |
Lumb | 5a93e171db | |
Lumb | 4695ab5db9 | |
multiple creatures | 1049c858ac | |
multiple creatures | 12d5f1edb6 | |
multiple creatures | c135018d9f | |
multiple creatures | efcd176d58 | |
multiple creatures | 5e3a120120 | |
multiple creatures | 38a3c2b7b9 | |
multiple creatures | 92406964f1 | |
multiple creatures | 2089a78f82 | |
dependabot-preview[bot] | 17a701b443 | |
multiple creatures | c3127be31e | |
multiple creatures | 6daeca8a09 | |
multiple creatures | 1c10ce6269 | |
multiple creatures | 0eeb7fa881 | |
multiple creatures | 6f1a07945e | |
multiple creatures | 037be68060 | |
multiple creatures | 09eb7fb78d | |
multiple creatures | 4f3618f7be | |
multiple creatures | 2bbc06de5c | |
multiple creatures | 31a7ca0468 | |
multiple creatures | b441174bd2 | |
multiple creatures | 6b72e8a4df | |
multiple creatures | 07013fba48 | |
multiple creatures | ab132569d7 | |
multiple creatures | 7147447254 | |
multiple creatures | 27d67e2d5c | |
multiple creatures | 441bead7ba | |
multiple creatures | 45408c3c01 | |
multiple creatures | 436f7984d9 | |
multiple creatures | 9a2f0131c6 | |
multiple creatures | 5e3ea221a8 | |
multiple creatures | cf3ec71aa5 | |
multiple creatures | 0a5eba734e | |
multiple creatures | 29643fd6c4 | |
multiple creatures | 992bd7c752 | |
multiple creatures | a1d091b552 | |
multiple creatures | 61ac01a6bb | |
multiple creatures | 7b6f8e5419 | |
multiple creatures | 2bdfbfe32c | |
multiple creatures | ec288a11a0 | |
multiple creatures | dd7164aac2 | |
multiple creatures | 9abf1ce535 | |
multiple creatures | 3c455a7b81 | |
multiple creatures | 6c6d5319d9 | |
multiple creatures | d9b62e5d11 | |
multiple creatures | 58f78a7af2 | |
multiple creatures | d4ca04f24d | |
multiple creatures | 641e5acc09 | |
multiple creatures | d6f37c6ae0 | |
multiple creatures | cbdadfb5fa | |
multiple creatures | 62d667dbf5 | |
multiple creatures | bf33771c80 | |
multiple creatures | 83c2c466fb | |
multiple creatures | 55e0484121 | |
multiple creatures | 811137ef69 | |
multiple creatures | 8534702269 | |
multiple creatures | 06b8b09fca | |
multiple creatures | 0f50698beb | |
multiple creatures | 01a5d51ef7 | |
multiple creatures | 181d9cd24b | |
multiple creatures | f0466aec02 | |
multiple creatures | 8b47cdef24 | |
multiple creatures | 46216a4030 | |
multiple creatures | ee83fe92f1 | |
multiple creatures | 1244c30349 | |
multiple creatures | 45e4449347 | |
multiple creatures | 1fa6d6e16b | |
multiple creatures | 8ff64df4dd | |
multiple creatures | 978ed2797f | |
multiple creatures | c66d932c22 | |
multiple creatures | 5e2a8f3d3c | |
multiple creatures | fd8e92438c | |
multiple creatures | 1a670573e5 | |
multiple creatures | 2705c6751e | |
multiple creatures | fd753d1201 | |
multiple creatures | 0a00a42c67 | |
multiple creatures | 23d2e5f97c | |
multiple creatures | e411b20711 | |
multiple creatures | 7a0dc34cad | |
multiple creatures | 09b7532805 | |
multiple creatures | 5c9aed40f6 | |
multiple creatures | d70e5afb6e | |
multiple creatures | edeb344b90 | |
multiple creatures | 8394452bae | |
multiple creatures | 7f460853c8 | |
multiple creatures | f0c9477a4b | |
multiple creatures | 21c3730703 | |
multiple creatures | dd021e8570 | |
multiple creatures | 75d114216e | |
multiple creatures | 8a1ac19777 | |
multiple creatures | 506d2e9cf0 | |
multiple creatures | e58efb8528 | |
multiple creatures | 3b6f8ddacc | |
multiple creatures | c961429dc2 | |
multiple creatures | 89c5d8ec4e | |
multiple creatures | a680595ecb | |
multiple creatures | 24a59d8f58 | |
multiple creatures | 47a251048c | |
multiple creatures | 8d12242216 | |
multiple creatures | 6834ddffc9 | |
multiple creatures | 3b06175e8f | |
multiple creatures | 5c59d1837f | |
multiple creatures | 0782dc3905 | |
multiple creatures | cb311a274c | |
multiple creatures | a3faf5b169 | |
multiple creatures | c2e07ecd7f | |
multiple creatures | edfabe44da | |
multiple creatures | 540728e063 | |
multiple creatures | 82f691e9a9 | |
multiple creatures | 976ec97ffe | |
multiple creatures | e85b8af051 | |
multiple creatures | 66886d4367 | |
multiple creatures | dca70079b1 | |
multiple creatures | 6c374b5153 | |
multiple creatures | 6e8ec7f0a5 | |
multiple creatures | 9f9ee606f3 | |
multiple creatures | e0c6d56f5f | |
multiple creatures | 59fd9c25dc | |
multiple creatures | feea4f6dc0 | |
multiple creatures | 46522d8c1b | |
multiple creatures | 3e8690f2c0 | |
multiple creatures | 163d42c04a | |
multiple creatures | 1ed7aca171 | |
multiple creatures | a1be3a11a9 | |
multiple creatures | 2f23d34e36 | |
multiple creatures | db6ae92c09 | |
multiple creatures | 726a99a6e4 | |
multiple creatures | 7ca4a2089c | |
multiple creatures | ecf21d3fc6 | |
multiple creatures | c983c4e952 | |
multiple creatures | a47b1daaeb | |
multiple creatures | 992218f05f | |
multiple creatures | 545330dc65 | |
multiple creatures | a7015f9202 | |
multiple creatures | 515688c547 | |
multiple creatures | 79cc6792a1 | |
multiple creatures | cfaed183aa | |
multiple creatures | db67333d62 | |
multiple creatures | 933d7afa87 | |
multiple creatures | 8dbefa3966 | |
multiple creatures | 1a12429051 | |
multiple creatures | 1e2977256c | |
multiple creatures | 89e54748d7 | |
multiple creatures | e87151f458 | |
multiple creatures | 1ab60fea48 | |
multiple creatures | ed9c8f67c4 | |
multiple creatures | 13262ea614 | |
multiple creatures | 26d90a36ff | |
multiple creatures | 2423830e3c | |
multiple creatures | d339d2bbb4 | |
multiple creatures | 4644a6245f | |
multiple creatures | 4d12e45d3b | |
multiple creatures | f573712f82 | |
multiple creatures | d8f182d235 | |
multiple creatures | 7ce2e174cf | |
multiple creatures | 9d4f42fb89 | |
multiple creatures | fb449f273a | |
multiple creatures | 2a5784c61f | |
multiple creatures | c1d2febf03 | |
multiple creatures | 05ed9b6cea | |
multiple creatures | 534e19cbe3 | |
multiple creatures | af7e3a88d4 | |
multiple creatures | 15b35d99ce | |
multiple creatures | 66c640fd38 | |
multiple creatures | bb9aa16284 | |
multiple creatures | 87f4b4d230 | |
multiple creatures | 19b78604e9 | |
multiple creatures | a230a6038b | |
multiple creatures | 4088e0a648 | |
multiple creatures | f7c5171a83 | |
multiple creatures | b8b525c54a | |
multiple creatures | 9753fd203e | |
multiple creatures | 1fe28ca9d6 | |
multiple creatures | fb47b6e120 | |
multiple creatures | 71302f6dec | |
multiple creatures | ea40ae8de7 | |
multiple creatures | acdfce2bba | |
multiple creatures | 1ca30982fa | |
multiple creatures | b53ddb8126 | |
multiple creatures | adea831c00 | |
multiple creatures | 036f422877 | |
multiple creatures | 500b485b77 | |
multiple creatures | cea2baf2e0 | |
multiple creatures | ec5c6e7fcb | |
multiple creatures | 3bfa72cbce | |
multiple creatures | cdacbb3c4c | |
multiple creatures | 50fae175fd | |
multiple creatures | 021fedeb2a | |
multiple creatures | 841776edfb | |
multiple creatures | 3f282fe433 | |
multiple creatures | 340f1e9149 | |
multiple creatures | dfbc2fc518 | |
multiple creatures | 1930b2332d | |
multiple creatures | beee1934b2 | |
multiple creatures | 08a32175fa | |
multiple creatures | cec0a7ff3c | |
multiple creatures | 89ad628e88 | |
multiple creatures | abb8848eb7 | |
multiple creatures | 1823e78aa7 | |
multiple creatures | 2db51e2f4c | |
multiple creatures | c86c4b95be | |
multiple creatures | f344170fd0 | |
multiple creatures | 6c7f1691ee | |
multiple creatures | 16147d73a2 | |
multiple creatures | dd5e02ad5d | |
multiple creatures | 4c170d2a98 | |
multiple creatures | d033327136 | |
multiple creatures | 2cc2089534 | |
multiple creatures | cd042a4ee3 | |
multiple creatures | 7370ff5677 | |
multiple creatures | 4550d9188d | |
multiple creatures | 0e5935e475 | |
multiple creatures | 8d19acc618 | |
multiple creatures | caf265bbeb | |
multiple creatures | 2ee72d3aaf | |
multiple creatures | bc77147a95 | |
multiple creatures | d7dd432727 | |
multiple creatures | 618627eb12 | |
multiple creatures | 8938e343de | |
multiple creatures | c85e467c0c | |
multiple creatures | d0631f446c | |
multiple creatures | e42f09c53d | |
multiple creatures | 7580036307 | |
multiple creatures | b507a598c5 | |
multiple creatures | 01acaa792a | |
multiple creatures | d00907014b | |
multiple creatures | dd70b4e463 | |
multiple creatures | 8bf596861b | |
multiple creatures | 6614d42c6e | |
multiple creatures | 90d2280dfe | |
multiple creatures | 5284cdc24d | |
multiple creatures | ba51d3f135 | |
multiple creatures | 3e8d7fd5f8 | |
multiple creatures | 475ef8bbf1 | |
multiple creatures | 02d5a52673 | |
multiple creatures | 85a9dee905 | |
multiple creatures | 57113accf6 | |
multiple creatures | 3a69196c47 | |
multiple creatures | f920593cd4 | |
multiple creatures | 6c00e2abcf | |
multiple creatures | dc5993191d | |
multiple creatures | e84f1bc4ef | |
multiple creatures | 758deeb818 | |
multiple creatures | 312bc14d06 | |
multiple creatures | 0554e7c3bd | |
multiple creatures | cbbed1863a | |
multiple creatures | 393f8aa4e1 | |
multiple creatures | bd55c63d93 | |
multiple creatures | 366676a69f | |
multiple creatures | 2f136e9889 | |
multiple creatures | b57383e025 | |
multiple creatures | 0bdb555f57 | |
multiple creatures | b5e6e77ca4 | |
multiple creatures | f21d4c3209 | |
multiple creatures | 3e55dcf944 | |
multiple creatures | ff02142601 | |
multiple creatures | 467170f4a0 | |
multiple creatures | b5cb68581b | |
multiple creatures | 1affcf73fb | |
multiple creatures | 9d559d790b | |
multiple creatures | ace01a82da | |
multiple creatures | 0697f20f2c | |
multiple creatures | 178a2dc9eb | |
multiple creatures | f1ed7bb675 | |
multiple creatures | 28c9b9ce6a | |
multiple creatures | 10b20607ac | |
multiple creatures | 1d5da39902 | |
multiple creatures | d6738df083 | |
multiple creatures | 1636a4e8ae | |
multiple creatures | a7aa2544e4 | |
multiple creatures | 9a94d5e2c5 | |
multiple creatures | fa7e6b5a63 | |
multiple creatures | 5c6a14ca68 | |
multiple creatures | cf557f1849 | |
Thibaut Girka | f119ef489c | |
multiple creatures | 6a0c65d461 | |
multiple creatures | 706c177c8e | |
multiple creatures | 3c17de7724 | |
multiple creatures | 17a7aeb807 | |
multiple creatures | 918f7b7478 | |
Daggertooth | 3f79556155 | |
Daggertooth | b90ad78073 | |
Daggertooth | 28724df663 | |
Daggertooth | 49353be9f1 | |
Daggertooth | 5db1301d7a | |
Daggertooth | b8ef3027da | |
Daggertooth | 41642c6668 | |
Daggertooth | 70649ca277 | |
Daggertooth | 71066e4ad6 | |
Daggertooth | a607195dc0 | |
Daggertooth | d0d22f3c68 | |
Daggertooth | cf3aee91ca | |
Daggertooth | ae1576691f | |
Daggertooth | 4df5e5c01c |
|
@ -160,6 +160,27 @@ STREAMING_CLUSTER_NUM=1
|
|||
# Maximum number of pinned posts
|
||||
# MAX_PINNED_TOOTS=5
|
||||
|
||||
# Maximim number of profile fields allowed
|
||||
# MAX_PROFILE_FIELDS=4
|
||||
|
||||
# Maximum allowed display name characters
|
||||
# MAX_DISPLAY_NAME_CHARS=30
|
||||
|
||||
# Maximum image and video upload sizes
|
||||
# Units are in megabytes
|
||||
# MAX_SIZE_LIMIT=66
|
||||
|
||||
# Maximum gif size limit
|
||||
# Units are in kilobytes
|
||||
# MAX_GIF_SIZE=333
|
||||
|
||||
# Maximum length of audio uploads in seconds
|
||||
# MAX_AUDIO_LENGTH=60
|
||||
|
||||
# Maximum number of search results
|
||||
# Only really matters if elasticsearch is enabled
|
||||
# MAX_SEARCH_RESULTS=100
|
||||
|
||||
# LDAP authentication (optional)
|
||||
# LDAP_ENABLED=true
|
||||
# LDAP_HOST=localhost
|
||||
|
|
|
@ -58,3 +58,5 @@ yarn-debug.log
|
|||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
||||
|
||||
# ignore misc directory
|
||||
/misc
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.6.1
|
||||
2.6.3
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -30,7 +30,6 @@ gem 'browser'
|
|||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.0'
|
||||
gem 'cld3', '~> 3.2.4'
|
||||
gem 'devise', '~> 4.6'
|
||||
gem 'devise-two-factor', '~> 3.0'
|
||||
|
||||
|
@ -62,7 +61,6 @@ gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
|
|||
gem 'nokogiri', '~> 1.10'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.7'
|
||||
gem 'ostatus2', '~> 2.0'
|
||||
gem 'ox', '~> 2.10'
|
||||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||
gem 'pundit', '~> 2.0'
|
||||
|
@ -151,3 +149,5 @@ group :production do
|
|||
end
|
||||
|
||||
gem 'concurrent-ruby', require: false
|
||||
|
||||
gem "ruby-bbcode", "~> 2.0"
|
||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -146,8 +146,6 @@ GEM
|
|||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.3.10)
|
||||
cld3 (3.2.4)
|
||||
ffi (>= 1.1.0, < 1.11.0)
|
||||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
|
@ -382,10 +380,6 @@ GEM
|
|||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.7)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (2.0.3)
|
||||
addressable (~> 2.5)
|
||||
http (~> 3.0)
|
||||
nokogiri (~> 1.8)
|
||||
ox (2.10.0)
|
||||
paperclip (6.0.0)
|
||||
activemodel (>= 4.2.0)
|
||||
|
@ -539,6 +533,8 @@ GEM
|
|||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
ruby-bbcode (2.1.0)
|
||||
activesupport (>= 4.2.2)
|
||||
ruby-progressbar (1.10.0)
|
||||
ruby-saml (1.9.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
|
@ -681,7 +677,6 @@ DEPENDENCIES
|
|||
capybara (~> 3.20)
|
||||
charlock_holmes (~> 0.7.6)
|
||||
chewy (~> 5.0)
|
||||
cld3 (~> 3.2.4)
|
||||
climate_control (~> 0.2)
|
||||
concurrent-ruby
|
||||
derailed_benchmarks
|
||||
|
@ -728,7 +723,6 @@ DEPENDENCIES
|
|||
omniauth (~> 1.9)
|
||||
omniauth-cas (~> 1.1)
|
||||
omniauth-saml (~> 1.10)
|
||||
ostatus2 (~> 2.0)
|
||||
ox (~> 2.10)
|
||||
paperclip (~> 6.0)
|
||||
paperclip-av-transcoder (~> 0.6)
|
||||
|
@ -758,6 +752,7 @@ DEPENDENCIES
|
|||
rspec-rails (~> 3.8)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.69)
|
||||
ruby-bbcode (~> 2.0)
|
||||
sanitize (~> 5.0)
|
||||
scss_lint (~> 0.58)
|
||||
sidekiq (~> 5.2)
|
||||
|
|
71
README.md
71
README.md
|
@ -1,12 +1,69 @@
|
|||
# Mastodon Glitch Edition #
|
||||
# Monsterfork
|
||||
|
||||
> Now with automated deploys!
|
||||
> *[Monsterpit](https://monsterpit.net/about/more) is a community of creatures and critters* /
|
||||
> *For those who love monsters to be monsters they love.* /
|
||||
> *Whether fur, scale, or skin; whether plural or ‘kin–* /
|
||||
> *If you don’t feel quite human, come!* /
|
||||
> *You’ll fit right on in.*
|
||||
|
||||
[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci]
|
||||
Monsterfork is a... well... fork of [Glitch-Soc](https://glitch-soc.github.io) used on [Monsterpit](https://monsterpit.net/about). It focuses on adding a *monstrous* number of community features with wild abandon along with improved accessibility, better moderation tools, and more user privacy options.
|
||||
|
||||
[circleci]: https://circleci.com/gh/glitch-soc/mastodon
|
||||
## Non-exhaustive feature list
|
||||
|
||||
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
|
||||
### Identity
|
||||
- [Signatures](https://monsterpit.blog/monsterpit-bangtags/i-am)
|
||||
- Account switching
|
||||
|
||||
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
|
||||
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).
|
||||
### Advanced
|
||||
- [Bangtag macros](https://monsterpit.blog/monsterpit-bangtags)
|
||||
|
||||
### Privacy
|
||||
- [Sharekeys](https://monsterpit.blog/monsterpit-bangtags/sharekey-new)
|
||||
- Self-destructing posts
|
||||
- Optional public profile pages and ActivityPub outbox
|
||||
- Option to limit the length of time posts are avaiable
|
||||
|
||||
### Accessibility
|
||||
- Media descriptions shown as captions in UI by default
|
||||
- High-contrast visibility icons by default
|
||||
- UI element size and spacing options
|
||||
|
||||
### Boundries
|
||||
- Respect "don't `@` me"
|
||||
- All threads can be muted
|
||||
|
||||
### Anxiety reduction
|
||||
- No metrics in the UI
|
||||
- Additional post and thread filtering options
|
||||
- Granular visibility options
|
||||
- [Community-curated world timeline](https://monsterpit.blog/monsterpit-creature-comforts/world-timeline)
|
||||
|
||||
### Publishing
|
||||
- Delayed posts
|
||||
- Queued boosts
|
||||
- Formatting (BBdown, BBcode, Markdown, HTML, console, plain)
|
||||
- Arbitary attachments
|
||||
|
||||
### Tagging
|
||||
- Scoped tags (`#monsters.kobolds`, `#local.minotaur.den` `#self.drafts`)
|
||||
- Unlisted tags (`#.hidden`)
|
||||
- Retroactive tagging (`#!parent:tag:art`)
|
||||
- Out-of-body tags
|
||||
- Glitch-Soc bookmarks as a tag (`#self.bookmarks`)
|
||||
|
||||
### Imports
|
||||
- Users can add their own custom emoji
|
||||
- Emoji can be imported from other posts (`#!parent:emoji`) or threads (`#!thread:emoji`)
|
||||
- Post importing from other ActivityPub software (currently text only)
|
||||
|
||||
### Moderation
|
||||
- Additional policies (force unlisted, force sensitive, reject unknown)
|
||||
- Moderator bangtags (`#!admin:silence`, `#!admin:suspend`, `#!admin:reset`, ...)
|
||||
- New admin transparancy log system, posted under a tag
|
||||
- Domain policy comments and list (`https://instance.site/policies`)
|
||||
|
||||
### Safety
|
||||
- Graylist-based federation by default
|
||||
- Domain suspensions include subdomains
|
||||
- Can block malicious servers by ActivityPub object propreties
|
||||
- Tools to block resource requests (see `/dist`)
|
||||
|
|
|
@ -47,6 +47,11 @@ class StatusesIndex < Chewy::Index
|
|||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :bookmarks do |collection|
|
||||
data = ::Bookmark.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
root date_detection: false do
|
||||
field :id, type: 'long'
|
||||
field :account_id, type: 'long'
|
||||
|
|
|
@ -11,6 +11,12 @@ class AccountsController < ApplicationController
|
|||
respond_to do |format|
|
||||
format.html do
|
||||
use_pack 'public'
|
||||
unless current_account && current_account.id == @account.id
|
||||
not_found if @account.hidden
|
||||
if @account&.user && @account.user.hides_public_profile?
|
||||
not_found unless current_account && current_account.following?(@account)
|
||||
end
|
||||
end
|
||||
mark_cacheable! unless user_signed_in?
|
||||
|
||||
@body_classes = 'with-modals'
|
||||
|
@ -22,7 +28,8 @@ class AccountsController < ApplicationController
|
|||
return
|
||||
end
|
||||
|
||||
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
||||
|
||||
@pinned_statuses = cache_collection(pinned_statuses, Status) if show_pinned_statuses?
|
||||
@statuses = filtered_status_page(params)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
|
@ -32,20 +39,6 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
format.atom do
|
||||
mark_cacheable!
|
||||
|
||||
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
|
||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? || entry.status.local_only? }))
|
||||
end
|
||||
|
||||
format.rss do
|
||||
mark_cacheable!
|
||||
|
||||
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
|
||||
render xml: RSS::AccountSerializer.render(@account, @statuses)
|
||||
end
|
||||
|
||||
format.json do
|
||||
mark_cacheable!
|
||||
|
||||
|
@ -58,39 +51,50 @@ class AccountsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def pinned_statuses
|
||||
if user_signed_in? && current_account.following?(@account)
|
||||
@account.pinned_statuses
|
||||
else
|
||||
@account.pinned_statuses.where.not(visibility: :private)
|
||||
end
|
||||
end
|
||||
|
||||
def show_pinned_statuses?
|
||||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
||||
[reblogs_requested?, replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
||||
end
|
||||
|
||||
def filtered_statuses
|
||||
default_statuses.tap do |statuses|
|
||||
statuses.merge!(hashtag_scope) if tag_requested?
|
||||
statuses.merge!(only_media_scope) if media_requested?
|
||||
statuses.merge!(no_replies_scope) unless replies_requested?
|
||||
if reblogs_requested?
|
||||
scope = default_statuses.reblogs
|
||||
elsif replies_requested?
|
||||
scope = @account.replies ? default_statuses : default_statuses.without_replies
|
||||
elsif media_requested?
|
||||
scope = default_statuses.where(id: account_media_status_ids)
|
||||
elsif tag_requested?
|
||||
scope = hashtag_scope
|
||||
else
|
||||
scope = default_statuses.without_replies.without_reblogs
|
||||
end
|
||||
return scope if current_user
|
||||
return Status.none unless @account&.user
|
||||
scope.where(created_at: @account.user.max_public_history.to_i.days.ago..Time.current)
|
||||
end
|
||||
|
||||
def default_statuses
|
||||
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
Status.where(id: account_media_status_ids)
|
||||
end
|
||||
|
||||
def account_media_status_ids
|
||||
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||
end
|
||||
|
||||
def no_replies_scope
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def hashtag_scope
|
||||
tag = Tag.find_normalized(params[:tag])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
return Status.none if !user_signed_in? && (tag.local || tag.private) || tag.private && current_account.id != @account.id
|
||||
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
|
||||
scope.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
|
@ -115,6 +119,8 @@ class AccountsController < ApplicationController
|
|||
short_account_media_url(@account, max_id: max_id, min_id: min_id)
|
||||
elsif replies_requested?
|
||||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
||||
elsif reblogs_requested?
|
||||
short_account_reblogs_url(@account, max_id: max_id, min_id: min_id)
|
||||
else
|
||||
short_account_url(@account, max_id: max_id, min_id: min_id)
|
||||
end
|
||||
|
@ -128,6 +134,10 @@ class AccountsController < ApplicationController
|
|||
request.path.ends_with?('/with_replies')
|
||||
end
|
||||
|
||||
def reblogs_requested?
|
||||
request.path.ends_with?('/reblogs')
|
||||
end
|
||||
|
||||
def tag_requested?
|
||||
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ class ActivityPub::CollectionsController < Api::BaseController
|
|||
def set_size
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@account.pinned_statuses.count
|
||||
@account.pinned_statuses.where.not(visibility: :private).count
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
@ -45,7 +45,7 @@ class ActivityPub::CollectionsController < Api::BaseController
|
|||
case params[:id]
|
||||
when 'featured'
|
||||
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
|
||||
scope.merge!(@account.pinned_statuses)
|
||||
scope.merge!(@account.pinned_statuses.where.not(visibility: :private))
|
||||
end
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
|
|
|
@ -10,7 +10,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||
if unknown_deleted_account?
|
||||
head 202
|
||||
elsif signed_request_account
|
||||
upgrade_account
|
||||
process_payload
|
||||
head 202
|
||||
else
|
||||
|
@ -38,16 +37,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||
@body
|
||||
end
|
||||
|
||||
def upgrade_account
|
||||
if signed_request_account.ostatus?
|
||||
signed_request_account.update(last_webfingered_at: nil)
|
||||
ResolveAccountWorker.perform_async(signed_request_account.acct)
|
||||
end
|
||||
|
||||
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
|
||||
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
|
||||
end
|
||||
|
||||
def process_payload
|
||||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
|
||||
end
|
||||
|
|
|
@ -55,8 +55,15 @@ class ActivityPub::OutboxesController < Api::BaseController
|
|||
|
||||
def set_statuses
|
||||
return unless page_requested?
|
||||
account_owner = current_account && current_account.id == @account.id
|
||||
outbox_hidden = @account&.user && @account.user.hides_public_outbox?
|
||||
local_follower = current_account && current_account.following?(@account)
|
||||
|
||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
||||
if account_owner || !@account.hidden? || (outbox_hidden && local_follower)
|
||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
||||
else
|
||||
@statuses = Status.none
|
||||
end
|
||||
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
end
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :mark_known, :mark_unknown, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :require_remote_account!, only: [:redownload]
|
||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||
|
||||
def index
|
||||
|
@ -19,18 +19,6 @@ module Admin
|
|||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
end
|
||||
|
||||
def subscribe
|
||||
authorize @account, :subscribe?
|
||||
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
authorize @account, :unsubscribe?
|
||||
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def memorialize
|
||||
authorize @account, :memorialize?
|
||||
@account.memorialize!
|
||||
|
@ -57,6 +45,48 @@ module Admin
|
|||
redirect_to admin_accounts_path(pending: '1')
|
||||
end
|
||||
|
||||
def mark_unknown
|
||||
authorize @account, :mark_unknown?
|
||||
@account.mark_unknown!
|
||||
log_action :mark_unknown, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def mark_known
|
||||
authorize @account, :mark_known?
|
||||
@account.mark_known!
|
||||
log_action :mark_known, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def force_sensitive
|
||||
authorize @account, :force_sensitive?
|
||||
@account.force_sensitive!
|
||||
log_action :force_sensitive, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def allow_nonsensitive
|
||||
authorize @account, :allow_nonsensitive?
|
||||
@account.allow_nonsensitive!
|
||||
log_action :allow_nonsensitive, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def force_unlisted
|
||||
authorize @account, :force_unlisted?
|
||||
@account.force_unlisted!
|
||||
log_action :force_unlisted, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def allow_public
|
||||
authorize @account, :allow_public?
|
||||
@account.allow_public!
|
||||
log_action :allow_public, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def unsilence
|
||||
authorize @account, :unsilence?
|
||||
@account.unsilence!
|
||||
|
|
|
@ -7,7 +7,7 @@ module Admin
|
|||
|
||||
layout 'admin'
|
||||
|
||||
before_action :require_staff!
|
||||
#before_action :require_staff!
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
|
||||
|
|
|
@ -2,36 +2,32 @@
|
|||
|
||||
module Admin
|
||||
class DomainBlocksController < BaseController
|
||||
before_action :set_domain_block, only: [:show, :destroy]
|
||||
before_action :set_domain_block, only: [:show, :destroy, :update]
|
||||
|
||||
def new
|
||||
authorize :domain_block, :create?
|
||||
@domain_block = DomainBlock.new(domain: params[:_domain])
|
||||
@domain_block = DomainBlock.new(domain: params[:_domain].present? ? params[:_domain].strip : nil)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :domain_block, :create?
|
||||
|
||||
resource_params[:domain].strip! if resource_params[:domain].present?
|
||||
resource_params[:reason].strip! if resource_params[:reason].present?
|
||||
@domain_block = DomainBlock.new(resource_params)
|
||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
|
||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain].strip) : nil
|
||||
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
|
||||
@domain_block.errors[:domain].clear
|
||||
render :new
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params.except(:undo))
|
||||
end
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instance_path(id: @domain_block.domain, limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params)
|
||||
end
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -41,9 +37,26 @@ module Admin
|
|||
|
||||
def destroy
|
||||
authorize @domain_block, :destroy?
|
||||
UnblockDomainService.new.call(@domain_block)
|
||||
DomainUnblockWorker.perform_async(@domain_block.id)
|
||||
log_action :destroy, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
flash[:notice] = I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
redirect_to controller: 'admin/instances', action: 'index', limited: '1'
|
||||
end
|
||||
|
||||
def update
|
||||
return destroy unless resource_params[:undo].to_i.zero?
|
||||
resource_params[:reason].strip! if resource_params[:reason].present?
|
||||
authorize @domain_block, :update?
|
||||
@domain_block.update(resource_params.except(:domain, :undo))
|
||||
changed = @domain_block.changed
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id) if (changed & %w(severity force_sensitive reject_media reject_unknown)).any?
|
||||
log_action :update, @domain_block
|
||||
flash[:notice] = I18n.t('admin.domain_blocks.updated_msg')
|
||||
else
|
||||
flash[:alert] = I18n.t('admin.domain_blocks.update_failed_msg')
|
||||
end
|
||||
redirect_to admin_instance_path(id: @domain_block.domain, limited: '1')
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -53,7 +66,7 @@ module Admin
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
|
||||
params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reject_unknown, :reason, :undo)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ module Admin
|
|||
helper_method :paginated_instances
|
||||
|
||||
def ordered_instances
|
||||
paginated_instances.map { |resource| Instance.new(resource) }
|
||||
paginated_instances.map { |resource| Instance.new(resource) }.sort_by(&:updated_at).reverse!
|
||||
end
|
||||
|
||||
def filter_params
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SubscriptionsController < BaseController
|
||||
def index
|
||||
authorize :subscription, :index?
|
||||
@subscriptions = ordered_subscriptions.page(requested_page)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ordered_subscriptions
|
||||
Subscription.order(id: :desc).includes(:account)
|
||||
end
|
||||
|
||||
def requested_page
|
||||
params[:page].to_i
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,73 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::PushController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def update
|
||||
response, status = process_push_request
|
||||
render plain: response, status: status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_push_request
|
||||
case hub_mode
|
||||
when 'subscribe'
|
||||
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
|
||||
when 'unsubscribe'
|
||||
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
|
||||
else
|
||||
["Unknown mode: #{hub_mode}", 422]
|
||||
end
|
||||
end
|
||||
|
||||
def hub_mode
|
||||
params['hub.mode']
|
||||
end
|
||||
|
||||
def hub_topic
|
||||
params['hub.topic']
|
||||
end
|
||||
|
||||
def hub_callback
|
||||
params['hub.callback']
|
||||
end
|
||||
|
||||
def hub_lease_seconds
|
||||
params['hub.lease_seconds']
|
||||
end
|
||||
|
||||
def hub_secret
|
||||
params['hub.secret']
|
||||
end
|
||||
|
||||
def account_from_topic
|
||||
if hub_topic.present? && local_domain? && account_feed_path?
|
||||
Account.find_local(hub_topic_params[:username])
|
||||
end
|
||||
end
|
||||
|
||||
def hub_topic_params
|
||||
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
|
||||
end
|
||||
|
||||
def hub_topic_uri
|
||||
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
|
||||
end
|
||||
|
||||
def local_domain?
|
||||
TagManager.instance.web_domain?(hub_topic_domain)
|
||||
end
|
||||
|
||||
def verified_domain
|
||||
return signed_request_account.domain if signed_request_account
|
||||
end
|
||||
|
||||
def hub_topic_domain
|
||||
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
|
||||
end
|
||||
|
||||
def account_feed_path?
|
||||
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::SalmonController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
before_action :set_account
|
||||
respond_to :txt
|
||||
|
||||
def update
|
||||
if verify_payload?
|
||||
process_salmon
|
||||
head 202
|
||||
elsif payload.present?
|
||||
render plain: signature_verification_failure_reason, status: 401
|
||||
else
|
||||
head 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def payload
|
||||
@_payload ||= request.body.read
|
||||
end
|
||||
|
||||
def verify_payload?
|
||||
payload.present? && VerifySalmonService.new.call(payload)
|
||||
end
|
||||
|
||||
def process_salmon
|
||||
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::SubscriptionsController < Api::BaseController
|
||||
before_action :set_account
|
||||
respond_to :txt
|
||||
|
||||
def show
|
||||
if subscription.valid?(params['hub.topic'])
|
||||
@account.update(subscription_expires_at: future_expires)
|
||||
render plain: encoded_challenge, status: 200
|
||||
else
|
||||
head 404
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
|
||||
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
|
||||
end
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscription
|
||||
@_subscription ||= @account.subscription(
|
||||
api_subscription_url(@account.id)
|
||||
)
|
||||
end
|
||||
|
||||
def body
|
||||
@_body ||= request.body.read
|
||||
end
|
||||
|
||||
def encoded_challenge
|
||||
HTMLEntities.new.encode(params['hub.challenge'])
|
||||
end
|
||||
|
||||
def future_expires
|
||||
Time.now.utc + lease_seconds_or_default
|
||||
end
|
||||
|
||||
def lease_seconds_or_default
|
||||
(params['hub.lease_seconds'] || 1.day).to_i.seconds
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -28,14 +28,14 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||
|
||||
def account_statuses
|
||||
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
|
||||
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
|
||||
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
||||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
||||
statuses = statuses.without_replies if !@account.replies || truthy_param?(:exclude_replies)
|
||||
statuses = statuses.without_reblogs if truthy_param?(:exclude_reblogs)
|
||||
statuses = statuses.reblogs if truthy_param?(:reblogs) && !truthy_param?(:exclude_reblogs)
|
||||
statuses = statuses.where(id: account_media_status_ids) if truthy_param?(:only_media)
|
||||
statuses = statuses.hashtag_scope if params[:tagged].present?
|
||||
|
||||
statuses
|
||||
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def permitted_account_statuses
|
||||
|
@ -57,7 +57,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||
end
|
||||
|
||||
def pinned_scope
|
||||
@account.pinned_statuses
|
||||
if user_signed_in? && current_account.following?(@account)
|
||||
@account.pinned_statuses
|
||||
else
|
||||
@account.pinned_statuses.where.not(visibility: :private)
|
||||
end
|
||||
end
|
||||
|
||||
def no_replies_scope
|
||||
|
@ -72,7 +76,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||
tag = Tag.find_normalized(params[:tagged])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
return Status.none if !user_signed_in && (tag.local || tag.private) || tag.private && current_account.id != @account.id
|
||||
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
|
||||
scope.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
|
|
|
@ -23,6 +23,7 @@ class Api::V1::DomainBlocksController < Api::BaseController
|
|||
|
||||
def destroy
|
||||
current_account.unblock_domain!(domain_block_params[:domain])
|
||||
AfterAccountDomainUnblockWorker.perform_async(current_account.id, domain_block_params[:domain])
|
||||
render_empty
|
||||
end
|
||||
|
||||
|
|
|
@ -43,6 +43,6 @@ class Api::V1::FiltersController < Api::BaseController
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
||||
params.permit(:phrase, :expires_in, :irreversible, :whole_word, :exclude_media, :media_only, :status_text, :spoiler, :tags, :custom_cw, :override_cw, context: [])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,6 +38,6 @@ class Api::V1::ListsController < Api::BaseController
|
|||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:title, :replies_policy)
|
||||
params.permit(:title, :replies_policy, :show_self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class Api::V1::SearchController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
RESULTS_LIMIT = 20
|
||||
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 100).to_i
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:search' }
|
||||
before_action :require_user!
|
||||
|
|
|
@ -30,10 +30,19 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
|
|||
|
||||
bookmark = Bookmark.find_or_create_by!(account: current_user.account, status: requested_status)
|
||||
|
||||
curate_status(requested_status)
|
||||
|
||||
bookmark.status.reload
|
||||
end
|
||||
|
||||
def requested_status
|
||||
Status.find(params[:status_id])
|
||||
end
|
||||
|
||||
def curate_status(status)
|
||||
return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id)
|
||||
status.curated = true
|
||||
status.save
|
||||
FanOutOnWriteService.new.call(status)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,11 +17,12 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_accounts
|
||||
return [] if @status.local? && @status.account.user.setting_hide_interactions
|
||||
default_accounts.merge(paginated_favourites).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account
|
||||
Account.without_unlisted
|
||||
.includes(:favourites, :account_stat)
|
||||
.references(:favourites)
|
||||
.where(favourites: { status_id: @status.id })
|
||||
|
|
|
@ -17,11 +17,12 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_accounts
|
||||
return [] if @status.local? && @status.account.user.setting_hide_interactions
|
||||
default_accounts.merge(paginated_statuses).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:statuses, :account_stat).references(:statuses)
|
||||
Account.without_unlisted.includes(:statuses, :account_stat).references(:statuses)
|
||||
end
|
||||
|
||||
def paginated_statuses
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Api::V1::StatusesController < Api::BaseController
|
||||
include Authorization
|
||||
include FilterHelper
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
||||
|
@ -18,6 +19,8 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
|
||||
def show
|
||||
@status = cache_collection([@status], Status).first
|
||||
# make sure any custom cws are applied
|
||||
phrase_filtered?(@status, current_account.id, 'thread') unless current_account.nil?
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
|
@ -52,12 +55,18 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
delete_after: status_params[:delete_after],
|
||||
sharekey: status_params[:sharekey],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
idempotency: request.headers['Idempotency-Key'])
|
||||
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
if @status.nil?
|
||||
raise Mastodon::ValidationError, 'Bangtags processed successfully.'
|
||||
else
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
@ -85,7 +94,9 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
:sharekey,
|
||||
:scheduled_at,
|
||||
:delete_after,
|
||||
:content_type,
|
||||
media_ids: [],
|
||||
poll: [
|
||||
|
|
|
@ -28,6 +28,8 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
def tagged_statuses
|
||||
if @tag.nil?
|
||||
[]
|
||||
elsif @tag.name.in?(['self.bookmarks', '.self.bookmarks'])
|
||||
Status.reorder(nil).joins(:bookmarks).merge(bookmark_results)
|
||||
else
|
||||
statuses = tag_timeline_statuses.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
|
@ -48,6 +50,18 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
|
||||
end
|
||||
|
||||
def bookmark_results
|
||||
@_results ||= account_bookmarks.paginate_by_max_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def account_bookmarks
|
||||
current_account.bookmarks
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
skip_before_action :require_no_authentication, only: [:create]
|
||||
skip_before_action :check_user_permissions, only: [:destroy]
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
prepend_before_action :switch_user
|
||||
prepend_before_action :set_pack
|
||||
before_action :set_instance_presenter, only: [:new]
|
||||
before_action :set_body_classes
|
||||
|
@ -52,6 +53,10 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
params.require(:user).permit(:email, :password, :otp_attempt)
|
||||
end
|
||||
|
||||
def switch_params
|
||||
params.permit(:switch_to)
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
last_url = stored_location_for(:user)
|
||||
|
||||
|
@ -107,6 +112,19 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
render :two_factor
|
||||
end
|
||||
|
||||
def switch_user
|
||||
return unless switch_params[:switch_to].present? && current_user.present?
|
||||
target_user = User.find_by(id: switch_params[:switch_to])
|
||||
return unless target_user.present? && current_user.in?(target_user.linked_users)
|
||||
self.resource = target_user
|
||||
remember_me(target_user)
|
||||
sign_in(target_user)
|
||||
flash.delete(:error)
|
||||
flash.delete(:alert)
|
||||
flash.delete(:notice)
|
||||
return root_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
|
|
|
@ -11,6 +11,7 @@ module AccountControllerConcern
|
|||
before_action :set_account
|
||||
before_action :check_account_approval
|
||||
before_action :check_account_suspension
|
||||
before_action :check_account_hidden
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers
|
||||
end
|
||||
|
@ -29,7 +30,6 @@ module AccountControllerConcern
|
|||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
webfinger_account_link,
|
||||
atom_account_url_link,
|
||||
actor_url_link,
|
||||
]
|
||||
)
|
||||
|
@ -46,13 +46,6 @@ module AccountControllerConcern
|
|||
]
|
||||
end
|
||||
|
||||
def atom_account_url_link
|
||||
[
|
||||
account_url(@account, format: 'atom'),
|
||||
[%w(rel alternate), %w(type application/atom+xml)],
|
||||
]
|
||||
end
|
||||
|
||||
def actor_url_link
|
||||
[
|
||||
ActivityPub::TagManager.instance.uri_for(@account),
|
||||
|
@ -75,4 +68,8 @@ module AccountControllerConcern
|
|||
gone
|
||||
end
|
||||
end
|
||||
|
||||
def check_account_hidden
|
||||
not_found if @account.hidden?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
module AccountableConcern
|
||||
extend ActiveSupport::Concern
|
||||
include LogHelper
|
||||
|
||||
def log_action(action, target)
|
||||
Admin::ActionLog.create(account: current_account, action: action, target: target)
|
||||
user_friendly_action_log(current_account, action, target)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -145,7 +145,7 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
return if account.local?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DomainPolicyController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
before_action :set_pack
|
||||
layout 'public'
|
||||
|
||||
before_action :set_instance_presenter, only: [:show]
|
||||
|
||||
def show
|
||||
@hide_navbar = true
|
||||
@domain_policies = DomainBlock.all.reorder('updated_at DESC').page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'common'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
return if user_signed_in?
|
||||
not_found
|
||||
end
|
||||
end
|
|
@ -58,7 +58,7 @@ class FiltersController < ApplicationController
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
||||
params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, :exclude_media, :media_only, :status_text, :spoiler, :tags, :custom_cw, :override_cw, context: [])
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -23,7 +23,7 @@ class HomeController < ApplicationController
|
|||
when 'statuses'
|
||||
status = Status.find_by(id: matches[2])
|
||||
|
||||
if status && (status.public_visibility? || status.unlisted_visibility?)
|
||||
if status && status.distributable?
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(status))
|
||||
return
|
||||
end
|
||||
|
|
|
@ -62,7 +62,7 @@ class RelationshipsController < ApplicationController
|
|||
end
|
||||
|
||||
def dormant_account_scope
|
||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
|
||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(3.months.ago)))
|
||||
end
|
||||
|
||||
def by_domain_scope
|
||||
|
|
|
@ -9,18 +9,10 @@ class RemoteFollowController < ApplicationController
|
|||
before_action :set_body_classes
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
raise Mastodon::NotPermittedError unless user_signed_in?
|
||||
|
||||
def create
|
||||
@remote_follow = RemoteFollow.new(resource_params)
|
||||
|
||||
if @remote_follow.valid?
|
||||
session[:remote_follow] = @remote_follow.acct
|
||||
redirect_to @remote_follow.subscribe_address_for(@account)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
FollowService.new.call(current_account, @account) unless current_account.following?(@account)
|
||||
redirect_to TagManager.instance.url_for(@account)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -5,24 +5,34 @@ class RemoteInteractionController < ApplicationController
|
|||
|
||||
layout 'modal'
|
||||
|
||||
before_action :set_interaction_type
|
||||
before_action :set_status
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
before_action :set_status
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
raise Mastodon::NotPermittedError unless user_signed_in?
|
||||
|
||||
def create
|
||||
@remote_follow = RemoteFollow.new(resource_params)
|
||||
|
||||
if @remote_follow.valid?
|
||||
session[:remote_follow] = @remote_follow.acct
|
||||
redirect_to @remote_follow.interact_address_for(@status)
|
||||
else
|
||||
render :new
|
||||
case params[:type]
|
||||
when 'reblog'
|
||||
if current_account.statuses.where(reblog: @status).exists?
|
||||
status = current_account.statuses.find_by(reblog: @status)
|
||||
RemoveStatusService.new.call(status)
|
||||
else
|
||||
ReblogService.new.call(current_account, @status)
|
||||
end
|
||||
when 'favourite'
|
||||
if Favourite.where(account: current_account, status: @status).exists?
|
||||
UnfavouriteService.new.call(current_account, @status)
|
||||
else
|
||||
FavouriteService.new.call(current_account, @status, skip_authorize: true)
|
||||
end
|
||||
when 'follow'
|
||||
FollowService.new.call(current_account, @status.account)
|
||||
when 'unfollow'
|
||||
UnfollowService.new.call(current_account, @status.account)
|
||||
end
|
||||
|
||||
redirect_to short_account_status_url(@status.account.username, @status.id, key: @sharekey)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -37,7 +47,13 @@ class RemoteInteractionController < ApplicationController
|
|||
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
@sharekey = params[:key]
|
||||
|
||||
if @status.sharekey.present? && @sharekey == @status.sharekey
|
||||
skip_authorization
|
||||
else
|
||||
authorize @status, :show?
|
||||
end
|
||||
rescue Mastodon::NotPermittedError
|
||||
# Reraise in order to get a 404
|
||||
raise ActiveRecord::RecordNotFound
|
||||
|
@ -51,8 +67,4 @@ class RemoteInteractionController < ApplicationController
|
|||
def set_pack
|
||||
use_pack 'modal'
|
||||
end
|
||||
|
||||
def set_interaction_type
|
||||
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module Exports
|
||||
class FollowersAccountsController < ApplicationController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
send_export_file
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_data
|
||||
@export.to_followers_accounts_csv
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,6 @@ class Settings::ImportsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def import_params
|
||||
params.require(:import).permit(:data, :type)
|
||||
params.require(:import).permit(:data, :type, :mode)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,39 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
|
||||
def user_settings_params
|
||||
params.require(:user).permit(
|
||||
:setting_default_local,
|
||||
:setting_always_local,
|
||||
:setting_rawr_federated,
|
||||
:setting_hide_stats,
|
||||
:setting_hide_captions,
|
||||
:setting_larger_menus,
|
||||
:setting_larger_buttons,
|
||||
:setting_larger_drawer,
|
||||
:setting_larger_emoji,
|
||||
:setting_remove_filtered,
|
||||
:setting_hide_replies_muted,
|
||||
:setting_hide_replies_blocked,
|
||||
:setting_hide_replies_blocker,
|
||||
:setting_hide_mntions_muted,
|
||||
:setting_hide_mntions_blocked,
|
||||
:setting_hide_mntions_blocker,
|
||||
:setting_hide_mntions_packm8,
|
||||
:setting_gently_kobolds,
|
||||
:setting_user_is_kobold,
|
||||
:setting_hide_mascot,
|
||||
:setting_hide_interactions,
|
||||
:setting_hide_public_profile,
|
||||
:setting_hide_public_outbox,
|
||||
:setting_max_public_history,
|
||||
:setting_roar_lifespan,
|
||||
:setting_delayed_roars,
|
||||
:setting_delayed_for,
|
||||
:setting_boost_interval,
|
||||
:setting_boost_random,
|
||||
:setting_boost_interval_from,
|
||||
:setting_boost_interval_to,
|
||||
:setting_show_cursor,
|
||||
|
||||
:setting_default_privacy,
|
||||
:setting_default_sensitive,
|
||||
:setting_default_language,
|
||||
|
|
|
@ -25,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController
|
|||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :replies, :locked, :hidden, :unlisted, :gently, :kobold, :adult_content, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class StatusesController < ApplicationController
|
||||
include SignatureAuthentication
|
||||
include Authorization
|
||||
include FilterHelper
|
||||
|
||||
ANCESTORS_LIMIT = 40
|
||||
DESCENDANTS_LIMIT = 60
|
||||
|
@ -12,6 +13,8 @@ class StatusesController < ApplicationController
|
|||
|
||||
before_action :set_account
|
||||
before_action :set_status
|
||||
before_action :handle_sharekey_change, only: [:show], if: :user_signed_in?
|
||||
before_action :handle_webapp_redirect, only: [:show], if: :user_signed_in?
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers
|
||||
before_action :check_account_suspension
|
||||
|
@ -179,7 +182,6 @@ class StatusesController < ApplicationController
|
|||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
|
||||
]
|
||||
)
|
||||
|
@ -189,13 +191,40 @@ class StatusesController < ApplicationController
|
|||
@status = @account.statuses.find(params[:id])
|
||||
@stream_entry = @status.stream_entry
|
||||
@type = @stream_entry.activity_type.downcase
|
||||
@sharekey = params[:key]
|
||||
|
||||
authorize @status, :show?
|
||||
# make sure any custom cws are applied
|
||||
phrase_filtered?(@status, current_account.id, 'thread') unless current_account.nil?
|
||||
|
||||
if @status.sharekey.present? && @sharekey == @status.sharekey
|
||||
skip_authorization
|
||||
else
|
||||
authorize @status, :show?
|
||||
end
|
||||
rescue Mastodon::NotPermittedError
|
||||
# Reraise in order to get a 404
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
def handle_sharekey_change
|
||||
return if params[:rekey].nil?
|
||||
raise Mastodon::NotPermittedError unless current_account.id == @status.account_id
|
||||
case params[:rekey]
|
||||
when '1'
|
||||
@status.sharekey = SecureRandom.urlsafe_base64(32)
|
||||
@status.save
|
||||
Rails.cache.delete("statuses/#{@status.id}")
|
||||
when '0'
|
||||
@status.sharekey = nil
|
||||
@status.save
|
||||
Rails.cache.delete("statuses/#{@status.id}")
|
||||
end
|
||||
end
|
||||
|
||||
def handle_webapp_redirect
|
||||
redirect_to "/web/statuses/#{@status.id}" if params[:toweb] == '1'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ class StreamEntriesController < ApplicationController
|
|||
before_action :check_account_suspension
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
@ -24,15 +25,6 @@ class StreamEntriesController < ApplicationController
|
|||
|
||||
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
|
||||
end
|
||||
|
||||
format.atom do
|
||||
unless @stream_entry.hidden?
|
||||
skip_session!
|
||||
expires_in 3.minutes, public: true
|
||||
end
|
||||
|
||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,7 +41,6 @@ class StreamEntriesController < ApplicationController
|
|||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
|
||||
]
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
|
|||
elsif log.target_type == 'User' && [:change_email].include?(log.action)
|
||||
log.recorded_changes.slice('email', 'unconfirmed_email')
|
||||
elsif log.target_type == 'DomainBlock'
|
||||
log.recorded_changes.slice('severity', 'reject_media')
|
||||
log.recorded_changes.slice('severity', 'reject_media', 'force_sensitive')
|
||||
elsif log.target_type == 'Status' && log.action == :update
|
||||
log.recorded_changes.slice('sensitive')
|
||||
end
|
||||
|
@ -55,13 +55,13 @@ module Admin::ActionLogsHelper
|
|||
|
||||
def class_for_log_icon(log)
|
||||
case log.action
|
||||
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
|
||||
when :enable, :allow_public, :allow_nonsensitive, :unsuspend, :unsilence, :confirm, :promote, :resolve
|
||||
'positive'
|
||||
when :create
|
||||
opposite_verbs?(log) ? 'negative' : 'positive'
|
||||
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
|
||||
'neutral'
|
||||
when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
|
||||
when :demote, :force_sensitive, :force_unlisted, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
|
||||
'negative'
|
||||
when :destroy
|
||||
opposite_verbs?(log) ? 'positive' : 'negative'
|
||||
|
@ -87,7 +87,7 @@ module Admin::ActionLogsHelper
|
|||
when 'Report'
|
||||
link_to "##{record.id}", admin_report_path(record)
|
||||
when 'DomainBlock', 'EmailDomainBlock'
|
||||
link_to record.domain, "https://#{record.domain}"
|
||||
link_to record.domain, admin_instance_path(id: record.domain)
|
||||
when 'Status'
|
||||
link_to record.account.acct, TagManager.instance.url_for(record)
|
||||
when 'AccountWarning'
|
||||
|
@ -100,7 +100,7 @@ module Admin::ActionLogsHelper
|
|||
when 'CustomEmoji'
|
||||
attributes['shortcode']
|
||||
when 'DomainBlock', 'EmailDomainBlock'
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
link_to attributes['domain'], admin_instance_path(id: attributes['domain'])
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
module AutorejectHelper
|
||||
include ModerationHelper
|
||||
|
||||
AUTOBLOCK_TRIGGERS = [:context, :context_starts_with, :context_contains]
|
||||
|
||||
def should_reject?(uri = nil)
|
||||
if uri.nil?
|
||||
if @object
|
||||
uri = object_uri.start_with?('http') ? object_uri : @object['url']
|
||||
elsif @json
|
||||
uri = @json['id']
|
||||
end
|
||||
end
|
||||
|
||||
return if uri.nil?
|
||||
|
||||
domain = uri.scan(/[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*/).first
|
||||
blocks = DomainBlock.suspend
|
||||
return [:domain, uri] if blocks.where(domain: domain).or(blocks.where('domain LIKE ?', "%.#{domain}")).exists?
|
||||
|
||||
return unless @json || @object
|
||||
|
||||
context = @object['@context'] if @object
|
||||
|
||||
if @json
|
||||
oid = @json['id']
|
||||
if oid
|
||||
return [:id_starts_with, uri] if ENV.fetch('REJECT_IF_ID_STARTS_WITH', '').split.any? { |r| oid.start_with?(r) }
|
||||
return [:id_contains, uri] if ENV.fetch('REJECT_IF_ID_CONTAINS', '').split.any? { |r| r.in?(oid) }
|
||||
end
|
||||
|
||||
username = @json['preferredUsername'] || @json['username']
|
||||
if username && username.is_a?(String)
|
||||
username = (@json['actor'] && @json['actor'].is_a?(String)) ? @json['actor'] : ''
|
||||
username = username.scan(/(?<=\/user\/|\/@|\/users\/)([^\s\/]+)/).first
|
||||
end
|
||||
|
||||
unless username.blank?
|
||||
username.downcase!
|
||||
return [:username, uri] if ENV.fetch('REJECT_IF_USERNAME_EQUALS', '').split.any? { |r| r == username }
|
||||
return [:username_starts_with, uri] if ENV.fetch('REJECT_IF_USERNAME_STARTS_WITH', '').split.any? { |r| username.start_with?(r) }
|
||||
return [:username_contains, uri] if ENV.fetch('REJECT_IF_USERNAME_CONTAINS', '').split.any? { |r| r.in?(username) }
|
||||
end
|
||||
|
||||
context = @json['@context'] unless @object && context
|
||||
end
|
||||
|
||||
return unless context
|
||||
|
||||
if context.is_a?(Array)
|
||||
inline_context = context.find { |item| item.is_a?(Hash) }
|
||||
if inline_context
|
||||
keys = inline_context.keys
|
||||
return [:context, uri] if ENV.fetch('REJECT_IF_CONTEXT_EQUALS', '').split.any? { |r| r.in?(keys) }
|
||||
return [:context_starts_with, uri] if ENV.fetch('REJECT_IF_CONTEXT_STARTS_WITH', '').split.any? { |r| keys.any? { |k| k.start_with?(r) } }
|
||||
return [:context_contains, uri] if ENV.fetch('REJECT_IF_CONTEXT_CONTAINS', '').split.any? { |r| keys.any? { |k| r.in?(k) } }
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def reject_reason(reason)
|
||||
case reason
|
||||
when :domain
|
||||
"the origin domain is blocked"
|
||||
when :id_starts_with
|
||||
"the object's URI starts with a blocked phrase"
|
||||
when :id_contains
|
||||
"the object's URI contains a blocked phrase"
|
||||
when :username
|
||||
"the author's username is blocked"
|
||||
when :username_starts_with
|
||||
"the author's username starts with a blocked phrase"
|
||||
when :username_contains
|
||||
"the author's username contains a blocked phrase"
|
||||
when :context
|
||||
"the object's JSON-LD context has a key matching a blocked phrase"
|
||||
when :context_starts_with
|
||||
"the object's JSON-LD context has a key starting with a blocked phrase"
|
||||
when :context_contains
|
||||
"the object's JSON-LD context has a key containing a blocked phrase"
|
||||
else
|
||||
"of an undefined reason"
|
||||
end
|
||||
end
|
||||
|
||||
def should_autoblock?(reason)
|
||||
@json['type'] == 'Create' && reason.in?(AUTOBLOCK_TRIGGERS)
|
||||
end
|
||||
|
||||
def autoblock!(uri, reason)
|
||||
return if uri.nil?
|
||||
domain = uri.scan(/[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*/).first
|
||||
domain_policy(uri, :suspend, "Sent an ActivityPub payload (#{uri}) where #{reason}.")
|
||||
end
|
||||
|
||||
def autoreject?(uri = nil)
|
||||
return false if @options && @options[:imported]
|
||||
reason, uri = should_reject?(uri)
|
||||
if reason
|
||||
reason = reject_reason(reason)
|
||||
if @json
|
||||
autoblock!(uri, reason) if should_autoblock?(reason)
|
||||
Rails.logger.info("Rejected an incoming '#{@json['type']}#{@object && " #{@object['type']}".rstrip}' from #{@json['id']} because #{reason}.")
|
||||
elsif uri
|
||||
Rails.logger.info("Rejected an outgoing request to #{uri} because #{reason}.")
|
||||
end
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
module BlocklistHelper
|
||||
FEDIVERSE_SPACE_URLS = ["https://fediverse.network/mastodon?build=gab"]
|
||||
VULPINE_CLUB_URL = "https://raw.githubusercontent.com/vulpineclub/vulpineclub.github.io/master/_data/blocks.yml"
|
||||
|
||||
def merged_blocklist
|
||||
# ordered by preference
|
||||
# prefer vulpine b/c they have easy-to-parse reason text
|
||||
blocklist = vulpine_club_blocks | fediverse_space_blocks
|
||||
blocklist.uniq { |entry| entry[:domain] }
|
||||
end
|
||||
|
||||
def domain_map(domains, reason)
|
||||
domains.map! do |domain|
|
||||
{domain: domain, severity: :suspend, reason: reason}
|
||||
end
|
||||
end
|
||||
|
||||
def vulpine_club_blocks
|
||||
body = Request.new(:get, VULPINE_CLUB_URL).perform do |response|
|
||||
response.code != 200 ? nil : response.body_with_limit(66.kilobytes)
|
||||
end
|
||||
|
||||
return [] unless body.present?
|
||||
|
||||
yaml = YAML::load(body)
|
||||
yaml.map! do |entry|
|
||||
domain = entry['domain']
|
||||
next if domain.blank?
|
||||
severity = entry['severity'].split('/')
|
||||
reject_media = 'nomedia'.in?(severity)
|
||||
severity = (severity[0].nil? || severity[0] == 'nomedia') ? 'noop' : severity[0]
|
||||
|
||||
reason = "Imported from <https://vulpine.club>: \"#{entry['reason']}\"#{entry['link'].present? ? " (#{entry['link']})" : ''}".rstrip
|
||||
{domain: domain, severity: severity.to_sym, reject_media: reject_media, reason: reason}
|
||||
end
|
||||
end
|
||||
|
||||
# shamelessly adapted from @zac@computerfox.xyz's `silence` tool
|
||||
# <https://github.com/theZacAttacks/silence/blob/master/silence>
|
||||
# which you'll find useful if you're a non-monsterfork mastoadmin
|
||||
def fediverse_space_fetch_domains(url)
|
||||
body = Request.new(:get, url).perform do |response|
|
||||
response.code != 200 ? nil : response.body_with_limit(66.kilobytes)
|
||||
end
|
||||
|
||||
return [] unless body.present?
|
||||
|
||||
document = Nokogiri::HTML(body)
|
||||
document.css('table.table-condensed td a').collect { |link| link.content.strip }
|
||||
end
|
||||
|
||||
def fediverse_space_blocks
|
||||
domains = FEDIVERSE_SPACE_URLS.flat_map { |url| fediverse_space_fetch_domains(url) }
|
||||
domains.uniq!
|
||||
|
||||
domain_map(domains, "Imported from <https://fediverse.space>.")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
module DomainPolicyHelper
|
||||
end
|
|
@ -0,0 +1,77 @@
|
|||
module FilterHelper
|
||||
include Redisable
|
||||
|
||||
def phrase_filtered?(status, receiver_id, context)
|
||||
if redis.sismember("filtered_statuses:#{receiver_id}", status.id)
|
||||
return !(redis.hexists("custom_cw:#{receiver_id}", status.id) || redis.hexists("custom_cw:#{receiver_id}", "c#{status.conversation_id}"))
|
||||
end
|
||||
|
||||
filters = cached_filters(receiver_id)
|
||||
filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? }
|
||||
|
||||
if status.media_attachments.any?
|
||||
filters.delete_if { |filter| filter.exclude_media }
|
||||
else
|
||||
filters.delete_if { |filter| filter.media_only }
|
||||
end
|
||||
|
||||
return false if filters.empty?
|
||||
|
||||
status = status.reblog if status.reblog?
|
||||
status_text = Formatter.instance.plaintext(status)
|
||||
spoiler_text = status.spoiler_text
|
||||
tags = status.tags.pluck(:name).join("\n")
|
||||
|
||||
filters.each do |filter|
|
||||
if filter.whole_word
|
||||
sb = filter.phrase =~ /\A[[:word:]]/ ? '\b' : ''
|
||||
eb = filter.phrase =~ /[[:word:]]\z/ ? '\b' : ''
|
||||
|
||||
regex = /(?mix:#{sb}#{Regexp.escape(filter.phrase)}#{eb})/
|
||||
else
|
||||
regex = /#{Regexp.escape(filter.phrase)}/i
|
||||
end
|
||||
|
||||
matched = false
|
||||
matched = true unless regex.match(status_text).nil?
|
||||
matched = true unless spoiler_text.blank? || regex.match(spoiler_text).nil?
|
||||
matched = true unless tags.empty? || regex.match(tags).nil?
|
||||
|
||||
if matched
|
||||
filter_thread(receiver_id, status.conversation_id) if filter.thread && filter.custom_cw.blank?
|
||||
|
||||
unless filter.custom_cw.blank?
|
||||
cw = if filter.override_cw || status.spoiler_text.blank?
|
||||
filter.custom_cw
|
||||
else
|
||||
"[#{filter.custom_cw}] #{status.spoiler_text}".rstrip
|
||||
end
|
||||
|
||||
if filter.thread
|
||||
redis.hset("custom_cw:#{receiver_id}", "c#{status.conversation_id}", cw)
|
||||
else
|
||||
redis.hset("custom_cw:#{receiver_id}", status.id, cw)
|
||||
end
|
||||
end
|
||||
|
||||
redis.sadd("filtered_statuses:#{receiver_id}", status.id)
|
||||
return filter.custom_cw.blank?
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def filter_thread(account_id, conversation_id)
|
||||
return if Status.where(account_id: account_id, conversation_id: conversation_id).exists?
|
||||
redis.sadd("filtered_threads:#{account_id}", conversation_id)
|
||||
end
|
||||
|
||||
def filtering_thread?(account_id, conversation_id)
|
||||
redis.sismember("filtered_threads:#{account_id}", conversation_id)
|
||||
end
|
||||
|
||||
def cached_filters(account_id)
|
||||
Rails.cache.fetch("filters:#{account_id}") { CustomFilter.where(account_id: account_id).to_a }.to_a
|
||||
end
|
||||
end
|
|
@ -0,0 +1,108 @@
|
|||
module LogHelper
|
||||
def user_friendly_action_log(source, action, target, reason = nil)
|
||||
source = source.username if source.is_a?(Account)
|
||||
web_domain = Rails.configuration.x.web_domain || Rails.configuration.x.local_domain
|
||||
|
||||
case action
|
||||
when :create
|
||||
if target.is_a? DomainBlock
|
||||
LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> applied a #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''} policy on '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
|
||||
elsif target.is_a? EmailDomainBlock
|
||||
LogWorker.perform_async("\u26d4 <#{source}> added a registration block on email domain '#{target.domain}'.\n\nReview (moderators only): https://#{web_domain}/admin/email_domain_blocks")
|
||||
elsif target.is_a? CustomEmoji
|
||||
LogWorker.perform_async("\xf0\x9f\x98\xba <#{source}> added the '#{target.shortcode}' emoji. :#{target.shortcode}:")
|
||||
elsif target.is_a? AccountWarning
|
||||
LogWorker.perform_async("\xe2\x9a\xa0\xef\xb8\x8f <#{source}> sent someone an admin notice.")
|
||||
end
|
||||
when :destroy
|
||||
if target.is_a? DomainBlock
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> reset the policy on #{target.domain}\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}")
|
||||
elsif target.is_a? EmailDomainBlock
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> removed the registration block on email domain '#{target.domain}'.")
|
||||
elsif target.is_a? CustomEmoji
|
||||
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the '#{target.shortcode}' emoji.")
|
||||
elsif target.is_a? Status
|
||||
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed post #{TagManager.instance.url_for(target)}\u200b.")
|
||||
end
|
||||
|
||||
when :update
|
||||
if target.is_a? DomainBlock
|
||||
LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on '#{target.domain}' to #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
|
||||
elsif target.is_a? Status
|
||||
LogWorker.perform_async("\xf0\x9f\x91\x81\xef\xb8\x8f <#{source}> changed visibility flags of post #{TagManager.instance.url_for(target)}\u200b.")
|
||||
elsif target.is_a? CustomEmoji
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> replaced the '#{target.shortcode}' emoji. :#{target.shortcode}:")
|
||||
end
|
||||
|
||||
when :enable
|
||||
if target.is_a? User
|
||||
LogWorker.perform_async("\xf0\x9f\x92\xa7 <#{source}> unfroze the account of <#{target.username}>.")
|
||||
elsif target.is_a? CustomEmoji
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> enabled the '#{target.shortcode}' emoji. :#{target.shortcode}:")
|
||||
end
|
||||
when :disable
|
||||
if target.is_a? User
|
||||
LogWorker.perform_async("\xe2\x9d\x84\xef\xb8\x8f <#{source}> froze the account of <#{target.username}>.")
|
||||
elsif target.is_a? CustomEmoji
|
||||
LogWorker.perform_async("\u26d4 <#{source}> disabled the '#{target.shortcode}' emoji.")
|
||||
end
|
||||
|
||||
when :mark_unknown
|
||||
if source.nil?
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x95 Federating with a new server at '#{target}'. Automatic reject unknown policy set.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target}")
|
||||
else
|
||||
LogWorker.perform_async("\u2753 <#{source}> marked <#{target.acct}> as an unknown account.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
end
|
||||
when :force_sensitive
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x9e <#{source}> forced the media of <#{target.acct}> to be marked sensitive.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :force_unlisted
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x89 <#{source}> forced the posts of <#{target.acct}> to be unlisted.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :silence
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x87 <#{source}> silenced <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :suspend
|
||||
LogWorker.perform_async("\u26d4 <#{source}> suspended <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
|
||||
when :mark_known
|
||||
LogWorker.perform_async("\u2705 <#{source}> marked <#{target.acct}> as a known account.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :allow_nonsensitive
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> allowed <#{target.acct}> to post media without a sensitive flag.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :allow_public
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x8a <#{source}> allowed <#{target.acct}> to post with public visibility.")
|
||||
when :unsilence
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x8a <#{source}> unsilenced <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
when :unsuspend
|
||||
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> unsuspended <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
|
||||
|
||||
when :remove_avatar
|
||||
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the avatar of <#{target.acct}>.")
|
||||
when :remove_header
|
||||
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the profile header of <#{target.acct}>.")
|
||||
|
||||
when :resolve
|
||||
LogWorker.perform_async("\u2705 <#{source}> resolved report #{target.id}.")
|
||||
when :reopen
|
||||
LogWorker.perform_async("\u2757 <#{source}> reopened report #{target.id}.")
|
||||
when :assigned_to_self
|
||||
LogWorker.perform_async("\xf0\x9f\x91\x80 <#{source}> is resolving report #{target.id}.")
|
||||
when :unassigned
|
||||
LogWorker.perform_async("\u274c <#{source}> is no longer assigned to report #{target.id}.")
|
||||
|
||||
when :promote
|
||||
LogWorker.perform_async("\xf0\x9f\x94\xba <#{source}> upgraded a local account from #{target.role}.")
|
||||
when :demote
|
||||
LogWorker.perform_async("\xf0\x9f\x94\xbb <#{source}> downgraded a local account from #{target.role}.")
|
||||
|
||||
when :confirm
|
||||
LogWorker.perform_async("\u2705 <#{source}> manually confirmed a local account.")
|
||||
when :reset_password
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> manually reset a local account's password.")
|
||||
when :disable_2fa
|
||||
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> manually reset a local account's 2-factor auth.")
|
||||
when :change_email
|
||||
LogWorker.perform_async("\xf0\x9f\x93\x9d <#{source}> manually changed a local account's email address.")
|
||||
|
||||
when :memorialize
|
||||
LogWorker.perform_async("\xf0\x9f\x8f\x85 <#{source}> memorialized an account.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,125 @@
|
|||
module ModerationHelper
|
||||
include LogHelper
|
||||
|
||||
POLICIES = %w(silence unsilence suspend unsuspend force_unlisted mark_known mark_unknown reject_unknown allow_public force_sensitive allow_nonsensitive reset)
|
||||
EXCLUDED_DOMAINS = %w(tailma.ws monsterpit.net monsterpit.cloud monsterpit.gallery monsterpit.blog)
|
||||
|
||||
def janitor_account
|
||||
account_id = ENV.fetch('JANITOR_USER', '').to_i
|
||||
return if account_id == 0
|
||||
Account.find_by(id: account_id)
|
||||
end
|
||||
|
||||
def account_policy(username, domain, policy, reason = nil)
|
||||
return if policy.blank?
|
||||
policy = policy.to_s
|
||||
return false unless policy.in?(POLICIES)
|
||||
|
||||
username, domain = username.split('@')[1..2] if username.start_with?('@')
|
||||
domain.downcase! unless domain.nil?
|
||||
|
||||
acct = Account.find_by(username: username, domain: domain)
|
||||
return false if acct.nil?
|
||||
|
||||
if policy == 'reset'
|
||||
Admin::ActionLog.create(account: @account, action: 'unsuspend', target: acct)
|
||||
user_friendly_action_log(@account, :unsuspend, acct, reason)
|
||||
else
|
||||
Admin::ActionLog.create(account: @account, action: policy, target: acct)
|
||||
user_friendly_action_log(@account, policy.to_sym, acct, reason)
|
||||
end
|
||||
|
||||
case policy
|
||||
when 'mark_unknown', 'reject_unknown'
|
||||
acct.mark_unknown!
|
||||
when 'mark_known'
|
||||
acct.mark_known!
|
||||
when 'silence'
|
||||
acct.silence!
|
||||
when 'unsilence'
|
||||
acct.unsilence!
|
||||
when 'suspend'
|
||||
SuspendAccountService.new.call(acct, include_user: true)
|
||||
return true
|
||||
when 'unsuspend'
|
||||
acct.unsuspend!
|
||||
when 'force_unlisted'
|
||||
acct.force_unlisted
|
||||
when 'allow_public'
|
||||
acct.allow_public!
|
||||
when 'force_sensitive'
|
||||
acct.force_sensitive!
|
||||
when 'allow_nonsensitive'
|
||||
acct.allow_nonsensitive!
|
||||
when 'reset'
|
||||
acct.unsuspend!
|
||||
acct.unsilence!
|
||||
acct.allow_public!
|
||||
acct.allow_nonsensitive!
|
||||
acct.mark_known!
|
||||
end
|
||||
|
||||
acct.save
|
||||
|
||||
return true unless reason && !reason.strip.blank?
|
||||
|
||||
AccountModerationNote.create(
|
||||
account_id: @account.id,
|
||||
target_account_id: acct.id,
|
||||
content: reason.strip
|
||||
)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def domain_exists?(domain)
|
||||
begin
|
||||
code = Request.new(:head, "https://#{domain}").perform(&:code)
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
return false if [404, 410].include?(code)
|
||||
true
|
||||
end
|
||||
|
||||
def domain_policy(domain, policy, reason = nil, force_sensitive: false, reject_unknown: false, reject_media: false, reject_reports: false)
|
||||
return if policy.blank?
|
||||
policy = policy.to_s
|
||||
return false unless policy.in?(POLICIES)
|
||||
return false unless domain.match?(/\A[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*\Z/)
|
||||
|
||||
domain.downcase!
|
||||
|
||||
return false if domain.in?(EXCLUDED_DOMAINS)
|
||||
|
||||
policy = 'noop' if policy == 'force_sensitive' || policy == 'reject_unknown'
|
||||
force_sensitive = true if policy == 'force_sensitive'
|
||||
reject_unknown = true if policy == 'reject_unknown'
|
||||
|
||||
if policy.in? %w(silence suspend force_unlisted)
|
||||
return false unless domain_exists?(domain)
|
||||
|
||||
domain_block = DomainBlock.find_or_create_by(domain: domain)
|
||||
domain_block.severity = policy
|
||||
domain_block.force_sensitive = force_sensitive
|
||||
domain_block.reject_unknown = reject_unknown
|
||||
domain_block.reject_media = reject_media
|
||||
domain_block.reject_reports = reject_reports
|
||||
domain_block.reason = reason.strip if reason && !reason.strip.blank?
|
||||
domain_block.save
|
||||
|
||||
Admin::ActionLog.create(account: @account, action: :create, target: domain_block)
|
||||
user_friendly_action_log(@account, :create, domain_block)
|
||||
DomainBlockWorker.perform_async(domain_block.id)
|
||||
else
|
||||
domain_block = DomainBlock.find_by(domain: domain)
|
||||
return false if domain_block.nil?
|
||||
|
||||
Admin::ActionLog.create(account: @account, action: :destroy, target: domain_block)
|
||||
user_friendly_action_log(@account, :destroy, domain_block)
|
||||
DomainUnblockWorker.perform_async(domain_block.id)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
|
@ -68,7 +68,7 @@ module SettingsHelper
|
|||
end
|
||||
|
||||
def filterable_languages
|
||||
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
|
||||
HUMAN_LOCALES.keys
|
||||
end
|
||||
|
||||
def hash_to_object(hash)
|
||||
|
|
|
@ -35,18 +35,27 @@ module StreamEntriesHelper
|
|||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
content_tag(:div, class: 'roles') do
|
||||
froze = account.local? ? (account&.user.nil? ? true : account.user.disabled?) : account.froze?
|
||||
roles = []
|
||||
roles << content_tag(:div, t('accounts.roles.froze'), class: 'account-role froze') if froze
|
||||
roles << content_tag(:div, t('accounts.roles.locked'), class: 'account-role locked') if account.locked?
|
||||
roles << content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot') if account.bot?
|
||||
roles << content_tag(:div, t('accounts.roles.adult'), class: 'account-role adult') if account.adult_content?
|
||||
roles << content_tag(:div, t('accounts.roles.gently'), class: 'account-role gently') if account.gently?
|
||||
roles << content_tag(:div, t('accounts.roles.kobold'), class: 'account-role kobold') if account.kobold?
|
||||
|
||||
if (Setting.show_staff_badge && account.user_staff?) || all
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
roles << content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
roles << content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
roles << content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
|
||||
roles.sum
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -64,24 +73,33 @@ module StreamEntriesHelper
|
|||
Setting.hide_followers_count || account.user&.setting_hide_followers_count
|
||||
end
|
||||
|
||||
def hide_stats?(account)
|
||||
Setting.hide_stats || account.user_hides_stats?
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
if hide_stats?(account)
|
||||
prepend_stats = []
|
||||
else
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
[prepend_stats.join(', '), account.note].join(' · ')
|
||||
|
@ -187,6 +205,8 @@ module StreamEntriesHelper
|
|||
fa_icon 'globe fw'
|
||||
when 'unlisted'
|
||||
fa_icon 'unlock fw'
|
||||
when 'local'
|
||||
fa_icon 'users fw'
|
||||
when 'private'
|
||||
fa_icon 'lock fw'
|
||||
when 'direct'
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
module UrlHelper
|
||||
def sanitize_query_string(url)
|
||||
return if url.blank?
|
||||
url = Addressable::URI.parse(url)
|
||||
return url.to_s if url.query.blank?
|
||||
return unless '='.in?(url.query)
|
||||
params = CGI.parse(url.query)
|
||||
params.delete_if do |key|
|
||||
k = key.downcase
|
||||
next true if k.start_with?(
|
||||
'_hs',
|
||||
'ic',
|
||||
'mc_',
|
||||
'mkt_',
|
||||
'ns_',
|
||||
'sr_',
|
||||
'utm',
|
||||
'vero_',
|
||||
'nr_',
|
||||
'ref',
|
||||
)
|
||||
next true if 'track'.in?(k)
|
||||
next true if [
|
||||
'fbclid',
|
||||
'gclid',
|
||||
'ncid',
|
||||
'ocid',
|
||||
'r',
|
||||
'spm',
|
||||
].include?(k)
|
||||
false
|
||||
end
|
||||
url.query_values = params
|
||||
return url.to_s
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
return '#'
|
||||
end
|
||||
end
|
|
@ -144,7 +144,9 @@ export function submitCompose(routerHistory) {
|
|||
|
||||
dispatch(submitComposeRequest());
|
||||
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
||||
status = status + ' 👁️';
|
||||
if (!/(?:#|#|#)(?:!|!|!)(?:<\/p>)?$/.test(status)) {
|
||||
status = status + ' #!';
|
||||
}
|
||||
}
|
||||
api(getState).post('/api/v1/statuses', {
|
||||
status,
|
||||
|
@ -227,7 +229,7 @@ export function doodleSet(options) {
|
|||
|
||||
export function uploadCompose(files) {
|
||||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
const uploadLimit = 6;
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
@ -245,7 +247,7 @@ export function uploadCompose(files) {
|
|||
dispatch(uploadComposeRequest());
|
||||
|
||||
for (const [i, f] of Array.from(files).entries()) {
|
||||
if (media.size + i > 3) break;
|
||||
if (media.size + i > 5) break;
|
||||
|
||||
resizeImage(f).then(file => {
|
||||
const data = new FormData();
|
||||
|
|
|
@ -22,7 +22,7 @@ export function normalizeAccount(account) {
|
|||
if (account.fields) {
|
||||
account.fields = account.fields.map(pair => ({
|
||||
...pair,
|
||||
name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
|
||||
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
|
||||
value_emojified: emojify(pair.value, emojiMap),
|
||||
value_plain: unescapeHTML(pair.value),
|
||||
}));
|
||||
|
|
|
@ -150,10 +150,10 @@ export const createListFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
|
||||
export const updateList = (id, title, shouldReset, replies_policy, show_self) => (dispatch, getState) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, show_self }).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
|
|
@ -37,7 +37,7 @@ export function submitSearch() {
|
|||
params: {
|
||||
q: value,
|
||||
resolve: true,
|
||||
limit: 10,
|
||||
limit: 33,
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.data.accounts) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { fetchFilters } from './filters';
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
import { resetCompose } from 'flavours/glitch/actions/compose';
|
||||
|
||||
const { messages } = getLocale();
|
||||
|
||||
|
@ -40,6 +41,14 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||
case 'filters_changed':
|
||||
dispatch(fetchFilters());
|
||||
break;
|
||||
case 'switch_accounts':
|
||||
dispatch(resetCompose());
|
||||
window.location.href = `/auth/sign_in?switch_to=${data.payload}`
|
||||
break;
|
||||
case 'refresh':
|
||||
dispatch(resetCompose());
|
||||
window.location.reload();
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -95,7 +95,7 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
|
|||
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies, reblogs } = {}) => expandTimeline(`account:${accountId}${reblogs ? ':reblogs' : withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, reblogs: reblogs, exclude_reblogs: !reblogs, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
|
|
|
@ -1,30 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from './permalink';
|
||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
||||
|
||||
const Hashtag = ({ hashtag }) => (
|
||||
<div className='trends__item'>
|
||||
<div className='trends__item__name'>
|
||||
<Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
|
||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
||||
</div>
|
||||
|
||||
<div className='trends__item__current'>
|
||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
|
||||
</div>
|
||||
|
||||
<div className='trends__item__sparkline'>
|
||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</div>
|
||||
</div>
|
||||
<Permalink className='hashtag' href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
);
|
||||
|
||||
Hashtag.propTypes = {
|
||||
|
|
|
@ -58,6 +58,9 @@ class Item extends React.PureComponent {
|
|||
handleMouseEnter = (e) => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
} else if (this.hoverToPlayClassicGif()) {
|
||||
const { attachment } = this.props;
|
||||
e.target.src = attachment.get('url');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +68,9 @@ class Item extends React.PureComponent {
|
|||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
} else if (this.hoverToPlayClassicGif()) {
|
||||
const { attachment } = this.props;
|
||||
e.target.src = attachment.get('preview_url');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +79,14 @@ class Item extends React.PureComponent {
|
|||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
||||
}
|
||||
|
||||
hoverToPlayClassicGif () {
|
||||
const { attachment } = this.props;
|
||||
return !autoPlayGif && (
|
||||
attachment.get('type') === 'image' &&
|
||||
attachment.get('url').split('.').pop().startsWith('gif')
|
||||
);
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const { index, onClick } = this.props;
|
||||
|
||||
|
@ -80,6 +94,9 @@ class Item extends React.PureComponent {
|
|||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
} else if (this.hoverToPlayClassicGif()) {
|
||||
const { attachment } = this.props;
|
||||
e.target.src = attachment.get('preview_url');
|
||||
}
|
||||
e.preventDefault();
|
||||
onClick(index);
|
||||
|
@ -124,54 +141,12 @@ class Item extends React.PureComponent {
|
|||
const { attachment, index, size, standalone, letterbox, displayWidth, visible } = this.props;
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
let top = 'auto';
|
||||
let left = 'auto';
|
||||
let bottom = 'auto';
|
||||
let right = 'auto';
|
||||
let height = 100 / Math.ceil(size/2);
|
||||
|
||||
if (size === 1) {
|
||||
if (size === 1 || size % 2 == 1 && index == 0) {
|
||||
width = 100;
|
||||
}
|
||||
|
||||
if (size === 4 || (size === 3 && index > 0)) {
|
||||
height = 50;
|
||||
}
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else {
|
||||
left = '2px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
} else {
|
||||
top = '2px';
|
||||
}
|
||||
}
|
||||
|
||||
let thumbnail = '';
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
|
@ -199,25 +174,54 @@ class Item extends React.PureComponent {
|
|||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<a
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
className={letterbox ? 'letterbox' : null}
|
||||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
const isGif = originalUrl.split('.').pop().startsWith('gif');
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
|
||||
if (isGif && !autoPlay) {
|
||||
thumbnail = (
|
||||
<a
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
className={letterbox ? 'letterbox' : null}
|
||||
src={previewUrl}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
thumbnail = (
|
||||
<a
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
className={letterbox ? 'letterbox' : null}
|
||||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
|
||||
|
@ -243,7 +247,7 @@ class Item extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ width: `${width}%`, height: `${height}%` }}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
||||
{visible && thumbnail}
|
||||
</div>
|
||||
|
@ -320,7 +324,7 @@ export default class MediaGallery extends React.PureComponent {
|
|||
render () {
|
||||
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props;
|
||||
const { visible } = this.state;
|
||||
const size = media.take(4).size;
|
||||
const size = media.take(6).size;
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
||||
|
@ -341,7 +345,7 @@ export default class MediaGallery extends React.PureComponent {
|
|||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />);
|
||||
children = media.take(6).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
|
@ -354,19 +358,75 @@ export default class MediaGallery extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
let parts = {};
|
||||
|
||||
media.map(
|
||||
(attachment, i) => {
|
||||
if (attachment.get('description')) {
|
||||
if (attachment.get('description') in parts) {
|
||||
parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]);
|
||||
} else {
|
||||
parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]];
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let descriptions = Object.entries(parts).map(
|
||||
part => {
|
||||
let [desc, idx] = part;
|
||||
if (idx.length == 1) {
|
||||
let url = idx[0][1];
|
||||
return (
|
||||
<p key={idx[0][2]}>
|
||||
<strong>
|
||||
<a href={url} title={url} target='_blank' rel='nofollow noopener'>
|
||||
Attachment #{1+idx[0][0]}
|
||||
</a>
|
||||
</strong>
|
||||
{': '} {desc}
|
||||
</p>
|
||||
);
|
||||
} else if (idx.length != 0) {
|
||||
let c=0;
|
||||
return (
|
||||
<p key={idx[0][2]}>
|
||||
<strong>
|
||||
Attachments
|
||||
{
|
||||
idx.map(i => {
|
||||
let url = i[1];
|
||||
c++;
|
||||
return (<span key={i[2]}>{c == 1 ? ' ' : ', '}<a href={url} title={url} target='_blank' rel='nofollow noopener'>#{1+i[0]}</a></span>);
|
||||
})
|
||||
}
|
||||
</strong>
|
||||
{': '} {desc}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={computedClass} style={style} ref={this.handleRef}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
||||
{spoilerButton}
|
||||
{visible && sensitive && (
|
||||
<span className='sensitive-marker'>
|
||||
<FormattedMessage {...messages.sensitive} />
|
||||
</span>
|
||||
)}
|
||||
<React.Fragment>
|
||||
<div className={computedClass} style={style} ref={this.handleRef}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
||||
{spoilerButton}
|
||||
{visible && sensitive && (
|
||||
<span className='sensitive-marker'>
|
||||
<FormattedMessage {...messages.sensitive} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
<div className='media-caption'>
|
||||
{descriptions}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -440,7 +440,6 @@ export default class Status extends ImmutablePureComponent {
|
|||
return (
|
||||
<HotKeys handlers={minHandlers}>
|
||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -192,11 +192,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||
const mutingConversation = status.get('muted');
|
||||
const anonymousAccess = !me;
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||
const reblogDisabled = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']));
|
||||
const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog;
|
||||
|
||||
let menu = [];
|
||||
let reblogIcon = 'retweet';
|
||||
let reblogIcon = 'repeat';
|
||||
let replyIcon;
|
||||
let replyTitle;
|
||||
|
||||
|
@ -209,13 +210,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
menu.push(null);
|
||||
|
||||
if (status.getIn(['account', 'id']) === me || withDismiss) {
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
}
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
|
||||
if (status.getIn(['account', 'id']) === me) {
|
||||
if (publicStatus) {
|
||||
if (pinnableStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,12 @@ export default class StatusIcons extends React.PureComponent {
|
|||
aria-hidden='true'
|
||||
/>
|
||||
) : null}
|
||||
{status.get('delete_after') ? (
|
||||
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} aria-hidden='true' />
|
||||
) : null}
|
||||
{status.get('reject_replies') ? (
|
||||
<i className='fa fa-microphone-slash' title='Rejecting replies' aria-hidden='true' />
|
||||
) : null}
|
||||
{(
|
||||
<VisibilityIcon visibility={status.get('visibility')} />
|
||||
)}
|
||||
|
|
|
@ -82,7 +82,7 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
|
||||
<i
|
||||
className={`fa fa-fw fa-${
|
||||
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))
|
||||
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'repeat'))
|
||||
} status__prepend-icon`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
|
||||
const messages = defineMessages({
|
||||
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
local: { id: 'privacy.local.short', defaultMessage: 'Community' },
|
||||
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
|
@ -25,6 +26,7 @@ export default class VisibilityIcon extends ImmutablePureComponent {
|
|||
|
||||
const visibilityClass = {
|
||||
public: 'globe',
|
||||
local: 'users',
|
||||
unlisted: 'unlock',
|
||||
private: 'lock',
|
||||
direct: 'envelope',
|
||||
|
|
|
@ -49,19 +49,16 @@ export default class ActionBar extends React.PureComponent {
|
|||
|
||||
<div className='account__action-bar'>
|
||||
<div className='account__action-bar-links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' title={account.get('statuses_count')} to={`/accounts/${account.get('id')}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
<strong><FormattedNumber value={account.get('statuses_count')} /></strong>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' title={account.get('following_count')} to={`/accounts/${account.get('id')}/following`}>
|
||||
<FormattedMessage id='account.follows' defaultMessage='Follows' />
|
||||
<strong><FormattedNumber value={account.get('following_count')} /></strong>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
|
||||
<NavLink exact activeClassName='active' className='account__action-bar__tab' title={account.get('followers_count') < 0 ? '(hidden)' : account.get('followers_count')} to={`/accounts/${account.get('id')}/followers`}>
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -80,7 +80,6 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
let info = [];
|
||||
let actionBtn = '';
|
||||
let lockedIcon = '';
|
||||
let menu = [];
|
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
||||
|
@ -114,10 +113,6 @@ class Header extends ImmutablePureComponent {
|
|||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (account.get('locked')) {
|
||||
lockedIcon = <Icon icon='lock' title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
if (account.get('id') !== me) {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
|
@ -189,7 +184,15 @@ class Header extends ImmutablePureComponent {
|
|||
const content = { __html: account.get('note_emojified') };
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const fields = account.get('fields');
|
||||
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
||||
|
||||
const badge_locked = account.get('locked') ? (<div className='account-role locked'><FormattedMessage id='account.badges.locked' defaultMessage='🔒 Locked' /></div>) : null;
|
||||
const badge_froze = account.get('froze') ? (<div className='account-role froze'><FormattedMessage id='account.badges.froze' defaultMessage='❄️ Frozen by admin' /></div>) : null;
|
||||
const badge_bot = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
||||
const badge_ac = account.get('adult_content') ? (<div className='account-role adult'><FormattedMessage id='account.badges.adult' defaultMessage="🔞 Adult content" /></div>) : null;
|
||||
const badge_gently = account.get('gently') ? (<div className='account-role gently'><FormattedMessage id='account.badges.gently' defaultMessage="Gentlies kobolds" /></div>) : null;
|
||||
const badge_kobold = account.get('kobold') ? (<div className='account-role kobold'><FormattedMessage id='account.badges.kobold' defaultMessage="Gently the kobold" /></div>) : null;
|
||||
const badge_mod = account.get('role') == 'moderator' ? (<div className='account-role moderator'><FormattedMessage id='account.badges.moderator' defaultMessage="Moderator" /></div>) : null;
|
||||
const badge_admin = account.get('role') == 'admin' ? (<div className='account-role admin'><FormattedMessage id='account.badges.admin' defaultMessage="Admin" /></div>) : null;
|
||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
|
||||
return (
|
||||
|
@ -219,8 +222,9 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__tabs__name'>
|
||||
<h1>
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
|
||||
<small>@{acct} {lockedIcon}</small>
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
||||
<small>@{acct}</small>
|
||||
<div className='roles'>{badge_admin}{badge_mod}{badge_froze}{badge_locked}{badge_ac}{badge_bot}{badge_gently}{badge_kobold}</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -119,7 +119,8 @@ export default class Header extends ImmutablePureComponent {
|
|||
{!hideTabs && (
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
|
||||
{account.get('replies') && (<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>)}
|
||||
<NavLink exact to={`/accounts/${account.get('id')}/reblogs`}><FormattedMessage id='account.reblogs' defaultMessage='Boosts' /></NavLink>
|
||||
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -15,13 +15,13 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false, reblogs = false }) => {
|
||||
const path = reblogs ? `${accountId}:reblogs` : withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
return {
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
||||
featuredStatusIds: (withReplies || reblogs) ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
};
|
||||
|
@ -38,28 +38,29 @@ export default class AccountTimeline extends ImmutablePureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
withReplies: PropTypes.bool,
|
||||
reblogs: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { accountId }, withReplies } = this.props;
|
||||
const { params: { accountId }, withReplies, reblogs } = this.props;
|
||||
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
if (!withReplies) {
|
||||
if (!withReplies && !reblogs) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
}
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies: withReplies, reblogs: reblogs }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies || nextProps.reblogs !== this.props.reblogs) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
|
||||
if (!nextProps.withReplies) {
|
||||
if (!nextProps.withReplies && !nextProps.reblogs) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||
}
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies, reblogs: nextProps.params.reblogs }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +69,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies, reblogs: this.props.reblogs }));
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
|
|
|
@ -17,7 +17,12 @@ import Publisher from './publisher';
|
|||
import TextareaIcons from './textarea_icons';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'Roar shamelessly!' },
|
||||
placeholder_as: {
|
||||
id: 'compose_form.placeholder_as',
|
||||
defaultMessage: "Signing as {nickname}.\nRoar shamelessly!",
|
||||
values: {nickname: 'yourself'}
|
||||
},
|
||||
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
|
||||
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
|
||||
missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm',
|
||||
|
@ -47,6 +52,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onClearAll: PropTypes.func,
|
||||
onClearSuggestions: PropTypes.func,
|
||||
onFetchSuggestions: PropTypes.func,
|
||||
onSuggestionSelected: PropTypes.func,
|
||||
|
@ -70,6 +76,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onUnmount: PropTypes.func,
|
||||
onPaste: PropTypes.func,
|
||||
onMediaDescriptionConfirm: PropTypes.func,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -105,6 +112,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
text,
|
||||
mediaDescriptionConfirmation,
|
||||
onMediaDescriptionConfirm,
|
||||
onClearAll,
|
||||
} = this.props;
|
||||
|
||||
// If something changes inside the textarea, then we update the
|
||||
|
@ -162,6 +170,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
this.handleSubmit();
|
||||
}
|
||||
|
||||
handleClearAll = () => {
|
||||
this.props.onClearAll();
|
||||
}
|
||||
|
||||
// Selects a suggestion from the autofill.
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
||||
|
@ -273,6 +285,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
handleSelect,
|
||||
handleSubmit,
|
||||
handleRefTextarea,
|
||||
handleClearAll,
|
||||
} = this;
|
||||
const {
|
||||
advancedOptions,
|
||||
|
@ -297,9 +310,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
suggestions,
|
||||
text,
|
||||
spoilersAlwaysOn,
|
||||
account,
|
||||
} = this.props;
|
||||
|
||||
let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
|
||||
let nickname = (this.props.account !== undefined) ? this.props.account.get('identity') : '';
|
||||
|
||||
return (
|
||||
<div className='composer'>
|
||||
|
@ -331,7 +346,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
placeholder={nickname ? intl.formatMessage(messages.placeholder_as, {nickname: nickname}) : intl.formatMessage(messages.placeholder)}
|
||||
disabled={isSubmitting}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
|
@ -368,6 +383,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
disabled={disabledButton}
|
||||
onSecondarySubmit={handleSecondarySubmit}
|
||||
onSubmit={handleSubmit}
|
||||
onClearAll={handleClearAll}
|
||||
privacy={privacy}
|
||||
sideArm={sideArm}
|
||||
/>
|
||||
|
|
|
@ -25,6 +25,22 @@ const messages = defineMessages({
|
|||
defaultMessage: 'Attach...',
|
||||
id: 'compose.attach',
|
||||
},
|
||||
bbcode: {
|
||||
defaultMessage: 'BBCode',
|
||||
id: 'compose.content-type.bbcode',
|
||||
},
|
||||
bbdown: {
|
||||
defaultMessage: 'BBdown',
|
||||
id: 'compose.content-type.bbdown',
|
||||
},
|
||||
local_short: {
|
||||
defaultMessage: 'Community',
|
||||
id: 'privacy.local.short'
|
||||
},
|
||||
local_long: {
|
||||
defaultMessage: 'Post to community timeline',
|
||||
id: 'privacy.local.long'
|
||||
},
|
||||
change_privacy: {
|
||||
defaultMessage: 'Adjust status privacy',
|
||||
id: 'privacy.change',
|
||||
|
@ -61,6 +77,10 @@ const messages = defineMessages({
|
|||
defaultMessage: 'Markdown',
|
||||
id: 'compose.content-type.markdown',
|
||||
},
|
||||
console: {
|
||||
defaultMessage: 'Console',
|
||||
id: 'compose.content-type.console',
|
||||
},
|
||||
plain: {
|
||||
defaultMessage: 'Plain text',
|
||||
id: 'compose.content-type.plain',
|
||||
|
@ -228,24 +248,45 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
name: 'unlisted',
|
||||
text: <FormattedMessage {...messages.unlisted_short} />,
|
||||
},
|
||||
local: {
|
||||
icon: 'users',
|
||||
meta: <FormattedMessage {...messages.local_long} />,
|
||||
name: 'local',
|
||||
text: <FormattedMessage {...messages.local_short} />,
|
||||
}
|
||||
};
|
||||
|
||||
const contentTypeItems = {
|
||||
plain: {
|
||||
icon: 'align-left',
|
||||
icon: 'file-text',
|
||||
name: 'text/plain',
|
||||
text: <FormattedMessage {...messages.plain} />,
|
||||
},
|
||||
console: {
|
||||
icon: 'terminal',
|
||||
name: 'text/console',
|
||||
text: <FormattedMessage {...messages.console} />,
|
||||
},
|
||||
html: {
|
||||
icon: 'code',
|
||||
name: 'text/html',
|
||||
text: <FormattedMessage {...messages.html} />,
|
||||
},
|
||||
markdown: {
|
||||
icon: 'arrow-circle-down',
|
||||
icon: 'hashtag',
|
||||
name: 'text/markdown',
|
||||
text: <FormattedMessage {...messages.markdown} />,
|
||||
},
|
||||
xbbcode: {
|
||||
icon: 'thumb-tack',
|
||||
name: 'text/x-bbcode',
|
||||
text: <FormattedMessage {...messages.bbcode} />,
|
||||
},
|
||||
xbbcodemarkdown: {
|
||||
icon: 'arrow-circle-down',
|
||||
name: 'text/x-bbcode+markdown',
|
||||
text: <FormattedMessage {...messages.bbdown} />,
|
||||
},
|
||||
};
|
||||
|
||||
// The result.
|
||||
|
@ -302,6 +343,7 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
icon={(privacyItems[privacy] || {}).icon}
|
||||
items={[
|
||||
privacyItems.public,
|
||||
privacyItems.local,
|
||||
privacyItems.unlisted,
|
||||
privacyItems.private,
|
||||
privacyItems.direct,
|
||||
|
@ -315,11 +357,14 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
{showContentTypeChoice && (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
|
||||
icon={(contentTypeItems[contentType.split('/')[1].replace(/[+-]/g, '')] || {}).icon}
|
||||
items={[
|
||||
contentTypeItems.plain,
|
||||
contentTypeItems.html,
|
||||
contentTypeItems.xbbcodemarkdown,
|
||||
contentTypeItems.markdown,
|
||||
contentTypeItems.xbbcode,
|
||||
contentTypeItems.html,
|
||||
contentTypeItems.plain,
|
||||
contentTypeItems.console,
|
||||
]}
|
||||
onChange={onChangeContentType}
|
||||
onModalClose={onModalClose}
|
||||
|
|
|
@ -144,6 +144,7 @@ class PollForm extends ImmutablePureComponent {
|
|||
</select>
|
||||
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
<option value={60}>{intl.formatMessage(messages.minutes, { number: 1 })}</option>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||
|
@ -151,6 +152,10 @@ class PollForm extends ImmutablePureComponent {
|
|||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||
<option value={1209600}>{intl.formatMessage(messages.days, { number: 14 })}</option>
|
||||
<option value={2592000}>{intl.formatMessage(messages.days, { number: 30 })}</option>
|
||||
<option value={5184000}>{intl.formatMessage(messages.days, { number: 60 })}</option>
|
||||
<option value={7776000}>{intl.formatMessage(messages.days, { number: 90 })}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,6 +23,10 @@ const messages = defineMessages({
|
|||
defaultMessage: '{publish}!',
|
||||
id: 'compose_form.publish_loud',
|
||||
},
|
||||
clear: {
|
||||
defaultMessage: 'Clear',
|
||||
id: 'compose_form.clear',
|
||||
},
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -34,12 +38,13 @@ class Publisher extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
onSecondarySubmit: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||
onClearAll: PropTypes.func,
|
||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'local', 'public']),
|
||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'local', 'public']),
|
||||
};
|
||||
|
||||
render () {
|
||||
const { countText, disabled, intl, onSecondarySubmit, onSubmit, privacy, sideArm } = this.props;
|
||||
const { countText, disabled, intl, onSecondarySubmit, onSubmit, onClearAll, privacy, sideArm } = this.props;
|
||||
|
||||
const diff = maxChars - length(countText || '');
|
||||
const computedClass = classNames('composer--publisher', {
|
||||
|
@ -49,6 +54,17 @@ class Publisher extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className={computedClass}>
|
||||
<Button
|
||||
className='clear'
|
||||
onClick={onClearAll}
|
||||
title={intl.formatMessage(messages.clear)}
|
||||
disabled={disabled || diff < 0}
|
||||
text={
|
||||
<span>
|
||||
<Icon icon='trash-o' />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<span className='count'>{diff}</span>
|
||||
{sideArm && sideArm !== 'none' ? (
|
||||
<Button
|
||||
|
@ -61,6 +77,7 @@ class Publisher extends ImmutablePureComponent {
|
|||
<Icon
|
||||
icon={{
|
||||
public: 'globe',
|
||||
local: 'users',
|
||||
unlisted: 'unlock',
|
||||
private: 'lock',
|
||||
direct: 'envelope',
|
||||
|
@ -86,6 +103,7 @@ class Publisher extends ImmutablePureComponent {
|
|||
private: 'lock',
|
||||
public: 'globe',
|
||||
unlisted: 'unlock',
|
||||
local: 'users',
|
||||
}[privacy]}
|
||||
/>
|
||||
{' '}
|
||||
|
|
|
@ -47,7 +47,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const account = status.get('account');
|
||||
const content = status.get('content');
|
||||
const content = status.get('contentHtml');
|
||||
const attachments = status.get('media_attachments');
|
||||
|
||||
// The result.
|
||||
|
|
|
@ -84,7 +84,9 @@ class SearchResults extends ImmutablePureComponent {
|
|||
<section>
|
||||
<h5><Icon icon='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||
|
||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
<div className='hashtags'>
|
||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -98,8 +100,8 @@ class SearchResults extends ImmutablePureComponent {
|
|||
</header>
|
||||
|
||||
{accounts}
|
||||
{statuses}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
submitCompose,
|
||||
unmountCompose,
|
||||
uploadCompose,
|
||||
resetCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import {
|
||||
openModal,
|
||||
|
@ -22,6 +23,8 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
|||
|
||||
import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
|
||||
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
|
||||
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
|
||||
|
@ -68,6 +71,7 @@ function mapStateToProps (state) {
|
|||
spoilersAlwaysOn: spoilersAlwaysOn,
|
||||
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
|
||||
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -131,6 +135,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}));
|
||||
},
|
||||
|
||||
onClearAll() {
|
||||
dispatch(resetCompose());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm));
|
||||
|
|
|
@ -16,7 +16,7 @@ function mapStateToProps (state) {
|
|||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
hasPoll: !!poll,
|
||||
allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true),
|
||||
allowMedia: !poll && (media ? media.size < 6 && !media.some(item => item.get('type') === 'video') : true),
|
||||
hasMedia: media && !!media.size,
|
||||
allowPoll: !(media && !!media.size),
|
||||
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
||||
|
|
|
@ -5,23 +5,16 @@ import PropTypes from 'prop-types';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
||||
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
|
||||
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
|
||||
});
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
|
||||
const WarningWrapper = ({ needsLockWarning, directMessageWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
|
||||
if (hashtagWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
|
||||
}
|
||||
|
||||
if (directMessageWarning) {
|
||||
const message = (
|
||||
<span>
|
||||
|
@ -37,7 +30,6 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
|||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import SearchContainer from './containers/search_container';
|
|||
import Motion from 'flavours/glitch/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import SearchResultsContainer from './containers/search_results_container';
|
||||
import { me, mascot } from 'flavours/glitch/util/initial_state';
|
||||
import { me, mascot, isStaff } from 'flavours/glitch/util/initial_state';
|
||||
import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
|
||||
import HeaderContainer from './containers/header_container';
|
||||
|
||||
|
@ -62,6 +62,20 @@ class Compose extends React.PureComponent {
|
|||
{!isSearchPage && <div className='drawer__inner'>
|
||||
<NavigationContainer />
|
||||
<ComposeFormContainer />
|
||||
{isStaff && multiColumn && (
|
||||
<div className='drawer__inner__admin'>
|
||||
<h2>Staff Tools</h2>
|
||||
<ul>
|
||||
<li><a href="/admin/action_logs" target="_blank" rel="nofollow noopener">Audit log</a></li>
|
||||
<li><a href="/admin/reports" target="_blank" rel="nofollow noopener">Reports</a></li>
|
||||
<li><a href="/admin/pending_accounts" target="_blank" rel="nofollow noopener">Pending accounts</a></li>
|
||||
<li><a href="/admin/domain_blocks/new" target="_blank" rel="nofollow noopener">Add domain policy...</a></li>
|
||||
<li><a href="/admin/instances" target="_blank" rel="nofollow noopener">Federation</a></li>
|
||||
<li><a href="/admin/accounts" target="_blank" rel="nofollow noopener">Accounts</a></li>
|
||||
<li><a href="/admin/custom_emojis" target="_blank" rel="nofollow noopener">Custom emojis</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{multiColumn && (
|
||||
<div className='drawer__inner__mastodon'>
|
||||
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
||||
|
|
|
@ -13,10 +13,12 @@ import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists
|
|||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
||||
show_self: { id: 'lists.show_self', defaultMessage: 'Include your own toots' },
|
||||
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'any followed user' },
|
||||
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'no one' },
|
||||
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'members of the list' },
|
||||
|
@ -114,6 +116,14 @@ export default class ListTimeline extends React.PureComponent {
|
|||
}));
|
||||
}
|
||||
|
||||
handleShowSelfChange = ({ target }) => {
|
||||
const { dispatch, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const show_self = list ? list.get('show_self') : false;
|
||||
this.props.dispatch(updateList(id, undefined, false, replies_policy, !show_self));
|
||||
}
|
||||
|
||||
handleRepliesPolicyChange = ({ target }) => {
|
||||
const { dispatch, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
@ -126,6 +136,7 @@ export default class ListTimeline extends React.PureComponent {
|
|||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const show_self = list ? list.get('show_self') : false;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
|
@ -167,13 +178,22 @@ export default class ListTimeline extends React.PureComponent {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={['setting', 'toggle', id, 'show_self'].join('-')} checked={show_self === true} onChange={this.handleShowSelfChange} />
|
||||
<label htmlFor={['setting', 'toggle', id, 'show_self'].join('-')} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.show_self' defaultMessage='Include your own toots' />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ replies_policy !== undefined && (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<fieldset>
|
||||
<legend><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></legend>
|
||||
{ ['no_replies', 'list_replies', 'all_replies'].map(policy => (
|
||||
<div className='setting-radio'>
|
||||
<div key={['setting', 'radio', id, policy].join('-')} className='setting-radio'>
|
||||
<input className='setting-radio__input' id={['setting', 'radio', id, policy].join('-')} type='radio' value={policy} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
<label className='setting-radio__label' htmlFor={['setting', 'radio', id, policy].join('-')}>
|
||||
<FormattedMessage {...messages[policy]} />
|
||||
|
|
|
@ -81,11 +81,13 @@ export default class NotificationFollow extends ImmutablePureComponent {
|
|||
<i className='fa fa-fw fa-user-plus' />
|
||||
</div>
|
||||
|
||||
<FormattedMessage
|
||||
id='notification.follow'
|
||||
defaultMessage='{name} followed you'
|
||||
values={{ name: link }}
|
||||
/>
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage
|
||||
id='notification.follow'
|
||||
defaultMessage='{name} followed you'
|
||||
values={{ name: link }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class Reblogs extends ImmutablePureComponent {
|
|||
return (
|
||||
<Column ref={this.setRef}>
|
||||
<ColumnHeader
|
||||
icon='retweet'
|
||||
icon='repeat'
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onClick={this.handleHeaderClick}
|
||||
showBackButton
|
||||
|
|
|
@ -143,6 +143,7 @@ export default class ActionBar extends React.PureComponent {
|
|||
const { status, intl } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||
const mutingConversation = status.get('muted');
|
||||
|
||||
let menu = [];
|
||||
|
@ -154,7 +155,7 @@ export default class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
if (me === status.getIn(['account', 'id'])) {
|
||||
if (publicStatus) {
|
||||
if (pinnableStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
}
|
||||
|
||||
|
@ -191,7 +192,7 @@ export default class ActionBar extends React.PureComponent {
|
|||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
|
||||
);
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
let reblogIcon = 'repeat';
|
||||
//if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
// else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
|
|
|
@ -15,6 +15,16 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
|
|||
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
|
||||
import classNames from 'classnames';
|
||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const dateFormatOptions = {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
export default class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
|
@ -114,10 +124,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
let media = null;
|
||||
let mediaIcon = null;
|
||||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
let reblogIcon = 'retweet';
|
||||
let reblogIcon = 'repeat';
|
||||
let favouriteLink = '';
|
||||
let sharekeyLinks = '';
|
||||
let destructIcon = '';
|
||||
let rejectIcon = '';
|
||||
|
||||
if (this.props.measureHeight) {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
|
@ -168,10 +180,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
mediaIcon = 'link';
|
||||
}
|
||||
|
||||
if (status.get('application')) {
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
reblogIcon = 'envelope';
|
||||
} else if (status.get('visibility') === 'private') {
|
||||
|
@ -183,43 +191,75 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
} else if (this.context.router) {
|
||||
reblogLink = (
|
||||
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<i className={`fa fa-${reblogIcon}`} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<FormattedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<i className={`fa fa-${reblogIcon}`} title={status.get('reblogs_count')} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
reblogLink = (
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<i className={`fa fa-${reblogIcon}`} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<FormattedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<i className={`fa fa-${reblogIcon}`} title={status.get('reblogs_count')} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.get('sharekey')) {
|
||||
sharekeyLinks = (
|
||||
<span>
|
||||
<a href={`${status.get('url')}?key=${status.get('sharekey')}`} target='_blank' className='detailed-status__link'>
|
||||
<i className='fa fa-key' title='Right-click or long press to copy share link with key' />
|
||||
</a>
|
||||
·
|
||||
<a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
|
||||
<i className='fa fa-user-plus' title='Generate a new share key' />
|
||||
</a>
|
||||
·
|
||||
<a href={`${status.get('url')}?rekey=0&toweb=1`} className='detailed-status__link'>
|
||||
<i className='fa fa-user-times' title='Revoke share key' />
|
||||
</a>
|
||||
·
|
||||
</span>
|
||||
);
|
||||
} else if (status.getIn(['account', 'id']) == me) {
|
||||
sharekeyLinks = (
|
||||
<span>
|
||||
<a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
|
||||
<i className='fa fa-user-plus' title='Generate a new share key' />
|
||||
</a>
|
||||
·
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.context.router) {
|
||||
favouriteLink = (
|
||||
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<i className='fa fa-star' />
|
||||
<span className='detailed-status__favorites'>
|
||||
<FormattedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<i className='fa fa-star' title={status.get('favourites_count')} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
favouriteLink = (
|
||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<i className='fa fa-star' />
|
||||
<span className='detailed-status__favorites'>
|
||||
<FormattedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<i className='fa fa-star' title={status.get('favourites_count')} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.get('delete_after')) {
|
||||
destructIcon = (
|
||||
<span>
|
||||
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} /> ·
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (status.get('reject_replies')) {
|
||||
rejectIcon = (
|
||||
<span>
|
||||
<i className='fa fa-microphone-slash' title='Rejecting replies' /> ·
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })} data-status-by={status.getIn(['account', 'acct'])}>
|
||||
|
@ -241,9 +281,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
{sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} {rejectIcon} <VisibilityIcon visibility={status.get('visibility')} />
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{applicationLink} · {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -30,8 +30,8 @@ class NotificationsIcon extends React.PureComponent {
|
|||
}
|
||||
|
||||
export const links = [
|
||||
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||
|
||||
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||
|
|
|
@ -500,6 +500,7 @@ export default class UI extends React.Component {
|
|||
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
|
||||
|
||||
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
|
||||
<WrappedRoute path='/accounts/:accountId/reblogs' component={AccountTimeline} content={children} componentParams={{ reblogs: true }} />
|
||||
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
|
||||
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
|
||||
|
|
|
@ -6,26 +6,26 @@ const messages = {
|
|||
'layout.current_is': 'Your current layout is:',
|
||||
'layout.desktop': 'Desktop',
|
||||
'layout.mobile': 'Mobile',
|
||||
'navigation_bar.app_settings': 'App settings',
|
||||
'getting_started.onboarding': 'Show me around',
|
||||
'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.',
|
||||
'navigation_bar.app_settings': 'UI options',
|
||||
'getting_started.onboarding': 'Tutorial',
|
||||
'onboarding.page_one.federation': '{domain} is a \'instance\' of Monsterpit. Monsterpit is a network of independent servers joining up to make one larger social network. We call these servers communities.',
|
||||
'onboarding.page_one.welcome': 'Welcome to {domain}!',
|
||||
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
|
||||
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Monsterpit}, and is compatible with any Monsterpit community or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
|
||||
'settings.auto_collapse': 'Automatic collapsing',
|
||||
'settings.auto_collapse_all': 'Everything',
|
||||
'settings.auto_collapse_lengthy': 'Lengthy toots',
|
||||
'settings.auto_collapse_media': 'Toots with media',
|
||||
'settings.auto_collapse_notifications': 'Notifications',
|
||||
'settings.auto_collapse_reblogs': 'Boosts',
|
||||
'settings.auto_collapse_lengthy': 'Lengthy roars',
|
||||
'settings.auto_collapse_media': 'Roars with media',
|
||||
'settings.auto_collapse_notifications': 'Growls',
|
||||
'settings.auto_collapse_reblogs': 'Repeats',
|
||||
'settings.auto_collapse_replies': 'Replies',
|
||||
'settings.show_action_bar': 'Show action buttons in collapsed toots',
|
||||
'settings.show_action_bar': 'Show action buttons in collapsed roars',
|
||||
'settings.close': 'Close',
|
||||
'settings.collapsed_statuses': 'Collapsed toots',
|
||||
'settings.enable_collapsed': 'Enable collapsed toots',
|
||||
'settings.collapsed_statuses': 'Collapsed roars',
|
||||
'settings.enable_collapsed': 'Enable collapsed roars',
|
||||
'settings.general': 'General',
|
||||
'settings.image_backgrounds': 'Image backgrounds',
|
||||
'settings.image_backgrounds_media': 'Preview collapsed toot media',
|
||||
'settings.image_backgrounds_users': 'Give collapsed toots an image background',
|
||||
'settings.image_backgrounds_media': 'Preview collapsed roar media',
|
||||
'settings.image_backgrounds_users': 'Give collapsed roars an image background',
|
||||
'settings.media': 'Media',
|
||||
'settings.media_letterbox': 'Letterbox media',
|
||||
'settings.media_fullwidth': 'Full-width media previews',
|
||||
|
@ -39,7 +39,7 @@ const messages = {
|
|||
|
||||
'favourite_modal.combo': 'You can press {combo} to skip this next time',
|
||||
|
||||
'home.column_settings.show_direct': 'Show DMs',
|
||||
'home.column_settings.show_direct': 'Show whispers',
|
||||
|
||||
'notification.markForDeletion': 'Mark for deletion',
|
||||
'notifications.clear': 'Clear all my notifications',
|
||||
|
@ -56,11 +56,11 @@ const messages = {
|
|||
'compose.attach': 'Attach...',
|
||||
|
||||
'advanced_options.local-only.short': 'Local-only',
|
||||
'advanced_options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.tooltip': 'This post is local-only',
|
||||
'advanced_options.local-only.long': 'Do not roar to other communities',
|
||||
'advanced_options.local-only.tooltip': 'This roar is local-only',
|
||||
'advanced_options.icon_title': 'Advanced options',
|
||||
'advanced_options.threaded_mode.short': 'Threaded mode',
|
||||
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
|
||||
'advanced_options.threaded_mode.long': 'Automatically opens a reply on roaring',
|
||||
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
|
||||
};
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ import { REDRAFT } from 'flavours/glitch/actions/statuses';
|
|||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
import uuid from 'flavours/glitch/util/uuid';
|
||||
import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
|
||||
import { me, defaultContentType } from 'flavours/glitch/util/initial_state';
|
||||
import { me, defaultContentType, defaultLocal, alwaysLocal } from 'flavours/glitch/util/initial_state';
|
||||
import { overwrite } from 'flavours/glitch/util/js_helpers';
|
||||
import { unescapeHTML } from 'flavours/glitch/util/html';
|
||||
import { recoverHashtags } from 'flavours/glitch/util/hashtag';
|
||||
|
@ -59,7 +59,7 @@ const glitchProbability = 1 - 0.0420215528;
|
|||
const initialState = ImmutableMap({
|
||||
mounted: false,
|
||||
advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
do_not_federate: defaultLocal || alwaysLocal,
|
||||
threaded_mode: false,
|
||||
}),
|
||||
sensitive: false,
|
||||
|
@ -82,7 +82,7 @@ const initialState = ImmutableMap({
|
|||
suggestion_token: null,
|
||||
suggestions: ImmutableList(),
|
||||
default_advanced_options: ImmutableMap({
|
||||
do_not_federate: false,
|
||||
do_not_federate: defaultLocal || alwaysLocal,
|
||||
threaded_mode: null, // Do not reset
|
||||
}),
|
||||
default_privacy: 'public',
|
||||
|
@ -177,7 +177,7 @@ function continueThread (state, status) {
|
|||
map.set('in_reply_to', status.id);
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /(?:#|#|#)(?:!|!|!)(?:<\/p>)?$/.test(status.content) }))
|
||||
);
|
||||
map.set('privacy', status.visibility);
|
||||
map.set('sensitive', false);
|
||||
|
@ -331,7 +331,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
|
||||
map => map.merge(new ImmutableMap({ do_not_federate: /(?:#|#|#)(?:!|!|!)(?:<\/p>)?$/.test(action.status.get('content')) }))
|
||||
);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
@ -352,6 +352,15 @@ export default function compose(state = initialState, action) {
|
|||
});
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', null);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.update(
|
||||
'advanced_options',
|
||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||
);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_RESET:
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', null);
|
||||
|
|
|
@ -11,15 +11,15 @@ const initialState = ImmutableMap({
|
|||
navbar_under : false,
|
||||
swipe_to_change_columns: true,
|
||||
side_arm : 'none',
|
||||
side_arm_reply_mode : 'keep',
|
||||
side_arm_reply_mode : 'restrict',
|
||||
show_reply_count : false,
|
||||
always_show_spoilers_field: false,
|
||||
always_show_spoilers_field: true,
|
||||
confirm_missing_media_description: false,
|
||||
confirm_before_clearing_draft: true,
|
||||
preselect_on_reply: true,
|
||||
inline_preview_cards: true,
|
||||
hicolor_privacy_icons: false,
|
||||
show_content_type_choice: false,
|
||||
hicolor_privacy_icons: true,
|
||||
show_content_type_choice: true,
|
||||
content_warnings : ImmutableMap({
|
||||
auto_unfold : false,
|
||||
filter : null,
|
||||
|
|
|
@ -80,8 +80,9 @@ const initialState = ImmutableMap({
|
|||
|
||||
const defaultColumns = fromJS([
|
||||
{ id: 'COMPOSE', uuid: uuid(), params: {} },
|
||||
{ id: 'HOME', uuid: uuid(), params: {} },
|
||||
{ id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
|
||||
{ id: 'HOME', uuid: uuid(), params: {} },
|
||||
{ id: 'COMMUNITY', uuid: uuid(), params: {} },
|
||||
]);
|
||||
|
||||
const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);
|
||||
|
|
|
@ -41,10 +41,16 @@ export const getFilters = (state, { contextType }) => state.get('filters', Immut
|
|||
const escapeRegExp = string =>
|
||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
|
||||
export const regexFromFilters = filters => {
|
||||
if (filters.size === 0) {
|
||||
return null;
|
||||
}
|
||||
export const regexFromFilters = (status, filters) => {
|
||||
if (filters === undefined || filters.size === 0) { return null; }
|
||||
|
||||
let has_media = status.get('media_attachments').size !== 0;
|
||||
|
||||
filters = filters.filter(filter => {
|
||||
return (!has_media && filter.get('exclude_media')) || (has_media && filter.get('media_only')) || (!filter.get('exclude_media') && !filter.get('media_only'))
|
||||
});
|
||||
|
||||
if (filters.size === 0) { return null; }
|
||||
|
||||
return new RegExp(filters.map(filter => {
|
||||
let expr = escapeRegExp(filter.get('phrase'));
|
||||
|
@ -78,10 +84,10 @@ export const makeGetStatus = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters);
|
||||
let filtered = false;
|
||||
|
||||
if (statusReblog) {
|
||||
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(statusReblog, filters);
|
||||
filtered = regex && regex.test(statusReblog.get('search_index'));
|
||||
statusReblog = statusReblog.set('account', accountReblog);
|
||||
statusReblog = statusReblog.set('filtered', filtered);
|
||||
|
@ -89,6 +95,7 @@ export const makeGetStatus = () => {
|
|||
statusReblog = null;
|
||||
}
|
||||
|
||||
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(statusBase, filters);
|
||||
filtered = filtered || regex && regex.test(statusBase.get('search_index'));
|
||||
|
||||
return statusBase.withMutations(map => {
|
||||
|
|
|
@ -201,6 +201,7 @@
|
|||
.account-role {
|
||||
display: inline-block;
|
||||
padding: 4px 6px;
|
||||
margin-right: 2px;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
|
@ -221,6 +222,30 @@
|
|||
background-color: rgba(lighten($error-red, 12%), 0.1);
|
||||
border-color: rgba(lighten($error-red, 12%), 0.5);
|
||||
}
|
||||
|
||||
&.gently {
|
||||
color: lighten(cyan, 25%);
|
||||
background-color: rgba(lighten(cyan, 25%), 0.1);
|
||||
border-color: rgba(lighten(cyan, 25%), 0.1);
|
||||
}
|
||||
|
||||
&.kobold {
|
||||
color: lighten(orange, 22%);
|
||||
background-color: rgba(lighten(orange, 33%), 0.1);
|
||||
border-color: rgba(lighten(orange, 33%), 0.1);
|
||||
}
|
||||
|
||||
&.locked {
|
||||
color: lighten(pink, 5%);
|
||||
background-color: rgba(pink, 0.1);
|
||||
border-color: rgba(pink, 0.5);
|
||||
}
|
||||
|
||||
&.froze {
|
||||
color: lighten($warning-red, 12%);
|
||||
background-color: rgba(lighten($warning-red, 12%), 0.1);
|
||||
border-color: rgba(lighten($warning-red, 12%), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.account__header__fields {
|
||||
|
@ -242,10 +267,7 @@
|
|||
box-sizing: border-box;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
max-height: 48px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
dt {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
@import 'variables';
|
||||
@import 'arachnia/variables';
|
||||
@import 'index';
|
||||
@import 'arachnia/diff';
|
|
@ -0,0 +1,378 @@
|
|||
*:root {
|
||||
// Main body
|
||||
body,
|
||||
body.app-body {
|
||||
background: #160011 url(/system/custom-images/monsterpit-bg.png) no-repeat center center fixed !important;
|
||||
background-size: cover !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
// Abouts: - dotted HRs
|
||||
// - HR fills width of content area
|
||||
// - HR adds line of blank space before/after
|
||||
hr { border: 1px dotted #600; }
|
||||
|
||||
// Small-caps links
|
||||
a[href], h1, h2, h3, h4,
|
||||
.column-header,
|
||||
.column-back-button {
|
||||
text-decoration: none !important;
|
||||
font-variant: small-caps !important;
|
||||
}
|
||||
|
||||
.mascot-container, .floats { display: none !important; }
|
||||
|
||||
.mascot-container, .floats { display: none !important; }
|
||||
|
||||
.about-short { background: transparent !important; }
|
||||
|
||||
.closed-registrations-message,
|
||||
.simple-form { min-height: inherit !important; }
|
||||
|
||||
.landing-page .heading { padding-bottom: 0 !important; }
|
||||
|
||||
.landing-page h1 { font-size: 32px !important; }
|
||||
|
||||
.landing-page p,
|
||||
.landing-page li,
|
||||
.features-list__row .text { color: #fff !important; }
|
||||
|
||||
.landing-page h1,
|
||||
.landing-page h2,
|
||||
.landing-page h3,
|
||||
.landing-page h6 { color: #906 !important; }
|
||||
|
||||
.about-body h2 { font-size: 28px !important; }
|
||||
|
||||
.name { font-size: 24px !important; }
|
||||
|
||||
// Public user TL: action icons aligned with left edge of status
|
||||
.activity-stream .pre-header .pre-header__icon {
|
||||
position: inherit !important;
|
||||
float: left;
|
||||
margin-right: 0.5em !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
// Public user TL: remove intentation from action text; move down
|
||||
.activity-stream .pre-header {
|
||||
padding-left: 0 !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
// User list: expand card size; one per row
|
||||
.account-grid-card { width: 100% !important; }
|
||||
|
||||
// TL status, user card: - black semi-trans bg with rounded border
|
||||
// - space between right edge and scrollbar
|
||||
.status,
|
||||
.detailed-status,
|
||||
.detailed-status__action-bar,
|
||||
.account-grid-card {
|
||||
background: #302 url(/system/custom-images/status-bg.png) repeat-x top center !important;
|
||||
border: 1px solid #604 !important;
|
||||
border-radius: 4px;
|
||||
margin: 0em 0.5em 1em 0em;
|
||||
}
|
||||
|
||||
.status.collapsed .status__content:after {
|
||||
background: linear-gradient(transparentize(#302, 1), #302) !important;
|
||||
}
|
||||
|
||||
// TL status prefix: move origin user text closer to icon
|
||||
.notification__message,
|
||||
.status__prepend {
|
||||
margin-left: 30px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// TL status prefix: hide boost/fav action text
|
||||
.notification__message span,
|
||||
.status__prepend span,
|
||||
.activity-stream .pre-header { font-size: 12px !important; }
|
||||
|
||||
// TL status: font size of user's friendly name
|
||||
.notification__message span a,
|
||||
.status__prepend span a,
|
||||
.activity-stream .pre-header__icon,
|
||||
.account__display-name strong,
|
||||
.status__display-name strong,
|
||||
.detailed-status__display-name strong,
|
||||
.account-grid-card .username,
|
||||
.name .username {
|
||||
font-size: 16px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
|
||||
// TL status prefix: move icon closer to left edge
|
||||
.notification__favourite-icon-wrapper,
|
||||
.status__prepend-icon-wrapper { left: -25px !important; }
|
||||
|
||||
// Spoilers
|
||||
.media-spoiler { background: #000 !important; }
|
||||
.media-gallery.full-width { margin-left: 0; margin-right: 0 }
|
||||
|
||||
// UI: remove borders and solid bg colors
|
||||
.ui,
|
||||
.drawer,
|
||||
.scrollable,
|
||||
.drawer__inner,
|
||||
.column-link,
|
||||
.sidebar ul ul,
|
||||
.column-header,
|
||||
.column-header__button,
|
||||
.column-back-button,
|
||||
.column-header__wrapper,
|
||||
.drawer__header,
|
||||
.activity-stream .entry,
|
||||
.accounts-grid,
|
||||
.account-grid-card__header,
|
||||
#mastodon-timeline,
|
||||
.header-wrapper,
|
||||
.about-mastodon,
|
||||
.container,
|
||||
.content,
|
||||
.empty-column-indicator,
|
||||
.learn-more-cta,
|
||||
.sidebar-wrapper,
|
||||
.closed-registrations-message,
|
||||
.simple_form,
|
||||
.information-board,
|
||||
.about-short {
|
||||
background: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// Column/Drawer headings: solid red bg and border; blank line after
|
||||
.column-back-button,
|
||||
.column-header,
|
||||
.drawer__header,
|
||||
.search,
|
||||
.search-results,
|
||||
.autosuggest-textarea__suggestions {
|
||||
background: #604 !important;
|
||||
margin-bottom: 1em !important;
|
||||
border: 1px solid #c08 !important
|
||||
}
|
||||
|
||||
.search-results, .autosuggest-textarea__suggestions { color: #fff !important; }
|
||||
|
||||
// Fix column back buttons.
|
||||
.column-header__back-button {
|
||||
color: #fff !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.column-back-button {
|
||||
top: calc(-1.5em - 42px) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
|
||||
// Search box: darken
|
||||
.search__input {
|
||||
background: #302 !important;
|
||||
border: 1px solid #201 !important;
|
||||
}
|
||||
|
||||
// Tootbox
|
||||
.autosuggest-textarea__textarea,
|
||||
.compose-form__modifiers,
|
||||
.compose-form__buttons,
|
||||
.spoiler-input__input {
|
||||
background: #302 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
// Reply indicator: theme like status
|
||||
.reply-indicator {
|
||||
background: #302 !important;
|
||||
border: 1px solid #604 !important;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
// TL status: move timestamp to bottom-right
|
||||
.status__relative-time {
|
||||
color: #906 !important;
|
||||
border-bottom: none !important;
|
||||
font-size: 10px !important;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
// TL status: color of user address; push down post content
|
||||
.account__header__username,
|
||||
.accounts-grid .account-grid-card .username,
|
||||
.activity-stream .status.light .display-name span,
|
||||
.detailed-status__display-name,
|
||||
.name .username,
|
||||
.name small,
|
||||
.status__display-name,
|
||||
.display-name__account {
|
||||
color: #a39 !important;
|
||||
margin-bottom: 1em;
|
||||
font-size: 12px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
// TL status: color of users' friendly names; on own line
|
||||
.account-grid-card .name a,
|
||||
.account__display-name strong,
|
||||
.detailed-status__display-name strong,
|
||||
.reply-indicator__display-name,
|
||||
.status__display-name strong,
|
||||
.account__header__display-name,
|
||||
.card__bio .name {
|
||||
color: #c06 !important;
|
||||
display: block;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.status__display-name strong { display: inherit !important; }
|
||||
|
||||
.status__prepend span { color: white }
|
||||
|
||||
// TL status prefix: color of users' friendly names
|
||||
.status__prepend .status__display-name,
|
||||
.notification__display-name,
|
||||
.status__display-name.muted,
|
||||
.status__display-name.muted strong, { color: #906 !important; font-size: 14px !important; }
|
||||
|
||||
// Opened status: add link icon on posts
|
||||
.detailed-status__datetime:before { content: "\1F517" }
|
||||
|
||||
// All status: message text
|
||||
.reply-indicator__content,
|
||||
.status__content,
|
||||
.account-grid-card .note {
|
||||
color: #dcd !important;
|
||||
font-size: 14px !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
// All status: use left space; add padding to top
|
||||
.status { padding-left: 10px !important; }
|
||||
|
||||
.status__info { padding-left: 0 !important; }
|
||||
|
||||
.status__content { padding-top: 10px !important; }
|
||||
|
||||
// All status: move icon to right side
|
||||
.status__avatar {
|
||||
left: inherit !important;
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
}
|
||||
|
||||
/// Expanded status: make header and padding match TL status
|
||||
.detailed-status { padding: 8px !important; }
|
||||
|
||||
.detailed-status__display-name { margin: 0 !important; }
|
||||
|
||||
.detailed-status__display-avatar {
|
||||
float: right !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// TL status prefix: text shouldn't clip icon
|
||||
//.display-name { max-width: calc(100% - 32px) !important; }
|
||||
|
||||
|
||||
// TL status: muted text
|
||||
.muted .status__content p { color: #a9a !important; }
|
||||
|
||||
// TL status: links in post
|
||||
.reply-indicator__content a,
|
||||
.status__content a { color: #e6c !important; }
|
||||
|
||||
// Expanded status: action bars
|
||||
.account__disclaimer, .account__action-bar { background: #160011 !important; }
|
||||
|
||||
// Default icon button color
|
||||
.icon-button { color: #604; }
|
||||
|
||||
.account__header__display-name,
|
||||
.account__header__username { font-variant: small-caps !important; }
|
||||
|
||||
.account__header__username,
|
||||
.name small {
|
||||
font-size: 12px !important;
|
||||
font-weight: bold !important;
|
||||
margin-bottom: 1em !important;
|
||||
}
|
||||
|
||||
.account__header__content { color: #fff !important; }
|
||||
|
||||
// Make status Emojos bigger
|
||||
.reply-indicator__content .emojione,
|
||||
.status__content .emojione { width: 32px !important; height: 32px !important; padding: 2px; }
|
||||
|
||||
// Locked posts animation
|
||||
@-webkit-keyframes blink-off {
|
||||
0% { opacity: .75 }
|
||||
50% { opacity: 0 }
|
||||
100% { opacity: .75 }
|
||||
}
|
||||
|
||||
@keyframes blink-off {
|
||||
0% { opacity: .75 }
|
||||
50% { opacity: 0 }
|
||||
100% { opacity: .75 }
|
||||
}
|
||||
|
||||
.icon-button.disabled {
|
||||
opacity: .75;
|
||||
|
||||
-moz-transition: all 2s ease-in-out;
|
||||
-webkit-transition: all 2s ease-in-out;
|
||||
-o-transition: all 2s ease-in-out;
|
||||
-ms-transition: all 2s ease-in-out;
|
||||
transition: all 2s ease-in-out;
|
||||
|
||||
-moz-animation: blink-off normal 4s 5 ease-in-out;
|
||||
-webkit-animation: blink-off normal 4s 5 ease-in-out;
|
||||
-ms-animation: blink-off normal 4s 5 ease-in-out;
|
||||
animation: blink-off normal 4s 5 ease-in-out;
|
||||
}
|
||||
|
||||
// Active item banimation
|
||||
@-webkit-keyframes blink-on {
|
||||
from { transform: scale(1.5) }
|
||||
50% { transform: scale(2) }
|
||||
to { transform: scale(1.5) }
|
||||
}
|
||||
|
||||
@keyframes blink-on {
|
||||
from { transform: scale(1.5) }
|
||||
50% { transform: scale(2) }
|
||||
to { transform: scale(1.5) }
|
||||
}
|
||||
|
||||
.column-header.active>.column-header__icon,
|
||||
.icon-button.active {
|
||||
color: #c08 !important;
|
||||
transform: scale(1.5);
|
||||
|
||||
-moz-transition: all 1s ease-in-out;
|
||||
-webkit-transition: all 1s ease-in-out;
|
||||
-o-transition: all 1s ease-in-out;
|
||||
-ms-transition: all 1s ease-in-out;
|
||||
transition: all 1s ease-in-out;
|
||||
|
||||
-moz-animation: blink-on normal 2s 5 ease-in-out;
|
||||
-webkit-animation: blink-on normal 2s 5 ease-in-out;
|
||||
-ms-animation: blink-on normal 2s 5 ease-in-out;
|
||||
animation: blink-on normal 2s 5 ease-in-out;
|
||||
}
|
||||
|
||||
// Scrollbar in Chrome/Webkit browsers
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #906 !important;
|
||||
border: 1px solid #c09 !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
$classic-base-color: #160011;
|
||||
$classic-primary-color: #c69;
|
||||
$classic-secondary-color: #906;
|
||||
$classic-highlight-color: #c08;
|
||||
|
||||
$base-shadow-color: $black;
|
||||
$base-overlay-background: $black;
|
||||
$base-border-color: $white;
|
||||
|
||||
$simple-background-color: $black;
|
||||
$valid-value-color: $success-green;
|
||||
$error-value-color: $error-red;
|
||||
|
||||
$ui-base-color: $classic-base-color; // Darkest
|
||||
$ui-base-lighter-color: lighten($ui-base-color, 26%); // Lighter darkest
|
||||
$ui-primary-color: $classic-primary-color; // Lighter
|
||||
$ui-secondary-color: $classic-secondary-color; // Lightest
|
||||
$ui-highlight-color: $classic-highlight-color;
|
||||
|
||||
$primary-text-color: $white;
|
||||
$darker-text-color: $ui-primary-color;
|
||||
$dark-text-color: $ui-base-lighter-color;
|
||||
$secondary-text-color: $ui-secondary-color;
|
||||
$highlight-text-color: $ui-highlight-color;
|
||||
$action-button-color: $ui-base-lighter-color;
|
||||
|
||||
$inverted-text-color: $ui-base-color;
|
||||
$lighter-text-color: $ui-base-lighter-color;
|
||||
$light-text-color: $ui-primary-color;
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
// Original:
|
||||
// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss
|
||||
*/
|
||||
|
||||
.bbcode {
|
||||
&__flip-horizontal {
|
||||
display: inline-block;
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
&__flip-vertical {
|
||||
display: inline-block;
|
||||
-webkit-transform: scale(1, -1);
|
||||
-ms-transform: scale(1, -1);
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
||||
@for $i from 1 through 6 {
|
||||
&__size-#{$i} {
|
||||
font-size: #{6 * $i}px;
|
||||
|
||||
& .emojione {
|
||||
width: #{6 * $i}px !important;
|
||||
height: #{6 * $i}px !important;
|
||||
}
|
||||
|
||||
& .hoverplay:hover {
|
||||
padding-left: #{6 * $i}px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 2 {
|
||||
&__size-#{$i}:hover {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__left { display: block; text-align: left; }
|
||||
&__center { display: block; text-align: center; }
|
||||
&__right { display: block; text-align: right; }
|
||||
&__lfloat { float: left; }
|
||||
&__rfloat { float: right; }
|
||||
&__spoiler-wrapper {
|
||||
background: black;
|
||||
color: black;
|
||||
padding: 1px 2em 1px 2em;
|
||||
}
|
||||
&__spoiler { color: black; visibility: hidden; }
|
||||
&__spoiler-wrapper:hover > &__spoiler,
|
||||
&__spoiler-wrapper:active > &__spoiler
|
||||
{ color: white; visibility: visible; }
|
||||
}
|
|
@ -179,12 +179,10 @@
|
|||
}
|
||||
|
||||
.notification__message {
|
||||
margin-left: 42px;
|
||||
padding: 8px 0 0 26px;
|
||||
margin-left: 25px;
|
||||
cursor: default;
|
||||
color: $darker-text-color;
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
|
||||
.fa {
|
||||
color: $highlight-text-color;
|
||||
|
@ -450,20 +448,140 @@
|
|||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
|
||||
p {
|
||||
.emojione {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -3px 0 0;
|
||||
}
|
||||
|
||||
.hoverplay:hover { padding-left: 20px }
|
||||
|
||||
p, pre, blockquote {
|
||||
margin-bottom: 20px;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid $darker-text-color;
|
||||
color: $darker-text-color;
|
||||
white-space: normal;
|
||||
font-style: italic;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
sub {
|
||||
font-size: smaller;
|
||||
text-align: sub;
|
||||
}
|
||||
|
||||
sup {
|
||||
vertical-align: super;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-left: 1em;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
s, del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: lighten($dark-text-color, 10%);
|
||||
}
|
||||
|
||||
pre, code {
|
||||
color: lighten($dark-text-color, 33%);
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #ccff15;
|
||||
color: black;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
background-color: lighten($ui-base-color, 7%);
|
||||
color: darken($secondary-text-color, 10%);
|
||||
text-decoration: none;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
color: lighten($dark-text-color, 10%);
|
||||
}
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -556,6 +674,8 @@
|
|||
height: 22px;
|
||||
}
|
||||
|
||||
.hoverplay:hover { padding-left: 22px }
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
@ -573,6 +693,10 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.roles {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue