Compare commits

..

19 Commits

Author SHA1 Message Date
kibigo!
da2b6dda6a This is a better way of detecting frontends 2017-06-22 21:10:02 -07:00
kibigo!
ed82421870 Forgot to delete a debugging thing sry 😰 2017-06-22 20:38:08 -07:00
kibigo!
b3904c2553 MORE FRONTENDS (EASY MODE) WIN!!! 2017-06-22 20:15:11 -07:00
beatrix
101a4c6913 glitch the getting started image 2017-06-22 13:04:56 -04:00
kibigo!
11da74dbb7 Very minor styling improvements to toot-collapsing 2017-06-22 00:54:33 -07:00
kibigo!
51a86b32c3 Updates height upon collapsing 2017-06-21 20:17:12 -07:00
kibigo!
51b783cdbc Minor collapsing button improvements~ 2017-06-21 19:54:41 -07:00
kibigo!
3915f06b02 Collapsable toots [1/??] 2017-06-21 19:40:53 -07:00
kibigo!
07b1171c73 Profile Metadata HACK 😈 2017-06-20 19:44:43 -07:00
Go Shoemake
571576d6f7 Fixes drawer so stuff doesn't overflow 2017-06-20 23:46:17 +00:00
Charlotte Fields
7151ec7680 cybre cleanup 2017-06-20 23:46:17 +00:00
Chronister
a4b3009c1c cybrespace to 1.4.2 2017-06-20 23:46:17 +00:00
Chronister
ef30a92c71 All cybrespace changes through 5/28 2017-06-20 23:46:17 +00:00
Charlotte Fields
613aa55e03 adding cybre changes 2017-06-20 23:46:17 +00:00
beatrix-bitrot
88a08d54b6 update local modifications for cors and cp 2017-06-20 23:46:17 +00:00
beatrix-bitrot
3a69d70eae silly readme update to test automated deploys 2017-06-20 23:46:17 +00:00
beatrix-bitrot
9c778f4abf update README.md 2017-06-20 23:46:17 +00:00
beatrix
ac61ce5826 Update README.md 2017-06-20 23:46:17 +00:00
Beatrix Bitrot
73d6da32be CORS tweaks 2017-06-20 23:46:17 +00:00
854 changed files with 7785 additions and 23316 deletions

View File

@@ -15,15 +15,13 @@
"plugins": [
"syntax-dynamic-import",
["transform-object-rest-spread", { "useBuiltIns": true }],
"transform-decorators-legacy",
"transform-class-properties",
[
"react-intl",
{
"messagesDir": "./build/messages"
}
],
"preval"
]
],
"env": {
"development": {
@@ -45,7 +43,6 @@
]
}
],
"transform-react-inline-elements",
[
"transform-runtime",
{

View File

@@ -4,6 +4,7 @@ public/system
public/assets
public/packs
node_modules
storybook
neo4j
vendor/bundle
.DS_Store

View File

@@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# CDN_HOST=https://assets.example.com
# CDN_HOST=assets.example.com
# S3 (optional)
# S3_ENABLED=true

View File

@@ -31,17 +31,6 @@ PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=
# VAPID keys (used for push notifications
# You can generate the keys using the following command (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=
# Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
@@ -69,7 +58,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system

View File

@@ -1,9 +1,7 @@
---
root: true
env:
browser: true
node: true
node: false
es6: true
parser: babel-eslint
@@ -54,14 +52,8 @@ rules:
no-mixed-spaces-and-tabs: warn
no-nested-ternary: warn
no-trailing-spaces: warn
no-undef: error
no-unreachable: error
no-unused-expressions: error
no-unused-vars:
- error
- vars: all
args: after-used
ignoreRestSiblings: true
object-curly-spacing:
- error
- always
@@ -89,10 +81,7 @@ rules:
- 2
react/jsx-no-bind: error
react/jsx-no-duplicate-props: error
react/jsx-no-undef: error
react/jsx-tag-spacing: error
react/jsx-uses-react: error
react/jsx-uses-vars: error
react/jsx-wrap-multilines: error
react/no-multi-comp: off
react/no-string-refs: error
@@ -112,7 +101,7 @@ rules:
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-has-alt: warn
jsx-a11y/img-redundant-alt: warn
jsx-a11y/label-has-for: off
jsx-a11y/label-has-for: warn
jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
@@ -121,6 +110,6 @@ rules:
jsx-a11y/onclick-has-focus: warn
jsx-a11y/onclick-has-role: warn
jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: off
jsx-a11y/role-supports-aria-props: warn
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn

14
.gitattributes vendored
View File

@@ -1,14 +0,0 @@
* text=auto eol=lf
*.eot -text
*.gif -text
*.gz -text
*.ico -text
*.jpg -text
*.mp3 -text
*.ogg -text
*.png -text
*.ttf -text
*.webm -text
*.woff -text
*.woff2 -text
spec/fixtures/requests/** -text !eol

View File

@@ -14,6 +14,7 @@ node_modules/
public/assets/
public/system/
spec/
storybook/
tmp/
.vagrant/
vendor/bundle/

View File

@@ -6,4 +6,3 @@ plugins:
- last 2 versions
- IE >= 11
- iOS >= 9
postcss-object-fit-images: {}

View File

@@ -27,7 +27,6 @@ Metrics/AbcSize:
Max: 100
Metrics/BlockLength:
Max: 35
Exclude:
- 'lib/tasks/**/*'
@@ -36,10 +35,10 @@ Metrics/BlockNesting:
Metrics/ClassLength:
CountComments: false
Max: 300
Max: 200
Metrics/CyclomaticComplexity:
Max: 25
Max: 15
Metrics/LineLength:
AllowURI: true
@@ -54,11 +53,11 @@ Metrics/ModuleLength:
Max: 200
Metrics/ParameterLists:
Max: 5
Max: 4
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 20
Max: 10
Rails:
Enabled: true

View File

@@ -2,3 +2,4 @@ node_modules/
.cache/
docs/
spec/
storybook/

View File

@@ -6,9 +6,8 @@ cache:
- node_modules
- public/assets
- public/packs-test
- tmp/cache/babel-loader
dist: trusty
sudo: required
sudo: false
notifications:
email: false
@@ -33,7 +32,6 @@ addons:
- g++-6
- libprotobuf-dev
- protobuf-compiler
- libicu-dev
rvm:
- 2.3.4

View File

@@ -1,9 +1,5 @@
ffmpeg
libicu-dev
libidn11
libidn11-dev
libpq-dev
protobuf-compiler
libprotobuf-dev
ffmpeg
libxdamage1
libxfixes3
protobuf-compiler

View File

@@ -1,36 +1,3 @@
# 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
============
@@ -82,5 +49,3 @@ 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>

View File

@@ -7,21 +7,16 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
EXPOSE 3000 4000
WORKDIR /mastodon
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
&& echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \
&& apk -U upgrade \
&& apk add -t build-dependencies \
build-base \
icu-dev \
libidn-dev \
libtool \
libxml2-dev \
libxslt-dev \
postgresql-dev \
protobuf-dev \
python \
@@ -30,34 +25,22 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
ffmpeg \
file \
git \
icu-libs \
imagemagick@edge \
libidn \
libpq \
libxml2 \
libxslt \
nodejs-npm@edge \
nodejs@edge \
protobuf \
su-exec \
tini \
yarn@edge \
&& npm install -g npm@3 && npm install -g yarn \
&& update-ca-certificates \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& mkdir -p /tmp/src \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
&& ./configure --prefix=/usr/local \
&& make -j$(getconf _NPROCESSORS_ONLN)\
&& make install \
&& libtool --finish /usr/local/lib \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
RUN bundle install --deployment --without test development \
&& yarn --ignore-optional --pure-lockfile
COPY . /mastodon

11
Gemfile
View File

@@ -18,27 +18,22 @@ gem 'aws-sdk', '~> 2.9'
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'browser'
gem 'charlock_holmes', '~> 0.7.3'
gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'
gem 'fast_blank', '~> 1.0'
gem 'goldfinger', '~> 2.0'
gem 'goldfinger', '~> 1.2'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.0'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.7'
gem 'oj', '~> 3.0'
gem 'ostatus2', '~> 2.0'
@@ -51,7 +46,6 @@ gem 'rack-timeout', '~> 0.4'
gem 'rails-i18n', '~> 5.0'
gem 'rails-settings-cached', '~> 0.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 'sanitize', '~> 4.4'
@@ -66,7 +60,6 @@ gem 'statsd-instrument', '~> 2.1'
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0'
gem 'webpush'
group :development, :test do
gem 'fabrication', '~> 2.16'
@@ -80,7 +73,7 @@ group :test do
gem 'capybara', '~> 2.14'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'microformats', '~> 4.0'
gem 'microformats2', '~> 3.0'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false

View File

@@ -1,52 +1,47 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.3)
actionpack (= 5.1.3)
actioncable (5.1.1)
actionpack (= 5.1.1)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
actionmailer (5.1.1)
actionpack (= 5.1.1)
actionview (= 5.1.1)
activejob (= 5.1.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.3)
actionview (= 5.1.3)
activesupport (= 5.1.3)
actionpack (5.1.1)
actionview (= 5.1.1)
activesupport (= 5.1.1)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.3)
activesupport (= 5.1.3)
actionview (5.1.1)
activesupport (= 5.1.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
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.2)
active_record_query_trace (1.5.4)
activejob (5.1.3)
activesupport (= 5.1.3)
activejob (5.1.1)
activesupport (= 5.1.1)
globalid (>= 0.3.6)
activemodel (5.1.3)
activesupport (= 5.1.3)
activerecord (5.1.3)
activemodel (= 5.1.3)
activesupport (= 5.1.3)
activemodel (5.1.1)
activesupport (= 5.1.1)
activerecord (5.1.1)
activemodel (= 5.1.1)
activesupport (= 5.1.1)
arel (~> 8.0)
activesupport (5.1.3)
activesupport (5.1.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.3.0)
airbrussh (1.2.0)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.2)
activerecord (>= 3.2, < 6.0)
@@ -57,14 +52,14 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sdk (2.9.37)
aws-sdk-resources (= 2.9.37)
aws-sdk-core (2.9.37)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
aws-sdk-resources (2.9.37)
aws-sdk-core (= 2.9.37)
aws-sigv4 (1.0.0)
bcrypt (3.1.11)
better_errors (2.1.1)
coderay (>= 1.0.0)
@@ -72,10 +67,9 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.1.2)
bootsnap (1.0.0)
msgpack (~> 1.0)
brakeman (3.6.2)
browser (2.4.0)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
@@ -83,7 +77,7 @@ GEM
bundler-audit (0.5.0)
bundler (~> 1.2)
thor (~> 0.18)
capistrano (3.8.2)
capistrano (3.8.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@@ -99,18 +93,15 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.14.4)
capybara (2.14.2)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
case_transform (0.2)
activesupport
charlock_holmes (0.7.3)
chunky_png (1.3.8)
cld3 (3.1.3)
cld3 (3.1.2)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
@@ -150,12 +141,12 @@ GEM
thread
thread_safe
encryptor (3.0.0)
erubi (1.6.1)
erubi (1.6.0)
erubis (2.7.0)
et-orbi (1.0.5)
et-orbi (1.0.4)
tzinfo
execjs (2.7.0)
fabrication (2.16.2)
fabrication (2.16.1)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
@@ -165,12 +156,11 @@ GEM
ruby-progressbar (~> 1.4)
globalid (0.4.0)
activesupport (>= 4.2.0)
goldfinger (2.0.1)
addressable (~> 2.5)
http (~> 2.2)
nokogiri (~> 1.8)
oj (~> 3.0)
hamlit (2.8.4)
goldfinger (1.2.0)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
hamlit (2.8.1)
temple (>= 0.8.0)
thor
tilt
@@ -179,10 +169,9 @@ GEM
activesupport (>= 4.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
hashdiff (0.3.5)
hashdiff (0.3.4)
highline (1.7.8)
hiredis (0.6.1)
hkdf (0.3.0)
htmlentities (4.3.4)
http (2.2.2)
addressable (~> 2.3)
@@ -192,13 +181,13 @@ GEM
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (1.0.3)
http_accept_language (2.1.1)
http_accept_language (2.1.0)
http_parser.rb (0.6.0)
httplog (0.99.7)
httplog (0.99.3)
colorize
rack
i18n (0.8.6)
i18n-tasks (0.9.16)
i18n (0.8.4)
i18n-tasks (0.9.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -208,11 +197,8 @@ GEM
parser (>= 2.2.3.0)
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
idn-ruby (0.1.0)
jmespath (1.3.1)
json (2.1.0)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
@@ -242,10 +228,8 @@ GEM
nokogiri (>= 1.5.9)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.8.2)
microformats (4.0.7)
microformats2 (3.1.0)
json
nokogiri
mime-types (3.1)
@@ -253,7 +237,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_portile2 (2.2.0)
minitest (5.10.3)
minitest (5.10.2)
msgpack (1.1.0)
multi_json (1.12.1)
net-scp (1.2.1)
@@ -264,8 +248,8 @@ GEM
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
oj (3.3.4)
openssl (2.0.4)
oj (3.1.0)
openssl (2.0.3)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
@@ -283,14 +267,14 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.11.2)
parallel_tests (2.14.2)
parallel_tests (2.14.1)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pg (0.20.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.4)
pkg-config (1.2.3)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -313,17 +297,17 @@ GEM
rack-test (0.6.3)
rack (>= 1.0)
rack-timeout (0.4.2)
rails (5.1.3)
actioncable (= 5.1.3)
actionmailer (= 5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
activemodel (= 5.1.3)
activerecord (= 5.1.3)
activesupport (= 5.1.3)
bundler (>= 1.3.0)
railties (= 5.1.3)
rails (5.1.1)
actioncable (= 5.1.1)
actionmailer (= 5.1.1)
actionpack (= 5.1.1)
actionview (= 5.1.1)
activejob (= 5.1.1)
activemodel (= 5.1.1)
activerecord (= 5.1.1)
activesupport (= 5.1.1)
bundler (>= 1.3.0, < 2.0)
railties (= 5.1.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -337,11 +321,11 @@ GEM
rails-i18n (5.0.4)
i18n (~> 0.7)
railties (~> 5.0)
rails-settings-cached (0.6.6)
rails-settings-cached (0.6.5)
rails (>= 4.2.0)
railties (5.1.3)
actionpack (= 5.1.3)
activesupport (= 5.1.3)
railties (5.1.1)
actionpack (= 5.1.1)
activesupport (= 5.1.1)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -353,7 +337,7 @@ GEM
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.3)
redis-activesupport (5.0.2)
activesupport (>= 3, < 6)
redis-store (~> 1.3.0)
redis-namespace (1.5.3)
@@ -389,7 +373,7 @@ GEM
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-support (~> 3.6.0)
rspec-sidekiq (3.0.3)
rspec-sidekiq (3.0.1)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.6.0)
@@ -410,10 +394,10 @@ GEM
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.24)
scss_lint (0.54.0)
scss_lint (0.53.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
sidekiq (5.0.4)
sidekiq (5.0.2)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
@@ -421,12 +405,12 @@ GEM
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.8)
sidekiq-scheduler (2.1.5)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.9)
sidekiq-unique-jobs (5.0.8)
sidekiq (>= 4.0, <= 6.0)
thor (~> 0)
simple-navigation (4.0.5)
@@ -450,15 +434,15 @@ GEM
sshkit (1.13.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-instrument (2.1.4)
statsd-instrument (2.1.2)
temple (0.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4)
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.8)
twitter-text (1.14.7)
tilt (2.0.7)
twitter-text (1.14.5)
unf (~> 0.1.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
@@ -469,7 +453,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicode-display_width (1.2.1)
uniform_notifier (1.10.0)
warden (1.2.7)
rack (>= 1.0)
@@ -481,9 +465,6 @@ GEM
activesupport (>= 4.2)
multi_json (~> 1.2)
railties (>= 4.2)
webpush (0.3.2)
hkdf (~> 0.2)
jwt
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -494,7 +475,6 @@ PLATFORMS
ruby
DEPENDENCIES
active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
@@ -503,7 +483,6 @@ DEPENDENCIES
binding_of_caller (~> 0.7)
bootsnap
brakeman (~> 3.6)
browser
bullet (~> 5.5)
bundler-audit (~> 0.5)
capistrano (~> 3.8)
@@ -511,7 +490,6 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 2.14)
charlock_holmes (~> 0.7.3)
cld3 (~> 3.1)
climate_control (~> 0.2)
devise (~> 4.2)
@@ -522,7 +500,7 @@ DEPENDENCIES
faker (~> 1.7)
fast_blank (~> 1.0)
fuubar (~> 2.2)
goldfinger (~> 2.0)
goldfinger (~> 1.2)
hamlit-rails (~> 0.2)
hiredis (~> 0.6)
htmlentities (~> 4.3)
@@ -530,15 +508,12 @@ DEPENDENCIES
http_accept_language (~> 2.1)
httplog (~> 0.99)
i18n-tasks (~> 0.9)
idn-ruby
kaminari (~> 1.0)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
link_header (~> 0.0)
lograge (~> 0.5)
mario-redis-lock (~> 1.2)
microformats (~> 4.0)
mime-types (~> 3.1)
microformats2 (~> 3.0)
nokogiri (~> 1.7)
oj (~> 3.0)
ostatus2 (~> 2.0)
@@ -584,10 +559,9 @@ DEPENDENCIES
uglifier (~> 3.2)
webmock (~> 3.0)
webpacker (~> 2.0)
webpush
RUBY VERSION
ruby 2.4.1p111
BUNDLED WITH
1.15.3
1.15.1

View File

@@ -1,10 +1,5 @@
# Mastodon Glitch Edition #
> Now with automated deploys!
[![Build Status](https://travis-ci.org/glitch-soc/mastodon.svg?branch=master)](https://travis-ci.org/glitch-soc/mastodon)
Mastodon Glitch Edition
========
Now with automated deploys!
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?
- 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/).

7
Vagrantfile vendored
View File

@@ -35,8 +35,6 @@ sudo apt-get install \
postgresql-contrib \
protobuf-compiler \
yarn \
libicu-dev \
libidn11-dev \
libprotobuf-dev \
libreadline-dev \
-y
@@ -44,12 +42,9 @@ sudo apt-get install \
# Install rvm
read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
curl -sSL https://get.rvm.io | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm
# Install Ruby
rvm install ruby-$RUBY_VERSION
# Configure database
sudo -u postgres createuser -U postgres vagrant -s
sudo -u postgres createdb -U postgres mastodon_development

View File

@@ -2,7 +2,7 @@
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite.png",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
"env": {
"HEROKU": {
"description": "Leave this as true",

View File

@@ -2,12 +2,9 @@
class AboutController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
before_action :set_instance_presenter, only: [:show, :more]
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def show; end
def more; end
@@ -18,7 +15,6 @@ class AboutController < ApplicationController
def new_user
User.new.tap(&:build_account)
end
helper_method :new_user
def set_instance_presenter
@@ -28,11 +24,4 @@ class AboutController < ApplicationController
def set_body_classes
@body_classes = 'about-body'
end
def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end

View File

@@ -2,7 +2,6 @@
class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
def show
respond_to do |format|
@@ -13,12 +12,10 @@ class AccountsController < ApplicationController
format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end
format.json do
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end
format.activitystreams2
end
end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController
before_action :set_account
def show
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter
ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account),
type: :ordered,
size: @account.statuses_count,
items: @statuses
)
end
end

View File

@@ -22,8 +22,8 @@ module Admin
end
def redownload
@account.reset_avatar!
@account.reset_header!
@account.avatar = @account.avatar_remote_url
@account.header = @account.header_remote_url
@account.save!
redirect_to admin_account_path(@account.id)

View File

@@ -6,26 +6,15 @@ module Admin
@instances = ordered_instances
end
def resubscribe
params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path
end
private
def paginated_instances
Account.remote.by_domain_accounts.page(params[:page])
end
helper_method :paginated_instances
def ordered_instances
paginated_instances.map { |account| Instance.new(account) }
end
def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain])
end
end
end

View File

@@ -5,14 +5,7 @@ module Admin
include Authorization
before_action :set_report
before_action :set_status, only: [:update, :destroy]
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
end
before_action :set_status
def update
@status.update(status_params)
@@ -22,7 +15,7 @@ module Admin
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
render json: @status
redirect_to admin_report_path(@report)
end
private
@@ -31,10 +24,6 @@ module Admin
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: [])
end
def set_report
@report = Report.find(params[:report_id])
end

View File

@@ -8,9 +8,7 @@ module Admin
@reports = filtered_reports.page(params[:page])
end
def show
@form = Form::StatusBatch.new
end
def show; end
def update
process_report

View File

@@ -8,21 +8,13 @@ module Admin
site_title
site_description
site_extended_description
site_terms
open_registrations
closed_registrations_message
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(open_registrations).freeze
def edit
@admin_settings = Form::AdminSettings.new
@settings = Setting.all_as_records
end
def update
@@ -31,19 +23,19 @@ module Admin
setting.update(value: value_for_update(key, value))
end
flash[:notice] = I18n.t('generic.changes_saved_msg')
flash[:notice] = 'Success!'
redirect_to edit_admin_settings_path
end
private
def settings_params
params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
params.permit(ADMIN_SETTINGS)
end
def value_for_update(key, value)
if BOOLEAN_SETTINGS.include?(key)
value == '1'
value == 'true'
else
value
end

View File

@@ -1,69 +0,0 @@
# frozen_string_literal: true
module Admin
class StatusesController < BaseController
include Authorization
helper_method :current_params
before_action :set_account
before_action :set_status, only: [:update, :destroy]
PAR_PAGE = 20
def index
@statuses = @account.statuses
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PAR_PAGE)
@form = Form::StatusBatch.new
end
def create
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def update
@status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
render json: @status
end
private
def status_params
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: [])
end
def set_status
@status = @account.statuses.find(params[:id])
end
def set_account
@account = Account.find(params[:account_id])
end
def current_params
page = (params[:page] || 1).to_i
{
media: params[:media],
page: page > 1 && page,
}.select { |_, value| value.present? }
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
class Api::ActivityPub::ActivitiesController < Api::BaseController
include Authorization
# before_action :set_follow, only: [:show_follow]
before_action :set_status, only: [:show_status]
respond_to :activitystreams2
# Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity.
def show_status
authorize @status, :show?
if @status.reblog?
render :show_status_announce
else
render :show_status_create
end
end
private
def set_status
@status = Status.find(params[:id])
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
class Api::ActivityPub::NotesController < Api::BaseController
include Authorization
before_action :set_status
respond_to :activitystreams2
def show
authorize @status, :show?
end
private
def set_status
@status = Status.find(params[:id])
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
class Api::ActivityPub::OutboxController < Api::BaseController
before_action :set_account
respond_to :activitystreams2
def show
if params[:max_id] || params[:since_id]
show_outbox_page
else
show_base_outbox
end
end
private
def show_base_outbox
@statuses = Status.as_outbox_timeline(@account)
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_first_last_page(@statuses)
render :show
end
def show_outbox_page
all_statuses = Status.as_outbox_timeline(@account)
@statuses = all_statuses.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
all_statuses = cache_collection(all_statuses)
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_first_last_page(all_statuses)
@next_page_url = api_activitypub_outbox_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
@prev_page_url = api_activitypub_outbox_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
@paginated = @next_page_url || @prev_page_url
@part_of_url = api_activitypub_outbox_url
set_pagination_headers(@next_page_url, @prev_page_url)
render :show_page
end
def cache_collection(raw)
super(raw, Status)
end
def set_account
@account = Account.find(params[:id])
end
def set_first_last_page(statuses) # rubocop:disable Style/AccessorMethodName
return if statuses.empty?
@first_page_url = api_activitypub_outbox_url(max_id: statuses.first.id + 1)
@last_page_url = api_activitypub_outbox_url(since_id: statuses.last.id - 1)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
end

View File

@@ -17,7 +17,11 @@ class Api::BaseController < ApplicationController
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
rescue_from Goldfinger::Error do
render json: { error: 'Remote account could not be resolved' }, status: 422
end
rescue_from HTTP::Error do
render json: { error: 'Remote data could not be fetched' }, status: 503
end

View File

@@ -5,7 +5,8 @@ class Api::OEmbedController < Api::BaseController
def show
@stream_entry = find_stream_entry.stream_entry
render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
@width = maxwidth_or_default
@height = maxheight_or_default
end
private

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
@@ -13,7 +11,7 @@ class Api::PushController < Api::BaseController
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
@@ -59,10 +57,6 @@ class Api::PushController < Api::BaseController
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end

View File

@@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 1.day).to_i.seconds
(params['hub.lease_seconds'] || 86_400).to_i.seconds
end
def set_account

View File

@@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def show
@account = current_account
render json: @account, serializer: REST::CredentialAccountSerializer
render 'api/v1/accounts/show'
end
def update
current_account.update!(account_params)
@account = current_account
render json: @account, serializer: REST::CredentialAccountSerializer
render 'api/v1/accounts/show'
end
private

View File

@@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
render 'api/v1/accounts/index'
end
private

View File

@@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
render 'api/v1/accounts/index'
end
private

View File

@@ -8,15 +8,16 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
def index
@accounts = Account.where(id: account_ids).select('id')
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
@following = Account.following_map(account_ids, current_user.account_id)
@followed_by = Account.followed_by_map(account_ids, current_user.account_id)
@blocking = Account.blocking_map(account_ids, current_user.account_id)
@muting = Account.muting_map(account_ids, current_user.account_id)
@requested = Account.requested_map(account_ids, current_user.account_id)
@domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id)
end
private
def relationships
AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
end
def account_ids
@_account_ids ||= Array(params[:id]).map(&:to_i)
end

View File

@@ -8,7 +8,8 @@ class Api::V1::Accounts::SearchController < Api::BaseController
def show
@accounts = account_search
render json: @accounts, each_serializer: REST::AccountSerializer
render 'api/v1/accounts/index'
end
private

View File

@@ -9,7 +9,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
@@ -19,7 +18,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def load_statuses
cached_account_statuses
cached_account_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_account_statuses

View File

@@ -8,38 +8,49 @@ class Api::V1::AccountsController < Api::BaseController
respond_to :json
def show
render json: @account, serializer: REST::AccountSerializer
end
def show; end
def follow
FollowService.new.call(current_user.account, @account.acct)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
set_relationship
render :relationship
end
def block
BlockService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
@following = { @account.id => false }
@followed_by = { @account.id => false }
@blocking = { @account.id => true }
@requested = { @account.id => false }
@muting = { @account.id => current_account.muting?(@account.id) }
@domain_blocking = { @account.id => current_account.domain_blocking?(@account.domain) }
render :relationship
end
def mute
MuteService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
set_relationship
render :relationship
end
def unfollow
UnfollowService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
set_relationship
render :relationship
end
def unblock
UnblockService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
set_relationship
render :relationship
end
def unmute
UnmuteService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
set_relationship
render :relationship
end
private
@@ -48,7 +59,12 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id])
end
def relationships
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
def set_relationship
@following = Account.following_map([@account.id], current_user.account_id)
@followed_by = Account.followed_by_map([@account.id], current_user.account_id)
@blocking = Account.blocking_map([@account.id], current_user.account_id)
@muting = Account.muting_map([@account.id], current_user.account_id)
@requested = Account.requested_map([@account.id], current_user.account_id)
@domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id)
end
end

View File

@@ -5,7 +5,6 @@ class Api::V1::AppsController < Api::BaseController
def create
@app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer
end
private

View File

@@ -9,7 +9,6 @@ class Api::V1::BlocksController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
private

View File

@@ -9,18 +9,21 @@ class Api::V1::FavouritesController < Api::BaseController
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_favourites
cached_favourites.tap do |statuses|
set_maps(statuses)
end
end
def cached_favourites
cache_collection(
Status.reorder(nil).joins(:favourites).merge(results),
Status.where(
id: results.map(&:status_id)
),
Status
)
end

View File

@@ -7,7 +7,6 @@ class Api::V1::FollowRequestsController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
def authorize

View File

@@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
render json: @account, serializer: REST::AccountSerializer
render :show
end
private

View File

@@ -3,7 +3,5 @@
class Api::V1::InstancesController < Api::BaseController
respond_to :json
def show
render json: {}, serializer: REST::InstanceSerializer
end
def show; end
end

View File

@@ -11,7 +11,6 @@ class Api::V1::MediaController < Api::BaseController
def create
@media = current_account.media_attachments.create!(file: media_params[:file])
render json: @media, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
rescue Paperclip::Error

View File

@@ -9,7 +9,6 @@ class Api::V1::MutesController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
private

View File

@@ -11,12 +11,11 @@ class Api::V1::NotificationsController < Api::BaseController
def index
@notifications = load_notifications
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
set_maps_for_notification_target_statuses
end
def show
@notification = current_account.notifications.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer
end
def clear
@@ -24,20 +23,11 @@ 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
@@ -56,6 +46,10 @@ class Api::V1::NotificationsController < Api::BaseController
current_account.notifications.browserable(exclude_types)
end
def set_maps_for_notification_target_statuses
set_maps target_statuses_from_notifications
end
def target_statuses_from_notifications
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end

View File

@@ -9,7 +9,6 @@ class Api::V1::ReportsController < Api::BaseController
def index
@reports = current_account.reports
render json: @reports, each_serializer: REST::ReportSerializer
end
def create
@@ -18,10 +17,7 @@ class Api::V1::ReportsController < Api::BaseController
status_ids: reported_status_ids,
comment: report_params[:comment]
)
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer
render :show
end
private

View File

@@ -1,16 +1,12 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
RESULTS_LIMIT = 10
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
RESULTS_LIMIT = 5
respond_to :json
def index
@search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer
@search = OpenStruct.new(search_results)
end
private

View File

@@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
render 'api/v1/statuses/accounts'
end
private

View File

@@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
def create
@status = favourited_status
render json: @status, serializer: REST::StatusSerializer
render 'api/v1/statuses/show'
end
def destroy
@@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, favourites_map: @favourites_map)
render 'api/v1/statuses/show'
end
private

View File

@@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController
current_account.mute_conversation!(@conversation)
@mutes_map = { @conversation.id => true }
render json: @status, serializer: REST::StatusSerializer
render 'api/v1/statuses/show'
end
def destroy
current_account.unmute_conversation!(@conversation)
@mutes_map = { @conversation.id => false }
render json: @status, serializer: REST::StatusSerializer
render 'api/v1/statuses/show'
end
private

View File

@@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
render 'api/v1/statuses/accounts'
end
private

View File

@@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
def create
@status = ReblogService.new.call(current_user.account, status_for_reblog)
render json: @status, serializer: REST::StatusSerializer
render 'api/v1/statuses/show'
end
def destroy
@@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
authorize status_for_destroy, :unreblog?
RemovalWorker.perform_async(status_for_destroy.id)
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
render 'api/v1/statuses/show'
end
private

View File

@@ -13,7 +13,6 @@ class Api::V1::StatusesController < Api::BaseController
def show
cached = Rails.cache.read(@status.cache_key)
@status = cached unless cached.nil?
render json: @status, serializer: REST::StatusSerializer
end
def context
@@ -22,20 +21,15 @@ class Api::V1::StatusesController < Api::BaseController
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
@context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context[:ancestors] + @context[:descendants]
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
set_maps(statuses)
end
def card
@card = PreviewCard.find_by(status: @status)
if @card.nil?
render_empty
else
render json: @card, serializer: REST::PreviewCardSerializer
end
render_empty if @card.nil?
end
def create
@@ -49,7 +43,7 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application,
idempotency: request.headers['Idempotency-Key'])
render json: @status, serializer: REST::StatusSerializer
render :show
end
def destroy

View File

@@ -9,13 +9,15 @@ class Api::V1::Timelines::HomeController < Api::BaseController
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
render 'api/v1/timelines/show'
end
private
def load_statuses
cached_home_statuses
cached_home_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_home_statuses

View File

@@ -7,13 +7,15 @@ class Api::V1::Timelines::PublicController < Api::BaseController
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
render 'api/v1/timelines/show'
end
private
def load_statuses
cached_public_statuses
cached_public_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_public_statuses

View File

@@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
render 'api/v1/timelines/show'
end
private
@@ -18,7 +18,9 @@ class Api::V1::Timelines::TagController < Api::BaseController
end
def load_statuses
cached_tagged_statuses
cached_tagged_statuses.tap do |statuses|
set_maps(statuses)
end
end
def cached_tagged_statuses

View File

@@ -1,52 +0,0 @@
# frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::BaseController
respond_to :json
before_action :require_user!
def create
params.require(:subscription).require(:endpoint)
params.require(:subscription).require(:keys).require([:auth, :p256dh])
active_session = current_session
unless active_session.web_push_subscription.nil?
active_session.web_push_subscription.destroy!
active_session.update!(web_push_subscription: nil)
end
# Mobile devices do not support regular notifications, so we enable push notifications by default
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
data = {
alerts: {
follow: alerts_enabled,
favourite: alerts_enabled,
reblog: alerts_enabled,
mention: alerts_enabled,
},
}
web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh],
key_auth: params[:subscription][:keys][:auth],
data: data
)
active_session.update!(web_push_subscription: web_subscription)
render json: web_subscription.as_payload
end
def update
params.require([:id, :data])
web_subscription = ::Web::PushSubscription.find(params[:id])
web_subscription.update!(data: params[:data])
render json: web_subscription.as_payload
end
end

View File

@@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base
include UserTrackingConcern
helper_method :current_account
helper_method :current_session
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
@@ -43,10 +42,6 @@ class ApplicationController < ActionController::Base
forbidden if current_user.account.suspended?
end
def after_sign_out_path_for(_resource_or_scope)
new_user_session_path
end
protected
def forbidden
@@ -73,10 +68,6 @@ class ApplicationController < ActionController::Base
@current_account ||= current_user.try(:account)
end
def current_session
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)

View File

@@ -1,20 +1,5 @@
# frozen_string_literal: true
class Auth::PasswordsController < Devise::PasswordsController
before_action :check_validity_of_reset_password_token, only: :edit
layout 'auth'
private
def check_validity_of_reset_password_token
unless reset_password_token_is_valid?
flash[:error] = I18n.t('auth.invalid_reset_password_token')
redirect_to new_password_path(resource_name)
end
end
def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present?
end
end

View File

@@ -5,7 +5,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
def destroy
not_found
@@ -42,8 +41,4 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth'
end
def set_sessions
@sessions = current_user.session_activations
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class AuthorizeFollowsController < ApplicationController
layout 'modal'
layout 'public'
before_action :authenticate_user!
@@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController
if @account.nil?
render :error
else
render :success
redirect_to web_url("accounts/#{@account.id}")
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render :error

View File

@@ -1,87 +0,0 @@
# frozen_string_literal: true
# Implemented according to HTTP signatures (Draft 6)
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
module SignatureVerification
extend ActiveSupport::Concern
def signed_request?
request.headers['Signature'].present?
end
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
unless signed_request?
@signed_request_account = nil
return
end
raw_signature = request.headers['Signature']
signature_params = {}
raw_signature.split(',').each do |part|
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
next if parsed_parts.nil? || parsed_parts.size != 3
signature_params[parsed_parts[1]] = parsed_parts[2]
end
if incompatible_signature?(signature_params)
@signed_request_account = nil
return
end
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
if account.nil?
@signed_request_account = nil
return
end
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(signature_params['headers'])
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
else
@signed_request_account = nil
end
end
private
def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank?
signed_headers.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
end.join("\n")
end
def matches_time_window?
begin
time_sent = DateTime.httpdate(request.headers['Date'])
rescue ArgumentError
return false
end
(Time.now.utc - time_sent).abs <= 30
end
def to_header_name(name)
name.split(/-/).map(&:capitalize).join('-')
end
def incompatible_signature?(signature_params)
signature_params['keyId'].blank? ||
signature_params['signature'].blank? ||
signature_params['algorithm'].blank? ||
signature_params['algorithm'] != 'rsa-sha256' ||
!signature_params['keyId'].start_with?('acct:')
end
end

View File

@@ -5,24 +5,5 @@ class FollowerAccountsController < ApplicationController
def index
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account),
type: :ordered,
size: @account.followers_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
)
end
end

View File

@@ -5,24 +5,5 @@ class FollowingAccountsController < ApplicationController
def index
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account),
type: :ordered,
size: @account.following_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
)
end
end

View File

@@ -2,11 +2,14 @@
class HomeController < ApplicationController
before_action :authenticate_user!
before_action :set_initial_state_json
def index
@body_classes = 'app-body'
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
@body_classes = 'app-body'
@token = find_or_create_access_token.token
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
@admin = Account.find_local(Setting.site_contact_username)
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
end
private
@@ -15,18 +18,13 @@ class HomeController < ApplicationController
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
end
def set_initial_state_json
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
}
def find_or_create_access_token
Doorkeeper::AccessToken.find_or_create_for(
Doorkeeper::Application.where(superapp: true).first,
current_user.id,
Doorkeeper::OAuth::Scopes.from_string('read write follow'),
Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled?
)
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class RemoteFollowController < ApplicationController
layout 'modal'
layout 'public'
before_action :set_account
before_action :gone, if: :suspended_account?

View File

@@ -34,13 +34,9 @@ class Settings::PreferencesController < ApplicationController
def user_settings_params
params.require(:user).permit(
:setting_default_privacy,
:setting_default_sensitive,
:setting_unfollow_modal,
:setting_boost_modal,
:setting_delete_modal,
:setting_auto_play_gif,
:setting_system_font_ui,
:setting_noindex,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
)

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Settings::SessionsController < ApplicationController
before_action :set_session, only: :destroy
def destroy
@session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success')
redirect_to edit_user_registration_path
end
private
def set_session
@session = current_user.session_activations.find(params[:id])
end
end

View File

@@ -7,9 +7,7 @@ module Settings
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
def show
@confirmation = Form::TwoFactorConfirmation.new
end
def show; end
def create
current_user.otp_secret = User.generate_otp_secret(32)
@@ -18,30 +16,15 @@ module Settings
end
def destroy
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
@confirmation = Form::TwoFactorConfirmation.new
render :show
end
current_user.otp_required_for_login = false
current_user.save!
redirect_to settings_two_factor_authentication_path
end
private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:code)
end
def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
end
end
end

View File

@@ -11,22 +11,10 @@ class StatusesController < ApplicationController
before_action :check_account_suspension
def show
respond_to do |format|
format.html do
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
render 'stream_entries/show'
end
format.json do
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end
end
def activity
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
render 'stream_entries/show'
end
private

View File

@@ -2,7 +2,6 @@
class StreamEntriesController < ApplicationController
include Authorization
include SignatureVerification
layout 'public'
@@ -19,7 +18,7 @@ class StreamEntriesController < ApplicationController
end
format.atom do
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
end
end
end

View File

@@ -5,26 +5,7 @@ class TagsController < ApplicationController
def show
@tag = Tag.find_by!(name: params[:id].downcase)
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = cache_collection(@statuses, Status)
respond_to do |format|
format.html
format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
end
private
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag),
type: :ordered,
size: @tag.statuses.count,
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end
end

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Activitystreams2BuilderHelper
# Gets a usable name for an account, using display name or username.
def account_name(account)
account.display_name.presence || account.username
end
end

View File

@@ -6,21 +6,15 @@ module Admin::FilterHelper
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)
new_class = filtered_url_for(link_class_params)
link_to text, new_url, class: filter_link_class(new_class)
def filter_link_to(text, more_params)
new_url = filtered_url_for(more_params)
link_to text, new_url, class: filter_link_class(new_url)
end
def table_link_to(icon, text, path, options = {})
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
end
def selected?(more_params)
new_url = filtered_url_for(more_params)
filter_link_class(new_url) == 'selected' ? true : false
end
private
def filter_params(more_params)

View File

@@ -31,11 +31,7 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || []
class_names << 'fa'
class_names += icon.split(' ').map { |cl| "fa-#{cl}" }
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
def fa_icon(icon)
content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' '))
end
end

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
module EmojiHelper
def emojify(text)
return text if text.blank?
text.gsub(emoji_pattern) do |match|
emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
if emoji
emoji
else
match
end
end
end
def emoji_pattern
@emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
(#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
(?=[^[:alnum:]:]|$)/x
end
end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module HttpHelper
def http_client(options = {})
timeout = { write: 10, connect: 10, read: 10 }.merge(options)
HTTP.headers(user_agent: user_agent)
.timeout(:per_operation, timeout)
.follow
end
private
def user_agent
@user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +http://#{Rails.configuration.x.local_domain}/)"
end
end

View File

@@ -2,7 +2,7 @@
module InstanceHelper
def site_title
Setting.site_title.presence || site_hostname
Setting.site_title.to_s
end
def site_hostname

View File

@@ -11,7 +11,7 @@ module RoutingHelper
end
end
def full_asset_url(source, options = {})
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source, options)).to_s
def full_asset_url(source)
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source)).to_s
end
end

View File

@@ -19,7 +19,6 @@ module SettingsHelper
io: 'Ido',
it: 'Italiano',
ja: '日本語',
ko: '한국어',
nl: 'Nederlands',
no: 'Norsk',
oc: 'Occitan',
@@ -42,16 +41,4 @@ module SettingsHelper
def hash_to_object(hash)
HashObject.new(hash)
end
def session_device_icon(session)
device = session.detection.device
if device.mobile?
'mobile'
elsif device.tablet?
'tablet'
else
'desktop'
end
end
end

View File

@@ -1,93 +0,0 @@
/*
`actions/local_settings`
========================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
This file provides our Redux actions related to local settings. It
consists of the following:
- __`changesLocalSetting(key, value)` :__
Changes the local setting with the given `key` to the given
`value`. `key` **MUST** be an array of strings, as required by
`Immutable.Map.prototype.getIn()`.
- __`saveLocalSettings()` :__
Saves the local settings to `localStorage` as a JSON object. We
shouldn't ever need to call this ourselves.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Constants:
----------
We provide the following constants:
- __`LOCAL_SETTING_CHANGE` :__
This string constant is used to dispatch a setting change to our
reducer in `reducers/local_settings`, where the setting is
actually changed.
*/
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
`changeLocalSetting(key, value)`:
---------------------------------
Changes the local setting with the given `key` to the given `value`.
`key` **MUST** be an array of strings, as required by
`Immutable.Map.prototype.getIn()`.
To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our
reducer in `reducers/local_settings`.
*/
export function changeLocalSetting(key, value) {
return dispatch => {
dispatch({
type: LOCAL_SETTING_CHANGE,
key,
value,
});
dispatch(saveLocalSettings());
};
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
`saveLocalSettings()`:
----------------------
Saves the local settings to `localStorage` as a JSON object.
`changeLocalSetting()` calls this whenever it changes a setting. We
shouldn't ever need to call this ourselves.
> __TODO :__
> Right now `saveLocalSettings()` doesn't keep track of which user
> is currently signed in, but it might be better to give each user
> their *own* local settings.
*/
export function saveLocalSettings() {
return (_, getState) => {
const localSettings = getState().get('local_settings').toJS();
localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
};
};

View File

@@ -1,237 +0,0 @@
/*
`<AccountHeader>`
=================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
Original file by @gargron@mastodon.social et al as part of
tootsuite/mastodon. We've expanded it in order to handle user bio
frontmatter.
The `<AccountHeader>` component provides the header for account
timelines. It is a fairly simple component which mostly just consists
of a `render()` method.
__Props:__
- __`account` (`ImmutablePropTypes.map`) :__
The account to render a header for.
- __`me` (`PropTypes.number.isRequired`) :__
The id of the currently-signed-in account.
- __`onFollow` (`PropTypes.func.isRequired`) :__
The function to call when the user clicks the "follow" button.
- __`intl` (`PropTypes.object.isRequired`) :__
Our internationalization object, inserted by `@injectIntl`.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports //
import emojify from '../../../mastodon/emoji';
import IconButton from '../../../mastodon/components/icon_button';
import Avatar from '../../../mastodon/components/avatar';
// Our imports //
import { processBio } from '../../util/bio_metadata';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Inital setup:
-------------
The `messages` constant is used to define any messages that we need
from inside props. In our case, these are the `unfollow`, `follow`, and
`requested` messages used in the `title` of our buttons.
*/
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Implementation:
---------------
*/
@injectIntl
export default class AccountHeader extends ImmutablePureComponent {
static propTypes = {
account : ImmutablePropTypes.map,
me : PropTypes.number.isRequired,
onFollow : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
/*
### `render()`
The `render()` function is used to render our component.
*/
render () {
const { account, me, intl } = this.props;
/*
If no `account` is provided, then we can't render a header. Otherwise,
we get the `displayName` for the account, if available. If it's blank,
then we set the `displayName` to just be the `username` of the account.
*/
if (!account) {
return null;
}
let displayName = account.get('display_name');
let info = '';
let actionBtn = '';
let following = false;
if (displayName.length === 0) {
displayName = account.get('username');
}
/*
Next, we handle the account relationships. If the account follows the
user, then we add an `info` message. If the user has requested a
follow, then we disable the `actionBtn` and display an hourglass.
Otherwise, if the account isn't blocked, we set the `actionBtn` to the
appropriate icon.
*/
if (me !== account.get('id')) {
if (account.getIn(['relationship', 'followed_by'])) {
info = (
<span className='account--follows-info'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
);
}
if (account.getIn(['relationship', 'requested'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
</div>
);
} else if (!account.getIn(['relationship', 'blocking'])) {
following = account.getIn(['relationship', 'following']);
actionBtn = (
<div className='account--action-button'>
<IconButton
size={26}
icon={following ? 'user-times' : 'user-plus'}
active={following}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.props.onFollow}
/>
</div>
);
}
}
/*
`displayNameHTML` processes the `displayName` and prepares it for
insertion into the document. Meanwhile, we extract the `text` and
`metadata` from our account's `note` using `processBio()`.
*/
const displayNameHTML = {
__html : emojify(escapeTextContentForBrowser(displayName)),
};
const { text, metadata } = processBio(account.get('note'));
/*
Here, we render our component using all the things we've defined above.
*/
return (
<div className='account__header__wrapper'>
<div
className='account__header'
style={{ backgroundImage: `url(${account.get('header')})` }}
>
<div>
<a href={account.get('url')} target='_blank' rel='noopener'>
<span className='account__header__avatar'>
<Avatar account={account} size={90} />
</span>
<span
className='account__header__display-name'
dangerouslySetInnerHTML={displayNameHTML}
/>
</a>
<span className='account__header__username'>
@{account.get('acct')}
{account.get('locked') ? <i className='fa fa-lock' /> : null}
</span>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
{info}
{actionBtn}
</div>
</div>
{metadata.length && (
<table className='account__metadata'>
<tbody>
{(() => {
let data = [];
for (let i = 0; i < metadata.length; i++) {
data.push(
<tr key={i}>
<th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th>
<td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td>
</tr>
);
}
return data;
})()}
</tbody>
</table>
) || null}
</div>
);
}
}

View File

@@ -1,113 +0,0 @@
// <CommonAvatar>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/avatar
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonAvatar extends React.PureComponent {
// Props and state.
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
circular: PropTypes.bool,
className: PropTypes.string,
comrade: ImmutablePropTypes.map,
}
state = {
hovering: false,
}
// Starts or stops animation on hover.
handleMouseEnter = () => {
if (this.props.animate) return;
this.setState({ hovering: true });
}
handleMouseLeave = () => {
if (this.props.animate) return;
this.setState({ hovering: false });
}
// Renders the component.
render () {
const {
handleMouseEnter,
handleMouseLeave,
} = this;
const {
account,
animate,
circular,
className,
comrade,
...others
} = this.props;
const { hovering } = this.state;
const computedClass = classNames('glitch', 'glitch__common__avatar', {
_circular: circular,
}, className);
// We store the image srcs here for later.
const src = account.get('avatar');
const staticSrc = account.get('avatar_static');
const comradeSrc = comrade ? comrade.get('avatar') : null;
const comradeStaticSrc = comrade ? comrade.get('avatar_static') : null;
// Avatars are a straightforward div with image(s) inside.
return comrade ? (
<div
className={computedClass}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...others}
>
<img
className='avatar\main'
src={hovering || animate ? src : staticSrc}
alt=''
/>
<img
className='avatar\comrade'
src={hovering || animate ? comradeSrc : comradeStaticSrc}
alt=''
/>
</div>
) : (
<div
className={computedClass}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...others}
>
<img
className='avatar\solo'
src={hovering || animate ? src : staticSrc}
alt=''
/>
</div>
);
}
}

View File

@@ -1,41 +0,0 @@
@import 'variables';
.glitch.glitch__common__avatar {
display: inline-block;
position: relative;
& > img {
display: block;
position: static;
margin: 0;
border-radius: $ui-avatar-border-size;
width: 100%;
height: 100%;
&.avatar\\comrade {
position: absolute;
right: 0;
bottom: 0;
width: 50%;
height: 50%;
}
&.avatar\\main {
margin: 0 30% 30% 0;
width: 70%;
height: 70%;
}
}
&._circular {
& > img {
transition: border-radius ($glitch-animation-speed * .3s);
}
&:not(:hover) {
& > img {
border-radius: 50%;
}
}
}
}

View File

@@ -1,146 +0,0 @@
// <CommonButton>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/button
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
// Our imports.
import CommonLink from 'glitch/components/common/link';
import CommonIcon from 'glitch/components/common/icon';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonButton extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
animate: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
href: PropTypes.string,
icon: PropTypes.string,
onClick: PropTypes.func,
showTitle: PropTypes.bool,
title: PropTypes.string,
}
state = {
loaded: false,
}
// The `loaded` state property activates our animations. We wait
// until an activation change in order to prevent unsightly
// animations when the component first mounts.
componentWillReceiveProps (nextProps) {
const { active } = this.props;
// The double "not"s here cast both arguments to booleans.
if (!nextProps.active !== !active) this.setState({ loaded: true });
}
handleClick = (e) => {
const { onClick } = this.props;
if (!onClick) return;
onClick(e);
e.preventDefault();
}
// Rendering the component.
render () {
const { handleClick } = this;
const {
active,
animate,
children,
className,
disabled,
href,
icon,
onClick,
showTitle,
title,
...others
} = this.props;
const { loaded } = this.state;
const computedClass = classNames('glitch', 'glitch__common__button', className, {
_active: active && !href, // Links can't be active
_animated: animate && loaded,
_disabled: disabled,
_link: href,
_star: icon === 'star',
'_with-text': children || title && showTitle,
});
let conditionalProps = {};
// If href is provided, we render a link.
if (href) {
if (!disabled && href) conditionalProps.href = href;
if (title && !showTitle) {
if (!children) conditionalProps.title = title;
else conditionalProps['aria-label'] = title;
}
if (onClick) {
if (!disabled) conditionalProps.onClick = handleClick;
else conditionalProps['aria-disabled'] = true;
conditionalProps.role = 'button';
conditionalProps.tabIndex = 0;
}
return (
<CommonLink
className={computedClass}
{...conditionalProps}
{...others}
>
{children}
{title && showTitle ? <span className='button\title'>{title}</span> : null}
<CommonIcon name={icon} className='button\icon' />
</CommonLink>
);
// Otherwise, we render a button.
} else {
if (active !== void 0) conditionalProps['aria-pressed'] = active;
if (title && !showTitle) {
if (!children) conditionalProps.title = title;
else conditionalProps['aria-label'] = title;
}
if (onClick && !disabled) {
conditionalProps.onClick = handleClick;
}
return (
<button
className={computedClass}
{...conditionalProps}
disabled={disabled}
{...others}
tabIndex='0'
type='button'
>
{children}
{title && showTitle ? <span className='button\title'>{title}</span> : null}
<CommonIcon name={icon} className='button\icon' />
</button>
);
}
};
}

View File

@@ -1,134 +0,0 @@
@import 'variables';
.glitch.glitch__common__button {
display: inline-block;
border: none;
padding: 0;
color: $ui-base-lighter-color;
background: transparent;
outline: thin transparent dotted;
font-size: inherit;
text-decoration: none;
cursor: pointer;
transition: color ($glitch-animation-speed * .15s) ease-in, outline-color ($glitch-animation-speed * .3s) ease-in-out;
&._animated .button\\icon {
animation-name: glitch__common__button__deactivate;
animation-duration: .9s;
animation-timing-function: ease-in-out;
@keyframes glitch__common__button__deactivate {
from {
transform: rotate(360deg);
}
57% {
transform: rotate(-60deg);
}
86% {
transform: rotate(30deg);
}
to {
transform: rotate(0deg);
}
}
}
&._active {
.button\\icon {
color: $ui-highlight-color;
}
&._animated .button\\icon {
animation-name: glitch__common__button__activate;
@keyframes glitch__common__button__activate {
from {
transform: rotate(0deg);
}
57% {
transform: rotate(420deg); // Blazin' 😎
}
86% {
transform: rotate(330deg);
}
to {
transform: rotate(360deg);
}
}
}
/*
The special `._star` class is given to buttons which have a star
icon (see JS). When they are active, we give them a gold star ⭐️.
*/
&._star .button\\icon {
color: $gold-star;
}
}
/*
For links, we consider them disabled if they don't have an `href`
attribute (see JS).
*/
&._disabled {
opacity: $glitch-disabled-opacity;
cursor: default;
}
/*
This is confusing becuase of the names, but the `color .3 ease-out`
transition is actually used when easing *in* to a hovering/active/
focusing state, and the default transition is used when leaving. Our
buttons are a little slower to glow than they are to fade.
*/
&:active,
&:focus,
&:hover {
color: $glitch-lighter-color;
transition: color ($glitch-animation-speed * .3s) ease-out, outline-color ($glitch-animation-speed * .15s) ease-in-out;
}
&:focus {
outline-color: currentColor;
}
/*
Buttons with text have a number of different styling rules and an
overall different appearance.
*/
&._with-text {
display: inline-block;
border: none;
border-radius: .35em;
padding: 0 .5em;
color: $glitch-texture-color;
background: $ui-base-lighter-color;
font-size: .75em;
font-weight: inherit;
text-transform: uppercase;
line-height: 1.6;
cursor: pointer;
vertical-align: baseline;
transition: background-color ($glitch-animation-speed * .15s) ease-in, outline-color ($glitch-animation-speed * .3s) ease-in-out;
.button\\icon {
display: inline-block;
font-size: 1.25em;
vertical-align: -.1em;
}
& > *:not(:first-child) {
margin: 0 0 0 .4em;
border-left: 1px solid currentColor;
padding: 0 0 0 .3em;
}
&:active,
&:hover,
&:focus {
color: $glitch-texture-color;
background: $glitch-lighter-color;
transition: background-color ($glitch-animation-speed * .3s) ease-out, outline-color ($glitch-animation-speed * .15s) ease-in-out;
}
}
}

View File

@@ -1,59 +0,0 @@
// <CommonIcon>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/icon
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
const CommonIcon = ({
className,
name,
proportional,
title,
...others
}) => name ? (
<span
className={classNames('glitch', 'glitch__common__icon', className)}
{...others}
>
<span
aria-hidden
className={`fa ${proportional ? '' : 'fa-fw'} fa-${name} icon\fa`}
{...(title ? { title } : {})}
/>
{title ? (
<span className='_for-screenreader'>{title}</span>
) : null}
</span>
) : null;
// Props.
CommonIcon.propTypes = {
className: PropTypes.string,
name: PropTypes.string,
proportional: PropTypes.bool,
title: PropTypes.string,
};
// Export.
export default CommonIcon;

View File

@@ -1,14 +0,0 @@
@import 'variables';
.glitch.glitch__common__icon {
display: inline-block;
._for-screenreader {
position: absolute;
margin: -1px -1px;
width: 1px;
height: 1px;
clip: rect(0, 0, 0, 0);
overflow: hidden;
}
}

View File

@@ -1,74 +0,0 @@
// <CommonLink>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/link
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
export default class CommonLink extends React.PureComponent {
// Props.
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
destination: PropTypes.string,
history: PropTypes.object,
href: PropTypes.string,
};
// We only reroute the link if it is an unadorned click, we have
// access to the router, and there is somewhere to reroute it *to*.
handleClick = (e) => {
const { destination, history } = this.props;
if (!history || !destination || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
history.push(destination);
e.preventDefault();
}
// Rendering.
render () {
const { handleClick } = this;
const { children, className, destination, history, href, ...others } = this.props;
const computedClass = classNames('glitch', 'glitch__common__link', className);
const conditionalProps = {};
if (href) {
conditionalProps.href = href;
conditionalProps.onClick = handleClick;
} else if (destination) {
conditionalProps.onClick = handleClick;
conditionalProps.role = 'link';
conditionalProps.tabIndex = 0;
} else conditionalProps.role = 'presentation';
return (
<a
className={computedClass}
{...conditionalProps}
{...others}
rel='noopener'
target='_blank'
>{children}</a>
);
}
}

View File

@@ -1,11 +0,0 @@
@import 'variables';
/*
Most link styling happens elsewhere but we disable text-decoration
here.
*/
.glitch.glitch__common__link {
display: inline;
color: $ui-secondary-color;
text-decoration: none;
}

View File

@@ -1,49 +0,0 @@
// <CommonSeparator>
// ========
// For code documentation, please see:
// https://glitch-soc.github.io/docs/javascript/glitch/common/separator
// For more information, please contact:
// @kibi@glitch.social
// * * * * * * * //
// Imports
// -------
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Stylesheet imports.
import './style';
// * * * * * * * //
// The component
// -------------
const CommonSeparator = ({
className,
visible,
...others
}) => visible ? (
<span
className={
classNames('glitch', 'glitch__common__separator', className)
}
{...others}
role='separator'
/> // Contents provided via CSS.
) : null;
// Props.
CommonSeparator.propTypes = {
className: PropTypes.string,
visible: PropTypes.bool,
};
// Export.
export default CommonSeparator;

View File

@@ -1,15 +0,0 @@
@import 'variables';
/*
The default contents for a separator is an interpunct, surrounded by
spaces. However, this can be changed using CSS selectors.
*/
.glitch.glitch__common__separator {
display: inline-block;
&::after {
display: inline-block;
padding: 0 .3em;
content: "·";
}
}

View File

@@ -1,66 +0,0 @@
/*
`<ComposeAdvancedOptionsContainer>`
===================================
This container connects `<ComposeAdvancedOptions>` to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports //
import { connect } from 'react-redux';
// Mastodon imports //
import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose';
// Our imports //
import ComposeAdvancedOptions from '.';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. The only property we care about is
`compose.advanced_options`.
*/
const mapStateToProps = state => ({
values: state.getIn(['compose', 'advanced_options']),
});
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We just need to provide a dispatch for
when an advanced option toggle changes.
*/
const mapDispatchToProps = dispatch => ({
onChange (option) {
dispatch(toggleComposeAdvancedOption(option));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions);

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