mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 00:08:46 +00:00
Compare commits
444 Commits
fix-column
...
hotkeys-gl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a48efe16c | ||
|
|
f3c3df62ab | ||
|
|
63d47e04c4 | ||
|
|
e77c3996a5 | ||
|
|
5de42665d7 | ||
|
|
86f4f8e158 | ||
|
|
167fe2ab08 | ||
|
|
faad820458 | ||
|
|
41c71565c2 | ||
|
|
2585649b20 | ||
|
|
12c0011fee | ||
|
|
2a5af9c10c | ||
|
|
3c83b7e06e | ||
|
|
b7a7eb84dd | ||
|
|
1a07b83c69 | ||
|
|
8f3e5f6128 | ||
|
|
a36a2c1796 | ||
|
|
e45cb0837b | ||
|
|
d083f7741a | ||
|
|
bcda3f85ce | ||
|
|
92cc79be72 | ||
|
|
08a01dd037 | ||
|
|
672ace5a20 | ||
|
|
f51f7b0e06 | ||
|
|
dec960c828 | ||
|
|
eed50514be | ||
|
|
a618df7998 | ||
|
|
fc2c8b50dd | ||
|
|
e19fc6a9f8 | ||
|
|
45c44989c8 | ||
|
|
5a9982b425 | ||
|
|
284e2cde81 | ||
|
|
130aa90d55 | ||
|
|
1ab12ba38e | ||
|
|
6f8ccbfcdf | ||
|
|
dc16d73bf5 | ||
|
|
b006bb82af | ||
|
|
94c5a11cda | ||
|
|
45f18b8f49 | ||
|
|
f6355f6ffb | ||
|
|
2a386ad88d | ||
|
|
fbf76feb37 | ||
|
|
392945f9a3 | ||
|
|
4dd19054d6 | ||
|
|
585758a373 | ||
|
|
b28b405b97 | ||
|
|
ee560abdbe | ||
|
|
35fbdc36f9 | ||
|
|
88627fd7aa | ||
|
|
c2a92dffc9 | ||
|
|
7e17e764a5 | ||
|
|
08652baab0 | ||
|
|
8fc54890e5 | ||
|
|
cb4ef24ac9 | ||
|
|
431503bae2 | ||
|
|
04508868b0 | ||
|
|
ca5440b93d | ||
|
|
dae8916544 | ||
|
|
d11b1a1aa7 | ||
|
|
a4dcabc11b | ||
|
|
6cd192b9fb | ||
|
|
bfd9230d61 | ||
|
|
656d54e945 | ||
|
|
9a42f7cbed | ||
|
|
48c705bbad | ||
|
|
5128c4261e | ||
|
|
b95c48748c | ||
|
|
4944515020 | ||
|
|
e1b7785788 | ||
|
|
bc8532359b | ||
|
|
a617060dfc | ||
|
|
e0298d66f8 | ||
|
|
73bf0ea7d1 | ||
|
|
276432790a | ||
|
|
254b74c71f | ||
|
|
870d71b78b | ||
|
|
656f5b6f87 | ||
|
|
dd28b94cf0 | ||
|
|
a2612d0d38 | ||
|
|
31814ddda0 | ||
|
|
42f2045c21 | ||
|
|
5f0268ab84 | ||
|
|
e40fe4092d | ||
|
|
d9485e6497 | ||
|
|
d5c8ebe205 | ||
|
|
d03b48cea0 | ||
|
|
9226257a1b | ||
|
|
641f90e73a | ||
|
|
f5a3283976 | ||
|
|
516eeeb43d | ||
|
|
664c9aa708 | ||
|
|
119d477c8b | ||
|
|
8410d33b49 | ||
|
|
4f01e6e8d5 | ||
|
|
a76b024228 | ||
|
|
3db80f75a6 | ||
|
|
af8f06413e | ||
|
|
1a60445a5f | ||
|
|
4c84513e04 | ||
|
|
4b68e82a19 | ||
|
|
19826774f0 | ||
|
|
ad86c86fa8 | ||
|
|
670e6a33f8 | ||
|
|
cd04e3df58 | ||
|
|
4a64181461 | ||
|
|
2e03a10059 | ||
|
|
4fa2f7e82d | ||
|
|
b4b657eb1d | ||
|
|
693c66dfde | ||
|
|
a4851100fd | ||
|
|
9f609bc94e | ||
|
|
603cf02b70 | ||
|
|
4745d6eeca | ||
|
|
9093e2de7a | ||
|
|
d589dd7cd0 | ||
|
|
a7be86e875 | ||
|
|
b15dd05514 | ||
|
|
21bafc6555 | ||
|
|
f5e2469485 | ||
|
|
9423553e5c | ||
|
|
90770f6d59 | ||
|
|
c756651278 | ||
|
|
eb907a5bab | ||
|
|
39c9cdf7fe | ||
|
|
86cf4468af | ||
|
|
42e8c8eb0e | ||
|
|
4421f6598f | ||
|
|
64a5e9a1de | ||
|
|
7364b26e4b | ||
|
|
313ba202ef | ||
|
|
7c44ad6355 | ||
|
|
37ff061d9b | ||
|
|
3d7de06db4 | ||
|
|
26f08f0791 | ||
|
|
8b9ee5f16b | ||
|
|
4b397adb5b | ||
|
|
65154869df | ||
|
|
7e9d93472c | ||
|
|
dbb1fce94d | ||
|
|
6cd5b3bbe5 | ||
|
|
f72ad67a39 | ||
|
|
56094cb4bd | ||
|
|
22df18e902 | ||
|
|
cdc22d23b9 | ||
|
|
531dadad86 | ||
|
|
723890b6fa | ||
|
|
daa04c39b6 | ||
|
|
2af964ef20 | ||
|
|
33e806217f | ||
|
|
df626fdd43 | ||
|
|
6a2b1ba91e | ||
|
|
fe1c20cfb9 | ||
|
|
edd1a00faf | ||
|
|
13e05fece2 | ||
|
|
eeaa8fba43 | ||
|
|
b0b5ff1b0f | ||
|
|
bd51a16150 | ||
|
|
0bf9261e65 | ||
|
|
3cc6255a7e | ||
|
|
35e2cad4eb | ||
|
|
8d6b9ba494 | ||
|
|
f0a2a6c875 | ||
|
|
92a3181dc6 | ||
|
|
7cc0da756d | ||
|
|
85d5da86df | ||
|
|
e10cff8226 | ||
|
|
979b0d66a7 | ||
|
|
6ca03a7f58 | ||
|
|
96ba3482b9 | ||
|
|
6610d57f91 | ||
|
|
1a8011648f | ||
|
|
37e141bccb | ||
|
|
dcd8ff5308 | ||
|
|
f82e90bf11 | ||
|
|
b1217242fc | ||
|
|
5dec67d964 | ||
|
|
ce3e0faf4d | ||
|
|
c027a7bd4d | ||
|
|
53f829dfa8 | ||
|
|
cf1f83ca2a | ||
|
|
1522795853 | ||
|
|
e9f9e3cc89 | ||
|
|
cf24ce7e03 | ||
|
|
74e4fc8f8a | ||
|
|
9a5418942c | ||
|
|
daad07b1d5 | ||
|
|
83bda6c1a8 | ||
|
|
fcf0d2078e | ||
|
|
9330ea1f4d | ||
|
|
06e299cef5 | ||
|
|
8a24ad5828 | ||
|
|
f7ca205f38 | ||
|
|
86e617a839 | ||
|
|
210e6776fc | ||
|
|
1924a71b5a | ||
|
|
67f8277526 | ||
|
|
169d83f532 | ||
|
|
31dd261375 | ||
|
|
c2b479efec | ||
|
|
a94dc21c79 | ||
|
|
9512db920c | ||
|
|
c89cce0219 | ||
|
|
fa3587645d | ||
|
|
9ed51cecd0 | ||
|
|
514edd3c23 | ||
|
|
a3760b7729 | ||
|
|
cbf00168f1 | ||
|
|
4f9a493d9d | ||
|
|
8c0733a14e | ||
|
|
36a35be2ad | ||
|
|
3783062450 | ||
|
|
15c9c2fd7e | ||
|
|
227dbb6adb | ||
|
|
769f62d96f | ||
|
|
003bfd094e | ||
|
|
fae8dce738 | ||
|
|
b0487488a7 | ||
|
|
f5d6bdd9c0 | ||
|
|
ad8e856a5b | ||
|
|
7ebd6ed03c | ||
|
|
59936b7a98 | ||
|
|
fd9a171129 | ||
|
|
6ba35630bc | ||
|
|
79d8911116 | ||
|
|
d880b3182b | ||
|
|
f9d7b8a94f | ||
|
|
211f0a9513 | ||
|
|
4a527154b7 | ||
|
|
df71eadaae | ||
|
|
323d437a09 | ||
|
|
3278c08c29 | ||
|
|
0284fd723b | ||
|
|
0e0703dbd8 | ||
|
|
7dbcc7ed3d | ||
|
|
83b3a0389c | ||
|
|
af2d793398 | ||
|
|
70592cdaba | ||
|
|
30b5254a5d | ||
|
|
0a207be99d | ||
|
|
500c465226 | ||
|
|
2ea9b164d3 | ||
|
|
b1576c52df | ||
|
|
4612f7caea | ||
|
|
0c547faf92 | ||
|
|
eaaf2170fe | ||
|
|
6f7d00bfdd | ||
|
|
5c2e1869f0 | ||
|
|
0f2af2a974 | ||
|
|
27f8d7069b | ||
|
|
44207b6af6 | ||
|
|
27e55da853 | ||
|
|
3cac5bc2c3 | ||
|
|
29c44fa5fa | ||
|
|
7a9c7d4e0b | ||
|
|
932571fa22 | ||
|
|
c9df53044a | ||
|
|
67ad453373 | ||
|
|
3dff74eecf | ||
|
|
14e1fb8d36 | ||
|
|
514fc908a3 | ||
|
|
b9f7bc149b | ||
|
|
e18ed4bbc7 | ||
|
|
667df47168 | ||
|
|
173a970752 | ||
|
|
cb42dd8497 | ||
|
|
4dc0ddc601 | ||
|
|
7a1ca8b0df | ||
|
|
b8791ae79b | ||
|
|
b9a2ceca35 | ||
|
|
70c5eccc12 | ||
|
|
eb7fc34708 | ||
|
|
b6bf04ece2 | ||
|
|
91836d577e | ||
|
|
7de0fa698d | ||
|
|
811d895f7b | ||
|
|
7b42d14f45 | ||
|
|
f34f33c19e | ||
|
|
8b58153583 | ||
|
|
8150689b48 | ||
|
|
b61e3daf98 | ||
|
|
6ff084dbbb | ||
|
|
9aaf3218d2 | ||
|
|
cb69e35b3b | ||
|
|
e82021e0e6 | ||
|
|
8925731c98 | ||
|
|
4c233b4f3a | ||
|
|
b7cf758fbe | ||
|
|
7e5691804d | ||
|
|
852acbd738 | ||
|
|
6913426e48 | ||
|
|
3ba7c1e725 | ||
|
|
9b74a12045 | ||
|
|
74a0cc6a11 | ||
|
|
984d2d4cb6 | ||
|
|
0244019ca1 | ||
|
|
604654ccb4 | ||
|
|
0efd7e7406 | ||
|
|
e7edb4d1ee | ||
|
|
d235224692 | ||
|
|
0a678cf377 | ||
|
|
7a77f7b3bb | ||
|
|
df74e26baf | ||
|
|
d69fa9e1f4 | ||
|
|
c727eae441 | ||
|
|
d0aad1ac85 | ||
|
|
21b04af524 | ||
|
|
3ea02314b9 | ||
|
|
4715161a93 | ||
|
|
144db8ea1d | ||
|
|
bc4202d00b | ||
|
|
09cfc079b0 | ||
|
|
08d021916d | ||
|
|
99f24ab0c7 | ||
|
|
3a526e2369 | ||
|
|
51e3ac2534 | ||
|
|
75aafc932e | ||
|
|
6ce806f913 | ||
|
|
35fda84ba8 | ||
|
|
5770d461b2 | ||
|
|
2e0645c26c | ||
|
|
66b1174d25 | ||
|
|
183f993b01 | ||
|
|
e53fbb4a09 | ||
|
|
79d898ae0a | ||
|
|
bcf7ee48e9 | ||
|
|
297921fce5 | ||
|
|
74eff5456c | ||
|
|
60d27b4302 | ||
|
|
08d19778d5 | ||
|
|
9f7a5aac1e | ||
|
|
945c5812d3 | ||
|
|
667b567606 | ||
|
|
8e2b1f79e4 | ||
|
|
345290a905 | ||
|
|
2fb78fefc6 | ||
|
|
dc2b8bdecd | ||
|
|
e3c2183c12 | ||
|
|
86f8df7903 | ||
|
|
d41cec90cf | ||
|
|
7859e6ad45 | ||
|
|
3464bb30f8 | ||
|
|
d87d70e89a | ||
|
|
0c7ee5c792 | ||
|
|
bba75c15f1 | ||
|
|
4cbbea5881 | ||
|
|
167c392efd | ||
|
|
193f354d3e | ||
|
|
6b67b91eb1 | ||
|
|
6b77424660 | ||
|
|
301c185878 | ||
|
|
cb7f54891f | ||
|
|
f6ce1a9592 | ||
|
|
aee64b996c | ||
|
|
0c71c0ccc8 | ||
|
|
49e82c1e0f | ||
|
|
556cede00f | ||
|
|
b73ee36949 | ||
|
|
dd49c10cdb | ||
|
|
85d5249479 | ||
|
|
ff9f2088f7 | ||
|
|
15227c713d | ||
|
|
30736f4886 | ||
|
|
c58877862d | ||
|
|
0e310f1ee3 | ||
|
|
7dd4d9de96 | ||
|
|
46f83bb28b | ||
|
|
ec2daae71c | ||
|
|
b525caf40a | ||
|
|
fc65b691df | ||
|
|
651c3d643c | ||
|
|
cc4cba8afd | ||
|
|
99889ea57d | ||
|
|
19690d3e33 | ||
|
|
0b371da971 | ||
|
|
2d8ebdcc72 | ||
|
|
595c6de32c | ||
|
|
6cbbdc805f | ||
|
|
7b1d233f4f | ||
|
|
03f9648377 | ||
|
|
6107e95404 | ||
|
|
36805a39db | ||
|
|
ab4632a41e | ||
|
|
ddafde942c | ||
|
|
e6300de142 | ||
|
|
a6f5111c79 | ||
|
|
59503a88ae | ||
|
|
5df7bc3a8b | ||
|
|
c806fef865 | ||
|
|
49ba78d6f8 | ||
|
|
7b53d4bbca | ||
|
|
4f36aad6e8 | ||
|
|
56ca33a6d3 | ||
|
|
aeff898137 | ||
|
|
b323e00bf3 | ||
|
|
a520b118e4 | ||
|
|
93fc8aa14c | ||
|
|
c0a665865e | ||
|
|
38a1299975 | ||
|
|
96e1f75679 | ||
|
|
3a99552f0c | ||
|
|
22cc5c0dec | ||
|
|
efa425206c | ||
|
|
e60f27d649 | ||
|
|
6a50e73089 | ||
|
|
b1f9892e63 | ||
|
|
d6e3918d92 | ||
|
|
6909bbdc9e | ||
|
|
ddc6b85912 | ||
|
|
4bc237fcfe | ||
|
|
efacfec3ed | ||
|
|
8ea779e59a | ||
|
|
7eda83a36a | ||
|
|
af178d0ba6 | ||
|
|
e4326b3f12 | ||
|
|
b8a5052d53 | ||
|
|
7427680e75 | ||
|
|
ca0d30c04b | ||
|
|
da05cde721 | ||
|
|
4c37f629bc | ||
|
|
ddba5d3b8c | ||
|
|
ceb545c080 | ||
|
|
a70468aa56 | ||
|
|
8b23bf7cbd | ||
|
|
f1a60d4b81 | ||
|
|
2513d92c54 | ||
|
|
414dfb3955 | ||
|
|
67adbcc60c | ||
|
|
453b9c6e7e | ||
|
|
d9b9bb8c5e | ||
|
|
40ecbfd4a9 | ||
|
|
4fe45dda9a | ||
|
|
4bd7482a7a | ||
|
|
93c52301ad | ||
|
|
0d3ec19e89 | ||
|
|
62a75891ab | ||
|
|
b27842dc70 | ||
|
|
39b6b37b74 | ||
|
|
65528fc54e | ||
|
|
382572c213 | ||
|
|
9bc593d675 | ||
|
|
09f7ad3614 | ||
|
|
7c2ea42cd5 | ||
|
|
ea785d0baf | ||
|
|
a337c5dbe5 |
@@ -11,11 +11,10 @@ DB_PASS=
|
||||
DB_PORT=5432
|
||||
|
||||
# Federation
|
||||
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
|
||||
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
|
||||
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
|
||||
LOCAL_DOMAIN=example.com
|
||||
|
||||
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
|
||||
LOCAL_HTTPS=true
|
||||
|
||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
|
||||
@@ -135,3 +134,6 @@ STREAMING_CLUSTER_NUM=1
|
||||
# If you use Docker, you may want to assign UID/GID manually.
|
||||
# UID=1000
|
||||
# GID=1000
|
||||
|
||||
# Maximum allowed character count
|
||||
# MAX_TOOT_CHARS=500
|
||||
|
||||
@@ -29,6 +29,11 @@ settings:
|
||||
import/ignore:
|
||||
- node_modules
|
||||
- \\.(css|scss|json)$
|
||||
import/resolver:
|
||||
node:
|
||||
moduleDirectory:
|
||||
- node_modules
|
||||
- app/javascript
|
||||
|
||||
rules:
|
||||
brace-style: warn
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "app/javascript/themes/mastodon-go"]
|
||||
path = app/javascript/themes/mastodon-go
|
||||
url = https://github.com/marrus-sh/mastodon-go
|
||||
@@ -27,14 +27,11 @@ addons:
|
||||
apt:
|
||||
sources:
|
||||
- trusty-media
|
||||
- sourceline: deb https://dl.yarnpkg.com/debian/ stable main
|
||||
key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
|
||||
packages:
|
||||
- ffmpeg
|
||||
- libicu-dev
|
||||
- libprotobuf-dev
|
||||
- protobuf-compiler
|
||||
- yarn
|
||||
- libicu-dev
|
||||
|
||||
rvm:
|
||||
- 2.3.4
|
||||
@@ -45,6 +42,7 @@ services:
|
||||
|
||||
install:
|
||||
- nvm install
|
||||
- npm install -g yarn
|
||||
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
|
||||
- yarn install
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
# Contributing to Mastodon Glitch Edition #
|
||||
|
||||
Thank you for your interest in contributing to the `glitch-soc` project!
|
||||
Here are some guidelines, and ways you can help.
|
||||
|
||||
> (This document is a bit of a work-in-progress, so please bear with us.
|
||||
> If you don't see what you're looking for here, please don't hesitate to reach out!)
|
||||
|
||||
## Planning ##
|
||||
|
||||
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.
|
||||
We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler.
|
||||
|
||||
## Documentation ##
|
||||
|
||||
The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
|
||||
Right now, we've mostly focused on the features that make this fork different from upstream in some manner.
|
||||
Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code.
|
||||
|
||||
## Frontend Development ##
|
||||
|
||||
Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information.
|
||||
|
||||
## Backend Development ##
|
||||
|
||||
See the guidelines below.
|
||||
|
||||
- - -
|
||||
|
||||
You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `tootsuite/mastodon`, reproduced below.
|
||||
|
||||
<blockquote>
|
||||
|
||||
CONTRIBUTING
|
||||
============
|
||||
|
||||
@@ -49,3 +82,5 @@ It is expected that you have a working development environment set up (see back-
|
||||
* If you are introducing new strings, they must be using localization methods
|
||||
|
||||
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
|
||||
|
||||
</blockquote>
|
||||
|
||||
@@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
|
||||
RAILS_SERVE_STATIC_FILES=true \
|
||||
RAILS_ENV=production NODE_ENV=production
|
||||
|
||||
ARG YARN_VERSION=1.3.2
|
||||
ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
|
||||
ARG YARN_VERSION=1.1.0
|
||||
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
|
||||
ARG LIBICONV_VERSION=1.15
|
||||
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
|
||||
|
||||
|
||||
6
Gemfile
6
Gemfile
@@ -14,7 +14,7 @@ gem 'pg', '~> 0.20'
|
||||
gem 'pghero', '~> 1.7'
|
||||
gem 'dotenv-rails', '~> 2.2'
|
||||
|
||||
gem 'aws-sdk', '~> 2.10', require: false
|
||||
gem 'fog-aws', '~> 1.4', require: false
|
||||
gem 'fog-core', '~> 1.45'
|
||||
gem 'fog-local', '~> 0.4', require: false
|
||||
gem 'fog-openstack', '~> 0.1', require: false
|
||||
@@ -28,7 +28,7 @@ gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.5'
|
||||
gem 'iso-639'
|
||||
gem 'cld3', '~> 3.2.0'
|
||||
gem 'devise', '~> 4.3'
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'devise-two-factor', '~> 3.0'
|
||||
gem 'doorkeeper', '~> 4.2'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
@@ -49,6 +49,7 @@ gem 'oj', '~> 3.3'
|
||||
gem 'ostatus2', '~> 2.0'
|
||||
gem 'ox', '~> 2.8'
|
||||
gem 'pundit', '~> 1.1'
|
||||
gem 'rabl', '~> 0.13'
|
||||
gem 'rack-attack', '~> 5.0'
|
||||
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
|
||||
gem 'rack-timeout', '~> 0.4'
|
||||
@@ -58,7 +59,6 @@ gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'rqrcode', '~> 0.10'
|
||||
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
|
||||
gem 'ruby-progressbar', '~> 1.4'
|
||||
gem 'sanitize', '~> 4.4'
|
||||
gem 'sidekiq', '~> 5.0'
|
||||
gem 'sidekiq-scheduler', '~> 2.1'
|
||||
|
||||
116
Gemfile.lock
116
Gemfile.lock
@@ -24,11 +24,11 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_model_serializers (0.10.7)
|
||||
active_model_serializers (0.10.6)
|
||||
actionpack (>= 4.1, < 6)
|
||||
activemodel (>= 4.1, < 6)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
|
||||
active_record_query_trace (1.5.4)
|
||||
activejob (5.1.4)
|
||||
activesupport (= 5.1.4)
|
||||
@@ -57,14 +57,6 @@ GEM
|
||||
encryptor (~> 3.0.0)
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-sdk (2.10.100)
|
||||
aws-sdk-resources (= 2.10.100)
|
||||
aws-sdk-core (2.10.100)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.10.100)
|
||||
aws-sdk-core (= 2.10.100)
|
||||
aws-sigv4 (1.0.2)
|
||||
bcrypt (3.1.11)
|
||||
better_errors (2.4.0)
|
||||
coderay (>= 1.0.0)
|
||||
@@ -91,15 +83,15 @@ GEM
|
||||
capistrano-bundler (1.3.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-rails (1.3.1)
|
||||
capistrano-rails (1.3.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rbenv (2.1.3)
|
||||
capistrano-rbenv (2.1.2)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (2.16.1)
|
||||
capybara (2.15.4)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (>= 1.3.3)
|
||||
@@ -121,7 +113,7 @@ GEM
|
||||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
crass (1.0.2)
|
||||
debug_inspector (0.0.3)
|
||||
devise (4.3.0)
|
||||
bcrypt (~> 3.0)
|
||||
@@ -129,11 +121,11 @@ GEM
|
||||
railties (>= 4.1.0, < 5.2)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (3.0.2)
|
||||
activesupport (< 5.2)
|
||||
devise-two-factor (3.0.0)
|
||||
activesupport
|
||||
attr_encrypted (>= 1.3, < 4, != 2)
|
||||
devise (~> 4.0)
|
||||
railties (< 5.2)
|
||||
railties
|
||||
rotp (~> 2.0)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
@@ -160,6 +152,11 @@ GEM
|
||||
i18n (~> 0.5)
|
||||
fast_blank (1.0.0)
|
||||
ffi (1.9.18)
|
||||
fog-aws (1.4.1)
|
||||
fog-core (~> 1.38)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
fog-core (1.45.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
@@ -173,6 +170,9 @@ GEM
|
||||
fog-core (>= 1.40)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
fog-xml (0.1.3)
|
||||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
formatador (0.2.5)
|
||||
fuubar (2.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
@@ -184,7 +184,7 @@ GEM
|
||||
http (~> 2.2)
|
||||
nokogiri (~> 1.8)
|
||||
oj (~> 3.0)
|
||||
hamlit (2.8.5)
|
||||
hamlit (2.8.4)
|
||||
temple (>= 0.8.0)
|
||||
thor
|
||||
tilt
|
||||
@@ -196,7 +196,7 @@ GEM
|
||||
hamster (3.0.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
hashdiff (0.3.7)
|
||||
highline (1.7.10)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
@@ -213,9 +213,9 @@ GEM
|
||||
httplog (0.99.7)
|
||||
colorize
|
||||
rack
|
||||
i18n (0.9.1)
|
||||
i18n (0.9.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.19)
|
||||
i18n-tasks (0.9.18)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
easy_translate (>= 0.5.0)
|
||||
@@ -228,7 +228,6 @@ GEM
|
||||
idn-ruby (0.1.0)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.2.8)
|
||||
jmespath (1.3.1)
|
||||
json (2.1.0)
|
||||
json-ld (2.1.7)
|
||||
multi_json (~> 1.12)
|
||||
@@ -237,8 +236,8 @@ GEM
|
||||
json-ld (~> 2.1, >= 2.1.5)
|
||||
multi_json (~> 1.11)
|
||||
rdf (~> 2.2)
|
||||
jsonapi-renderer (0.2.0)
|
||||
jwt (2.1.0)
|
||||
jsonapi-renderer (0.1.3)
|
||||
jwt (1.5.6)
|
||||
kaminari (1.1.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
@@ -268,8 +267,8 @@ GEM
|
||||
loofah (2.1.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
mario-redis-lock (1.2.0)
|
||||
redis (~> 3, >= 3.0.5)
|
||||
method_source (0.9.0)
|
||||
@@ -280,7 +279,7 @@ GEM
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mimemagic (0.3.2)
|
||||
mini_mime (1.0.0)
|
||||
mini_mime (0.1.4)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.10.3)
|
||||
msgpack (1.1.0)
|
||||
@@ -299,12 +298,14 @@ GEM
|
||||
sidekiq (>= 3.5.0)
|
||||
statsd-ruby (~> 1.2.0)
|
||||
oj (3.3.9)
|
||||
openssl (2.0.6)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (2.0.2)
|
||||
ostatus2 (2.0.1)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
ox (2.8.2)
|
||||
openssl (~> 2.0)
|
||||
ox (2.8.1)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
@@ -315,24 +316,26 @@ GEM
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parallel (1.12.0)
|
||||
parallel_tests (2.19.0)
|
||||
parallel_tests (2.17.0)
|
||||
parallel
|
||||
parser (2.4.0.2)
|
||||
ast (~> 2.3)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
pg (0.21.0)
|
||||
pghero (1.7.0)
|
||||
activerecord
|
||||
pkg-config (1.2.8)
|
||||
powerpack (0.1.1)
|
||||
pry (0.11.3)
|
||||
pry (0.11.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-rails (0.3.6)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (3.0.1)
|
||||
puma (3.11.0)
|
||||
public_suffix (3.0.0)
|
||||
puma (3.10.0)
|
||||
pundit (1.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
rabl (0.13.1)
|
||||
activesupport (>= 2.3.14)
|
||||
rack (2.0.3)
|
||||
rack-attack (5.0.1)
|
||||
rack
|
||||
@@ -341,7 +344,7 @@ GEM
|
||||
rack
|
||||
rack-proxy (0.6.2)
|
||||
rack
|
||||
rack-test (0.8.2)
|
||||
rack-test (0.7.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.4.2)
|
||||
rails (5.1.4)
|
||||
@@ -378,11 +381,8 @@ GEM
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
rake (12.3.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rdf (2.2.12)
|
||||
rake (12.2.1)
|
||||
rdf (2.2.11)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.3.2)
|
||||
@@ -395,8 +395,8 @@ GEM
|
||||
redis-activesupport (5.0.4)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.6.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-namespace (1.5.3)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
redis-rack (2.0.3)
|
||||
rack (>= 1.5, < 3)
|
||||
redis-store (>= 1.2, < 2)
|
||||
@@ -421,7 +421,7 @@ GEM
|
||||
rspec-mocks (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-rails (3.7.2)
|
||||
rspec-rails (3.7.1)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
@@ -449,14 +449,10 @@ GEM
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
sass (3.5.3)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
scss_lint (0.56.0)
|
||||
sass (3.4.25)
|
||||
scss_lint (0.55.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.5.3)
|
||||
sass (~> 3.4.20)
|
||||
sidekiq (5.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
@@ -490,7 +486,7 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.15.1)
|
||||
sshkit (1.14.0)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
statsd-ruby (1.2.1)
|
||||
@@ -518,7 +514,7 @@ GEM
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webmock (3.1.1)
|
||||
webmock (3.1.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
@@ -526,12 +522,12 @@ GEM
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
webpush (0.3.3)
|
||||
webpush (0.3.2)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
jwt
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.3)
|
||||
websocket-extensions (0.1.2)
|
||||
xpath (2.1.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
@@ -543,7 +539,6 @@ DEPENDENCIES
|
||||
active_record_query_trace (~> 1.5)
|
||||
addressable (~> 2.5)
|
||||
annotate (~> 2.7)
|
||||
aws-sdk (~> 2.10)
|
||||
better_errors (~> 2.4)
|
||||
binding_of_caller (~> 0.7)
|
||||
bootsnap
|
||||
@@ -559,13 +554,14 @@ DEPENDENCIES
|
||||
charlock_holmes (~> 0.7.5)
|
||||
cld3 (~> 3.2.0)
|
||||
climate_control (~> 0.2)
|
||||
devise (~> 4.3)
|
||||
devise (~> 4.2)
|
||||
devise-two-factor (~> 3.0)
|
||||
doorkeeper (~> 4.2)
|
||||
dotenv-rails (~> 2.2)
|
||||
fabrication (~> 2.18)
|
||||
faker (~> 1.7)
|
||||
fast_blank (~> 1.0)
|
||||
fog-aws (~> 1.4)
|
||||
fog-core (~> 1.45)
|
||||
fog-local (~> 0.4)
|
||||
fog-openstack (~> 0.1)
|
||||
@@ -603,6 +599,7 @@ DEPENDENCIES
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 3.10)
|
||||
pundit (~> 1.1)
|
||||
rabl (~> 0.13)
|
||||
rack-attack (~> 5.0)
|
||||
rack-cors (~> 0.4)
|
||||
rack-timeout (~> 0.4)
|
||||
@@ -619,7 +616,6 @@ DEPENDENCIES
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop
|
||||
ruby-oembed (~> 0.12)
|
||||
ruby-progressbar (~> 1.4)
|
||||
sanitize (~> 4.4)
|
||||
scss_lint (~> 0.55)
|
||||
sidekiq (~> 5.0)
|
||||
@@ -642,4 +638,4 @@ RUBY VERSION
|
||||
ruby 2.4.2p198
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
1.15.4
|
||||
|
||||
87
README.md
87
README.md
@@ -1,85 +1,10 @@
|
||||

|
||||
========
|
||||
# Mastodon Glitch Edition #
|
||||
|
||||
[][travis]
|
||||
[][code_climate]
|
||||
> Now with automated deploys!
|
||||
|
||||
[travis]: https://travis-ci.org/tootsuite/mastodon
|
||||
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
|
||||
[](https://travis-ci.org/glitch-soc/mastodon)
|
||||
|
||||
Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools.
|
||||
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?
|
||||
|
||||
Click on the screenshot below to watch a demo of the UI:
|
||||
|
||||
[][youtube_demo]
|
||||
|
||||
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
||||
|
||||
**Ruby on Rails** is used for the back-end, while **React.js** and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||
|
||||
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
||||
|
||||
[patreon]: https://www.patreon.com/user?u=619786
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||
- [Use this tool to find Twitter friends on Mastodon](https://bridge.joinmastodon.org)
|
||||
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||
- [List of sponsors](https://joinmastodon.org/sponsors)
|
||||
|
||||
## Features
|
||||
|
||||
**No vendor lock-in: Fully interoperable with any conforming platform**
|
||||
|
||||
It doesn't have to be Mastodon, whatever implements ActivityPub or OStatus is part of the social network!
|
||||
|
||||
**Real-time timeline updates**
|
||||
|
||||
See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
|
||||
|
||||
**Federated thread resolving**
|
||||
|
||||
If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
|
||||
|
||||
**Media attachments like images and short videos**
|
||||
|
||||
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
|
||||
|
||||
**OAuth2 and a straightforward REST API**
|
||||
|
||||
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API
|
||||
|
||||
**Fast response times**
|
||||
|
||||
Mastodon tries to be as fast and responsive as possible, so all long-running tasks are delegated to background processing
|
||||
|
||||
**Deployable via Docker**
|
||||
|
||||
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
|
||||
|
||||
## Deployment
|
||||
|
||||
There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon).
|
||||
|
||||
## Contributing
|
||||
|
||||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md)
|
||||
|
||||
**IRC channel**: #mastodon on irc.freenode.net
|
||||
|
||||
---
|
||||
|
||||
## Extra credits
|
||||
|
||||
The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo)
|
||||
- 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/).
|
||||
|
||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@@ -83,7 +83,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.name = "mastodon"
|
||||
vb.customize ["modifyvm", :id, "--memory", "2048"]
|
||||
vb.customize ["modifyvm", :id, "--memory", "4096"]
|
||||
|
||||
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
|
||||
# https://github.com/mitchellh/vagrant/issues/1172
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
class AccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
|
||||
before_action :set_cache_headers
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -27,11 +26,10 @@ class AccountsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
render json: @account,
|
||||
serializer: ActivityPub::ActorSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FollowsController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
render json: follow_request,
|
||||
serializer: ActivityPub::FollowSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def follow_request
|
||||
FollowRequest.includes(:account).references(:account).find_by!(
|
||||
id: params.require(:id),
|
||||
accounts: { domain: nil, username: params.require(:account_username) },
|
||||
target_account: signed_request_account
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -89,8 +89,7 @@ module Admin
|
||||
:username,
|
||||
:display_name,
|
||||
:email,
|
||||
:ip,
|
||||
:staff
|
||||
:ip
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
module Admin
|
||||
class CustomEmojisController < BaseController
|
||||
before_action :set_custom_emoji, except: [:index, :new, :create]
|
||||
before_action :set_filter_params
|
||||
|
||||
def index
|
||||
authorize :custom_emoji, :index?
|
||||
@@ -33,26 +32,23 @@ module Admin
|
||||
|
||||
if @custom_emoji.update(resource_params)
|
||||
log_action :update, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
||||
else
|
||||
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
|
||||
end
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @custom_emoji, :destroy?
|
||||
@custom_emoji.destroy!
|
||||
log_action :destroy, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
||||
end
|
||||
|
||||
def copy
|
||||
authorize @custom_emoji, :copy?
|
||||
|
||||
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
|
||||
shortcode: @custom_emoji.shortcode)
|
||||
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
|
||||
emoji.image = @custom_emoji.image
|
||||
|
||||
if emoji.save
|
||||
@@ -62,23 +58,21 @@ module Admin
|
||||
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||
end
|
||||
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
redirect_to admin_custom_emojis_path(page: params[:page])
|
||||
end
|
||||
|
||||
def enable
|
||||
authorize @custom_emoji, :enable?
|
||||
@custom_emoji.update!(disabled: false)
|
||||
log_action :enable, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
||||
end
|
||||
|
||||
def disable
|
||||
authorize @custom_emoji, :disable?
|
||||
@custom_emoji.update!(disabled: true)
|
||||
log_action :disable, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
||||
end
|
||||
|
||||
private
|
||||
@@ -87,10 +81,6 @@ module Admin
|
||||
@custom_emoji = CustomEmoji.find(params[:id])
|
||||
end
|
||||
|
||||
def set_filter_params
|
||||
@filter_params = filter_params.to_hash.symbolize_keys
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
|
||||
end
|
||||
@@ -102,9 +92,7 @@ module Admin
|
||||
def filter_params
|
||||
params.permit(
|
||||
:local,
|
||||
:remote,
|
||||
:by_domain,
|
||||
:shortcode
|
||||
:remote
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ module Admin
|
||||
def index
|
||||
authorize :invite, :index?
|
||||
|
||||
@invites = filtered_invites.includes(user: :account).page(params[:page])
|
||||
@invites = Invite.includes(user: :account).page(params[:page])
|
||||
@invite = Invite.new
|
||||
end
|
||||
|
||||
@@ -29,19 +29,5 @@ module Admin
|
||||
@invite.expire!
|
||||
redirect_to admin_invites_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:invite).permit(:max_uses, :expires_in)
|
||||
end
|
||||
|
||||
def filtered_invites
|
||||
InviteFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(:available, :expired)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,8 +17,6 @@ module Admin
|
||||
bootstrap_timeline_accounts
|
||||
thumbnail
|
||||
min_invite_role
|
||||
activity_api_enabled
|
||||
peers_api_enabled
|
||||
).freeze
|
||||
|
||||
BOOLEAN_SETTINGS = %w(
|
||||
@@ -26,8 +24,6 @@ module Admin
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
activity_api_enabled
|
||||
peers_api_enabled
|
||||
).freeze
|
||||
|
||||
UPLOAD_SETTINGS = %w(
|
||||
|
||||
@@ -72,4 +72,19 @@ class Api::BaseController < ApplicationController
|
||||
def render_empty
|
||||
render json: {}, status: 200
|
||||
end
|
||||
|
||||
def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
|
||||
if current_account.nil?
|
||||
@reblogs_map = {}
|
||||
@favourites_map = {}
|
||||
@mutes_map = {}
|
||||
return
|
||||
end
|
||||
|
||||
status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
|
||||
conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
|
||||
@reblogs_map = Status.reblogs_map(status_ids, current_account)
|
||||
@favourites_map = Status.favourites_map(status_ids, current_account)
|
||||
@mutes_map = Status.mutes_map(conversation_ids, current_account)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::ListsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@lists = @account.lists.where(account: current_account)
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
@@ -17,13 +17,12 @@ class Api::V1::Accounts::SearchController < Api::BaseController
|
||||
AccountSearchService.new.call(
|
||||
params[:q],
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
current_account,
|
||||
resolve: truthy_param?(:resolve),
|
||||
following: truthy_param?(:following)
|
||||
resolving_search?,
|
||||
current_account
|
||||
)
|
||||
end
|
||||
|
||||
def truthy_param?(key)
|
||||
params[key] == 'true'
|
||||
def resolving_search?
|
||||
params[:resolve] == 'true'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def follow
|
||||
FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
|
||||
reblogs_arg = { reblogs: params[:reblogs] }
|
||||
|
||||
FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
|
||||
|
||||
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
|
||||
options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
|
||||
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||
end
|
||||
@@ -51,7 +53,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def relationships(**options)
|
||||
def relationships(options = {})
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity
|
||||
weeks = []
|
||||
|
||||
12.times do |i|
|
||||
day = i.weeks.ago.to_date
|
||||
week_id = day.cweek
|
||||
week = Date.commercial(day.cwyear, week_id)
|
||||
|
||||
weeks << {
|
||||
week: week.to_time.to_i.to_s,
|
||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0,
|
||||
logins: Redis.current.pfcount("activity:logins:#{week_id}"),
|
||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0,
|
||||
}
|
||||
end
|
||||
|
||||
weeks
|
||||
end
|
||||
|
||||
def require_enabled_api!
|
||||
head 404 unless Setting.activity_api_enabled
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::PeersController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_enabled_api!
|
||||
head 404 unless Setting.peers_api_enabled
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||
after_action :insert_pagination_headers, only: :show
|
||||
|
||||
def show
|
||||
@accounts = load_accounts
|
||||
@accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
@@ -35,14 +35,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||
@list = List.where(account: current_account).find(params[:list_id])
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
if unlimited?
|
||||
@list.accounts.all
|
||||
else
|
||||
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
end
|
||||
end
|
||||
|
||||
def list_accounts
|
||||
Account.find(account_ids)
|
||||
end
|
||||
@@ -60,16 +52,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def next_path
|
||||
return if unlimited?
|
||||
|
||||
if records_continue?
|
||||
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
return if unlimited?
|
||||
|
||||
unless @accounts.empty?
|
||||
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
@@ -90,8 +78,4 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||
def pagination_params(core_params)
|
||||
params.permit(:limit).merge(core_params)
|
||||
end
|
||||
|
||||
def unlimited?
|
||||
params[:limit] == '0'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::ListsController < Api::BaseController
|
||||
LISTS_LIMIT = 50
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_list, except: [:index, :create]
|
||||
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
def index
|
||||
@lists = List.where(account: current_account).all
|
||||
@lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
@@ -40,4 +44,36 @@ class Api::V1::ListsController < Api::BaseController
|
||||
def list_params
|
||||
params.permit(:title)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
if records_continue?
|
||||
api_v1_lists_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless @lists.empty?
|
||||
api_v1_lists_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@lists.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@lists.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@lists.size == limit_param(LISTS_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.permit(:limit).merge(core_params)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,10 +8,15 @@ class Api::V1::MutesController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
@data = @accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
def details
|
||||
@data = @mutes = load_mutes
|
||||
render json: @mutes, each_serializer: REST::MuteSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_accounts
|
||||
@@ -22,6 +27,10 @@ class Api::V1::MutesController < Api::BaseController
|
||||
Account.includes(:muted_by).references(:muted_by)
|
||||
end
|
||||
|
||||
def load_mutes
|
||||
paginated_mutes.includes(:account, :target_account).to_a
|
||||
end
|
||||
|
||||
def paginated_mutes
|
||||
Mute.where(account: current_account).paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
@@ -36,26 +45,34 @@ class Api::V1::MutesController < Api::BaseController
|
||||
|
||||
def next_path
|
||||
if records_continue?
|
||||
api_v1_mutes_url pagination_params(max_id: pagination_max_id)
|
||||
url_for pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless @accounts.empty?
|
||||
api_v1_mutes_url pagination_params(since_id: pagination_since_id)
|
||||
unless@data.empty?
|
||||
url_for pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@accounts.last.muted_by_ids.last
|
||||
if params[:action] == "details"
|
||||
@mutes.last.id
|
||||
else
|
||||
@accounts.last.muted_by_ids.last
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@accounts.first.muted_by_ids.first
|
||||
if params[:action] == "details"
|
||||
@mutes.first.id
|
||||
else
|
||||
@accounts.first.muted_by_ids.first
|
||||
end
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||
@data.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
|
||||
@@ -24,11 +24,20 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
dismiss
|
||||
end
|
||||
|
||||
def dismiss
|
||||
current_account.notifications.find_by!(id: params[:id]).destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy_multiple
|
||||
current_account.notifications.where(id: params[:ids]).destroy_all
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_notifications
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class Api::V1::SearchController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
RESULTS_LIMIT = 5
|
||||
RESULTS_LIMIT = 10
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
before_action :require_user!
|
||||
|
||||
60
app/controllers/api/v1/timelines/direct_controller.rb
Normal file
60
app/controllers/api/v1/timelines/direct_controller.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::DirectController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read }, only: [:show]
|
||||
before_action :require_user!, only: [:show]
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_statuses
|
||||
cached_direct_statuses
|
||||
end
|
||||
|
||||
def cached_direct_statuses
|
||||
cache_collection direct_statuses, Status
|
||||
end
|
||||
|
||||
def direct_statuses
|
||||
direct_timeline_statuses.paginate_by_max_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def direct_timeline_statuses
|
||||
Status.as_direct_timeline(current_account)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.permit(:local, :limit).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
||||
@@ -28,8 +28,6 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
|
||||
},
|
||||
}
|
||||
|
||||
data.deep_merge!(params[:data]) if params[:data]
|
||||
|
||||
web_subscription = ::Web::PushSubscription.create!(
|
||||
endpoint: params[:subscription][:endpoint],
|
||||
key_p256dh: params[:subscription][:keys][:p256dh],
|
||||
|
||||
@@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
helper_method :current_theme
|
||||
helper_method :theme_data
|
||||
helper_method :single_user_mode?
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
@@ -88,6 +89,10 @@ class ApplicationController < ActionController::Base
|
||||
current_user.setting_theme
|
||||
end
|
||||
|
||||
def theme_data
|
||||
Themes.instance.get(current_theme)
|
||||
end
|
||||
|
||||
def cache_collection(raw, klass)
|
||||
return raw unless klass.respond_to?(:with_includes)
|
||||
|
||||
@@ -121,26 +126,4 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_cached_json(cache_key, **options)
|
||||
options[:expires_in] ||= 3.minutes
|
||||
options[:public] ||= true
|
||||
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
|
||||
content_type = options.delete(:content_type) || 'application/json'
|
||||
|
||||
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
|
||||
yield.to_json
|
||||
end
|
||||
|
||||
expires_in options[:expires_in], public: options[:public]
|
||||
render json: data, content_type: content_type
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Vary'] = 'Accept'
|
||||
end
|
||||
|
||||
def skip_session!
|
||||
request.session_options[:skip] = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,4 +2,10 @@
|
||||
|
||||
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||
layout 'auth'
|
||||
|
||||
def show
|
||||
super do |user|
|
||||
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,10 +37,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
new_user_session_path
|
||||
end
|
||||
|
||||
def after_update_path_for(_resource)
|
||||
edit_user_registration_path
|
||||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ class AuthorizeFollowsController < ApplicationController
|
||||
layout 'modal'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@account = located_account || render(:error)
|
||||
@@ -59,8 +58,4 @@ class AuthorizeFollowsController < ApplicationController
|
||||
def acct_params
|
||||
params.fetch(:acct, '')
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,8 +44,7 @@ module RateLimitHeaders
|
||||
end
|
||||
|
||||
def api_throttle_data
|
||||
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
|
||||
request.env['rack.attack.throttle_data'][most_limited_type]
|
||||
request.env['rack.attack.throttle_data']['api']
|
||||
end
|
||||
|
||||
def request_time
|
||||
|
||||
@@ -17,7 +17,6 @@ module UserTrackingConcern
|
||||
|
||||
# Mark as signed-in today
|
||||
current_user.update_tracked_fields!(request)
|
||||
ActivityTracker.record('activity:logins', current_user.id)
|
||||
|
||||
# Regenerate feed if needed
|
||||
regenerate_feed! if user_needs_feed_update?
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
class EmojisController < ApplicationController
|
||||
before_action :set_emoji
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
|
||||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
render json: @emoji,
|
||||
serializer: ActivityPub::EmojiSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,8 +38,4 @@ class RemoteFollowController < ApplicationController
|
||||
def suspended_account?
|
||||
@account.suspended?
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout'
|
||||
end
|
||||
end
|
||||
|
||||
64
app/controllers/settings/keyword_mutes_controller.rb
Normal file
64
app/controllers/settings/keyword_mutes_controller.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::KeywordMutesController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@keyword_mutes = paginated_keyword_mutes_for_account
|
||||
end
|
||||
|
||||
def new
|
||||
@keyword_mute = keyword_mutes_for_account.build
|
||||
end
|
||||
|
||||
def create
|
||||
@keyword_mute = keyword_mutes_for_account.create(keyword_mute_params)
|
||||
|
||||
if @keyword_mute.persisted?
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @keyword_mute.update(keyword_mute_params)
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@keyword_mute.destroy!
|
||||
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
keyword_mutes_for_account.delete_all
|
||||
|
||||
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyword_mutes_for_account
|
||||
Glitch::KeywordMute.where(account: current_account)
|
||||
end
|
||||
|
||||
def load_keyword_mute
|
||||
@keyword_mute = keyword_mutes_for_account.find(params[:id])
|
||||
end
|
||||
|
||||
def keyword_mute_params
|
||||
params.require(:keyword_mute).permit(:keyword, :whole_word)
|
||||
end
|
||||
|
||||
def paginated_keyword_mutes_for_account
|
||||
keyword_mutes_for_account.order(:keyword).page params[:page]
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::MigrationsController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
@migration = Form::Migration.new(account: current_account.moved_to_account)
|
||||
end
|
||||
|
||||
def update
|
||||
@migration = Form::Migration.new(resource_params)
|
||||
|
||||
if @migration.valid? && migration_account_changed?
|
||||
current_account.update!(moved_to_account: @migration.account)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:migration).permit(:acct)
|
||||
end
|
||||
|
||||
def migration_account_changed?
|
||||
current_account.moved_to_account_id != @migration.account&.id &&
|
||||
current_account.id != @migration.account&.id
|
||||
end
|
||||
end
|
||||
@@ -25,6 +25,6 @@ class SharesController < ApplicationController
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout compose-standalone'
|
||||
@body_classes = 'compose-standalone'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,6 @@ class StatusesController < ApplicationController
|
||||
before_action :set_link_headers
|
||||
before_action :check_account_suspension
|
||||
before_action :redirect_to_original, only: [:show]
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -22,21 +21,19 @@ class StatusesController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
skip_session! unless @stream_entry.hidden?
|
||||
|
||||
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
render json: @status,
|
||||
serializer: ActivityPub::NoteSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def activity
|
||||
skip_session!
|
||||
|
||||
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
|
||||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
render json: @status,
|
||||
serializer: ActivityPub::ActivitySerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
def embed
|
||||
|
||||
@@ -48,7 +48,7 @@ class StreamEntriesController < ApplicationController
|
||||
@type = @stream_entry.activity_type.downcase
|
||||
|
||||
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
|
||||
authorize @stream_entry.activity, :show? if @stream_entry.hidden?
|
||||
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
|
||||
rescue Mastodon::NotPermittedError
|
||||
# Reraise in order to get a 404
|
||||
raise ActiveRecord::RecordNotFound
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class HostMetaController < ActionController::Base
|
||||
class HostMetaController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
|
||||
def show
|
||||
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
||||
|
||||
respond_to do |format|
|
||||
format.xml { render content_type: 'application/xrd+xml' }
|
||||
end
|
||||
|
||||
expires_in(3.days, public: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class WebfingerController < ActionController::Base
|
||||
class WebfingerController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
|
||||
def show
|
||||
@account = Account.find_local!(username_from_resource)
|
||||
@canonical_account_uri = @account.to_webfinger_s
|
||||
@magic_key = pem_to_magic_key(@account.keypair.public_key)
|
||||
|
||||
respond_to do |format|
|
||||
format.any(:json, :html) do
|
||||
render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
|
||||
render formats: :json, content_type: 'application/jrd+json'
|
||||
end
|
||||
|
||||
format.xml do
|
||||
render content_type: 'application/xrd+xml'
|
||||
end
|
||||
end
|
||||
|
||||
expires_in(3.days, public: true)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
head 404
|
||||
end
|
||||
@@ -37,6 +35,21 @@ module WellKnown
|
||||
WebfingerResource.new(resource_user).username
|
||||
end
|
||||
|
||||
def pem_to_magic_key(public_key)
|
||||
modulus, exponent = [public_key.n, public_key.e].map do |component|
|
||||
result = []
|
||||
|
||||
until component.zero?
|
||||
result << [component % 256].pack('C')
|
||||
component >>= 8
|
||||
end
|
||||
|
||||
result.reverse.join
|
||||
end
|
||||
|
||||
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
|
||||
end
|
||||
|
||||
def resource_param
|
||||
params.require(:resource)
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes)
|
||||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
|
||||
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin::FilterHelper
|
||||
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
|
||||
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
|
||||
INVITE_FILTER = %i(available expired).freeze
|
||||
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
|
||||
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
|
||||
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
|
||||
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||
|
||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||
new_url = filtered_url_for(link_to_params)
|
||||
@@ -14,7 +12,7 @@ module Admin::FilterHelper
|
||||
link_to text, new_url, class: filter_link_class(new_class)
|
||||
end
|
||||
|
||||
def table_link_to(icon, text, path, **options)
|
||||
def table_link_to(icon, text, path, options = {})
|
||||
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ module ApplicationHelper
|
||||
current_page?(path) ? 'active' : ''
|
||||
end
|
||||
|
||||
def active_link_to(label, path, **options)
|
||||
def active_link_to(label, path, options = {})
|
||||
link_to label, path, options.merge(class: active_nav_class(path))
|
||||
end
|
||||
|
||||
|
||||
@@ -9,24 +9,6 @@ module JsonLdHelper
|
||||
value.is_a?(Array) ? value.first : value
|
||||
end
|
||||
|
||||
# The url attribute can be a string, an array of strings, or an array of objects.
|
||||
# The objects could include a mimeType. Not-included mimeType means it's text/html.
|
||||
def url_to_href(value, preferred_type = nil)
|
||||
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
|
||||
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
|
||||
elsif value.is_a?(Array)
|
||||
value.first
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
if single_value.nil? || single_value.is_a?(String)
|
||||
single_value
|
||||
else
|
||||
single_value['href']
|
||||
end
|
||||
end
|
||||
|
||||
def as_array(value)
|
||||
value.is_a?(Array) ? value : [value]
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ module RoutingHelper
|
||||
extend ActiveSupport::Concern
|
||||
include Rails.application.routes.url_helpers
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include Webpacker::Helper
|
||||
|
||||
included do
|
||||
def default_url_options
|
||||
@@ -12,16 +11,12 @@ module RoutingHelper
|
||||
end
|
||||
end
|
||||
|
||||
def full_asset_url(source, **options)
|
||||
def full_asset_url(source, options = {})
|
||||
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
|
||||
|
||||
URI.join(root_url, source).to_s
|
||||
end
|
||||
|
||||
def full_pack_url(source, **options)
|
||||
full_asset_url(asset_pack_path(source, options))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def use_storage?
|
||||
|
||||
2
app/helpers/settings/keyword_mutes_helper.rb
Normal file
2
app/helpers/settings/keyword_mutes_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module Settings::KeywordMutesHelper
|
||||
end
|
||||
@@ -10,7 +10,6 @@ module SettingsHelper
|
||||
eo: 'Esperanto',
|
||||
es: 'Español',
|
||||
fa: 'فارسی',
|
||||
gl: 'Galego',
|
||||
fi: 'Suomi',
|
||||
fr: 'Français',
|
||||
he: 'עברית',
|
||||
@@ -28,9 +27,6 @@ module SettingsHelper
|
||||
pt: 'Português',
|
||||
'pt-BR': 'Português do Brasil',
|
||||
ru: 'Русский',
|
||||
sk: 'Slovensky',
|
||||
sr: 'Српски',
|
||||
'sr-Latn': 'Srpski (latinica)',
|
||||
sv: 'Svenska',
|
||||
th: 'ภาษาไทย',
|
||||
tr: 'Türkçe',
|
||||
|
||||
44
app/javascript/glitch/locales/en.json
Normal file
44
app/javascript/glitch/locales/en.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"getting_started.open_source_notice": "Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.",
|
||||
"layout.auto": "Auto",
|
||||
"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.",
|
||||
"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}.",
|
||||
"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_replies": "Replies",
|
||||
"settings.close": "Close",
|
||||
"settings.collapsed_statuses": "Collapsed toots",
|
||||
"settings.enable_collapsed": "Enable collapsed toots",
|
||||
"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.media": "Media",
|
||||
"settings.media_letterbox": "Letterbox media",
|
||||
"settings.media_fullwidth": "Full-width media previews",
|
||||
"settings.preferences": "User preferences",
|
||||
"settings.wide_view": "Wide view (Desktop mode only)",
|
||||
"settings.navbar_under": "Navbar at the bottom (Mobile only)",
|
||||
"status.collapse": "Collapse",
|
||||
"status.uncollapse": "Uncollapse",
|
||||
|
||||
"notification.markForDeletion": "Mark for deletion",
|
||||
"notifications.clear": "Clear all my notifications",
|
||||
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
|
||||
"notifications.marked_clear": "Clear selected notifications",
|
||||
|
||||
"notification_purge.btn_all": "Select\nall",
|
||||
"notification_purge.btn_none": "Select\nnone",
|
||||
"notification_purge.btn_invert": "Invert\nselection",
|
||||
"notification_purge.btn_apply": "Clear\nselected"
|
||||
}
|
||||
44
app/javascript/glitch/locales/pl.json
Normal file
44
app/javascript/glitch/locales/pl.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"getting_started.open_source_notice": "Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.",
|
||||
"layout.auto": "Automatyczny",
|
||||
"layout.current_is": "Twój obecny układ to:",
|
||||
"layout.desktop": "Desktopowy",
|
||||
"layout.mobile": "Mobilny",
|
||||
"navigation_bar.app_settings": "Ustawienia aplikacji",
|
||||
"getting_started.onboarding": "Rozejrzyj się",
|
||||
"onboarding.page_one.federation": "{domain} jest 'instancją' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.",
|
||||
"onboarding.page_one.welcome": "Witamy na {domain}!",
|
||||
"onboarding.page_six.github": "{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.",
|
||||
"settings.auto_collapse": "Automatyczne zwijanie",
|
||||
"settings.auto_collapse_all": "Wszystko",
|
||||
"settings.auto_collapse_lengthy": "Długie wpisy",
|
||||
"settings.auto_collapse_media": "Wpisy z zawartością multimedialną",
|
||||
"settings.auto_collapse_notifications": "Powiadomienia",
|
||||
"settings.auto_collapse_reblogs": "Podbicia",
|
||||
"settings.auto_collapse_replies": "Odpowiedzi",
|
||||
"settings.close": "Zamknij",
|
||||
"settings.collapsed_statuses": "Zwijanie wpisów",
|
||||
"settings.enable_collapsed": "Włącz zwijanie wpisów",
|
||||
"settings.general": "Ogólne",
|
||||
"settings.image_backgrounds": "Obrazy w tle",
|
||||
"settings.image_backgrounds_media": "Wyświetlaj zawartość multimedialną zwiniętych wpisów",
|
||||
"settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom",
|
||||
"settings.media": "Zawartość multimedialna",
|
||||
"settings.media_letterbox": "Letterbox media",
|
||||
"settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości",
|
||||
"settings.preferences": "Preferencje użyytkownika",
|
||||
"settings.wide_view": "Szeroki widok (tylko w trybie desktopowym)",
|
||||
"settings.navbar_under": "Pasek nawigacji na dole (tylko w trybie mobilnym)",
|
||||
"status.collapse": "Zwiń",
|
||||
"status.uncollapse": "Rozwiń",
|
||||
|
||||
"notification.markForDeletion": "Oznacz do usunięcia",
|
||||
"notifications.clear": "Wyczyść wszystkie powiadomienia",
|
||||
"notifications.marked_clear_confirmation": "Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?",
|
||||
"notifications.marked_clear": "Usuń zaznaczone powiadomienia",
|
||||
|
||||
"notification_purge.btn_all": "Zaznacz\nwszystkie",
|
||||
"notification_purge.btn_none": "Odznacz\nwszystkie",
|
||||
"notification_purge.btn_invert": "Odwróć\nzaznaczenie",
|
||||
"notification_purge.btn_apply": "Usuń\nzaznaczone"
|
||||
}
|
||||
BIN
app/javascript/images/mastodon-getting-started.png
Normal file
BIN
app/javascript/images/mastodon-getting-started.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -105,13 +105,12 @@ export function fetchAccountFail(id, error) {
|
||||
};
|
||||
};
|
||||
|
||||
export function followAccount(id, reblogs = true) {
|
||||
export function followAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||
dispatch(followAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => {
|
||||
dispatch(followAccountSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(followAccountFail(error));
|
||||
});
|
||||
@@ -137,11 +136,10 @@ export function followAccountRequest(id) {
|
||||
};
|
||||
};
|
||||
|
||||
export function followAccountSuccess(relationship, alreadyFollowing) {
|
||||
export function followAccountSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
||||
relationship,
|
||||
alreadyFollowing,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,10 +10,6 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA
|
||||
|
||||
export function fetchFavouritedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/favourites').then(response => {
|
||||
@@ -50,7 +46,7 @@ export function expandFavouritedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,52 +4,12 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||
|
||||
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
|
||||
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
||||
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
|
||||
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
|
||||
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
|
||||
|
||||
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
|
||||
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
|
||||
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
|
||||
|
||||
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
|
||||
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
|
||||
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
|
||||
|
||||
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
||||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||
|
||||
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
|
||||
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
|
||||
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
|
||||
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
|
||||
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
|
||||
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
|
||||
|
||||
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
|
||||
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
|
||||
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const fetchList = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['lists', id])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchListRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${id}`)
|
||||
.then(({ data }) => dispatch(fetchListSuccess(data)))
|
||||
.catch(err => dispatch(fetchListFail(id, err)));
|
||||
.catch(err => dispatch(fetchListFail(err)));
|
||||
};
|
||||
|
||||
export const fetchListRequest = id => ({
|
||||
@@ -62,252 +22,7 @@ export const fetchListSuccess = list => ({
|
||||
list,
|
||||
});
|
||||
|
||||
export const fetchListFail = (id, error) => ({
|
||||
export const fetchListFail = error => ({
|
||||
type: LIST_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchLists = () => (dispatch, getState) => {
|
||||
dispatch(fetchListsRequest());
|
||||
|
||||
api(getState).get('/api/v1/lists')
|
||||
.then(({ data }) => dispatch(fetchListsSuccess(data)))
|
||||
.catch(err => dispatch(fetchListsFail(err)));
|
||||
};
|
||||
|
||||
export const fetchListsRequest = () => ({
|
||||
type: LISTS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchListsSuccess = lists => ({
|
||||
type: LISTS_FETCH_SUCCESS,
|
||||
lists,
|
||||
});
|
||||
|
||||
export const fetchListsFail = error => ({
|
||||
type: LISTS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const submitListEditor = shouldReset => (dispatch, getState) => {
|
||||
const listId = getState().getIn(['listEditor', 'listId']);
|
||||
const title = getState().getIn(['listEditor', 'title']);
|
||||
|
||||
if (listId === null) {
|
||||
dispatch(createList(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateList(listId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupListEditor = listId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_EDITOR_SETUP,
|
||||
list: getState().getIn(['lists', listId]),
|
||||
});
|
||||
|
||||
dispatch(fetchListAccounts(listId));
|
||||
};
|
||||
|
||||
export const changeListEditorTitle = value => ({
|
||||
type: LIST_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createList = (title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(createListRequest());
|
||||
|
||||
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
dispatch(createListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createListFail(err)));
|
||||
};
|
||||
|
||||
export const createListRequest = () => ({
|
||||
type: LIST_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createListSuccess = list => ({
|
||||
type: LIST_CREATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const createListFail = error => ({
|
||||
type: LIST_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateListFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateListRequest = id => ({
|
||||
type: LIST_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateListSuccess = list => ({
|
||||
type: LIST_UPDATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const updateListFail = (id, error) => ({
|
||||
type: LIST_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListEditor = () => ({
|
||||
type: LIST_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteList = id => (dispatch, getState) => {
|
||||
dispatch(deleteListRequest(id));
|
||||
|
||||
api(getState).delete(`/api/v1/lists/${id}`)
|
||||
.then(() => dispatch(deleteListSuccess(id)))
|
||||
.catch(err => dispatch(deleteListFail(id, err)));
|
||||
};
|
||||
|
||||
export const deleteListRequest = id => ({
|
||||
type: LIST_DELETE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteListSuccess = id => ({
|
||||
type: LIST_DELETE_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteListFail = (id, error) => ({
|
||||
type: LIST_DELETE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListAccounts = listId => (dispatch, getState) => {
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
|
||||
.then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
|
||||
.catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||
};
|
||||
|
||||
export const fetchListAccountsRequest = id => ({
|
||||
type: LIST_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchListAccountsSuccess = (id, accounts, next) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchListAccountsFail = (id, error) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListSuggestions = q => (dispatch, getState) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
following: true,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params })
|
||||
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
|
||||
};
|
||||
|
||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearListSuggestions = () => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeListSuggestions = value => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const addToList = (listId, accountId) => (dispatch, getState) => {
|
||||
dispatch(addToListRequest(listId, accountId));
|
||||
|
||||
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(addToListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addToListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_ADD_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
|
||||
dispatch(removeFromListRequest(listId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeFromListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_REMOVE_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import api, { getLinks } from '../api';
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { openModal } from './modal';
|
||||
import { openModal } from '../../mastodon/actions/modal';
|
||||
|
||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||
@@ -100,4 +100,4 @@ export function toggleHideNotifications() {
|
||||
return dispatch => {
|
||||
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||
|
||||
const unescapeHTML = (html) => {
|
||||
const wrapper = document.createElement('div');
|
||||
html = html.replace(/<br \/>|<br>|\n/g, ' ');
|
||||
html = html.replace(/<br \/>|<br>|\n/, ' ');
|
||||
wrapper.innerHTML = html;
|
||||
return wrapper.textContent;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
|
||||
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
|
||||
export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
|
||||
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
|
||||
|
||||
export function setBrowserSupport (value) {
|
||||
return {
|
||||
@@ -23,12 +25,28 @@ export function clearSubscription () {
|
||||
};
|
||||
}
|
||||
|
||||
export function setAlerts (path, value) {
|
||||
export function changeAlerts(key, value) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: SET_ALERTS,
|
||||
path,
|
||||
type: ALERTS_CHANGE,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
|
||||
export function saveSettings() {
|
||||
return (_, getState) => {
|
||||
const state = getState().get('push_notifications');
|
||||
const subscription = state.get('subscription');
|
||||
const alerts = state.get('alerts');
|
||||
|
||||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
data: {
|
||||
alerts,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
setAlerts,
|
||||
} from './setter';
|
||||
import { register, saveSettings } from './registerer';
|
||||
|
||||
export {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
register,
|
||||
};
|
||||
|
||||
export function changeAlerts(path, value) {
|
||||
return dispatch => {
|
||||
dispatch(setAlerts(path, value));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { pushNotificationsSetting } from '../../settings';
|
||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
const urlBase64ToUint8Array = (base64String) => {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||
|
||||
const getRegistration = () => navigator.serviceWorker.ready;
|
||||
|
||||
const getPushSubscription = (registration) =>
|
||||
registration.pushManager.getSubscription()
|
||||
.then(subscription => ({ registration, subscription }));
|
||||
|
||||
const subscribe = (registration) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
|
||||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||
|
||||
const sendSubscriptionToBackend = (subscription, me) => {
|
||||
const params = { subscription };
|
||||
|
||||
if (me) {
|
||||
const data = pushNotificationsSetting.get(me);
|
||||
if (data) {
|
||||
params.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||
};
|
||||
|
||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
|
||||
|
||||
export function register () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setBrowserSupport(supportsPushNotifications));
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
|
||||
if (me && !pushNotificationsSetting.get(me)) {
|
||||
const alerts = getState().getIn(['push_notifications', 'alerts']);
|
||||
if (alerts) {
|
||||
pushNotificationsSetting.set(me, { alerts: alerts });
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsPushNotifications) {
|
||||
if (!getApplicationServerKey()) {
|
||||
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
|
||||
return;
|
||||
}
|
||||
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(({ registration, subscription }) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
|
||||
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
|
||||
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
|
||||
|
||||
// If the VAPID public key did not change and the endpoint corresponds
|
||||
// to the endpoint saved in the backend, the subscription is valid
|
||||
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
|
||||
return subscription;
|
||||
} else {
|
||||
// Something went wrong, try to subscribe again
|
||||
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
||||
subscription => sendSubscriptionToBackend(subscription, me));
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(registration).then(
|
||||
subscription => sendSubscriptionToBackend(subscription, me));
|
||||
})
|
||||
.then(subscription => {
|
||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||
// it means that the backend subscription is valid (and was set during hydration)
|
||||
if (!(subscription instanceof PushSubscription)) {
|
||||
dispatch(setSubscription(subscription));
|
||||
if (me) {
|
||||
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code === 20 && error.name === 'AbortError') {
|
||||
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
|
||||
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
|
||||
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
|
||||
}
|
||||
|
||||
// Clear alerts and hide UI settings
|
||||
dispatch(clearSubscription());
|
||||
if (me) {
|
||||
pushNotificationsSetting.remove(me);
|
||||
}
|
||||
|
||||
try {
|
||||
getRegistration()
|
||||
.then(getPushSubscription)
|
||||
.then(unsubscribe);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Your browser does not support Web Push Notifications.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveSettings() {
|
||||
return (_, getState) => {
|
||||
const state = getState().get('push_notifications');
|
||||
const subscription = state.get('subscription');
|
||||
const alerts = state.get('alerts');
|
||||
const data = { alerts };
|
||||
|
||||
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||
data,
|
||||
}).then(() => {
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
if (me) {
|
||||
pushNotificationsSetting.set(me, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import { debounce } from 'lodash';
|
||||
export const SETTING_CHANGE = 'SETTING_CHANGE';
|
||||
export const SETTING_SAVE = 'SETTING_SAVE';
|
||||
|
||||
export function changeSetting(path, value) {
|
||||
export function changeSetting(key, value) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: SETTING_CHANGE,
|
||||
path,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||
const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS();
|
||||
|
||||
axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
|
||||
}, 5000, { trailing: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from './avatar';
|
||||
@@ -27,7 +27,6 @@ export default class Account extends ImmutablePureComponent {
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onMuteNotifications: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
@@ -82,18 +81,18 @@ export default class Account extends ImmutablePureComponent {
|
||||
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
let hidingNotificationsButton;
|
||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||
if (muting.get('notifications')) {
|
||||
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
|
||||
} else {
|
||||
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
|
||||
}
|
||||
buttons = (
|
||||
<Fragment>
|
||||
<div>
|
||||
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
|
||||
{hidingNotificationsButton}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
} else if (!account.get('moved')) {
|
||||
} else {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { autoPlayGif } from '../initial_state';
|
||||
|
||||
export default class Avatar extends React.PureComponent {
|
||||
|
||||
@@ -9,12 +8,12 @@ export default class Avatar extends React.PureComponent {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
style: PropTypes.object,
|
||||
inline: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
animate: autoPlayGif,
|
||||
animate: false,
|
||||
size: 20,
|
||||
inline: false,
|
||||
};
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { autoPlayGif } from '../initial_state';
|
||||
|
||||
export default class AvatarOverlay extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
friend: ImmutablePropTypes.map.isRequired,
|
||||
animate: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
animate: autoPlayGif,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, friend, animate } = this.props;
|
||||
const { account, friend } = this.props;
|
||||
|
||||
const baseStyle = {
|
||||
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||
backgroundImage: `url(${account.get('avatar_static')})`,
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||
backgroundImage: `url(${friend.get('avatar_static')})`,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
icon: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
focusable: PropTypes.bool,
|
||||
showBackButton: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
@@ -31,6 +32,10 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
focusable: true,
|
||||
}
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
@@ -63,7 +68,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
@@ -130,13 +135,11 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
<span className='column-header__title'>
|
||||
{title}
|
||||
</span>
|
||||
</button>
|
||||
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
<span className='column-header__title'>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{backButton}
|
||||
|
||||
@@ -20,8 +20,6 @@ const messages = defineMessages({
|
||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
@@ -32,7 +30,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onReblogToggle: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
@@ -63,14 +60,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
if (account.get('id') === me) {
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
}
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,6 @@ export default class Header extends ImmutablePureComponent {
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onReblogToggle: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
@@ -41,10 +40,6 @@ export default class Header extends ImmutablePureComponent {
|
||||
this.props.onReport(this.props.account);
|
||||
}
|
||||
|
||||
handleReblogToggle = () => {
|
||||
this.props.onReblogToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
@@ -85,7 +80,6 @@ export default class Header extends ImmutablePureComponent {
|
||||
account={account}
|
||||
onBlock={this.handleBlock}
|
||||
onMention={this.handleMention}
|
||||
onReblogToggle={this.handleReblogToggle}
|
||||
onReport={this.handleReport}
|
||||
onMute={this.handleMute}
|
||||
onBlockDomain={this.handleBlockDomain}
|
||||
|
||||
@@ -67,14 +67,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onReblogToggle (account) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
dispatch(followAccount(account.get('id'), false));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id'), true));
|
||||
}
|
||||
},
|
||||
|
||||
onReport (account) {
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
|
||||
@@ -156,8 +156,6 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='compose-form'>
|
||||
<WarningContainer />
|
||||
|
||||
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
|
||||
<div className='spoiler-input'>
|
||||
<label>
|
||||
@@ -167,6 +165,8 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
|
||||
<div className='compose-form__autosuggest-wrapper'>
|
||||
@@ -199,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
<SensitiveButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
</div>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
|
||||
</div>
|
||||
|
||||
<div className='compose-form__publish'>
|
||||
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
|
||||
<div className='compose-form__publish'>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
|
||||
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import IconButton from '../../../components/icon_button';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { isRtl } from '../../../rtl';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
@@ -43,10 +42,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
const style = {
|
||||
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
|
||||
};
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
@@ -59,7 +55,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
|
||||
<div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { intl, media } = this.props;
|
||||
const active = this.state.hovered || this.state.focused;
|
||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
|
||||
const description = this.state.dirtyDescription || media.get('description') || '';
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
|
||||
@@ -5,27 +5,20 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from '../../../initial_state';
|
||||
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/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'])),
|
||||
});
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning }) => {
|
||||
const WarningWrapper = ({ needsLockWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
if (hashtagWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
||||
|
||||
@@ -94,7 +94,6 @@ export default class Compose extends React.PureComponent {
|
||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer />
|
||||
<div className='mastodon' />
|
||||
</div>
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||
@@ -17,7 +16,6 @@ const messages = defineMessages({
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
});
|
||||
|
||||
@@ -32,7 +30,6 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@@ -62,12 +59,12 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleScrollToBottom = debounce(() => {
|
||||
handleScrollToBottom = () => {
|
||||
this.props.dispatch(expandFavouritedStatuses());
|
||||
}, 300, { leading: true })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
@@ -88,7 +85,6 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
statusIds={statusIds}
|
||||
scrollKey={`favourited_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onScrollToBottom={this.handleScrollToBottom}
|
||||
/>
|
||||
</Column>
|
||||
|
||||
@@ -25,8 +25,6 @@ const messages = defineMessages({
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
@@ -48,7 +46,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { intl, myAccount, columns, multiColumn } = this.props;
|
||||
|
||||
const navItems = [];
|
||||
let navItems = [];
|
||||
|
||||
if (multiColumn) {
|
||||
if (!columns.find(item => item.get('id') === 'HOME')) {
|
||||
@@ -68,20 +66,19 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
navItems.push(
|
||||
navItems = navItems.concat([
|
||||
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
);
|
||||
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
||||
]);
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
if (multiColumn) {
|
||||
navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
|
||||
navItems = navItems.concat([
|
||||
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
|
||||
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
|
||||
@@ -89,24 +86,24 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
|
||||
{navItems}
|
||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
|
||||
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
||||
<div className='static-content getting-started'>
|
||||
<p>
|
||||
<a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> }}
|
||||
/>
|
||||
</p>
|
||||
<div className='getting-started__footer scrollable optionally-scrollable'>
|
||||
<div className='static-content getting-started'>
|
||||
<p>
|
||||
<a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -27,11 +27,11 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class KeyboardShortcuts extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Column icon='question' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<div className='keyboard-shortcuts scrollable optionally-scrollable'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
<th><FormattedMessage id='keyboard_shortcuts.description' defaultMessage='Description' /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><kbd>r</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>m</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>f</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>b</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>enter</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>up</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>down</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>1</kbd>-<kbd>9</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>n</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>alt</kbd>+<kbd>n</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>backspace</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>s</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>esc</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>?</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromListEditor(accountId)),
|
||||
onAdd: () => dispatch(addToListEditor(accountId)),
|
||||
});
|
||||
|
||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
export default class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchListSuggestions(value)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onChange: value => dispatch(changeListSuggestions(value)),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
export default class Search extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<i className={classNames('fa fa-search', { active: !hasValue })} />
|
||||
<i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Account from './components/account';
|
||||
import Search from './components/search';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
title: state.getIn(['listEditor', 'title']),
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
export default class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, accountIds, searchAccountIds, onClear } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<h4>{title}</h4>
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) =>
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,18 +6,10 @@ import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connectListStream } from '../../actions/streaming';
|
||||
import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
|
||||
import { fetchList, deleteList } from '../../actions/lists';
|
||||
import { openModal } from '../../actions/modal';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
|
||||
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' },
|
||||
});
|
||||
import { fetchList } from '../../actions/lists';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
@@ -25,21 +17,15 @@ const mapStateToProps = (state, props) => ({
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
@injectIntl
|
||||
export default class ListTimeline extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
list: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
@@ -49,7 +35,6 @@ export default class ListTimeline extends React.PureComponent {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('LIST', { id: this.props.params.id }));
|
||||
this.context.router.history.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,49 +73,12 @@ export default class ListTimeline extends React.PureComponent {
|
||||
this.props.dispatch(expandListTimeline(id));
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteList(id));
|
||||
|
||||
if (!!columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
this.context.router.history.push('/lists');
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
} else if (list === false) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column ref={this.setRef}>
|
||||
<ColumnHeader
|
||||
@@ -142,26 +90,14 @@ export default class ListTimeline extends React.PureComponent {
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-header__links'>
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
|
||||
<i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
|
||||
<i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</ColumnHeader>
|
||||
/>
|
||||
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`list_timeline-${columnId}`}
|
||||
timelineId={`list:${id}`}
|
||||
loadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
|
||||
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
||||
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: state.getIn(['listEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(true)),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
export default class NewListForm extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='plus'
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import { fetchLists } from '../../actions/lists';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import NewListForm from './components/new_list_form';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
});
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
@injectIntl
|
||||
export default class Lists extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, lists } = this.props;
|
||||
|
||||
if (!lists) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='bars' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
|
||||
<NewListForm />
|
||||
|
||||
<div className='scrollable'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='bars' text={list.get('title')} />
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,11 +11,12 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pushSettings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onPushChange = (path, checked) => {
|
||||
this.props.onChange(['push', ...path], checked);
|
||||
onPushChange = (key, checked) => {
|
||||
this.props.onChange(['push', ...key], checked);
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -39,10 +40,10 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,10 +51,10 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,10 +62,10 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,10 +73,10 @@ export default class ColumnSettings extends React.PureComponent {
|
||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,23 +8,23 @@ export default class SettingToggle extends React.PureComponent {
|
||||
static propTypes = {
|
||||
prefix: PropTypes.string,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingPath: PropTypes.array.isRequired,
|
||||
settingKey: PropTypes.array.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
meta: PropTypes.node,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
this.props.onChange(this.props.settingPath, target.checked);
|
||||
this.props.onChange(this.props.settingKey, target.checked);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { prefix, settings, settingPath, label, meta } = this.props;
|
||||
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
|
||||
const { prefix, settings, settingKey, label, meta } = this.props;
|
||||
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
|
||||
|
||||
return (
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||
<Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
|
||||
{meta && <span className='setting-meta__label'>{meta}</span>}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||
import { clearNotifications } from '../../../actions/notifications';
|
||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -18,14 +18,19 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onChange (path, checked) {
|
||||
if (path[0] === 'push') {
|
||||
dispatch(changePushNotifications(path.slice(1), checked));
|
||||
onChange (key, checked) {
|
||||
if (key[0] === 'push') {
|
||||
dispatch(changePushNotifications(key.slice(1), checked));
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
dispatch(changeSetting(['notifications', ...key], checked));
|
||||
}
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
dispatch(savePushNotificationSettings());
|
||||
},
|
||||
|
||||
onClear () {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '../../../actions/timelines';
|
||||
import Column from '../../../components/column';
|
||||
import ColumnHeader from '../../../components/column_header';
|
||||
import { connectHashtagStream } from '../../../actions/streaming';
|
||||
|
||||
@connect()
|
||||
export default class HashtagTimeline extends React.PureComponent {
|
||||
@@ -30,13 +29,16 @@ export default class HashtagTimeline extends React.PureComponent {
|
||||
const { dispatch, hashtag } = this.props;
|
||||
|
||||
dispatch(refreshHashtagTimeline(hashtag));
|
||||
this.disconnect = dispatch(connectHashtagStream(hashtag));
|
||||
|
||||
this.polling = setInterval(() => {
|
||||
dispatch(refreshHashtagTimeline(hashtag));
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
if (typeof this.polling !== 'undefined') {
|
||||
clearInterval(this.polling);
|
||||
this.polling = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import Column from '../../../components/column';
|
||||
import ColumnHeader from '../../../components/column_header';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connectPublicStream } from '../../../actions/streaming';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
|
||||
@@ -36,13 +35,16 @@ export default class PublicTimeline extends React.PureComponent {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(refreshPublicTimeline());
|
||||
this.disconnect = dispatch(connectPublicStream());
|
||||
|
||||
this.polling = setInterval(() => {
|
||||
dispatch(refreshPublicTimeline());
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
if (typeof this.polling !== 'undefined') {
|
||||
clearInterval(this.polling);
|
||||
this.polling = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ const messages = defineMessages({
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
block: { id: 'status.block', defaultMessage: 'Block @{name}' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
@@ -38,9 +34,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
onMuteConversation: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onReport: PropTypes.func,
|
||||
onPin: PropTypes.func,
|
||||
onEmbed: PropTypes.func,
|
||||
@@ -67,18 +60,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
|
||||
handleMuteClick = () => {
|
||||
this.props.onMute(this.props.status.get('account'));
|
||||
}
|
||||
|
||||
handleConversationMuteClick = () => {
|
||||
this.props.onMuteConversation(this.props.status);
|
||||
}
|
||||
|
||||
handleBlockClick = () => {
|
||||
this.props.onBlock(this.props.status.get('account'));
|
||||
}
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.status);
|
||||
}
|
||||
@@ -102,7 +83,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
const { status, intl } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const mutingConversation = status.get('muted');
|
||||
|
||||
let menu = [];
|
||||
|
||||
@@ -115,15 +95,10 @@ export default class ActionBar extends React.PureComponent {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class Card extends React.PureComponent {
|
||||
Immutable.fromJS([
|
||||
{
|
||||
type: 'image',
|
||||
url: card.get('embed_url'),
|
||||
url: card.get('url'),
|
||||
description: card.get('title'),
|
||||
meta: {
|
||||
original: {
|
||||
@@ -59,8 +59,6 @@ export default class Card extends React.PureComponent {
|
||||
|
||||
renderLink () {
|
||||
const { card, maxDescription } = this.props;
|
||||
const { width } = this.state;
|
||||
const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width);
|
||||
|
||||
let image = '';
|
||||
let provider = card.get('provider_name');
|
||||
@@ -77,15 +75,17 @@ export default class Card extends React.PureComponent {
|
||||
provider = decodeIDNA(getHostname(card.get('url')));
|
||||
}
|
||||
|
||||
const className = classnames('status-card', { horizontal });
|
||||
const className = classnames('status-card', {
|
||||
'horizontal': card.get('width') > card.get('height'),
|
||||
});
|
||||
|
||||
return (
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener'>
|
||||
{image}
|
||||
|
||||
<div className='status-card__content'>
|
||||
<strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
|
||||
{!horizontal && <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>}
|
||||
<p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
|
||||
<span className='status-card__host'>{provider}</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -101,7 +101,7 @@ export default class Card extends React.PureComponent {
|
||||
onClick={this.handlePhotoClick}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
src={card.get('embed_url')}
|
||||
src={card.get('url')}
|
||||
alt={card.get('title')}
|
||||
width={card.get('width')}
|
||||
height={card.get('height')}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user