This commit is contained in:
Baptiste Lemoine 2019-03-04 09:51:46 +01:00
commit 6c86d67a7b
590 changed files with 19844 additions and 7381 deletions

View File

@ -3,7 +3,7 @@ version: 2
aliases: aliases:
- &defaults - &defaults
docker: docker:
- image: circleci/ruby:2.5.1-stretch-node - image: circleci/ruby:2.6.0-stretch-node
environment: &ruby_environment environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/ BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost DB_HOST: localhost
@ -98,21 +98,21 @@ jobs:
<<: *defaults <<: *defaults
<<: *install_steps <<: *install_steps
install-ruby2.6:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.5: install-ruby2.5:
<<: *defaults <<: *defaults
docker:
- image: circleci/ruby:2.5.3-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies <<: *install_ruby_dependencies
install-ruby2.4: install-ruby2.4:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.4.4-stretch-node - image: circleci/ruby:2.4.5-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby2.3:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
environment: *ruby_environment environment: *ruby_environment
<<: *install_ruby_dependencies <<: *install_ruby_dependencies
@ -128,43 +128,43 @@ jobs:
- ./mastodon/public/assets - ./mastodon/public/assets
- ./mastodon/public/packs-test/ - ./mastodon/public/packs-test/
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6.0-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:5.0.3-alpine3.8
<<: *test_steps
test-ruby2.5: test-ruby2.5:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.5.1-stretch-node - image: circleci/ruby:2.5.3-stretch-node
environment: *ruby_environment environment: *ruby_environment
- image: circleci/postgres:10.3-alpine - image: circleci/postgres:10.6-alpine
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine - image: circleci/redis:4.0.12-alpine
<<: *test_steps <<: *test_steps
test-ruby2.4: test-ruby2.4:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.4.4-stretch-node - image: circleci/ruby:2.4.5-stretch-node
environment: *ruby_environment environment: *ruby_environment
- image: circleci/postgres:10.3-alpine - image: circleci/postgres:10.6-alpine
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine - image: circleci/redis:4.0.12-alpine
<<: *test_steps
test-ruby2.3:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
<<: *test_steps <<: *test_steps
test-webui: test-webui:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/node:8.11.1-stretch - image: circleci/node:8.15.0-stretch
steps: steps:
- *attach_workspace - *attach_workspace
- run: ./bin/retry yarn test:jest - run: ./bin/retry yarn test:jest
@ -183,20 +183,24 @@ workflows:
build-and-test: build-and-test:
jobs: jobs:
- install - install
- install-ruby2.6:
requires:
- install
- install-ruby2.5: - install-ruby2.5:
requires: requires:
- install - install
- install-ruby2.6
- install-ruby2.4: - install-ruby2.4:
requires: requires:
- install - install
- install-ruby2.5 - install-ruby2.6
- install-ruby2.3:
requires:
- install
- install-ruby2.5
- build: - build:
requires: requires:
- install-ruby2.5 - install-ruby2.6
- test-ruby2.6:
requires:
- install-ruby2.6
- build
- test-ruby2.5: - test-ruby2.5:
requires: requires:
- install-ruby2.5 - install-ruby2.5
@ -205,13 +209,9 @@ workflows:
requires: requires:
- install-ruby2.4 - install-ruby2.4
- build - build
- test-ruby2.3:
requires:
- install-ruby2.3
- build
- test-webui: - test-webui:
requires: requires:
- install - install
- check-i18n: - check-i18n:
requires: requires:
- install-ruby2.5 - install-ruby2.6

View File

@ -27,7 +27,7 @@ plugins:
enabled: true enabled: true
eslint: eslint:
enabled: true enabled: true
channel: eslint-4 channel: eslint-5
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-54 channel: rubocop-0-54

View File

@ -1,30 +1,13 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files. /build/**
# /coverage/**
# If you find yourself ignoring temporary files generated by your text editor /db/**
# or operating system, you probably want to add a global ignore instead: /lib/**
# git config --global core.excludesfile '~/.gitignore_global' /log/**
/node_modules/**
# Ignore bundler config. /nonobox/**
/.bundle /public/**
!/public/embed.js
# Ignore the default SQLite database. /spec/**
/db/*.sqlite3 /tmp/**
/db/*.sqlite3-journal /vendor/**
!.eslintrc.js
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
coverage
public/system
public/assets
.env
.env.production
node_modules/
neo4j/
# Ignore Vagrant files
.vagrant/
# Ignore Capistrano customizations
config/deploy/*

204
.eslintrc.js Normal file
View File

@ -0,0 +1,204 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
jest: true,
},
globals: {
ATTACHMENT_HOST: false,
},
parser: 'babel-eslint',
plugins: [
'react',
'jsx-a11y',
'import',
'promise',
],
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
ecmaVersion: 2018,
},
settings: {
react: {
version: 'detect',
},
'import/extensions': [
'.js',
],
'import/ignore': [
'node_modules',
'\\.(css|scss|json)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
},
rules: {
'brace-style': 'warn',
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': [
'warn',
{
before: false,
after: true,
},
],
'comma-style': ['warn', 'last'],
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'],
'no-catch-shadow': 'error',
'no-cond-assign': 'error',
'no-console': [
'warn',
{
allow: [
'error',
'warn',
],
},
],
'no-fallthrough': 'error',
'no-irregular-whitespace': 'error',
'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'],
'padded-blocks': [
'error',
{
classes: 'always',
},
],
quotes: ['error', 'single'],
semi: 'error',
strict: 'off',
'valid-typeof': 'error',
'react/jsx-boolean-value': 'error',
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-curly-spacing': 'error',
'react/jsx-equals-spacing': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
'react/jsx-indent': ['error', 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',
'react/prop-types': 'error',
'react/self-closing-comp': 'error',
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{
components: [
'Link',
'NavLink',
],
specialLink: [
'to',
],
aspect: [
'noHref',
'invalidHref',
'preferButton',
],
},
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': 'warn',
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/html-has-lang': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/interactive-supports-focus': 'warn',
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/mouse-events-have-key-events': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-noninteractive-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
'jsx-a11y/no-onchange': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/no-static-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'off',
'jsx-a11y/scope': 'warn',
'jsx-a11y/tabindex-no-positive': 'warn',
'import/extensions': [
'error',
'always',
{
js: 'never',
},
],
'import/newline-after-import': 'error',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'config/webpack/**',
'app/javascript/mastodon/test_setup.js',
'app/javascript/**/__tests__/**',
],
},
],
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
'promise/catch-or-return': 'error',
},
};

View File

@ -1,170 +0,0 @@
---
root: true
env:
browser: true
node: true
es6: true
jest: true
globals:
ATTACHMENT_HOST: false
parser: babel-eslint
plugins:
- react
- jsx-a11y
- import
- promise
parserOptions:
sourceType: module
ecmaFeatures:
experimentalObjectRestSpread: true
jsx: true
ecmaVersion: 2018
settings:
import/extensions:
- .js
import/ignore:
- node_modules
- \\.(css|scss|json)$
rules:
brace-style: warn
comma-dangle:
- error
- always-multiline
comma-spacing:
- warn
- before: false
after: true
comma-style:
- warn
- last
consistent-return: error
dot-notation: error
eqeqeq: error
indent:
- warn
- 2
jsx-quotes:
- error
- prefer-single
no-catch-shadow: error
no-cond-assign: error
no-console:
- warn
- allow:
- error
- warn
no-fallthrough: error
no-irregular-whitespace: error
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
padded-blocks:
- error
- classes: always
quotes:
- error
- single
semi: error
strict: off
valid-typeof: error
react/jsx-boolean-value: error
react/jsx-closing-bracket-location:
- error
- line-aligned
react/jsx-curly-spacing: error
react/jsx-equals-spacing: error
react/jsx-first-prop-new-line:
- error
- multiline-multiprop
react/jsx-indent:
- error
- 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
react/prop-types: error
react/self-closing-comp: error
jsx-a11y/accessible-emoji: warn
jsx-a11y/alt-text: warn
jsx-a11y/anchor-has-content: warn
jsx-a11y/anchor-is-valid:
- warn
- components:
- Link
- NavLink
specialLink:
- to
aspect:
- noHref
- invalidHref
- preferButton
jsx-a11y/aria-activedescendant-has-tabindex: warn
jsx-a11y/aria-props: warn
jsx-a11y/aria-proptypes: warn
jsx-a11y/aria-role: warn
jsx-a11y/aria-unsupported-elements: warn
jsx-a11y/heading-has-content: warn
jsx-a11y/html-has-lang: warn
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-redundant-alt: warn
jsx-a11y/interactive-supports-focus: warn
jsx-a11y/label-has-for: off
jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
jsx-a11y/no-noninteractive-element-interactions:
- warn
- handlers:
- onClick
jsx-a11y/no-onchange: warn
jsx-a11y/no-redundant-roles: warn
jsx-a11y/no-static-element-interactions:
- warn
- handlers:
- onClick
jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn
import/extensions:
- error
- always
- js: never
import/newline-after-import: error
import/no-extraneous-dependencies:
- error
- devDependencies:
- "config/webpack/**"
- "app/javascript/mastodon/test_setup.js"
- "app/javascript/**/__tests__/**"
import/no-unresolved: error
import/no-webpack-loader-syntax: error
promise/catch-or-return: error

View File

@ -1,9 +0,0 @@
plugins:
postcss-smart-import: {}
precss: {}
autoprefixer:
browsers:
- last 2 versions
- IE >= 11
- iOS >= 9
postcss-object-fit-images: {}

View File

@ -1 +1 @@
2.5.3 2.6.1

View File

@ -1,41 +1,44 @@
Authors
=======
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon) Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
and provided thanks to the work of the following contributors: and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
* [ykzts](https://github.com/ykzts) * [ykzts](https://github.com/ykzts)
* [akihikodaki](https://github.com/akihikodaki) * [akihikodaki](https://github.com/akihikodaki)
* [mjankowski](https://github.com/mjankowski)
* [ThibG](https://github.com/ThibG) * [ThibG](https://github.com/ThibG)
* [mjankowski](https://github.com/mjankowski)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [unarist](https://github.com/unarist) * [unarist](https://github.com/unarist)
* [m4sk1n](https://github.com/m4sk1n) * [m4sk1n](https://github.com/m4sk1n)
* [yiskah](https://github.com/yiskah) * [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson) * [nolanlawson](https://github.com/nolanlawson)
* [ysksn](https://github.com/ysksn)
* [sorin-davidoi](https://github.com/sorin-davidoi) * [sorin-davidoi](https://github.com/sorin-davidoi)
* [abcang](https://github.com/abcang) * [abcang](https://github.com/abcang)
* [lynlynlynx](https://github.com/lynlynlynx) * [lynlynlynx](https://github.com/lynlynlynx)
* [dependabot[bot]](https://github.com/apps/dependabot) * [mayaeh](https://github.com/mayaeh)
* [renatolond](https://github.com/renatolond)
* [alpaca-tc](https://github.com/alpaca-tc) * [alpaca-tc](https://github.com/alpaca-tc)
* [nclm](https://github.com/nclm) * [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble) * [ineffyble](https://github.com/ineffyble)
* [renatolond](https://github.com/renatolond)
* [jeroenpraat](https://github.com/jeroenpraat) * [jeroenpraat](https://github.com/jeroenpraat)
* [mayaeh](https://github.com/mayaeh)
* [blackle](https://github.com/blackle) * [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in) * [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP) * [JantsoP](https://github.com/JantsoP)
* [mabkenar](https://github.com/mabkenar)
* [Kjwon15](https://github.com/Kjwon15)
* [nullkal](https://github.com/nullkal) * [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala) * [yookoala](https://github.com/yookoala)
* [mabkenar](https://github.com/mabkenar)
* [ysksn](https://github.com/ysksn)
* [shuheiktgw](https://github.com/shuheiktgw) * [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow) * [ashfurrow](https://github.com/ashfurrow)
* [Kjwon15](https://github.com/Kjwon15) * [Quenty31](https://github.com/Quenty31)
* [zunda](https://github.com/zunda) * [zunda](https://github.com/zunda)
* [eramdam](https://github.com/eramdam) * [eramdam](https://github.com/eramdam)
* [masarakki](https://github.com/masarakki)
* [takayamaki](https://github.com/takayamaki) * [takayamaki](https://github.com/takayamaki)
* [masarakki](https://github.com/masarakki)
* [ticky](https://github.com/ticky) * [ticky](https://github.com/ticky)
* [Quenty31](https://github.com/Quenty31)
* [danhunsaker](https://github.com/danhunsaker) * [danhunsaker](https://github.com/danhunsaker)
* [ThisIsMissEm](https://github.com/ThisIsMissEm) * [ThisIsMissEm](https://github.com/ThisIsMissEm)
* [hcmiya](https://github.com/hcmiya) * [hcmiya](https://github.com/hcmiya)
@ -45,16 +48,16 @@ and provided thanks to the work of the following contributors:
* [rkarabut](https://github.com/rkarabut) * [rkarabut](https://github.com/rkarabut)
* [yukimochi](https://github.com/yukimochi) * [yukimochi](https://github.com/yukimochi)
* [Artoria2e5](https://github.com/Artoria2e5) * [Artoria2e5](https://github.com/Artoria2e5)
* [nightpool](https://github.com/nightpool)
* [marrus-sh](https://github.com/marrus-sh) * [marrus-sh](https://github.com/marrus-sh)
* [krainboltgreene](https://github.com/krainboltgreene) * [krainboltgreene](https://github.com/krainboltgreene)
* [patf](https://github.com/patf) * [pfigel](https://github.com/pfigel)
* [Aldarone](https://github.com/Aldarone) * [Aldarone](https://github.com/Aldarone)
* [BoFFire](https://github.com/BoFFire) * [BoFFire](https://github.com/BoFFire)
* [clworld](https://github.com/clworld) * [clworld](https://github.com/clworld)
* [dracos](https://github.com/dracos) * [dracos](https://github.com/dracos)
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem) * [Sylvhem](https://github.com/Sylvhem)
* [nightpool](https://github.com/nightpool)
* [MasterGroosha](https://github.com/MasterGroosha) * [MasterGroosha](https://github.com/MasterGroosha)
* [JeanGauthier](https://github.com/JeanGauthier) * [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper) * [kschaper](https://github.com/kschaper)
@ -74,11 +77,14 @@ and provided thanks to the work of the following contributors:
* [johnsudaar](https://github.com/johnsudaar) * [johnsudaar](https://github.com/johnsudaar)
* [trebmuh](https://github.com/trebmuh) * [trebmuh](https://github.com/trebmuh)
* [Rakib Hasan](mailto:rmhasan@gmail.com) * [Rakib Hasan](mailto:rmhasan@gmail.com)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [lindwurm](https://github.com/lindwurm) * [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site) * [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction) * [voidsatisfaction](https://github.com/voidsatisfaction)
* [rinsuki](https://github.com/rinsuki)
* [hikari-no-yume](https://github.com/hikari-no-yume) * [hikari-no-yume](https://github.com/hikari-no-yume)
* [angristan](https://github.com/angristan) * [angristan](https://github.com/angristan)
* [hinaloe](https://github.com/hinaloe)
* [seefood](https://github.com/seefood) * [seefood](https://github.com/seefood)
* [jackjennings](https://github.com/jackjennings) * [jackjennings](https://github.com/jackjennings)
* [spla](mailto:spla@mastodont.cat) * [spla](mailto:spla@mastodont.cat)
@ -88,28 +94,31 @@ and provided thanks to the work of the following contributors:
* [mistydemeo](https://github.com/mistydemeo) * [mistydemeo](https://github.com/mistydemeo)
* [dunn](https://github.com/dunn) * [dunn](https://github.com/dunn)
* [xqus](https://github.com/xqus) * [xqus](https://github.com/xqus)
* [hugogameiro](https://github.com/hugogameiro)
* [ariasuni](https://github.com/ariasuni)
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp) * [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
* [fakenine](https://github.com/fakenine) * [fakenine](https://github.com/fakenine)
* [tsuwatch](https://github.com/tsuwatch) * [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck) * [victorhck](https://github.com/victorhck)
* [kedamaDQ](https://github.com/kedamaDQ)
* [puckipedia](https://github.com/puckipedia) * [puckipedia](https://github.com/puckipedia)
* [fvh-P](https://github.com/fvh-P) * [fvh-P](https://github.com/fvh-P)
* [contraexemplo](https://github.com/contraexemplo) * [contraexemplo](https://github.com/contraexemplo)
* [hugogameiro](https://github.com/hugogameiro) * [Aditoo17](https://github.com/Aditoo17)
* [kazu9su](https://github.com/kazu9su) * [kazu9su](https://github.com/kazu9su)
* [Komic](https://github.com/Komic) * [Komic](https://github.com/Komic)
* [lmorchard](https://github.com/lmorchard)
* [diomed](https://github.com/diomed) * [diomed](https://github.com/diomed)
* [ariasuni](https://github.com/ariasuni)
* [Neetshin](mailto:neetshin@neetsh.in) * [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday) * [rainyday](https://github.com/rainyday)
* [ProgVal](https://github.com/ProgVal) * [ProgVal](https://github.com/ProgVal)
* [valentin2105](https://github.com/valentin2105) * [valentin2105](https://github.com/valentin2105)
* [yuntan](https://github.com/yuntan) * [yuntan](https://github.com/yuntan)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [goofy-bz](mailto:goofy@babelzilla.org) * [goofy-bz](mailto:goofy@babelzilla.org)
* [kadiix](https://github.com/kadiix) * [kadiix](https://github.com/kadiix)
* [kodacs](https://github.com/kodacs) * [kodacs](https://github.com/kodacs)
* [rtucker](https://github.com/rtucker) * [trwnh](https://github.com/trwnh)
* [JMendyk](https://github.com/JMendyk)
* [KScl](https://github.com/KScl) * [KScl](https://github.com/KScl)
* [sterdev](https://github.com/sterdev) * [sterdev](https://github.com/sterdev)
* [TheKinrar](https://github.com/TheKinrar) * [TheKinrar](https://github.com/TheKinrar)
@ -119,18 +128,20 @@ and provided thanks to the work of the following contributors:
* [northerner](https://github.com/northerner) * [northerner](https://github.com/northerner)
* [fhemberger](https://github.com/fhemberger) * [fhemberger](https://github.com/fhemberger)
* [greysteil](https://github.com/greysteil) * [greysteil](https://github.com/greysteil)
* [hnrysmth](https://github.com/hnrysmth) * [hensmith](https://github.com/hensmith)
* [d6rkaiz](https://github.com/d6rkaiz) * [d6rkaiz](https://github.com/d6rkaiz)
* [JMendyk](https://github.com/JMendyk) * [Reverite](https://github.com/Reverite)
* [JohnD28](https://github.com/JohnD28) * [JohnD28](https://github.com/JohnD28)
* [znz](https://github.com/znz) * [znz](https://github.com/znz)
* [Naouak](https://github.com/Naouak) * [Naouak](https://github.com/Naouak)
* [pawelngei](https://github.com/pawelngei)
* [rtucker](https://github.com/rtucker)
* [reneklacan](https://github.com/reneklacan) * [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru) * [ekiru](https://github.com/ekiru)
* [noellabo](https://github.com/noellabo)
* [tcitworld](https://github.com/tcitworld) * [tcitworld](https://github.com/tcitworld)
* [geta6](https://github.com/geta6) * [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana) * [happycoloredbanana](https://github.com/happycoloredbanana)
* [kedamaDQ](https://github.com/kedamaDQ)
* [leopku](https://github.com/leopku) * [leopku](https://github.com/leopku)
* [SansPseudoFix](https://github.com/SansPseudoFix) * [SansPseudoFix](https://github.com/SansPseudoFix)
* [tomfhowe](https://github.com/tomfhowe) * [tomfhowe](https://github.com/tomfhowe)
@ -138,18 +149,19 @@ and provided thanks to the work of the following contributors:
* [theboss](https://github.com/theboss) * [theboss](https://github.com/theboss)
* [178inaba](https://github.com/178inaba) * [178inaba](https://github.com/178inaba)
* [alyssais](https://github.com/alyssais) * [alyssais](https://github.com/alyssais)
* [kodnaplakal](https://github.com/kodnaplakal) * [hiphref](https://github.com/hiphref)
* [BenLubar](https://github.com/BenLubar)
* [stalker314314](https://github.com/stalker314314) * [stalker314314](https://github.com/stalker314314)
* [huertanix](https://github.com/huertanix) * [huertanix](https://github.com/huertanix)
* [genesixx](https://github.com/genesixx) * [genesixx](https://github.com/genesixx)
* [halkeye](https://github.com/halkeye) * [halkeye](https://github.com/halkeye)
* [hinaloe](https://github.com/hinaloe)
* [treby](https://github.com/treby) * [treby](https://github.com/treby)
* [Reverite](https://github.com/Reverite)
* [jpdevries](https://github.com/jpdevries) * [jpdevries](https://github.com/jpdevries)
* [H-C-F](https://github.com/H-C-F) * [gdpelican](https://github.com/gdpelican)
* [kmichl](https://github.com/kmichl)
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
* [saper](https://github.com/saper) * [saper](https://github.com/saper)
* [marek-lach](https://github.com/marek-lach)
* [nevillepark](https://github.com/nevillepark) * [nevillepark](https://github.com/nevillepark)
* [ornithocoder](https://github.com/ornithocoder) * [ornithocoder](https://github.com/ornithocoder)
* [pierreozoux](https://github.com/pierreozoux) * [pierreozoux](https://github.com/pierreozoux)
@ -165,20 +177,24 @@ and provided thanks to the work of the following contributors:
* [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl) * [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl)
* [R0ckweb](https://github.com/R0ckweb) * [R0ckweb](https://github.com/R0ckweb)
* [caasi](https://github.com/caasi) * [caasi](https://github.com/caasi)
* [chr-1x](https://github.com/chr-1x)
* [esetomo](https://github.com/esetomo) * [esetomo](https://github.com/esetomo)
* [foxiehkins](https://github.com/foxiehkins) * [foxiehkins](https://github.com/foxiehkins)
* [hoodie](mailto:hoodiekitten@outlook.com) * [hoodie](mailto:hoodiekitten@outlook.com)
* [luzi82](https://github.com/luzi82) * [luzi82](https://github.com/luzi82)
* [duxovni](https://github.com/duxovni) * [duxovni](https://github.com/duxovni)
* [tmm576](https://github.com/tmm576)
* [unsmell](https://github.com/unsmell) * [unsmell](https://github.com/unsmell)
* [valerauko](https://github.com/valerauko)
* [chriswmartin](https://github.com/chriswmartin) * [chriswmartin](https://github.com/chriswmartin)
* [vahnj](https://github.com/vahnj) * [vahnj](https://github.com/vahnj)
* [ikuradon](https://github.com/ikuradon) * [ikuradon](https://github.com/ikuradon)
* [AndreLewin](https://github.com/AndreLewin) * [AndreLewin](https://github.com/AndreLewin)
* [rinsuki](https://github.com/rinsuki) * [0xflotus](https://github.com/0xflotus)
* [redtachyons](https://github.com/redtachyons) * [redtachyons](https://github.com/redtachyons)
* [thurloat](https://github.com/thurloat) * [thurloat](https://github.com/thurloat)
* [aaribaud](https://github.com/aaribaud) * [aaribaud](https://github.com/aaribaud)
* [pointlessone](https://github.com/pointlessone)
* [Andrew](mailto:andrewlchronister@gmail.com) * [Andrew](mailto:andrewlchronister@gmail.com)
* [estuans](https://github.com/estuans) * [estuans](https://github.com/estuans)
* [dissolve](https://github.com/dissolve) * [dissolve](https://github.com/dissolve)
@ -192,7 +208,7 @@ and provided thanks to the work of the following contributors:
* [cdutson](https://github.com/cdutson) * [cdutson](https://github.com/cdutson)
* [farlistener](https://github.com/farlistener) * [farlistener](https://github.com/farlistener)
* [DavidLibeau](https://github.com/DavidLibeau) * [DavidLibeau](https://github.com/DavidLibeau)
* [SirCmpwn](https://github.com/SirCmpwn) * [ddevault](https://github.com/ddevault)
* [Fjoerfoks](https://github.com/Fjoerfoks) * [Fjoerfoks](https://github.com/Fjoerfoks)
* [fmauNeko](https://github.com/fmauNeko) * [fmauNeko](https://github.com/fmauNeko)
* [gloaec](https://github.com/gloaec) * [gloaec](https://github.com/gloaec)
@ -203,10 +219,12 @@ and provided thanks to the work of the following contributors:
* [ErikXXon](https://github.com/ErikXXon) * [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling) * [ian-kelling](https://github.com/ian-kelling)
* [immae](https://github.com/immae) * [immae](https://github.com/immae)
* [J0WI](https://github.com/J0WI)
* [foozmeat](https://github.com/foozmeat) * [foozmeat](https://github.com/foozmeat)
* [jasonrhodes](https://github.com/jasonrhodes) * [jasonrhodes](https://github.com/jasonrhodes)
* [Jason Snell](mailto:jason@newrelic.com) * [Jason Snell](mailto:jason@newrelic.com)
* [jviide](https://github.com/jviide) * [jviide](https://github.com/jviide)
* [YuleZ](https://github.com/YuleZ)
* [crakaC](https://github.com/crakaC) * [crakaC](https://github.com/crakaC)
* [tkbky](https://github.com/tkbky) * [tkbky](https://github.com/tkbky)
* [Kaylee](mailto:kaylee@codethat.sucks) * [Kaylee](mailto:kaylee@codethat.sucks)
@ -216,6 +234,7 @@ and provided thanks to the work of the following contributors:
* [Lorenz Diener](mailto:halcyon@icosahedron.website) * [Lorenz Diener](mailto:halcyon@icosahedron.website)
* [alimony](https://github.com/alimony) * [alimony](https://github.com/alimony)
* [mig5](https://github.com/mig5) * [mig5](https://github.com/mig5)
* [moritzheiber](https://github.com/moritzheiber)
* [ndarville](https://github.com/ndarville) * [ndarville](https://github.com/ndarville)
* [Abzol](https://github.com/Abzol) * [Abzol](https://github.com/Abzol)
* [pwoolcoc](https://github.com/pwoolcoc) * [pwoolcoc](https://github.com/pwoolcoc)
@ -223,10 +242,13 @@ and provided thanks to the work of the following contributors:
* [petzah](https://github.com/petzah) * [petzah](https://github.com/petzah)
* [ignisf](https://github.com/ignisf) * [ignisf](https://github.com/ignisf)
* [raymestalez](https://github.com/raymestalez) * [raymestalez](https://github.com/raymestalez)
* [remram44](https://github.com/remram44)
* [sts10](https://github.com/sts10)
* [sascha-sl](https://github.com/sascha-sl) * [sascha-sl](https://github.com/sascha-sl)
* [u1-liquid](https://github.com/u1-liquid) * [u1-liquid](https://github.com/u1-liquid)
* [sim6](https://github.com/sim6) * [sim6](https://github.com/sim6)
* [stemid](https://github.com/stemid) * [stemid](https://github.com/stemid)
* [sumdog](https://github.com/sumdog)
* [ThomasLeister](https://github.com/ThomasLeister) * [ThomasLeister](https://github.com/ThomasLeister)
* [mcat-ee](https://github.com/mcat-ee) * [mcat-ee](https://github.com/mcat-ee)
* [tototoshi](https://github.com/tototoshi) * [tototoshi](https://github.com/tototoshi)
@ -243,7 +265,6 @@ and provided thanks to the work of the following contributors:
* [aus-social](https://github.com/aus-social) * [aus-social](https://github.com/aus-social)
* [imbsky](https://github.com/imbsky) * [imbsky](https://github.com/imbsky)
* [bsky](mailto:me@imbsky.net) * [bsky](mailto:me@imbsky.net)
* [chr-1x](https://github.com/chr-1x)
* [codl](https://github.com/codl) * [codl](https://github.com/codl)
* [cpsdqs](https://github.com/cpsdqs) * [cpsdqs](https://github.com/cpsdqs)
* [barzamin](https://github.com/barzamin) * [barzamin](https://github.com/barzamin)
@ -252,6 +273,7 @@ and provided thanks to the work of the following contributors:
* [ik11235](https://github.com/ik11235) * [ik11235](https://github.com/ik11235)
* [kawax](https://github.com/kawax) * [kawax](https://github.com/kawax)
* [007lva](https://github.com/007lva) * [007lva](https://github.com/007lva)
* [mbajur](https://github.com/mbajur)
* [matsurai25](https://github.com/matsurai25) * [matsurai25](https://github.com/matsurai25)
* [mecab](https://github.com/mecab) * [mecab](https://github.com/mecab)
* [nicobz25](https://github.com/nicobz25) * [nicobz25](https://github.com/nicobz25)
@ -259,7 +281,6 @@ and provided thanks to the work of the following contributors:
* [pinfort](https://github.com/pinfort) * [pinfort](https://github.com/pinfort)
* [rbaumert](https://github.com/rbaumert) * [rbaumert](https://github.com/rbaumert)
* [rhoio](https://github.com/rhoio) * [rhoio](https://github.com/rhoio)
* [trwnh](https://github.com/trwnh)
* [usagi-f](https://github.com/usagi-f) * [usagi-f](https://github.com/usagi-f)
* [vidarlee](https://github.com/vidarlee) * [vidarlee](https://github.com/vidarlee)
* [vjackson725](https://github.com/vjackson725) * [vjackson725](https://github.com/vjackson725)
@ -269,11 +290,12 @@ and provided thanks to the work of the following contributors:
* [Awea](https://github.com/Awea) * [Awea](https://github.com/Awea)
* [halcy](https://github.com/halcy) * [halcy](https://github.com/halcy)
* [naaaaaaaaaaaf](https://github.com/naaaaaaaaaaaf) * [naaaaaaaaaaaf](https://github.com/naaaaaaaaaaaf)
* [NecroTechno](https://github.com/NecroTechno)
* [8398a7](https://github.com/8398a7) * [8398a7](https://github.com/8398a7)
* [857b](https://github.com/857b) * [857b](https://github.com/857b)
* [insom](https://github.com/insom) * [insom](https://github.com/insom)
* [Aditoo17](https://github.com/Aditoo17) * [tachyons](https://github.com/tachyons)
* [acid-chicken](https://github.com/acid-chicken)
* [Esteth](https://github.com/Esteth)
* [unascribed](https://github.com/unascribed) * [unascribed](https://github.com/unascribed)
* [Aguay-val](https://github.com/Aguay-val) * [Aguay-val](https://github.com/Aguay-val)
* [Akihiko Odaki](mailto:nekomanma@pixiv.co.jp) * [Akihiko Odaki](mailto:nekomanma@pixiv.co.jp)
@ -282,7 +304,7 @@ and provided thanks to the work of the following contributors:
* [unleashed](https://github.com/unleashed) * [unleashed](https://github.com/unleashed)
* [alxrcs](https://github.com/alxrcs) * [alxrcs](https://github.com/alxrcs)
* [console-cowboy](https://github.com/console-cowboy) * [console-cowboy](https://github.com/console-cowboy)
* [pointlessone](https://github.com/pointlessone) * [Alkarex](https://github.com/Alkarex)
* [a2](https://github.com/a2) * [a2](https://github.com/a2)
* [0xa](https://github.com/0xa) * [0xa](https://github.com/0xa)
* [palindromordnilap](https://github.com/palindromordnilap) * [palindromordnilap](https://github.com/palindromordnilap)
@ -299,7 +321,6 @@ and provided thanks to the work of the following contributors:
* [ayumin](https://github.com/ayumin) * [ayumin](https://github.com/ayumin)
* [BaptisteGelez](https://github.com/BaptisteGelez) * [BaptisteGelez](https://github.com/BaptisteGelez)
* [bzg](https://github.com/bzg) * [bzg](https://github.com/bzg)
* [BenLubar](https://github.com/BenLubar)
* [benediktg](https://github.com/benediktg) * [benediktg](https://github.com/benediktg)
* [blakebarnett](https://github.com/blakebarnett) * [blakebarnett](https://github.com/blakebarnett)
* [bradj](https://github.com/bradj) * [bradj](https://github.com/bradj)
@ -314,6 +335,7 @@ and provided thanks to the work of the following contributors:
* [Motoma](https://github.com/Motoma) * [Motoma](https://github.com/Motoma)
* [chriswk](https://github.com/chriswk) * [chriswk](https://github.com/chriswk)
* [csu](https://github.com/csu) * [csu](https://github.com/csu)
* [clarcharr](https://github.com/clarcharr)
* [kklleemm](https://github.com/kklleemm) * [kklleemm](https://github.com/kklleemm)
* [colindean](https://github.com/colindean) * [colindean](https://github.com/colindean)
* [dachinat](https://github.com/dachinat) * [dachinat](https://github.com/dachinat)
@ -341,6 +363,8 @@ and provided thanks to the work of the following contributors:
* [espenronnevik](https://github.com/espenronnevik) * [espenronnevik](https://github.com/espenronnevik)
* [Finariel](https://github.com/Finariel) * [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying) * [siuying](https://github.com/siuying)
* [zoc](https://github.com/zoc)
* [fwenzel](https://github.com/fwenzel)
* [GenbuHase](https://github.com/GenbuHase) * [GenbuHase](https://github.com/GenbuHase)
* [hattori6789](https://github.com/hattori6789) * [hattori6789](https://github.com/hattori6789)
* [algernon](https://github.com/algernon) * [algernon](https://github.com/algernon)
@ -375,10 +399,9 @@ and provided thanks to the work of the following contributors:
* [jguerder](https://github.com/jguerder) * [jguerder](https://github.com/jguerder)
* [Jehops](https://github.com/Jehops) * [Jehops](https://github.com/Jehops)
* [joshuap](https://github.com/joshuap) * [joshuap](https://github.com/joshuap)
* [YuleZ](https://github.com/YuleZ)
* [Tiwy57](https://github.com/Tiwy57) * [Tiwy57](https://github.com/Tiwy57)
* [xuv](https://github.com/xuv) * [xuv](https://github.com/xuv)
* [Jnsll](https://github.com/Jnsll) * [June Sallou](mailto:jnsll@users.noreply.github.com)
* [j0k3r](https://github.com/j0k3r) * [j0k3r](https://github.com/j0k3r)
* [KEINOS](https://github.com/KEINOS) * [KEINOS](https://github.com/KEINOS)
* [futoase](https://github.com/futoase) * [futoase](https://github.com/futoase)
@ -389,7 +412,6 @@ and provided thanks to the work of the following contributors:
* [k0ta0uchi](https://github.com/k0ta0uchi) * [k0ta0uchi](https://github.com/k0ta0uchi)
* [KrzysiekJ](https://github.com/KrzysiekJ) * [KrzysiekJ](https://github.com/KrzysiekJ)
* [leowzukw](https://github.com/leowzukw) * [leowzukw](https://github.com/leowzukw)
* [lmorchard](https://github.com/lmorchard)
* [Tak](https://github.com/Tak) * [Tak](https://github.com/Tak)
* [cacheflow](https://github.com/cacheflow) * [cacheflow](https://github.com/cacheflow)
* [ldidry](https://github.com/ldidry) * [ldidry](https://github.com/ldidry)
@ -402,6 +424,7 @@ and provided thanks to the work of the following contributors:
* [martymcguire](https://github.com/martymcguire) * [martymcguire](https://github.com/martymcguire)
* [marvinkopf](https://github.com/marvinkopf) * [marvinkopf](https://github.com/marvinkopf)
* [otsune](https://github.com/otsune) * [otsune](https://github.com/otsune)
* [mbugowski](https://github.com/mbugowski)
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
* [matt-auckland](https://github.com/matt-auckland) * [matt-auckland](https://github.com/matt-auckland)
* [webroo](https://github.com/webroo) * [webroo](https://github.com/webroo)
@ -420,12 +443,12 @@ and provided thanks to the work of the following contributors:
* [premist](https://github.com/premist) * [premist](https://github.com/premist)
* [Mnkai](https://github.com/Mnkai) * [Mnkai](https://github.com/Mnkai)
* [mitchhentges](https://github.com/mitchhentges) * [mitchhentges](https://github.com/mitchhentges)
* [moritzheiber](https://github.com/moritzheiber)
* [mouse-reeve](https://github.com/mouse-reeve) * [mouse-reeve](https://github.com/mouse-reeve)
* [Mozinet-fr](https://github.com/Mozinet-fr) * [Mozinet-fr](https://github.com/Mozinet-fr)
* [lae](https://github.com/lae) * [lae](https://github.com/lae)
* [Nanamachi](https://github.com/Nanamachi) * [Nanamachi](https://github.com/Nanamachi)
* [orinthe](https://github.com/orinthe) * [orinthe](https://github.com/orinthe)
* [NecroTechno](https://github.com/NecroTechno)
* [Dar13](https://github.com/Dar13) * [Dar13](https://github.com/Dar13)
* [ngerakines](https://github.com/ngerakines) * [ngerakines](https://github.com/ngerakines)
* [vonneudeck](https://github.com/vonneudeck) * [vonneudeck](https://github.com/vonneudeck)
@ -443,31 +466,34 @@ and provided thanks to the work of the following contributors:
* [Pangoraw](https://github.com/Pangoraw) * [Pangoraw](https://github.com/Pangoraw)
* [peterkeen](https://github.com/peterkeen) * [peterkeen](https://github.com/peterkeen)
* [pgate](https://github.com/pgate) * [pgate](https://github.com/pgate)
* [remram44](https://github.com/remram44) * [Reto Kromer](mailto:retokromer@users.noreply.github.com)
* [retokromer](https://github.com/retokromer) * [Rey Tucker](mailto:git@reytucker.us)
* [rfwatson](https://github.com/rfwatson) * [Rob Watson](mailto:rfwatson@users.noreply.github.com)
* [rfreebern](https://github.com/rfreebern) * [Ryan Freebern](mailto:ryan@freebern.org)
* [Ryan Wade](mailto:ryan.wade@protonmail.com) * [Ryan Wade](mailto:ryan.wade@protonmail.com)
* [sylph01](https://github.com/sylph01) * [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info)
* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) * [S.H](mailto:gamelinks007@gmail.com)
* [staticsafe](https://github.com/staticsafe) * [Sadiq Saif](mailto:staticsafe@users.noreply.github.com)
* [snwh](https://github.com/snwh) * [Sam Hewitt](mailto:hewittsamuel@gmail.com)
* [sts10](https://github.com/sts10) * [Satoshi KOJIMA](mailto:skoji@mac.com)
* [skoji](https://github.com/skoji) * [ScienJus](mailto:i@scienjus.com)
* [ScienJus](https://github.com/ScienJus) * [Scott Larkin](mailto:scott@codeclimate.com)
* [larkinscott](https://github.com/larkinscott) * [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
* [imolein](https://github.com/imolein) * [Sebastian Morr](mailto:sebastian@morr.cc)
* [blinry](https://github.com/blinry) * [Sergei Č](mailto:noiwex1911@gmail.com)
* [Noiwex](https://github.com/Noiwex) * [Setuu](mailto:yuki764setuu@gmail.com)
* [yuki764](https://github.com/yuki764) * [Shaun Gillies](mailto:me@shaungillies.net)
* [shnjp](https://github.com/shnjp) * [Shin Adachi](mailto:shn@glucose.jp)
* [ernix](https://github.com/ernix) * [Shin Kojima](mailto:shin@kojima.org)
* [rosylilly](https://github.com/rosylilly) * [Sho Kusano](mailto:rosylilly@aduca.org)
* [shouko](https://github.com/shouko) * [Shouko Yu](mailto:imshouko@gmail.com)
* [Sina Mashek](mailto:sina@mashek.xyz) * [Sina Mashek](mailto:sina@mashek.xyz)
* [sossii](https://github.com/sossii) * [Sir-Boops](mailto:admin@boops.me)
* [Soshi Kato](mailto:mail@sossii.com)
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
* [Stanislas](mailto:angristan@pm.me)
* [StefOfficiel](mailto:pichard.stephane@free.fr) * [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net)
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
* [Sébastien Santoro](mailto:dereckson@espace-win.org) * [Sébastien Santoro](mailto:dereckson@espace-win.org)
* [Tad Thorley](mailto:phaedryx@users.noreply.github.com) * [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
@ -515,26 +541,30 @@ and provided thanks to the work of the following contributors:
* [fsubal](mailto:fsubal@users.noreply.github.com) * [fsubal](mailto:fsubal@users.noreply.github.com)
* [fusshi-](mailto:dikky1218@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [gentaro](mailto:gentaroooo@gmail.com) * [gentaro](mailto:gentaroooo@gmail.com)
* [gol-cha](mailto:info@mevo.xyz)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com) * [haosbvnker](mailto:github@chaosbunker.com)
* [isati](mailto:phil@juchnowi.cz) * [isati](mailto:phil@juchnowi.cz)
* [jacob](mailto:jacobherringtondeveloper@gmail.com) * [jacob](mailto:jacobherringtondeveloper@gmail.com)
* [jenn kaplan](mailto:me@jkap.io) * [jenn kaplan](mailto:me@jkap.io)
* [jirayudech](mailto:jirayudech@gmail.com) * [jirayudech](mailto:jirayudech@gmail.com)
* [jomo](mailto:github@jomo.tv)
* [jooops](mailto:joops@autistici.org) * [jooops](mailto:joops@autistici.org)
* [jukper](mailto:jukkaperanto@gmail.com) * [jukper](mailto:jukkaperanto@gmail.com)
* [jumoru](mailto:jumoru@mailbox.org) * [jumoru](mailto:jumoru@mailbox.org)
* [karlyeurl](mailto:karl.yeurl@gmail.com) * [karlyeurl](mailto:karl.yeurl@gmail.com)
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [kodai](mailto:shirafuta.kodai@gmail.com)
* [kuro5hin](mailto:rusty@kuro5hin.org) * [kuro5hin](mailto:rusty@kuro5hin.org)
* [luzpaz](mailto:luzpaz@users.noreply.github.com) * [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [maxypy](mailto:maxime@mpigou.fr) * [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com) * [mhe](mailto:mail@marcus-herrmann.com)
* [mike castleman](mailto:m@mlcastle.net)
* [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mimikun](mailto:dzdzble_effort_311@outlook.jp)
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
* [muan](mailto:muan@github.com) * [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in) * [neetshin](mailto:neetshin@neetsh.in)
* [nightpool](mailto:nightpool@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com) * [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com) * [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl) * [rysiekpl](mailto:rysiek@hackerspace.pl)
@ -564,3 +594,248 @@ and provided thanks to the work of the following contributors:
* [雨宮美羽](mailto:k737566@gmail.com) * [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead. This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.
## Translators
Following people have contributed to translation of Mastodon:
- **Arabic**
- ButterflyOfFire
- **Asturian**
- ButterflyOfFire
- Enol P.
- **Basque**
- Aitzol
- ButterflyOfFire
- Gorka Azkarate
- Osoitz
- Peru Iparragirre
- **Bulgarian**
- ButterflyOfFire
- **Catalan**
- ButterflyOfFire
- Joan Montané
- Jose Luis
- spla
- **Chinese (Hong Kong)**
- ButterflyOfFire
- Luzi Leung
- **Chinese (Simplified)**
- Allen Zhong
- ButterflyOfFire
- SerCom_KC
- **Chinese (Traditional)**
- ButterflyOfFire
- James58899
- Jeff Huang
- S1ttidoe477
- SHA265
- **Corsican**
- Alix D. R.
- ButterflyOfFire
- **Croatian**
- ButterflyOfFire
- **Czech**
- ButterflyOfFire
- Lorem Ipsum
- Marek Ľach
- **Danish**
- ButterflyOfFire
- Rasmus Sæderup
- **Dutch**
- ButterflyOfFire
- Jelv
- jeroenpraat
- rscmbbng
- **English**
- ButterflyOfFire
- Renato "Lond" Cerqueira
- **Esperanto**
- ButterflyOfFire
- Jeong Arm
- Martin Bodin
- Mélanie Chauvel
- Vanege
- tuxayo/Victor Grousset
- **Finnish**
- ButterflyOfFire
- Jonne Arjoranta
- S Heija
- Taru Luojola
- **French**
- Alda Marteau-Hardi
- Alix D. R.
- Baptiste Jonglez
- ButterflyOfFire
- Franck Paul
- Jean-Baptiste Holcroft
- Jonathan Chan
- Letiteuf55
- Martin Bodin
- Mélanie Chauvel
- Olivier Humbert
- Paul Marques Mota
- Sylvhem
- Technowix
- Thibaut Girka
- Théodore
- azenet
- codl
- **Galician**
- ButterflyOfFire
- Xose M.
- manequim
- **Georgian**
- ButterflyOfFire
- **German**
- Benedikt Geißler
- ButterflyOfFire
- Daniel
- Eugen Rochko
- Koyu Berteon
- Patrick Figel
- Weblate Admin
- averageunicorn
- ePirat
- koyu
- larsreineke
- lilo
- **Greek**
- Antonis
- ButterflyOfFire
- Dimitris Maroulidis
- Konstantinos Grevenitis
- **Hebrew**
- ButterflyOfFire
- Ira
- Yaron Shahrabani
- **Hungarian**
- Adam Paszternak
- ButterflyOfFire
- Tibike Miklós
- **Ido**
- ButterflyOfFire
- **Indonesian**
- Alfiana Sibuea
- ButterflyOfFire
- Dito Kurnia Pratama
- Eirworks
- afachri
- se7entime
- **Italian**
- Alessandro Levati
- ButterflyOfFire
- Giuseppe Pignataro
- Stefano
- **Japanese**
- ButterflyOfFire
- Kumasun Morino
- Yamagishi Kazutoshi
- mayaeh
- osapon
- unarist
- 小鳥遊まりあ
- 森の子リスのミーコの大冒険
- **Korean**
- ButterflyOfFire
- Jeong Arm
- Minori Hiraoka
- Yamagishi Kazutoshi
- **Malay**
- ButterflyOfFire
- Muhammad Nur Hidayat (MNH48)
- **Norwegian (old code)**
- ButterflyOfFire
- Espen Rønnevik
- Tale
- **Occitan**
- ButterflyOfFire
- Maxenç
- Quenti2
- Quentí
- **Persian**
- ButterflyOfFire
- Masoud Abkenar
- **Polish**
- ButterflyOfFire
- Jakub Mendyk
- Marcin Mikołajczak
- Marek Ľach
- Stasiek Michalski
- krkk
- **Portuguese**
- ButterflyOfFire
- Hugo Gameiro
- manequim
- **Portuguese (Brazil)**
- André Andrade
- Anna e só
- ButterflyOfFire
- Renato "Lond" Cerqueira
- **Romanian**
- ButterflyOfFire
- adrianbblk
- **Russian**
- Andrew Zyabin
- ButterflyOfFire
- Evgeny Petrov
- Yaron Shahrabani
- **Serbian**
- Branko Kokanovic
- Burekz Finezt
- ButterflyOfFire
- **Serbian (latin)**
- ButterflyOfFire
- **Slovak**
- ButterflyOfFire
- Ivan Pleva
- Lorem Ipsum
- Marek Ľach
- Peter
- **Slovenian**
- ButterflyOfFire
- Kristijan Tkalec
- **Spanish**
- Angeles Broullón
- Antón López
- ButterflyOfFire
- Carlos Mondragon
- David Charte
- Emmanuel
- Lothar Wolf
- Pablo de la Concepción Sanz
- **Swedish**
- ButterflyOfFire
- Elias Mårtenson
- Isak Holmström
- Shellkr
- Stefan Midjich
- Tim Stahel
- **Telugu**
- ButterflyOfFire
- Joseph Nuthalapati
- Ranjith Tellakula
- avndp
- **Thai**
- ButterflyOfFire
- **Turkish**
- ButterflyOfFire
- **Ukrainian**
- ButterflyOfFire
- Ivan Verchenko
- alexcleac
- **Welsh**
- ButterflyOfFire
- Jaz-Michael King
- Kevin Beynon
- Owain Rhys Lewis
- Renato "Lond" Cerqueira
- Rhoslyn Prys
- carl morris
- **Armenian**
- ButterflyOfFire
- **Latvian**
- ButterflyOfFire
- **Tamil**
- ButterflyOfFire
- Prasanna Venkadesh

View File

@ -3,199 +3,400 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [2.6.5] - 2018-12-01 ## [2.7.3] - 2019-02-23
### Changed ### Added
- Change lists to display replies to others on the list and list owner (#9324) - Add domain filter to the admin federation page ([ThibG](https://github.com/tootsuite/mastodon/pull/10071))
- Add quick link from admin account view to block/unblock instance ([ThibG](https://github.com/tootsuite/mastodon/pull/10073))
### Fixed ### Fixed
- Fix failures caused by commonly-used JSON-LD contexts being unavailable (#9412) - Fix video player width not being updated to fit container width ([ThibG](https://github.com/tootsuite/mastodon/pull/10069))
- Fix domain filter being shown in admin page when local filter is active ([ThibG](https://github.com/tootsuite/mastodon/pull/10074))
- Fix crash when conversations have no valid participants ([ThibG](https://github.com/tootsuite/mastodon/pull/10078))
- Fix error when performing admin actions on no statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10094))
### Changed
- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/tootsuite/mastodon/pull/10090))
## [2.7.2] - 2019-02-17
### Added
- Add support for IPv6 in e-mail validation ([zoc](https://github.com/tootsuite/mastodon/pull/10009))
- Add record of IP address used for signing up ([ThibG](https://github.com/tootsuite/mastodon/pull/10026))
- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/tootsuite/mastodon/pull/10042))
- Add support for embedded `Announce` objects attributed to the same actor ([ThibG](https://github.com/tootsuite/mastodon/pull/9998), [Gargron](https://github.com/tootsuite/mastodon/pull/10065))
- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/tootsuite/mastodon/pull/10005), [Gargron](https://github.com/tootsuite/mastodon/pull/10041), [Gargron](https://github.com/tootsuite/mastodon/pull/10062))
- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/10060))
- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/tootsuite/mastodon/pull/10058))
### Fixed
- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/tootsuite/mastodon/pull/9949), [Gargron](https://github.com/tootsuite/mastodon/pull/10028))
- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/tootsuite/mastodon/pull/8447), [hinaloe](https://github.com/tootsuite/mastodon/pull/9991))
- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/tootsuite/mastodon/pull/9997))
- Fix authorized applications page design ([rinsuki](https://github.com/tootsuite/mastodon/pull/9969))
- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/tootsuite/mastodon/pull/9970))
- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/tootsuite/mastodon/pull/9968))
- Fix misleading e-mail hint being displayed in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/9973))
- Fix tombstones not being cleared out ([abcang](https://github.com/tootsuite/mastodon/pull/9978))
- Fix some timeline jumps ([ThibG](https://github.com/tootsuite/mastodon/pull/9982), [ThibG](https://github.com/tootsuite/mastodon/pull/10001), [rinsuki](https://github.com/tootsuite/mastodon/pull/10046))
- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/tootsuite/mastodon/pull/10017))
- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/tootsuite/mastodon/pull/10029))
- Fix style regressions on landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10030))
- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/tootsuite/mastodon/pull/10040))
- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/tootsuite/mastodon/pull/10048))
- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/tootsuite/mastodon/pull/10057))
- Fix crash on public hashtag pages when streaming fails ([ThibG](https://github.com/tootsuite/mastodon/pull/10061))
### Changed
- Change icon for unlisted visibility level ([clarcharr](https://github.com/tootsuite/mastodon/pull/9952))
- Change queue of actor deletes from push to pull for non-follower recipients ([ThibG](https://github.com/tootsuite/mastodon/pull/10016))
- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/tootsuite/mastodon/pull/10038))
- Change upload description input to allow line breaks ([BenLubar](https://github.com/tootsuite/mastodon/pull/10036))
- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10032))
- Change conversations to always show names of other participants ([Gargron](https://github.com/tootsuite/mastodon/pull/10047))
- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/tootsuite/mastodon/pull/10054))
- Change error graphic to hover-to-play ([Gargron](https://github.com/tootsuite/mastodon/pull/10055))
## [2.7.1] - 2019-01-28
### Fixed
- Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/tootsuite/mastodon/pull/9915))
- Fix slow fallback of CopyAccountStats migration setting stats to 0 ([Gargron](https://github.com/tootsuite/mastodon/pull/9930))
- Fix wrong command in migration error message ([angristan](https://github.com/tootsuite/mastodon/pull/9877))
- Fix initial value of volume slider in video player and handle volume changes ([ThibG](https://github.com/tootsuite/mastodon/pull/9929))
- Fix missing hotkeys for notifications ([ThibG](https://github.com/tootsuite/mastodon/pull/9927))
- Fix being able to attach unattached media created by other users ([ThibG](https://github.com/tootsuite/mastodon/pull/9921))
- Fix unrescued SSL error during link verification ([renatolond](https://github.com/tootsuite/mastodon/pull/9914))
- Fix Firefox scrollbar color regression ([trwnh](https://github.com/tootsuite/mastodon/pull/9908))
- Fix scheduled status with media immediately creating a status ([ThibG](https://github.com/tootsuite/mastodon/pull/9894))
- Fix missing strong style for landing page description ([Kjwon15](https://github.com/tootsuite/mastodon/pull/9892))
## [2.7.0] - 2019-01-20
### Added
- Add link for adding a user to a list from their profile ([namelessGonbai](https://github.com/tootsuite/mastodon/pull/9062))
- Add joining several hashtags in a single column ([gdpelican](https://github.com/tootsuite/mastodon/pull/8904))
- Add volume sliders for videos ([sumdog](https://github.com/tootsuite/mastodon/pull/9366))
- Add a tooltip explaining what a locked account is ([pawelngei](https://github.com/tootsuite/mastodon/pull/9403))
- Add preloaded cache for common JSON-LD contexts ([ThibG](https://github.com/tootsuite/mastodon/pull/9412))
- Add profile directory ([Gargron](https://github.com/tootsuite/mastodon/pull/9427))
- Add setting to not group reblogs in home feed ([ThibG](https://github.com/tootsuite/mastodon/pull/9248))
- Add admin ability to remove a user's header image ([ThibG](https://github.com/tootsuite/mastodon/pull/9495))
- Add account hashtags to ActivityPub actor JSON ([Gargron](https://github.com/tootsuite/mastodon/pull/9450))
- Add error message for avatar image that's too large ([sumdog](https://github.com/tootsuite/mastodon/pull/9518))
- Add notification quick-filter bar ([pawelngei](https://github.com/tootsuite/mastodon/pull/9399))
- Add new first-time tutorial ([Gargron](https://github.com/tootsuite/mastodon/pull/9531))
- Add moderation warnings ([Gargron](https://github.com/tootsuite/mastodon/pull/9519))
- Add emoji codepoint mappings for v11.0 ([Gargron](https://github.com/tootsuite/mastodon/pull/9618))
- Add REST API for creating an account ([Gargron](https://github.com/tootsuite/mastodon/pull/9572))
- Add support for Malayalam in language filter ([tachyons](https://github.com/tootsuite/mastodon/pull/9624))
- Add exclude_reblogs option to account statuses API ([Gargron](https://github.com/tootsuite/mastodon/pull/9640))
- Add local followers page to admin account UI ([chr-1x](https://github.com/tootsuite/mastodon/pull/9610))
- Add healthcheck commands to docker-compose.yml ([BenLubar](https://github.com/tootsuite/mastodon/pull/9143))
- Add handler for Move activity to migrate followers ([Gargron](https://github.com/tootsuite/mastodon/pull/9629))
- Add CSV export for lists and domain blocks ([Gargron](https://github.com/tootsuite/mastodon/pull/9677))
- Add `tootctl accounts follow ACCT` ([Gargron](https://github.com/tootsuite/mastodon/pull/9414))
- Add scheduled statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/9706))
- Add immutable caching for S3 objects ([nolanlawson](https://github.com/tootsuite/mastodon/pull/9722))
- Add cache to custom emojis API ([Gargron](https://github.com/tootsuite/mastodon/pull/9732))
- Add preview cards to non-detailed statuses on public pages ([Gargron](https://github.com/tootsuite/mastodon/pull/9714))
- Add `mod` and `moderator` to list of default reserved usernames ([Gargron](https://github.com/tootsuite/mastodon/pull/9713))
- Add quick links to the admin interface in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/8545))
- Add `tootctl domains crawl` ([Gargron](https://github.com/tootsuite/mastodon/pull/9809))
- Add attachment list fallback to public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/9780))
- Add `tootctl --version` ([Gargron](https://github.com/tootsuite/mastodon/pull/9835))
- Add information about how to opt-in to the directory on the directory ([Gargron](https://github.com/tootsuite/mastodon/pull/9834))
- Add timeouts for S3 ([Gargron](https://github.com/tootsuite/mastodon/pull/9842))
- Add support for non-public reblogs from ActivityPub ([Gargron](https://github.com/tootsuite/mastodon/pull/9841))
- Add sending of `Reject` activity when sending a `Block` activity ([ThibG](https://github.com/tootsuite/mastodon/pull/9811))
### Changed
- Temporarily pause timeline if mouse moved recently ([lmorchard](https://github.com/tootsuite/mastodon/pull/9200))
- Change the password form order ([mayaeh](https://github.com/tootsuite/mastodon/pull/9267))
- Redesign admin UI for accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/9340), [Gargron](https://github.com/tootsuite/mastodon/pull/9643))
- Redesign admin UI for instances/domain blocks ([Gargron](https://github.com/tootsuite/mastodon/pull/9645))
- Swap avatar and header input fields in profile page ([ThibG](https://github.com/tootsuite/mastodon/pull/9271))
- When posting in mobile mode, go back to previous history location ([ThibG](https://github.com/tootsuite/mastodon/pull/9502))
- Split out is_changing_upload from is_submitting ([ThibG](https://github.com/tootsuite/mastodon/pull/9536))
- Back to the getting-started when pins the timeline. ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/9561))
- Allow unauthenticated REST API access to GET /api/v1/accounts/:id/statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/9573))
- Limit maximum visibility of local silenced users to unlisted ([ThibG](https://github.com/tootsuite/mastodon/pull/9583))
- Change API error message for unconfirmed accounts ([noellabo](https://github.com/tootsuite/mastodon/pull/9625))
- Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/tootsuite/mastodon/pull/9378))
- Do not ignore federated reports targetting already-reported accounts ([ThibG](https://github.com/tootsuite/mastodon/pull/9534))
- Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/tootsuite/mastodon/pull/9688))
- Change e-mail digest frequency ([Gargron](https://github.com/tootsuite/mastodon/pull/9689))
- Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/tootsuite/mastodon/pull/9438))
- Display fallback link card thumbnail when none is given ([Gargron](https://github.com/tootsuite/mastodon/pull/9715))
- Change account bio length validation to ignore mention domains and URLs ([Gargron](https://github.com/tootsuite/mastodon/pull/9717))
- Use configured contact user for "anonymous" federation activities ([yukimochi](https://github.com/tootsuite/mastodon/pull/9661))
- Change remote interaction dialog to use specific actions instead of generic "interact" ([Gargron](https://github.com/tootsuite/mastodon/pull/9743))
- Always re-fetch public key when signature verification fails to support blind key rotation ([ThibG](https://github.com/tootsuite/mastodon/pull/9667))
- Make replies to boosts impossible, connect reply to original status instead ([valerauko](https://github.com/tootsuite/mastodon/pull/9129))
- Change e-mail MX validation to check both A and MX records against blacklist ([Gargron](https://github.com/tootsuite/mastodon/pull/9489))
- Hide floating action button on search and getting started pages ([tmm576](https://github.com/tootsuite/mastodon/pull/9826))
- Redesign public hashtag page to use a masonry layout ([Gargron](https://github.com/tootsuite/mastodon/pull/9822))
- Use `summary` as summary instead of content warning for converted ActivityPub objects ([Gargron](https://github.com/tootsuite/mastodon/pull/9823))
- Display a double reply arrow on public pages for toots that are replies ([ThibG](https://github.com/tootsuite/mastodon/pull/9808))
- Change admin UI right panel size to be wider ([Kjwon15](https://github.com/tootsuite/mastodon/pull/9768))
### Removed
- Remove links to bridge.joinmastodon.org (non-functional) ([Gargron](https://github.com/tootsuite/mastodon/pull/9608))
- Remove LD-Signatures from activities that do not need them ([ThibG](https://github.com/tootsuite/mastodon/pull/9659))
### Fixed
- Remove unused computation of reblog references from updateTimeline ([ThibG](https://github.com/tootsuite/mastodon/pull/9244))
- Fix loaded embeds resetting if a status arrives from API again ([ThibG](https://github.com/tootsuite/mastodon/pull/9270))
- Fix race condition causing shallow status with only a "favourited" attribute ([ThibG](https://github.com/tootsuite/mastodon/pull/9272))
- Remove intermediary arrays when creating hash maps from results ([Gargron](https://github.com/tootsuite/mastodon/pull/9291))
- Extract counters from accounts table to account_stats table to improve performance ([Gargron](https://github.com/tootsuite/mastodon/pull/9295))
- Change identities id column to a bigint ([Gargron](https://github.com/tootsuite/mastodon/pull/9371))
- Fix conversations API pagination ([ThibG](https://github.com/tootsuite/mastodon/pull/9407))
- Improve account suspension speed and completeness ([Gargron](https://github.com/tootsuite/mastodon/pull/9290))
- Fix thread depth computation in statuses_controller ([ThibG](https://github.com/tootsuite/mastodon/pull/9426))
- Fix database deadlocks by moving account stats update outside transaction ([ThibG](https://github.com/tootsuite/mastodon/pull/9437))
- Escape HTML in profile name preview in profile settings ([pawelngei](https://github.com/tootsuite/mastodon/pull/9446))
- Use same CORS policy for /@:username and /users/:username ([ThibG](https://github.com/tootsuite/mastodon/pull/9485))
- Make custom emoji domains case insensitive ([Esteth](https://github.com/tootsuite/mastodon/pull/9474))
- Various fixes to scrollable lists and media gallery ([ThibG](https://github.com/tootsuite/mastodon/pull/9501))
- Fix bootsnap cache directory being declared relatively ([Gargron](https://github.com/tootsuite/mastodon/pull/9511))
- Fix timeline pagination in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9516))
- Fix padding on dropdown elements in preferences ([ThibG](https://github.com/tootsuite/mastodon/pull/9517))
- Make avatar and headers respect GIF autoplay settings ([ThibG](https://github.com/tootsuite/mastodon/pull/9515))
- Do no retry Web Push workers if the server returns a 4xx response ([Gargron](https://github.com/tootsuite/mastodon/pull/9434))
- Minor scrollable list fixes ([ThibG](https://github.com/tootsuite/mastodon/pull/9551))
- Ignore low-confidence CharlockHolmes guesses when parsing link cards ([ThibG](https://github.com/tootsuite/mastodon/pull/9510))
- Fix `tootctl accounts rotate` not updating public keys ([Gargron](https://github.com/tootsuite/mastodon/pull/9556))
- Fix CSP / X-Frame-Options for media players ([jomo](https://github.com/tootsuite/mastodon/pull/9558))
- Fix unnecessary loadMore calls when the end of a timeline has been reached ([ThibG](https://github.com/tootsuite/mastodon/pull/9581))
- Skip mailer job retries when a record no longer exists ([Gargron](https://github.com/tootsuite/mastodon/pull/9590))
- Fix composer not getting focus after reply confirmation dialog ([ThibG](https://github.com/tootsuite/mastodon/pull/9602))
- Fix signature verification stoplight triggering on non-timeout errors ([Gargron](https://github.com/tootsuite/mastodon/pull/9617))
- Fix ThreadResolveWorker getting queued with invalid URLs ([Gargron](https://github.com/tootsuite/mastodon/pull/9628))
- Fix crash when clearing uninitialized timeline ([ThibG](https://github.com/tootsuite/mastodon/pull/9662))
- Avoid duplicate work by merging ReplyDistributionWorker into DistributionWorker ([ThibG](https://github.com/tootsuite/mastodon/pull/9660))
- Skip full text search if it fails, instead of erroring out completely ([Kjwon15](https://github.com/tootsuite/mastodon/pull/9654))
- Fix profile metadata links not verifying correctly sometimes ([shrft](https://github.com/tootsuite/mastodon/pull/9673))
- Ensure blocked user unfollows blocker if Block/Undo-Block activities are processed out of order ([ThibG](https://github.com/tootsuite/mastodon/pull/9687))
- Fix unreadable text color in report modal for some statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/9716))
- Stop GIFV timeline preview explicitly when it's opened in modal ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/9749))
- Fix scrollbar width compensation ([ThibG](https://github.com/tootsuite/mastodon/pull/9824))
- Fix race conditions when processing deleted toots ([ThibG](https://github.com/tootsuite/mastodon/pull/9815))
- Fix SSO issues on WebKit browsers by disabling Same-Site cookie again ([moritzheiber](https://github.com/tootsuite/mastodon/pull/9819))
- Fix empty OEmbed error ([renatolond](https://github.com/tootsuite/mastodon/pull/9807))
- Fix drag & drop modal not disappearing sometimes ([hinaloe](https://github.com/tootsuite/mastodon/pull/9797))
- Fix statuses with content warnings being displayed in web push notifications sometimes ([ThibG](https://github.com/tootsuite/mastodon/pull/9778))
- Fix scroll-to-detailed status not working on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/9773))
- Fix media modal loading indicator ([ThibG](https://github.com/tootsuite/mastodon/pull/9771))
- Fix hashtag search results not having a permalink fallback in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9810))
- Fix slightly cropped font on settings page dropdowns when using system font ([ariasuni](https://github.com/tootsuite/mastodon/pull/9839))
- Fix not being able to drag & drop text into forms ([tmm576](https://github.com/tootsuite/mastodon/pull/9840))
### Security
- Sanitize and sandbox toot embeds in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9552))
- Add tombstones for remote statuses to prevent replay attacks ([ThibG](https://github.com/tootsuite/mastodon/pull/9830))
## [2.6.5] - 2018-12-01
### Changed
- Change lists to display replies to others on the list and list owner ([ThibG](https://github.com/tootsuite/mastodon/pull/9324))
### Fixed
- Fix failures caused by commonly-used JSON-LD contexts being unavailable ([ThibG](https://github.com/tootsuite/mastodon/pull/9412))
## [2.6.4] - 2018-11-30 ## [2.6.4] - 2018-11-30
### Fixed ### Fixed
- Fix yarn dependencies not installing due to yanked event-stream package (#9401) - Fix yarn dependencies not installing due to yanked event-stream package ([Gargron](https://github.com/tootsuite/mastodon/pull/9401))
## [2.6.3] - 2018-11-30 ## [2.6.3] - 2018-11-30
### Added ### Added
- Add hyphen to characters allowed in remote usernames (#9345) - Add hyphen to characters allowed in remote usernames ([ThibG](https://github.com/tootsuite/mastodon/pull/9345))
### Changed ### Changed
- Change server user count to exclude suspended accounts (#9380) - Change server user count to exclude suspended accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/9380))
### Fixed ### Fixed
- Fix ffmpeg processing sometimes stalling due to overfilled stdout buffer (#9368) - Fix ffmpeg processing sometimes stalling due to overfilled stdout buffer ([hugogameiro](https://github.com/tootsuite/mastodon/pull/9368))
- Fix missing DNS records raising the wrong kind of exception (#9379) - Fix missing DNS records raising the wrong kind of exception ([Gargron](https://github.com/tootsuite/mastodon/pull/9379))
- Fix already queued deliveries still trying to reach inboxes marked as unavailable (#9358) - Fix already queued deliveries still trying to reach inboxes marked as unavailable ([Gargron](https://github.com/tootsuite/mastodon/pull/9358))
### Security ### Security
- Fix TLS handshake timeout not being enforced (#9381) - Fix TLS handshake timeout not being enforced ([Gargron](https://github.com/tootsuite/mastodon/pull/9381))
## [2.6.2] - 2018-11-23 ## [2.6.2] - 2018-11-23
### Added ### Added
- Add Page to whitelisted ActivityPub types (#9188) - Add Page to whitelisted ActivityPub types ([mbajur](https://github.com/tootsuite/mastodon/pull/9188))
- Add 20px to column width in web UI (#9227) - Add 20px to column width in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9227))
- Add amount of freed disk space in `tootctl media remove` (#9229, #9239, #9288) - Add amount of freed disk space in `tootctl media remove` ([Gargron](https://github.com/tootsuite/mastodon/pull/9229), [Gargron](https://github.com/tootsuite/mastodon/pull/9239), [mayaeh](https://github.com/tootsuite/mastodon/pull/9288))
- Add "Show thread" link to self-replies (#9228) - Add "Show thread" link to self-replies ([Gargron](https://github.com/tootsuite/mastodon/pull/9228))
### Changed ### Changed
- Change order of Atom and RSS links so Atom is first (#9302) - Change order of Atom and RSS links so Atom is first ([Alkarex](https://github.com/tootsuite/mastodon/pull/9302))
- Change Nginx configuration for Nanobox apps (#9310) - Change Nginx configuration for Nanobox apps ([danhunsaker](https://github.com/tootsuite/mastodon/pull/9310))
- Change the follow action to appear instant in web UI (#9220) - Change the follow action to appear instant in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9220))
- Change how the ActiveRecord connection is instantiated in on_worker_boot (#9238) - Change how the ActiveRecord connection is instantiated in on_worker_boot ([Gargron](https://github.com/tootsuite/mastodon/pull/9238))
- Change `tootctl accounts cull` to always touch accounts so they can be skipped (#9293) - Change `tootctl accounts cull` to always touch accounts so they can be skipped ([renatolond](https://github.com/tootsuite/mastodon/pull/9293))
- Change mime type comparison to ignore JSON-LD profile (#9179) - Change mime type comparison to ignore JSON-LD profile ([valerauko](https://github.com/tootsuite/mastodon/pull/9179))
### Fixed ### Fixed
- Fix web UI crash when conversation has no last status (#9207) - Fix web UI crash when conversation has no last status ([sammy8806](https://github.com/tootsuite/mastodon/pull/9207))
- Fix follow limit validator reporting lower number past threshold (#9230) - Fix follow limit validator reporting lower number past threshold ([Gargron](https://github.com/tootsuite/mastodon/pull/9230))
- Fix form validation flash message color and input borders (#9235) - Fix form validation flash message color and input borders ([Gargron](https://github.com/tootsuite/mastodon/pull/9235))
- Fix invalid twitter:player cards being displayed (#9254) - Fix invalid twitter:player cards being displayed ([ThibG](https://github.com/tootsuite/mastodon/pull/9254))
- Fix emoji update date being processed incorrectly (#9255) - Fix emoji update date being processed incorrectly ([ThibG](https://github.com/tootsuite/mastodon/pull/9255))
- Fix playing embed resetting if status is reloaded in web UI (#9270, #9275) - Fix playing embed resetting if status is reloaded in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9270), [Gargron](https://github.com/tootsuite/mastodon/pull/9275))
- Fix web UI crash when favouriting a deleted status (#9272) - Fix web UI crash when favouriting a deleted status ([ThibG](https://github.com/tootsuite/mastodon/pull/9272))
- Fix intermediary arrays being created for hash maps (#9291) - Fix intermediary arrays being created for hash maps ([Gargron](https://github.com/tootsuite/mastodon/pull/9291))
- Fix filter ID not being a string in REST API (#9303) - Fix filter ID not being a string in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9303))
### Security ### Security
- Fix multiple remote account deletions being able to deadlock the database (#9292) - Fix multiple remote account deletions being able to deadlock the database ([Gargron](https://github.com/tootsuite/mastodon/pull/9292))
- Fix HTTP connection timeout of 10s not being enforced (#9329) - Fix HTTP connection timeout of 10s not being enforced ([Gargron](https://github.com/tootsuite/mastodon/pull/9329))
## [2.6.1] - 2018-10-30 ## [2.6.1] - 2018-10-30
### Fixed ### Fixed
- Fix resolving resources by URL not working due to a regression in #9132 (#9171) - Fix resolving resources by URL not working due to a regression in [valerauko](https://github.com/tootsuite/mastodon/pull/9132) ([Gargron](https://github.com/tootsuite/mastodon/pull/9171))
- Fix reducer error in web UI when a conversation has no last status (#9173) - Fix reducer error in web UI when a conversation has no last status ([Gargron](https://github.com/tootsuite/mastodon/pull/9173))
## [2.6.0] - 2018-10-30 ## [2.6.0] - 2018-10-30
### Added ### Added
- Add link ownership verification (#8703) - Add link ownership verification ([Gargron](https://github.com/tootsuite/mastodon/pull/8703))
- Add conversations API (#8832) - Add conversations API ([Gargron](https://github.com/tootsuite/mastodon/pull/8832))
- Add limit for the number of people that can be followed from one account (#8807) - Add limit for the number of people that can be followed from one account ([Gargron](https://github.com/tootsuite/mastodon/pull/8807))
- Add admin setting to customize mascot (#8766) - Add admin setting to customize mascot ([ashleyhull-versent](https://github.com/tootsuite/mastodon/pull/8766))
- Add support for more granular ActivityPub audiences from other software, i.e. circles (#8950, #9093, #9150) - Add support for more granular ActivityPub audiences from other software, i.e. circles ([Gargron](https://github.com/tootsuite/mastodon/pull/8950), [Gargron](https://github.com/tootsuite/mastodon/pull/9093), [Gargron](https://github.com/tootsuite/mastodon/pull/9150))
- Add option to block all reports from a domain (#8830) - Add option to block all reports from a domain ([Gargron](https://github.com/tootsuite/mastodon/pull/8830))
- Add user preference to always expand toots marked with content warnings (#8762) - Add user preference to always expand toots marked with content warnings ([webroo](https://github.com/tootsuite/mastodon/pull/8762))
- Add user preference to always hide all media (#8569) - Add user preference to always hide all media ([fvh-P](https://github.com/tootsuite/mastodon/pull/8569))
- Add `force_login` param to OAuth authorize page (#8655) - Add `force_login` param to OAuth authorize page ([Gargron](https://github.com/tootsuite/mastodon/pull/8655))
- Add `tootctl accounts backup` (#8642, #8811) - Add `tootctl accounts backup` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl accounts create` (#8642, #8811) - Add `tootctl accounts create` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl accounts cull` (#8642, #8811) - Add `tootctl accounts cull` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl accounts delete` (#8642, #8811) - Add `tootctl accounts delete` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl accounts modify` (#8642, #8811) - Add `tootctl accounts modify` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl accounts refresh` (#8642, #8811) - Add `tootctl accounts refresh` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl feeds build` (#8642, #8811) - Add `tootctl feeds build` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl feeds clear` (#8642, #8811) - Add `tootctl feeds clear` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl settings registrations open` (#8642, #8811) - Add `tootctl settings registrations open` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `tootctl settings registrations close` (#8642, #8811) - Add `tootctl settings registrations close` ([Gargron](https://github.com/tootsuite/mastodon/pull/8642), [Gargron](https://github.com/tootsuite/mastodon/pull/8811))
- Add `min_id` param to REST API to support backwards pagination (#8736) - Add `min_id` param to REST API to support backwards pagination ([Gargron](https://github.com/tootsuite/mastodon/pull/8736))
- Add a confirmation dialog when hitting reply and the compose box isn't empty (#8893) - Add a confirmation dialog when hitting reply and the compose box isn't empty ([ThibG](https://github.com/tootsuite/mastodon/pull/8893))
- Add PostgreSQL disk space growth tracking in PGHero (#8906) - Add PostgreSQL disk space growth tracking in PGHero ([Gargron](https://github.com/tootsuite/mastodon/pull/8906))
- Add button for disabling local account to report quick actions bar (#9024) - Add button for disabling local account to report quick actions bar ([Gargron](https://github.com/tootsuite/mastodon/pull/9024))
- Add Czech language (#8594) - Add Czech language ([Aditoo17](https://github.com/tootsuite/mastodon/pull/8594))
- Add `same-site` (`lax`) attribute to cookies (#8626) - Add `same-site` (`lax`) attribute to cookies ([sorin-davidoi](https://github.com/tootsuite/mastodon/pull/8626))
- Add support for styled scrollbars in Firefox Nightly (#8653) - Add support for styled scrollbars in Firefox Nightly ([sorin-davidoi](https://github.com/tootsuite/mastodon/pull/8653))
- Add highlight to the active tab in web UI profiles (#8673) - Add highlight to the active tab in web UI profiles ([rhoio](https://github.com/tootsuite/mastodon/pull/8673))
- Add auto-focus for comment textarea in report modal (#8689) - Add auto-focus for comment textarea in report modal ([ThibG](https://github.com/tootsuite/mastodon/pull/8689))
- Add auto-focus for emoji picker's search field (#8688) - Add auto-focus for emoji picker's search field ([ThibG](https://github.com/tootsuite/mastodon/pull/8688))
- Add nginx and systemd templates to `dist/` directory (#8770) - Add nginx and systemd templates to `dist/` directory ([Gargron](https://github.com/tootsuite/mastodon/pull/8770))
- Add support for `/.well-known/change-password` (#8828) - Add support for `/.well-known/change-password` ([Gargron](https://github.com/tootsuite/mastodon/pull/8828))
- Add option to override FFMPEG binary path (#8855) - Add option to override FFMPEG binary path ([sascha-sl](https://github.com/tootsuite/mastodon/pull/8855))
- Add `dns-prefetch` tag when using different host for assets or uploads (#8942) - Add `dns-prefetch` tag when using different host for assets or uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/8942))
- Add `description` meta tag (#8941) - Add `description` meta tag ([Gargron](https://github.com/tootsuite/mastodon/pull/8941))
- Add `Content-Security-Policy` header (#8957) - Add `Content-Security-Policy` header ([ThibG](https://github.com/tootsuite/mastodon/pull/8957))
- Add cache for the instance info API (#8765) - Add cache for the instance info API ([ykzts](https://github.com/tootsuite/mastodon/pull/8765))
- Add suggested follows to search screen in mobile layout (#9010) - Add suggested follows to search screen in mobile layout ([Gargron](https://github.com/tootsuite/mastodon/pull/9010))
- Add CORS header to `/.well-known/*` routes (#9083) - Add CORS header to `/.well-known/*` routes ([BenLubar](https://github.com/tootsuite/mastodon/pull/9083))
- Add `card` attribute to statuses returned from REST API (#9120) - Add `card` attribute to statuses returned from REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9120))
- Add in-stream link preview (#9120) - Add in-stream link preview ([Gargron](https://github.com/tootsuite/mastodon/pull/9120))
- Add support for ActivityPub `Page` objects (#9121) - Add support for ActivityPub `Page` objects ([mbajur](https://github.com/tootsuite/mastodon/pull/9121))
### Changed ### Changed
- Change forms design (#8703) - Change forms design ([Gargron](https://github.com/tootsuite/mastodon/pull/8703))
- Change reports overview to group by target account (#8674) - Change reports overview to group by target account ([Gargron](https://github.com/tootsuite/mastodon/pull/8674))
- Change web UI to show "read more" link on overly long in-stream statuses (#8205) - Change web UI to show "read more" link on overly long in-stream statuses ([lanodan](https://github.com/tootsuite/mastodon/pull/8205))
- Change design of direct messages column (#8832, #9022) - Change design of direct messages column ([Gargron](https://github.com/tootsuite/mastodon/pull/8832), [Gargron](https://github.com/tootsuite/mastodon/pull/9022))
- Change home timelines to exclude DMs (#8940) - Change home timelines to exclude DMs ([Gargron](https://github.com/tootsuite/mastodon/pull/8940))
- Change list timelines to exclude all replies (#8683) - Change list timelines to exclude all replies ([cbayerlein](https://github.com/tootsuite/mastodon/pull/8683))
- Change admin accounts UI default sort to most recent (#8813) - Change admin accounts UI default sort to most recent ([Gargron](https://github.com/tootsuite/mastodon/pull/8813))
- Change documentation URL in the UI (#8898) - Change documentation URL in the UI ([Gargron](https://github.com/tootsuite/mastodon/pull/8898))
- Change style of success and failure messages (#8973) - Change style of success and failure messages ([Gargron](https://github.com/tootsuite/mastodon/pull/8973))
- Change DM filtering to always allow DMs from staff (#8993) - Change DM filtering to always allow DMs from staff ([qguv](https://github.com/tootsuite/mastodon/pull/8993))
- Change recommended Ruby version to 2.5.3 (#9003) - Change recommended Ruby version to 2.5.3 ([zunda](https://github.com/tootsuite/mastodon/pull/9003))
- Change docker-compose default to persist volumes in current directory (#9055) - Change docker-compose default to persist volumes in current directory ([Gargron](https://github.com/tootsuite/mastodon/pull/9055))
- Change character counters on edit profile page to input length limit (#9100) - Change character counters on edit profile page to input length limit ([Gargron](https://github.com/tootsuite/mastodon/pull/9100))
- Change notification filtering to always let through messages from staff (#9152) - Change notification filtering to always let through messages from staff ([Gargron](https://github.com/tootsuite/mastodon/pull/9152))
- Change "hide boosts from user" function also hiding notifications about boosts (#9147) - Change "hide boosts from user" function also hiding notifications about boosts ([ThibG](https://github.com/tootsuite/mastodon/pull/9147))
- Change CSS `detailed-status__wrapper` class actually wrap the detailed status (#8547) - Change CSS `detailed-status__wrapper` class actually wrap the detailed status ([trwnh](https://github.com/tootsuite/mastodon/pull/8547))
### Deprecated ### Deprecated
- `GET /api/v1/timelines/direct``GET /api/v1/conversations` (#8832) - `GET /api/v1/timelines/direct``GET /api/v1/conversations` ([Gargron](https://github.com/tootsuite/mastodon/pull/8832))
- `POST /api/v1/notifications/dismiss``POST /api/v1/notifications/:id/dismiss` (#8905) - `POST /api/v1/notifications/dismiss``POST /api/v1/notifications/:id/dismiss` ([Gargron](https://github.com/tootsuite/mastodon/pull/8905))
- `GET /api/v1/statuses/:id/card``card` attributed included in status (#9120) - `GET /api/v1/statuses/:id/card``card` attributed included in status ([Gargron](https://github.com/tootsuite/mastodon/pull/9120))
### Removed ### Removed
- Remove "on this device" label in column push settings (#8704) - Remove "on this device" label in column push settings ([rhoio](https://github.com/tootsuite/mastodon/pull/8704))
- Remove rake tasks in favour of tootctl commands (#8675) - Remove rake tasks in favour of tootctl commands ([Gargron](https://github.com/tootsuite/mastodon/pull/8675))
### Fixed ### Fixed
- Fix remote statuses using instance's default locale if no language given (#8861) - Fix remote statuses using instance's default locale if no language given ([Kjwon15](https://github.com/tootsuite/mastodon/pull/8861))
- Fix streaming API not exiting when port or socket is unavailable (#9023) - Fix streaming API not exiting when port or socket is unavailable ([Gargron](https://github.com/tootsuite/mastodon/pull/9023))
- Fix network calls being performed in database transaction in ActivityPub handler (#8951) - Fix network calls being performed in database transaction in ActivityPub handler ([Gargron](https://github.com/tootsuite/mastodon/pull/8951))
- Fix dropdown arrow position (#8637) - Fix dropdown arrow position ([ThibG](https://github.com/tootsuite/mastodon/pull/8637))
- Fix first element of dropdowns being focused even if not using keyboard (#8679) - Fix first element of dropdowns being focused even if not using keyboard ([ThibG](https://github.com/tootsuite/mastodon/pull/8679))
- Fix tootctl requiring `bundle exec` invocation (#8619) - Fix tootctl requiring `bundle exec` invocation ([abcang](https://github.com/tootsuite/mastodon/pull/8619))
- Fix public pages not using animation preference for avatars (#8614) - Fix public pages not using animation preference for avatars ([renatolond](https://github.com/tootsuite/mastodon/pull/8614))
- Fix OEmbed/OpenGraph cards not understanding relative URLs (#8669) - Fix OEmbed/OpenGraph cards not understanding relative URLs ([ThibG](https://github.com/tootsuite/mastodon/pull/8669))
- Fix some dark emojis not having a white outline (#8597) - Fix some dark emojis not having a white outline ([ThibG](https://github.com/tootsuite/mastodon/pull/8597))
- Fix media description not being displayed in various media modals (#8678) - Fix media description not being displayed in various media modals ([ThibG](https://github.com/tootsuite/mastodon/pull/8678))
- Fix generated URLs of desktop notifications missing base URL (#8758) - Fix generated URLs of desktop notifications missing base URL ([GenbuHase](https://github.com/tootsuite/mastodon/pull/8758))
- Fix RTL styles (#8764, #8767, #8823, #8897, #9005, #9007, #9018, #9021, #9145, #9146) - Fix RTL styles ([mabkenar](https://github.com/tootsuite/mastodon/pull/8764), [mabkenar](https://github.com/tootsuite/mastodon/pull/8767), [mabkenar](https://github.com/tootsuite/mastodon/pull/8823), [mabkenar](https://github.com/tootsuite/mastodon/pull/8897), [mabkenar](https://github.com/tootsuite/mastodon/pull/9005), [mabkenar](https://github.com/tootsuite/mastodon/pull/9007), [mabkenar](https://github.com/tootsuite/mastodon/pull/9018), [mabkenar](https://github.com/tootsuite/mastodon/pull/9021), [mabkenar](https://github.com/tootsuite/mastodon/pull/9145), [mabkenar](https://github.com/tootsuite/mastodon/pull/9146))
- Fix crash in streaming API when tag param missing (#8955) - Fix crash in streaming API when tag param missing ([Gargron](https://github.com/tootsuite/mastodon/pull/8955))
- Fix hotkeys not working when no element is focused (#8998) - Fix hotkeys not working when no element is focused ([ThibG](https://github.com/tootsuite/mastodon/pull/8998))
- Fix some hotkeys not working on detailed status view (#9006) - Fix some hotkeys not working on detailed status view ([ThibG](https://github.com/tootsuite/mastodon/pull/9006))
- Fix og:url on status pages (#9047) - Fix og:url on status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/9047))
- Fix upload option buttons only being visible on hover (#9074) - Fix upload option buttons only being visible on hover ([Gargron](https://github.com/tootsuite/mastodon/pull/9074))
- Fix tootctl not returning exit code 1 on wrong arguments (#9094) - Fix tootctl not returning exit code 1 on wrong arguments ([sascha-sl](https://github.com/tootsuite/mastodon/pull/9094))
- Fix preview cards for appearing for profiles mentioned in toot (#6934, #9158) - Fix preview cards for appearing for profiles mentioned in toot ([ThibG](https://github.com/tootsuite/mastodon/pull/6934), [ThibG](https://github.com/tootsuite/mastodon/pull/9158))
- Fix local accounts sometimes being duplicated as faux-remote (#9109) - Fix local accounts sometimes being duplicated as faux-remote ([Gargron](https://github.com/tootsuite/mastodon/pull/9109))
- Fix emoji search when the shortcode has multiple separators (#9124) - Fix emoji search when the shortcode has multiple separators ([ThibG](https://github.com/tootsuite/mastodon/pull/9124))
- Fix dropdowns sometimes being partially obscured by other elements (#9126) - Fix dropdowns sometimes being partially obscured by other elements ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/9126))
- Fix cache not updating when reply/boost/favourite counters or media sensitivity update (#9119) - Fix cache not updating when reply/boost/favourite counters or media sensitivity update ([Gargron](https://github.com/tootsuite/mastodon/pull/9119))
- Fix empty display name precedence over username in web UI (#9163) - Fix empty display name precedence over username in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9163))
- Fix td instead of th in sessions table header (#9162) - Fix td instead of th in sessions table header ([Gargron](https://github.com/tootsuite/mastodon/pull/9162))
- Fix handling of content types with profile (#9132) - Fix handling of content types with profile ([valerauko](https://github.com/tootsuite/mastodon/pull/9132))
## [2.5.2] - 2018-10-12 ## [2.5.2] - 2018-10-12
### Security ### Security
- Fix XSS vulnerability (#8959) - Fix XSS vulnerability ([Gargron](https://github.com/tootsuite/mastodon/pull/8959))
## [2.5.1] - 2018-10-07 ## [2.5.1] - 2018-10-07
### Fixed ### Fixed
- Fix database migrations for PostgreSQL below 9.5 (#8903) - Fix database migrations for PostgreSQL below 9.5 ([Gargron](https://github.com/tootsuite/mastodon/pull/8903))
- Fix class autoloading issue in ActivityPub Create handler (#8820) - Fix class autoloading issue in ActivityPub Create handler ([Gargron](https://github.com/tootsuite/mastodon/pull/8820))
- Fix cache statistics not being sent via statsd when statsd enabled (#8831) - Fix cache statistics not being sent via statsd when statsd enabled ([ykzts](https://github.com/tootsuite/mastodon/pull/8831))
- Bump puma from 3.11.4 to 3.12.0 (#8883) - Bump puma from 3.11.4 to 3.12.0 ([dependabot[bot]](https://github.com/tootsuite/mastodon/pull/8883))
### Security ### Security
- Fix some local images not having their EXIF metadata stripped on upload (#8714) - Fix some local images not having their EXIF metadata stripped on upload ([ThibG](https://github.com/tootsuite/mastodon/pull/8714))
- Fix being able to enable a disabled relay via ActivityPub Accept handler (#8864) - Fix being able to enable a disabled relay via ActivityPub Accept handler ([ThibG](https://github.com/tootsuite/mastodon/pull/8864))
- Bump nokogiri from 1.8.4 to 1.8.5 (#8881) - Bump nokogiri from 1.8.4 to 1.8.5 ([dependabot[bot]](https://github.com/tootsuite/mastodon/pull/8881))
- Fix being able to report statuses not belonging to the reported account (#8916) - Fix being able to report statuses not belonging to the reported account ([ThibG](https://github.com/tootsuite/mastodon/pull/8916))

View File

@ -10,6 +10,8 @@ You can contribute in the following ways:
- Contributing code to Mastodon by fixing bugs or implementing features - Contributing code to Mastodon by fixing bugs or implementing features
- Improving the documentation - Improving the documentation
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
## Bug reports ## Bug reports
Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles. Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles.

View File

@ -1,86 +1,126 @@
FROM node:8.14.0-alpine as node FROM ubuntu:18.04 as build-dep
FROM ruby:2.4.5-alpine3.8
LABEL maintainer="https://github.com/tootsuite/mastodon" \ # Use bash for the shell
description="Your self-hosted, globally interconnected microblogging community" SHELL ["bash", "-c"]
# Install Node
ENV NODE_VER="8.15.0"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y dist-upgrade && \
apt -y install wget make gcc g++ python && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
tar xf node-v$NODE_VER.tar.gz && \
cd node-v$NODE_VER && \
./configure --prefix=/opt/node && \
make -j$(nproc) > /dev/null && \
make install
# Install jemalloc
ENV JE_VER="5.1.0"
RUN apt -y install autoconf && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib
# Install ruby
ENV RUBY_VER="2.6.1"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
tar xf ruby-$RUBY_VER.tar.gz && \
cd ruby-$RUBY_VER && \
./configure --prefix=/opt/ruby \
--with-jemalloc \
--with-shared \
--disable-install-doc && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make -j$(nproc) > /dev/null && \
make install
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \
gem install bundler
COPY . /opt/mastodon
RUN apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler && \
cd /opt/mastodon && \
bundle install -j$(nproc) --deployment --without development test && \
yarn install --pure-lockfile
FROM ubuntu:18.04
# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
# Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
# Create the mastodon user
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
RUN apt update && \
echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt -y dist-upgrade && \
apt install -y whois wget && \
addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
ENV PATH=/mastodon/bin:$PATH \ # Copy over masto source from building and set permissions
RAILS_SERVE_STATIC_FILES=true \ COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
RAILS_ENV=production \
NODE_ENV=production
ARG LIBICONV_VERSION=1.15 # Install masto runtime deps
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline7 && \
apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \
gem install bundler
EXPOSE 3000 4000 # Clean up more dirs
RUN rm -rf /var/cache && \
rm -rf /var/apt
WORKDIR /mastodon # Add tini
ENV TINI_VERSION="0.18.0"
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini
COPY --from=node /usr/local/bin/node /usr/local/bin/node # Run masto services in prod mode
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules ENV RAILS_ENV="production"
COPY --from=node /usr/local/bin/npm /usr/local/bin/npm ENV NODE_ENV="production"
COPY --from=node /opt/yarn-* /opt/yarn
RUN apk -U upgrade \ # Tell rails to serve static files
&& apk add -t build-dependencies \ ENV RAILS_SERVE_STATIC_FILES="true"
build-base \
icu-dev \
libidn-dev \
libressl \
libtool \
postgresql-dev \
protobuf-dev \
python \
&& apk add \
ca-certificates \
ffmpeg \
file \
git \
icu-libs \
imagemagick \
libidn \
libpq \
protobuf \
tini \
tzdata \
&& update-ca-certificates \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \
&& mkdir -p /tmp/src /opt \
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& 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 .yarnclean /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 \
&& yarn install --pure-lockfile --ignore-engines \
&& yarn cache clean
RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \
&& mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \
&& chown -R mastodon:mastodon /mastodon/public
COPY . /mastodon
RUN chown -R mastodon:mastodon /mastodon
VOLUME /mastodon/public/system
# Set the run user
USER mastodon USER mastodon
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile # Precompile assets
RUN cd ~ && \
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \
yarn cache clean
ENTRYPOINT ["/sbin/tini", "--"] # Set the work dir and the container entry point
WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"]

34
Gemfile
View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.6.0' ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3' gem 'pkg-config', '~> 1.3'
@ -13,7 +13,7 @@ gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.2' gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.5' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.30', require: false gem 'aws-sdk-s3', '~> 1.30', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
@ -23,13 +23,13 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0' gem 'streamio-ffmpeg', '~> 3.0'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.6'
gem 'bootsnap', '~> 1.3', require: false gem 'bootsnap', '~> 1.4', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.0' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0' gem 'cld3', '~> 3.2.3'
gem 'devise', '~> 4.5' gem 'devise', '~> 4.5'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
@ -52,12 +52,12 @@ gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.3' gem 'http', '~> 3.3'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'httplog', '~> 1.1' gem 'httplog', '~> 1.2'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.9' gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.7' gem 'oj', '~> 3.7'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
@ -75,7 +75,7 @@ gem 'rqrcode', '~> 0.10'
gem 'sanitize', '~> 5.0' gem 'sanitize', '~> 5.0'
gem 'sidekiq', '~> 5.2' gem 'sidekiq', '~> 5.2'
gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.0' gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 4.1' gem 'simple_form', '~> 4.1'
@ -89,7 +89,7 @@ gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.5' gem 'webpacker', '~> 3.5'
gem 'webpush' gem 'webpush'
gem 'json-ld', '~> 2.2' gem 'json-ld', '~> 3.0'
gem 'json-ld-preloaded', '~> 3.0' gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3' gem 'rdf-normalize', '~> 0.3'
@ -97,7 +97,7 @@ group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3' gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.6' gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8' gem 'rspec-rails', '~> 3.8'
end end
@ -107,15 +107,15 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.12' gem 'capybara', '~> 3.14'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.4' gem 'webmock', '~> 3.5'
gem 'parallel_tests', '~> 2.27' gem 'parallel_tests', '~> 2.28'
end end
group :development do group :development do
@ -127,8 +127,8 @@ group :development do
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.61', require: false gem 'rubocop', '~> 0.65', require: false
gem 'brakeman', '~> 4.3', require: false gem 'brakeman', '~> 4.4', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false gem 'scss_lint', '~> 0.57', require: false
@ -145,3 +145,5 @@ group :production do
gem 'lograge', '~> 0.10' gem 'lograge', '~> 0.10'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end
gem 'concurrent-ruby', require: false

View File

@ -38,7 +38,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.8) active_model_serializers (0.10.9)
actionpack (>= 4.1, < 6) actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6) activemodel (>= 4.1, < 6)
case_transform (>= 0.2) case_transform (>= 0.2)
@ -62,7 +62,7 @@ GEM
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.2) addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@ -76,8 +76,8 @@ GEM
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.1) aws-eventstream (1.0.1)
aws-partitions (1.122.0) aws-partitions (1.131.0)
aws-sdk-core (3.43.0) aws-sdk-core (3.45.0)
aws-eventstream (~> 1.0) aws-eventstream (~> 1.0)
aws-partitions (~> 1.0) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
@ -85,31 +85,31 @@ GEM
aws-sdk-kms (1.13.0) aws-sdk-kms (1.13.0)
aws-sdk-core (~> 3, >= 3.39.0) aws-sdk-core (~> 3, >= 3.39.0)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.30.0) aws-sdk-s3 (1.30.1)
aws-sdk-core (~> 3, >= 3.39.0) aws-sdk-core (~> 3, >= 3.39.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.3) aws-sigv4 (1.0.3)
bcrypt (3.1.12) bcrypt (3.1.12)
benchmark-ips (2.7.2) benchmark-ips (2.7.2)
better_errors (2.5.0) better_errors (2.5.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.2) bootsnap (1.4.1)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.3.1) brakeman (4.4.0)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.9.0) bullet (5.9.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.6.0) bundler-audit (0.6.1)
bundler (~> 1.2) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (~> 0.18)
byebug (10.0.2) byebug (11.0.0)
capistrano (3.11.0) capistrano (3.11.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
@ -126,7 +126,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.12.0) capybara (3.14.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -142,13 +142,13 @@ GEM
elasticsearch (>= 2.0.0) elasticsearch (>= 2.0.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.3.10) chunky_png (1.3.10)
cld3 (3.2.2) cld3 (3.2.3)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0) climate_control (0.2.0)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.2) coderay (1.1.2)
concurrent-ruby (1.0.5) concurrent-ruby (1.1.4)
connection_pool (2.2.2) connection_pool (2.2.2)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
@ -185,10 +185,10 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.0.2) doorkeeper (5.0.2)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.5.0) dotenv (2.7.1)
dotenv-rails (2.5.0) dotenv-rails (2.7.1)
dotenv (= 2.5.0) dotenv (= 2.7.1)
railties (>= 3.2, < 6.0) railties (>= 3.2, < 6.1)
elasticsearch (6.0.2) elasticsearch (6.0.2)
elasticsearch-api (= 6.0.2) elasticsearch-api (= 6.0.2)
elasticsearch-transport (= 6.0.2) elasticsearch-transport (= 6.0.2)
@ -200,12 +200,12 @@ GEM
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.5.0) equatable (0.5.0)
erubi (1.7.1) erubi (1.8.0)
et-orbi (1.1.6) et-orbi (1.1.6)
tzinfo tzinfo
excon (0.62.0) excon (0.62.0)
fabrication (2.20.1) fabrication (2.20.1)
faker (1.9.1) faker (1.9.3)
i18n (>= 0.7) i18n (>= 0.7)
faraday (0.15.0) faraday (0.15.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -231,7 +231,7 @@ GEM
fuubar (2.3.2) fuubar (2.3.2)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.2) get_process_mem (0.2.3)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.1.0) goldfinger (2.1.0)
@ -239,11 +239,11 @@ GEM
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
oj (~> 3.0) oj (~> 3.0)
hamlit (2.8.8) hamlit (2.9.2)
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
hamlit-rails (0.2.0) hamlit-rails (0.2.2)
actionpack (>= 4.0.1) actionpack (>= 4.0.1)
activesupport (>= 4.0.1) activesupport (>= 4.0.1)
hamlit (>= 1.2.0) hamlit (>= 1.2.0)
@ -266,10 +266,10 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.1.1) httplog (1.2.1)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.1.1) i18n (1.5.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.28) i18n-tasks (0.9.28)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -284,14 +284,14 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8) iso-639 (0.2.8)
jaro_winkler (1.5.1) jaro_winkler (1.5.2)
jmespath (1.4.0) jmespath (1.4.0)
json (2.1.0) json (2.1.0)
json-ld (2.2.1) json-ld (3.0.2)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (>= 2.2.8, < 4.0) rdf (>= 2.2.8, < 4.0)
json-ld-preloaded (3.0.0) json-ld-preloaded (3.0.2)
json-ld (>= 2.2, < 4.0) json-ld (~> 3.0)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 3.0) rdf (~> 3.0)
jsonapi-renderer (0.2.0) jsonapi-renderer (0.2.0)
@ -335,9 +335,9 @@ GEM
redis (>= 3.0.5) redis (>= 3.0.5)
memory_profiler (0.9.12) memory_profiler (0.9.12)
method_source (0.9.2) method_source (0.9.2)
microformats (4.0.7) microformats (4.1.0)
json json (~> 2.1)
nokogiri nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.2.2) mime-types (3.2.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812) mime-types-data (3.2018.0812)
@ -345,7 +345,7 @@ GEM
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
msgpack (1.2.4) msgpack (1.2.6)
multi_json (1.13.1) multi_json (1.13.1)
multipart-post (2.0.0) multipart-post (2.0.0)
necromancer (0.4.0) necromancer (0.4.0)
@ -354,16 +354,16 @@ GEM
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (5.0.2) net-ssh (5.0.2)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.9.1) nokogiri (1.10.1)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.0) nokogumbo (2.0.0)
nokogiri (~> 1.8, >= 1.8.4) nokogiri (~> 1.8, >= 1.8.4)
nsa (0.2.4) nsa (0.2.7)
activesupport (>= 4.2, < 6) activesupport (>= 4.2, < 6)
concurrent-ruby (~> 1.0.0) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5.0) sidekiq (>= 3.5)
statsd-ruby (~> 1.2.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.7.4) oj (3.7.9)
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -389,18 +389,18 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.1) parallel (1.13.0)
parallel_tests (2.27.0) parallel_tests (2.28.0)
parallel parallel
parser (2.5.3.0) parser (2.6.0.0)
ast (~> 2.4.0) ast (~> 2.4.0)
pastel (0.7.2) pastel (0.7.2)
equatable (~> 0.5.0) equatable (~> 0.5.0)
tty-color (~> 0.4.0) tty-color (~> 0.4.0)
pg (1.1.3) pg (1.1.4)
pghero (2.2.0) pghero (2.2.0)
activerecord activerecord
pkg-config (1.3.2) pkg-config (1.3.4)
powerpack (0.1.2) powerpack (0.1.2)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
@ -413,21 +413,22 @@ GEM
pry (0.12.2) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
pry-byebug (3.6.0) pry-byebug (3.7.0)
byebug (~> 10.0) byebug (~> 11.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.8) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
psych (3.1.0)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0) puma (3.12.0)
pundit (2.0.0) pundit (2.0.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.6) rack (2.0.6)
rack-attack (5.4.2) rack-attack (5.4.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.2) rack-cors (1.0.2)
rack-protection (2.0.4) rack-protection (2.0.5)
rack rack
rack-proxy (0.6.4) rack-proxy (0.6.4)
rack rack
@ -455,7 +456,7 @@ GEM
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4) rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.2) rails-i18n (5.1.3)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
@ -467,11 +468,11 @@ GEM
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (12.3.1) rake (12.3.2)
rb-fsevent (0.10.3) rb-fsevent (0.10.3)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
rdf (3.0.7) rdf (3.0.9)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3) rdf-normalize (0.3.3)
@ -513,7 +514,7 @@ GEM
rspec-mocks (3.8.0) rspec-mocks (3.8.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-rails (3.8.1) rspec-rails (3.8.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
@ -525,11 +526,12 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rubocop (0.61.1) rubocop (0.65.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1) powerpack (~> 0.1)
psych (>= 3.1.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.4.0) unicode-display_width (~> 1.4.0)
@ -551,8 +553,9 @@ GEM
scss_lint (0.57.1) scss_lint (0.57.1)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.5, >= 3.5.5) sass (~> 3.5, >= 3.5.5)
sidekiq (5.2.3) sidekiq (5.2.5)
connection_pool (~> 2.2, >= 2.2.2) connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5) redis (>= 3.3.5, < 5)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
@ -562,8 +565,9 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.10) sidekiq-unique-jobs (6.0.12)
sidekiq (>= 4.0, <= 6.0) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
@ -586,7 +590,7 @@ GEM
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.12) stackprof (0.2.12)
statsd-ruby (1.2.1) statsd-ruby (1.4.0)
stoplight (2.1.3) stoplight (2.1.3)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
@ -599,13 +603,13 @@ GEM
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (0.20.3) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.9)
timers (4.2.0) timers (4.2.0)
tty-color (0.4.3) tty-color (0.4.3)
tty-command (0.8.2) tty-command (0.8.2)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.6.0) tty-cursor (0.6.0)
tty-prompt (0.18.0) tty-prompt (0.18.1)
necromancer (~> 0.4.0) necromancer (~> 0.4.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
timers (~> 4.0) timers (~> 4.0)
@ -620,16 +624,16 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.5) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2018.7) tzinfo-data (1.2018.9)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.4.0) unicode-display_width (1.4.1)
uniform_notifier (1.12.1) uniform_notifier (1.12.1)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.4.2) webmock (3.5.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
@ -637,7 +641,7 @@ GEM
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.4) webpush (0.3.6)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.0) websocket-driver (0.7.0)
@ -653,13 +657,13 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.5) active_record_query_trace (~> 1.5)
addressable (~> 2.5) addressable (~> 2.6)
annotate (~> 2.7) annotate (~> 2.7)
aws-sdk-s3 (~> 1.30) aws-sdk-s3 (~> 1.30)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap (~> 1.3) bootsnap (~> 1.4)
brakeman (~> 4.3) brakeman (~> 4.4)
browser browser
bullet (~> 5.9) bullet (~> 5.9)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
@ -667,17 +671,18 @@ DEPENDENCIES
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.12) capybara (~> 3.14)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.0) cld3 (~> 3.2.3)
climate_control (~> 0.2) climate_control (~> 0.2)
concurrent-ruby
derailed_benchmarks derailed_benchmarks
devise (~> 4.5) devise (~> 4.5)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.0) doorkeeper (~> 5.0)
dotenv-rails (~> 2.5) dotenv-rails (~> 2.7)
fabrication (~> 2.20) fabrication (~> 2.20)
faker (~> 1.9) faker (~> 1.9)
fast_blank (~> 1.0) fast_blank (~> 1.0)
@ -692,11 +697,11 @@ DEPENDENCIES
http (~> 3.3) http (~> 3.3)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)! http_parser.rb (~> 0.6)!
httplog (~> 1.1) httplog (~> 1.2)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639 iso-639
json-ld (~> 2.2) json-ld (~> 3.0)
json-ld-preloaded (~> 3.0) json-ld-preloaded (~> 3.0)
kaminari (~> 1.1) kaminari (~> 1.1)
letter_opener (~> 1.7) letter_opener (~> 1.7)
@ -706,10 +711,10 @@ DEPENDENCIES
makara (~> 0.4) makara (~> 0.4)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0) microformats (~> 4.1)
mime-types (~> 3.2) mime-types (~> 3.2)
net-ldap (~> 0.10) net-ldap (~> 0.10)
nokogiri (~> 1.9) nokogiri (~> 1.10)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.7) oj (~> 3.7)
omniauth (~> 1.9) omniauth (~> 1.9)
@ -719,14 +724,14 @@ DEPENDENCIES
ox (~> 2.10) ox (~> 2.10)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.27) parallel_tests (~> 2.28)
pg (~> 1.1) pg (~> 1.1)
pghero (~> 2.2) pghero (~> 2.2)
pkg-config (~> 1.3) pkg-config (~> 1.3)
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.6) pry-byebug (~> 3.7)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.12) puma (~> 3.12)
pundit (~> 2.0) pundit (~> 2.0)
@ -743,13 +748,13 @@ DEPENDENCIES
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.8) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.61) rubocop (~> 0.65)
sanitize (~> 5.0) sanitize (~> 5.0)
scss_lint (~> 0.57) scss_lint (~> 0.57)
sidekiq (~> 5.2) sidekiq (~> 5.2)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0) sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 5.0) sidekiq-unique-jobs (~> 6.0)
simple-navigation (~> 4.0) simple-navigation (~> 4.0)
simple_form (~> 4.1) simple_form (~> 4.1)
simplecov (~> 0.16) simplecov (~> 0.16)
@ -763,12 +768,12 @@ DEPENDENCIES
tty-prompt (~> 0.18) tty-prompt (~> 0.18)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2018) tzinfo-data (~> 1.2018)
webmock (~> 3.4) webmock (~> 3.5)
webpacker (~> 3.5) webpacker (~> 3.5)
webpush webpush
RUBY VERSION RUBY VERSION
ruby 2.5.3p105 ruby 2.6.1p33
BUNDLED WITH BUNDLED WITH
1.16.6 1.17.3

View File

@ -80,13 +80,13 @@ A **Vagrant** configuration is included for development purposes.
Mastodon is **free, open source software** licensed under **AGPLv3**. Mastodon is **free, open source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md) You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
**IRC channel**: #mastodon on irc.freenode.net **IRC channel**: #mastodon on irc.freenode.net
## License ## License
Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

2
Vagrantfile vendored
View File

@ -44,7 +44,7 @@ sudo apt-get install \
# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm source /home/vagrant/.rvm/scripts/rvm

View File

@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
}, },
} }
define_type ::Status.unscoped.without_reblogs do define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
crutch :mentions do |collection| crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
@ -48,14 +48,14 @@ class StatusesIndex < Chewy::Index
end end
root date_detection: false do root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long' field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content' field :stemmed, type: 'text', analyzer: 'content'
end end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
field :created_at, type: 'date'
end end
end end
end end

View File

@ -52,11 +52,12 @@ class AccountsController < ApplicationController
private private
def show_pinned_statuses? def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested? statuses.merge!(no_replies_scope) unless replies_requested?
end end
@ -78,12 +79,15 @@ class AccountsController < ApplicationController
Status.without_replies Status.without_replies
end end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
end
def set_account def set_account
@account = Account.find_local!(params[:username]) @account = Account.find_local!(params[:username])
end end
def older_url def older_url
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
pagination_url(max_id: @statuses.last.id) pagination_url(max_id: @statuses.last.id)
end end
@ -92,7 +96,9 @@ class AccountsController < ApplicationController
end end
def pagination_url(max_id: nil, min_id: nil) def pagination_url(max_id: nil, min_id: nil)
if media_requested? if tag_requested?
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
elsif media_requested?
short_account_media_url(@account, max_id: max_id, min_id: min_id) short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested? elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
@ -109,6 +115,10 @@ class AccountsController < ApplicationController
request.path.ends_with?('/with_replies') request.path.ends_with?('/with_replies')
end end
def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def filtered_status_page(params) def filtered_status_page(params)
if params[:min_id].present? if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse

View File

@ -17,7 +17,7 @@ module Admin
account_action.save! account_action.save!
if account_action.with_report? if account_action.with_report?
redirect_to admin_report_path(account_action.report) redirect_to admin_reports_path
else else
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View File

@ -2,7 +2,7 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :memorialize] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :memorialize] before_action :require_local_account!, only: [:enable, :memorialize]
@ -62,9 +62,8 @@ module Admin
def redownload def redownload
authorize @account, :redownload? authorize @account, :redownload?
@account.reset_avatar! @account.update!(last_webfingered_at: nil)
@account.reset_header! ResolveAccountService.new.call(@account)
@account.save!
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View File

@ -5,6 +5,9 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params before_action :set_filter_params
include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]
def index def index
authorize :custom_emoji, :index? authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])

View File

@ -4,14 +4,9 @@ module Admin
class DomainBlocksController < BaseController class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy] before_action :set_domain_block, only: [:show, :destroy]
def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page])
end
def new def new
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new @domain_block = DomainBlock.new(domain: params[:_domain])
end end
def create def create
@ -22,7 +17,7 @@ module Admin
if @domain_block.save if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id) DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else else
render :new render :new
end end
@ -36,7 +31,7 @@ module Admin
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
log_action :destroy, @domain_block log_action :destroy, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end
private private

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Admin
class FollowersController < BaseController
before_action :set_account
PER_PAGE = 40
def index
authorize :account, :index?
@followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
end
end

View File

@ -4,14 +4,21 @@ module Admin
class InstancesController < BaseController class InstancesController < BaseController
def index def index
authorize :instance, :index? authorize :instance, :index?
@instances = ordered_instances @instances = ordered_instances
end end
def resubscribe def show
authorize :instance, :resubscribe? authorize :instance, :show?
params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) @instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id]))
redirect_to admin_instances_path @following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@domain_block = DomainBlock.find_by(domain: params[:id])
end end
private private
@ -27,17 +34,11 @@ module Admin
helper_method :paginated_instances helper_method :paginated_instances
def ordered_instances def ordered_instances
paginated_instances.map { |account| Instance.new(account) } paginated_instances.map { |resource| Instance.new(resource) }
end
def subscribeable_accounts
Account.remote.where(protocol: :ostatus).where(domain: params[:by_domain])
end end
def filter_params def filter_params
params.permit( params.permit(:limited, :by_domain)
:domain_name
)
end end
end end
end end

View File

@ -10,6 +10,10 @@ module Admin
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@ -22,6 +22,15 @@ module Admin
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def show
authorize :status, :index?
@statuses = @account.statuses.where(id: params[:id])
authorize @statuses.first, :show?
@form = Form::StatusBatch.new
end
def create def create
authorize :status, :update? authorize :status, :update?

View File

@ -68,12 +68,14 @@ class Api::BaseController < ApplicationController
end end
def require_user! def require_user!
if current_user && !current_user.disabled? if !current_user
set_user_activity
elsif current_user
render json: { error: 'Your login is currently disabled' }, status: 403
else
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Email confirmation is not completed' }, status: 403
else
set_user_activity
end end
end end

View File

@ -16,10 +16,11 @@ class Api::V1::Accounts::SearchController < Api::BaseController
def account_search def account_search
AccountSearchService.new.call( AccountSearchService.new.call(
params[:q], params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
current_account, current_account,
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolve: truthy_param?(:resolve), resolve: truthy_param?(:resolve),
following: truthy_param?(:following) following: truthy_param?(:following),
offset: params[:offset]
) )
end end
end end

View File

@ -28,13 +28,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id( statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses statuses
end end
@ -52,7 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`. # Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
# and the table will be joined by `Merge Semi Join`, so the query will be slow. # and the table will be joined by `Merge Semi Join`, so the query will be slow.
Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id) .reorder(id: :desc).distinct(:id).pluck(:id)
end end
@ -65,6 +64,14 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_replies Status.without_replies
end end
def no_reblogs_scope
Status.without_reblogs
end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end

View File

@ -1,14 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
before_action :require_user!, except: [:show] before_action :require_user!, except: [:show, :create]
before_action :set_account before_action :set_account, except: [:create]
before_action :check_account_suspension, only: [:show] before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
respond_to :json respond_to :json
@ -16,6 +18,16 @@ class Api::V1::AccountsController < Api::BaseController
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end
def create
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
end
def follow def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
@ -62,4 +74,12 @@ class Api::V1::AccountsController < Api::BaseController
def check_account_suspension def check_account_suspension
gone if @account.suspended? gone if @account.suspended?
end end
def account_params
params.permit(:username, :email, :password, :agreement, :locale)
end
def check_enabled_registrations
forbidden if single_user_mode? || !Setting.open_registrations
end
end end

View File

@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
respond_to :json respond_to :json
def show def show
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
end end
end end

View File

@ -4,6 +4,8 @@ class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json respond_to :json
def index def index
render json: CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
end
end end
end end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class Api::V1::Polls::VotesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
before_action :set_poll
respond_to :json
def create
VoteService.new.call(current_account, @poll, vote_params[:choices])
render json: @poll, serializer: REST::PollSerializer
end
private
def set_poll
@poll = Poll.attached.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def vote_params
params.permit(choices: [])
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Api::V1::PollsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
respond_to :json
def show
@poll = Poll.attached.find(params[:id])
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
render json: @poll, serializer: REST::PollSerializer, include_results: true
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
class Api::V1::ScheduledStatusesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
before_action :set_statuses, only: :index
before_action :set_status, except: :index
after_action :insert_pagination_headers, only: :index
def index
render json: @statuses, each_serializer: REST::ScheduledStatusSerializer
end
def show
render json: @status, serializer: REST::ScheduledStatusSerializer
end
def update
@status.update!(scheduled_status_params)
render json: @status, serializer: REST::ScheduledStatusSerializer
end
def destroy
@status.destroy!
render_empty
end
private
def set_statuses
@statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_status
@status = current_account.scheduled_statuses.find(params[:id])
end
def scheduled_status_params
params.permit(:scheduled_at)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @statuses.empty?
api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id)
end
end
def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@ -3,7 +3,7 @@
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
include Authorization include Authorization
RESULTS_LIMIT = 5 RESULTS_LIMIT = 20
before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user! before_action :require_user!
@ -11,30 +11,22 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json respond_to :json
def index def index
@search = Search.new(search) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
end end
private private
def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end
def search_results def search_results
SearchService.new.call( SearchService.new.call(
params[:q], params[:q],
RESULTS_LIMIT, current_account,
truthy_param?(:resolve), limit_param(RESULTS_LIMIT),
current_account search_params.merge(resolve: truthy_param?(:resolve))
) )
end end
def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
end
end end

View File

@ -25,7 +25,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
end end
def paginated_statuses def paginated_statuses
Status.where(reblog_of_id: @status.id).paginate_by_max_id( Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id]

View File

@ -45,16 +45,18 @@ class Api::V1::StatusesController < Api::BaseController
def create def create
@status = PostStatusService.new.call(current_user.account, @status = PostStatusService.new.call(current_user.account,
status_params[:status], text: status_params[:status],
status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility], visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application, application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key']) idempotency: request.headers['Idempotency-Key'])
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end end
def destroy def destroy
@ -72,12 +74,25 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.find(params[:id]) @status = Status.find(params[:id])
authorize @status, :show? authorize @status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
def status_params def status_params
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: []) params.permit(
:status,
:in_reply_to_id,
:sensitive,
:spoiler_text,
:visibility,
:scheduled_at,
media_ids: [],
poll: [
:multiple,
:hide_totals,
:expires_in,
options: [],
]
)
end end
def pagination_params(core_params) def pagination_params(core_params)

View File

@ -2,7 +2,7 @@
class Api::V2::SearchController < Api::V1::SearchController class Api::V2::SearchController < Api::V1::SearchController
def index def index
@search = Search.new(search) @search = Search.new(search_results)
render json: @search, serializer: REST::V2::SearchSerializer render json: @search, serializer: REST::V2::SearchSerializer
end end
end end

View File

@ -6,9 +6,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
before_action :set_body_classes before_action :set_body_classes
before_action :set_user, only: [:finish_signup] before_action :set_user, only: [:finish_signup]
# GET/PATCH /users/:id/finish_signup
def finish_signup def finish_signup
return unless request.patch? && params[:user] return unless request.patch? && params[:user]
if @user.update(user_params) if @user.update(user_params)
@user.skip_reconfirmation! @user.skip_reconfirmation!
bypass_sign_in(@user) bypass_sign_in(@user)
@ -31,4 +31,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
def user_params def user_params
params.require(:user).permit(:email) params.require(:user).permit(:email)
end end
def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.redirect_uri
else
super
end
end
end end

View File

@ -26,7 +26,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController
resource.locale = I18n.locale resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank? resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.agreement = true
resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil?
resource.build_account if resource.account.nil? resource.build_account if resource.account.nil?
end end

View File

@ -47,6 +47,7 @@ module SignatureVerification
.with_fallback { nil } .with_fallback { nil }
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
account = account_stoplight.run account = account_stoplight.run
@ -59,24 +60,27 @@ module SignatureVerification
signature = Base64.decode64(signature_params['signature']) signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(signature_params['headers']) compare_signed_string = build_signed_string(signature_params['headers'])
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string) return account unless verify_signature(account, signature, compare_signed_string).nil?
@signed_request_account = account
@signed_request_account account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
elsif account.possibly_stale? .with_fallback { nil }
account = account.refresh! .with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
account = account_stoplight.run
if account.nil?
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
@signed_request_account = nil
return
end
return account unless verify_signature(account, signature, compare_signed_string).nil?
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
else
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}" @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
@signed_request_account = nil @signed_request_account = nil
end end
else
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
@signed_request_account = nil
end
end
def request_body def request_body
@request_body ||= request.raw_post @request_body ||= request.raw_post
@ -84,6 +88,15 @@ module SignatureVerification
private private
def verify_signature(account, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
end
rescue OpenSSL::PKey::RSAError
nil
end
def build_signed_string(signed_headers) def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank? signed_headers = 'date' if signed_headers.blank?
@ -130,4 +143,9 @@ module SignatureVerification
account account
end end
end end
def account_refresh_key(account)
return if account.local? || !account.activitypub?
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
end
end end

View File

@ -28,7 +28,7 @@ class DirectoriesController < ApplicationController
end end
def set_tags def set_tags
@tags = Tag.discoverable.limit(30) @tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? }
end end
def set_accounts def set_accounts

View File

@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
before_action :set_body_classes
include Localized include Localized
@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
private private
def set_body_classes
@body_classes = 'admin'
end
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end

View File

@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController
layout 'modal' layout 'modal'
before_action :set_interaction_type
before_action :set_status before_action :set_status
before_action :set_body_classes before_action :set_body_classes
@ -45,4 +46,8 @@ class RemoteInteractionController < ApplicationController
@body_classes = 'modal-layout' @body_classes = 'modal-layout'
@hide_header = true @hide_header = true
end end
def set_interaction_type
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
end
end end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Settings
module Exports
class BlockedDomainsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data
@export.to_blocked_domains_csv
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Settings
module Exports
class ListsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data
@export.to_lists_csv
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
class Settings::FeaturedTagsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
before_action :set_most_used_tags, only: :index
def index
@featured_tag = FeaturedTag.new
end
def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data
if @featured_tag.save
redirect_to settings_featured_tags_path
else
set_featured_tags
set_most_used_tags
render :index
end
end
def destroy
@featured_tag.destroy!
redirect_to settings_featured_tags_path
end
private
def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end
def set_featured_tags
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
end
def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
end
def featured_tag_params
params.require(:featured_tag).permit(:name)
end
end

View File

@ -48,6 +48,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_theme, :setting_theme,
:setting_hide_network, :setting_hide_network,
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
:setting_show_application,
notification_emails: %i(follow follow_request reblog favourite mention digest report), notification_emails: %i(follow follow_request reblog favourite mention digest report),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )

View File

@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController
end end
def set_account def set_account
@account = current_user.account @account = current_account
end end
end end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Settings::SessionsController < Settings::BaseController class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy before_action :set_session, only: :destroy
def destroy def destroy

View File

@ -18,6 +18,7 @@ class StatusesController < ApplicationController
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show] before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_replies, only: [:replies]
content_security_policy only: :embed do |p| content_security_policy only: :embed do |p|
p.frame_ancestors(false) p.frame_ancestors(false)
@ -63,8 +64,37 @@ class StatusesController < ApplicationController
render 'stream_entries/embed', layout: 'embedded' render 'stream_entries/embed', layout: 'embedded'
end end
def replies
skip_session!
render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end
private private
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status, page_params),
type: :unordered,
part_of: replies_account_status_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
if page_requested?
page
else
ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status),
type: :unordered,
first: page
)
end
end
def create_descendant_thread(starting_depth, statuses) def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT if depth < DESCENDANTS_DEPTH_LIMIT
@ -174,4 +204,27 @@ class StatusesController < ApplicationController
return if @status.public_visibility? || @status.unlisted_visibility? return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin' response.headers['Referrer-Policy'] = 'origin'
end end
def page_requested?
params[:page] == 'true'
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def next_page
last_reply = @replies.last
return if last_reply.nil?
same_account = last_reply.account_id == @account.id
return unless same_account || @replies.size == DESCENDANTS_LIMIT
same_account = false unless @replies.size == DESCENDANTS_LIMIT
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
end
def page_params
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
end
end end

View File

@ -3,6 +3,8 @@
class TagsController < ApplicationController class TagsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
layout 'public'
before_action :set_body_classes before_action :set_body_classes
before_action :set_instance_presenter before_action :set_instance_presenter

View File

@ -6,8 +6,9 @@ module Admin::FilterHelper
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(hidden).freeze TAGS_FILTERS = %i(hidden).freeze
INSTANCES_FILTERS = %i(limited by_domain).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params) def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params) new_url = filtered_url_for(link_to_params)

View File

@ -69,8 +69,12 @@ module ApplicationHelper
tag(:meta, content: content, property: property) tag(:meta, content: content, property: property)
end end
def react_component(name, props = {}) def react_component(name, props = {}, &block)
if block.nil?
content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) }) content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
else
content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block)
end
end end
def body_classes def body_classes

View File

@ -68,6 +68,7 @@ module JsonLdHelper
return body_to_json(response.body_with_limit) if response.code == 200 return body_to_json(response.body_with_limit) if response.code == 200
end end
# If request failed, retry without doing it on behalf of a user # If request failed, retry without doing it on behalf of a user
return if on_behalf_of.nil?
build_request(uri).perform do |response| build_request(uri).perform do |response|
response.code == 200 ? body_to_json(response.body_with_limit) : nil response.code == 200 ? body_to_json(response.body_with_limit) : nil
end end

View File

@ -4,7 +4,7 @@ module SettingsHelper
HUMAN_LOCALES = { HUMAN_LOCALES = {
en: 'English', en: 'English',
ar: 'العربية', ar: 'العربية',
ast: 'l\'asturianu', ast: 'Asturianu',
bg: 'Български', bg: 'Български',
ca: 'Català', ca: 'Català',
co: 'Corsu', co: 'Corsu',
@ -29,23 +29,29 @@ module SettingsHelper
it: 'Italiano', it: 'Italiano',
ja: '日本語', ja: '日本語',
ka: 'ქართული', ka: 'ქართული',
kk: 'Қазақша',
ko: '한국어', ko: '한국어',
lt: 'Lietuvių',
lv: 'Latviešu',
ml: 'മലയാളം',
ms: 'Bahasa Melayu',
nl: 'Nederlands', nl: 'Nederlands',
no: 'Norsk', no: 'Norsk',
oc: 'Occitan', oc: 'Occitan',
pl: 'Polszczyzna', pl: 'Polski',
pt: 'Português', pt: 'Português',
'pt-BR': 'Português do Brasil', 'pt-BR': 'Português do Brasil',
ro: 'Limba română', ro: 'Română',
ru: 'Русский', ru: 'Русский',
sk: 'Slovenčina', sk: 'Slovenčina',
sl: 'Slovenščina', sl: 'Slovenščina',
sq: 'Shqip',
sr: 'Српски', sr: 'Српски',
'sr-Latn': 'Srpski (latinica)', 'sr-Latn': 'Srpski (latinica)',
sv: 'Svenska', sv: 'Svenska',
ta: 'தமிழ்', ta: 'தமிழ்',
te: 'తెలుగు', te: 'తెలుగు',
th: 'ภาษาไทย', th: 'ไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
zh: '中文', zh: '中文',

View File

@ -170,7 +170,7 @@ module StreamEntriesHelper
when 'public' when 'public'
fa_icon 'globe fw' fa_icon 'globe fw'
when 'unlisted' when 'unlisted'
fa_icon 'unlock-alt fw' fa_icon 'unlock fw'
when 'private' when 'private'
fa_icon 'lock fw' fa_icon 'lock fw'
when 'direct' when 'direct'

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -22,7 +22,7 @@ export function clearAlert() {
}; };
}; };
export function showAlert(title, message) { export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
return { return {
type: ALERT_SHOW, type: ALERT_SHOW,
title, title,
@ -44,6 +44,6 @@ export function showAlertForError(error) {
return showAlert(title, message); return showAlert(title, message);
} else { } else {
console.error(error); console.error(error);
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); return showAlert();
} }
} }

View File

@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts'; import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';
let cancelFetchComposeSuggestionsAccounts; let cancelFetchComposeSuggestionsAccounts;
@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
});
export function changeCompose(text) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
@ -130,6 +136,12 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
}, },
}).then(function (response) { }).then(function (response) {
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
@ -142,12 +154,6 @@ export function submitCompose(routerHistory) {
} }
}; };
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}
if (response.data.visibility !== 'direct') { if (response.data.visibility !== 'direct') {
insertIfOnline('home'); insertIfOnline('home');
} }
@ -184,22 +190,34 @@ export function submitComposeFail(error) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) { const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']);
const total = Array.from(files).reduce((a, v) => a + v.size, 0);
const progress = new Array(files.length).fill(0);
if (files.length + media.size > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
return; return;
} }
dispatch(uploadComposeRequest()); dispatch(uploadComposeRequest());
resizeImage(files[0]).then(file => { for (const [i, f] of Array.from(files).entries()) {
if (media.size + i > 3) break;
resizeImage(f).then(file => {
const data = new FormData(); const data = new FormData();
data.append('file', file); data.append('file', file);
return api(getState).post('/api/v1/media', data, { return api(getState).post('/api/v1/media', data, {
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data))); }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error))); }).catch(error => dispatch(uploadComposeFail(error)));
}; };
}; };
};
export function changeUploadCompose(id, params) { export function changeUploadCompose(id, params) {
return (dispatch, getState) => { return (dispatch, getState) => {

View File

@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
} }
const isLoadingRecent = !!params.since_id;
api(getState).get('/api/v1/conversations', { params }) api(getState).get('/api/v1/conversations', { params })
.then(response => { .then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null)); dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
}) })
.catch(err => dispatch(expandConversationsFail(err))); .catch(err => dispatch(expandConversationsFail(err)));
}; };
@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({
type: CONVERSATIONS_FETCH_REQUEST, type: CONVERSATIONS_FETCH_REQUEST,
}); });
export const expandConversationsSuccess = (conversations, next) => ({ export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
type: CONVERSATIONS_FETCH_SUCCESS, type: CONVERSATIONS_FETCH_SUCCESS,
conversations, conversations,
next, next,
isLoadingRecent,
}); });
export const expandConversationsFail = error => ({ export const expandConversationsFail = error => ({

View File

@ -1,11 +1,10 @@
// import { autoPlayGif } from '../../initial_state';
// import { putAccounts, putStatuses } from '../../storage/modifier';
import { normalizeAccount, normalizeStatus } from './normalizer'; import { normalizeAccount, normalizeStatus } from './normalizer';
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
function pushUnique(array, object) { function pushUnique(array, object) {
if (array.every(element => element.id !== object.id)) { if (array.every(element => element.id !== object.id)) {
@ -29,6 +28,10 @@ export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses }; return { type: STATUSES_IMPORT, statuses };
} }
export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}
export function importFetchedAccount(account) { export function importFetchedAccount(account) {
return importFetchedAccounts([account]); return importFetchedAccounts([account]);
} }
@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) {
} }
accounts.forEach(processAccount); accounts.forEach(processAccount);
//putAccounts(normalAccounts, !autoPlayGif);
return importAccounts(normalAccounts); return importAccounts(normalAccounts);
} }
@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) {
return (dispatch, getState) => { return (dispatch, getState) => {
const accounts = []; const accounts = [];
const normalStatuses = []; const normalStatuses = [];
const polls = [];
function processStatus(status) { function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
@ -66,11 +69,15 @@ export function importFetchedStatuses(statuses) {
if (status.reblog && status.reblog.id) { if (status.reblog && status.reblog.id) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.poll && status.poll.id) {
pushUnique(polls, status.poll);
}
} }
statuses.forEach(processStatus); statuses.forEach(processStatus);
//putStatuses(normalStatuses);
dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses)); dispatch(importStatuses(normalStatuses));
}; };

View File

@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.reblog = status.reblog.id; normalStatus.reblog = status.reblog.id;
} }
if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id;
}
// Only calculate these values when status first encountered // Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer // Otherwise keep the ones already in the reducer
if (normalOldStatus) { if (normalOldStatus) {

View File

@ -0,0 +1,53 @@
import api from '../api';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
export const vote = (pollId, choices) => (dispatch, getState) => {
dispatch(voteRequest());
api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
.then(({ data }) => dispatch(voteSuccess(data)))
.catch(err => dispatch(voteFail(err)));
};
export const fetchPoll = pollId => (dispatch, getState) => {
dispatch(fetchPollRequest());
api(getState).get(`/api/v1/polls/${pollId}`)
.then(({ data }) => dispatch(fetchPollSuccess(data)))
.catch(err => dispatch(fetchPollFail(err)));
};
export const voteRequest = () => ({
type: POLL_VOTE_REQUEST,
});
export const voteSuccess = poll => ({
type: POLL_VOTE_SUCCESS,
poll,
});
export const voteFail = error => ({
type: POLL_VOTE_FAIL,
error,
});
export const fetchPollRequest = () => ({
type: POLL_FETCH_REQUEST,
});
export const fetchPollSuccess = poll => ({
type: POLL_FETCH_SUCCESS,
poll,
});
export const fetchPollFail = error => ({
type: POLL_FETCH_FAIL,
error,
});

View File

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready'; import ready from './ready';
import LinkHeader from './link_header';
export const getLinks = response => { export const getLinks = response => {
const value = response.headers.link; const value = response.headers.link;

View File

@ -4,5 +4,9 @@ export function start() {
require('font-awesome/css/font-awesome.css'); require('font-awesome/css/font-awesome.css');
require.context('../images/', true); require.context('../images/', true);
try {
Rails.start(); Rails.start();
} catch (e) {
// If called twice
}
}; };

View File

@ -68,10 +68,10 @@ class Account extends ImmutablePureComponent {
if (hidden) { if (hidden) {
return ( return (
<div> <Fragment>
{account.get('display_name')} {account.get('display_name')}
{account.get('username')} {account.get('username')}
</div> </Fragment>
); );
} }
@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
if (requested) { if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) { } else if (muting) {
let hidingNotificationsButton; let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) { if (account.getIn(['relationship', 'muting_notifications'])) {

View File

@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a> <a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
</li> </li>
); );
})} })}
@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<div className='attachment-list'> <div className='attachment-list'>
<div className='attachment-list__icon'> <div className='attachment-list__icon'>
<i className='fa fa-link' /> <Icon id='link' />
</div> </div>
<ul className='attachment-list__list'> <ul className='attachment-list__list'>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButton extends React.PureComponent { export default class ColumnBackButton extends React.PureComponent {
@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () { render () {
return ( return (
<button onClick={this.handleClick} className='column-back-button'> <button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button> </button>
); );

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ColumnBackButton from './column_back_button'; import ColumnBackButton from './column_back_button';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButtonSlim extends ColumnBackButton { export default class ColumnBackButtonSlim extends ColumnBackButton {
@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return ( return (
<div className='column-back-button--slim'> <div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
} }
if (multiColumn && pinned) { if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
</div> </div>
); );
} else if (multiColumn) { } else if (multiColumn) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
} }
if (!pinned && (multiColumn || showBackButton)) { if (!pinned && (multiColumn || showBackButton)) {
backButton = ( backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'> <button onClick={this.handleBackClick} className='column-header__back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button> </button>
); );
@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
} }
if (children || multiColumn) { if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
} }
const hasTitle = icon && title; const hasTitle = icon && title;
@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}> <h1 className={buttonClassName}>
{hasTitle && ( {hasTitle && (
<button onClick={this.handleTitleClick}> <button onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <Icon id={icon} fixedWidth className='column-header__icon' />
{title} {title}
</button> </button>
)} )}

View File

@ -1,28 +1,46 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
export default class DisplayName extends React.PureComponent { export default class DisplayName extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list, others: ImmutablePropTypes.list,
localDomain: PropTypes.string,
}; };
render () { render () {
const { account, others } = this.props; const { others, localDomain } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
let suffix; let displayName, suffix, account;
if (others && others.size > 1) { if (others && others.size > 1) {
suffix = `+${others.size}`; displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else { } else {
suffix = <span className='display-name__account'>@{account.get('acct')}</span>; if (others && others.size > 0) {
account = others.first();
} else {
account = this.props.account;
}
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
suffix = <span className='display-name__account'>@{acct}</span>;
} }
return ( return (
<span className='display-name'> <span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix} {displayName} {suffix}
</span> </span>
); );
} }

View File

@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
</span> </span>
<div className='domain__buttons'> <div className='domain__buttons'>
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { Link } from 'react-router-dom';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink';
import { shortNumberFormat } from '../utils/numbers'; import { shortNumberFormat } from '../utils/numbers';
const Hashtag = ({ hashtag }) => ( const Hashtag = ({ hashtag }) => (
<div className='trends__item'> <div className='trends__item'>
<div className='trends__item__name'> <div className='trends__item__name'>
<Link to={`/timelines/tag/${hashtag.get('name')}`}> <Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
#<span>{hashtag.get('name')}</span> #<span>{hashtag.get('name')}</span>
</Link> </Permalink>
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
</div> </div>

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class Icon extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};
render () {
const { id, className, fixedWidth, ...other } = this.props;
return (
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
);
}
}

View File

@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent { export default class IconButton extends React.PureComponent {
@ -86,7 +87,7 @@ export default class IconButton extends React.PureComponent {
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
> >
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> <Icon id={icon} fixedWidth aria-hidden='true' />
</button> </button>
); );
} }
@ -104,7 +105,7 @@ export default class IconButton extends React.PureComponent {
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
> >
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button> </button>
)} )}
</Motion> </Motion>

View File

@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
} }
updateStateAfterIntersection = (prevState) => { updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting && !this.entry.isIntersecting) { if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting); scheduleIdleTask(this.hideIfNotIntersecting);
} }
return { return {

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent {
return ( return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}> <button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<i className='fa fa-ellipsis-h' /> <Icon id='ellipsis-h' />
</button> </button>
); );
} }

View File

@ -57,6 +57,10 @@ class Item extends React.PureComponent {
const { index, onClick } = this.props; const { index, onClick } = this.props;
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
e.preventDefault(); e.preventDefault();
onClick(index); onClick(index);
} }
@ -231,6 +235,8 @@ class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -239,6 +245,7 @@ class MediaGallery extends React.PureComponent {
state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
width: this.props.defaultWidth,
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@ -258,6 +265,7 @@ class MediaGallery extends React.PureComponent {
handleRef = (node) => { handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) { if (node /*&& this.isStandaloneEligible()*/) {
// offsetWidth triggers a layout, so only calculate when we need to // offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
this.setState({ this.setState({
width: node.offsetWidth, width: node.offsetWidth,
}); });
@ -270,8 +278,10 @@ class MediaGallery extends React.PureComponent {
} }
render () { render () {
const { media, intl, sensitive, height } = this.props; const { media, intl, sensitive, height, defaultWidth } = this.props;
const { width, visible } = this.state; const { visible } = this.state;
const width = this.state.width || defaultWidth;
let children; let children;

View File

@ -0,0 +1,154 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { vote, fetchPoll } from 'mastodon/actions/polls';
import Motion from 'mastodon/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
const messages = defineMessages({
moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
});
const SECOND = 1000;
const MINUTE = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;
const timeRemainingString = (intl, date, now) => {
const delta = date.getTime() - now;
let relativeTime;
if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.moments);
} else if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
}
return relativeTime;
};
export default @injectIntl
class Poll extends ImmutablePureComponent {
static propTypes = {
poll: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func,
disabled: PropTypes.bool,
};
state = {
selected: {},
};
handleOptionChange = e => {
const { target: { value } } = e;
if (this.props.poll.get('multiple')) {
const tmp = { ...this.state.selected };
if (tmp[value]) {
delete tmp[value];
} else {
tmp[value] = true;
}
this.setState({ selected: tmp });
} else {
const tmp = {};
tmp[value] = true;
this.setState({ selected: tmp });
}
};
handleVote = () => {
if (this.props.disabled) {
return;
}
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
};
handleRefresh = () => {
if (this.props.disabled) {
return;
}
this.props.dispatch(fetchPoll(this.props.poll.get('id')));
};
renderOption (option, optionIndex) {
const { poll, disabled } = this.props;
const percent = (option.get('votes_count') / poll.get('votes_count')) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
const active = !!this.state.selected[`${optionIndex}`];
const showResults = poll.get('voted') || poll.get('expired');
return (
<li key={option.get('title')}>
{showResults && (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
}
</Motion>
)}
<label className={classNames('poll__text', { selectable: !showResults })}>
<input
name='vote-options'
type={poll.get('multiple') ? 'checkbox' : 'radio'}
value={optionIndex}
checked={active}
onChange={this.handleOptionChange}
disabled={disabled}
/>
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
{showResults && <span className='poll__number'>{Math.floor(percent)}%</span>}
{option.get('title')}
</label>
</li>
);
}
render () {
const { poll, intl } = this.props;
if (!poll) {
return null;
}
const timeRemaining = timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now());
const showResults = poll.get('voted') || poll.get('expired');
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
return (
<div className='poll'>
<ul>
{poll.get('options').map((option, i) => this.renderOption(option, i))}
</ul>
<div className='poll__footer'>
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} /> · {timeRemaining}
</div>
</div>
);
}
}

View File

@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent {
state = { state = {
fullscreen: null, fullscreen: null,
cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
}; };
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
@ -130,6 +131,20 @@ export default class ScrollableList extends PureComponent {
this.handleScroll(); this.handleScroll();
} }
getScrollPosition = () => {
if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
return { height: this.node.scrollHeight, top: this.node.scrollTop };
} else {
return null;
}
}
updateScrollBottom = (snapshot) => {
const newScrollTop = this.node.scrollHeight - snapshot;
this.setScrollTop(newScrollTop);
}
getSnapshotBeforeUpdate (prevProps) { getSnapshotBeforeUpdate (prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 && const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) && React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
@ -150,6 +165,12 @@ export default class ScrollableList extends PureComponent {
} }
} }
cacheMediaWidth = (width) => {
if (width && this.state.cachedMediaWidth !== width) {
this.setState({ cachedMediaWidth: width });
}
}
componentWillUnmount () { componentWillUnmount () {
this.clearMouseIdleTimer(); this.clearMouseIdleTimer();
this.detachScrollListener(); this.detachScrollListener();
@ -239,7 +260,12 @@ export default class ScrollableList extends PureComponent {
intersectionObserverWrapper={this.intersectionObserverWrapper} intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
> >
{child} {React.cloneElement(child, {
getScrollPosition: this.getScrollPosition,
updateScrollBottom: this.updateScrollBottom,
cachedMediaWidth: this.state.cachedMediaWidth,
cacheMediaWidth: this.cacheMediaWidth,
})}
</IntersectionObserverArticleContainer> </IntersectionObserverArticleContainer>
))} ))}

View File

@ -15,6 +15,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components'; import { MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
@ -68,6 +70,10 @@ class Status extends ImmutablePureComponent {
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown: PropTypes.func,
showThread: PropTypes.bool, showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -77,7 +83,44 @@ class Status extends ImmutablePureComponent {
'account', 'account',
'muted', 'muted',
'hidden', 'hidden',
] ];
// Track height changes we know about to compensate scrolling
componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
getSnapshotBeforeUpdate () {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
return null;
}
}
// Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
}
handleClick = () => { handleClick = () => {
if (this.props.onClick) { if (this.props.onClick) {
@ -165,6 +208,10 @@ class Status extends ImmutablePureComponent {
} }
} }
handleRef = c => {
this.node = c;
}
render () { render () {
let media = null; let media = null;
let statusAvatar, prepend, rebloggedByText; let statusAvatar, prepend, rebloggedByText;
@ -179,7 +226,7 @@ class Status extends ImmutablePureComponent {
if (hidden) { if (hidden) {
return ( return (
<div> <div ref={this.handleRef}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')} {status.get('content')}
</div> </div>
@ -194,7 +241,7 @@ class Status extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'> <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' /> <FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div> </div>
</HotKeys> </HotKeys>
@ -204,7 +251,7 @@ class Status extends ImmutablePureComponent {
if (featured) { if (featured) {
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div> <div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' /> <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
</div> </div>
); );
@ -213,7 +260,7 @@ class Status extends ImmutablePureComponent {
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> <div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} /> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div> </div>
); );
@ -224,7 +271,9 @@ class Status extends ImmutablePureComponent {
status = status.get('reblog'); status = status.get('reblog');
} }
if (status.get('media_attachments').size > 0) { if (status.get('poll')) {
media = <PollContainer pollId={status.get('poll')} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) { if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
media = ( media = (
<AttachmentList <AttachmentList
@ -242,11 +291,12 @@ class Status extends ImmutablePureComponent {
preview={video.get('preview_url')} preview={video.get('preview_url')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
width={239} width={this.props.cachedMediaWidth}
height={110} height={110}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
/> />
)} )}
</Bundle> </Bundle>
@ -254,7 +304,16 @@ class Status extends ImmutablePureComponent {
} else { } else {
media = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />} {Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
/>
)}
</Bundle> </Bundle>
); );
} }
@ -264,11 +323,13 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
card={status.get('card')} card={status.get('card')}
compact compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
/> />
); );
} }
if (otherAccounts) { if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />; statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) { } else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />; statusAvatar = <Avatar account={status.get('account')} size={48} />;
@ -290,7 +351,7 @@ class Status extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}> <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>

View File

@ -5,7 +5,7 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state'; import { me, isStaff } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -30,6 +30,9 @@ const messages = defineMessages({
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
}); });
const obfuscatedCount = count => { const obfuscatedCount = count => {
@ -75,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent {
] ]
handleReplyClick = () => { handleReplyClick = () => {
if (me) {
this.props.onReply(this.props.status, this.context.router.history); this.props.onReply(this.props.status, this.context.router.history);
} else {
this._openInteractionDialog('reply');
}
} }
handleShareClick = () => { handleShareClick = () => {
@ -88,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent {
} }
handleFavouriteClick = () => { handleFavouriteClick = () => {
if (me) {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} else {
this._openInteractionDialog('favourite');
}
} }
handleReblogClick = (e) => { handleReblogClick = e => {
if (me) {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
} else {
this._openInteractionDialog('reblog');
}
}
_openInteractionDialog = type => {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
} }
handleDeleteClick = () => { handleDeleteClick = () => {
@ -139,6 +158,25 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
} }
handleCopy = () => {
const url = this.props.status.get('url');
const textarea = document.createElement('textarea');
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
document.body.removeChild(textarea);
}
}
render () { render () {
const { status, intl, withDismiss } = this.props; const { status, intl, withDismiss } = this.props;
@ -148,11 +186,13 @@ class StatusActionBar extends ImmutablePureComponent {
let menu = []; let menu = [];
let reblogIcon = 'retweet'; let reblogIcon = 'retweet';
let replyIcon;
let replyTitle; let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
@ -181,6 +221,12 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); 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.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
} }
if (status.get('visibility') === 'direct') { if (status.get('visibility') === 'direct') {
@ -190,8 +236,10 @@ class StatusActionBar extends ImmutablePureComponent {
} }
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply); replyTitle = intl.formatMessage(messages.reply);
} else { } else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
@ -201,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>

View File

@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Permalink from './permalink'; import Permalink from './permalink';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
@ -160,7 +161,7 @@ export default class StatusContent extends React.PureComponent {
const readMoreButton = ( const readMoreButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'> <button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' /> <FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
</button> </button>
); );

View File

@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import Compose from '../features/standalone/compose'; import Compose from '../features/standalone/compose';
import initialState from '../initial_state'; import initialState from '../initial_state';
import { fetchCustomEmojis } from '../actions/custom_emojis';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
@ -17,6 +18,8 @@ if (initialState) {
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }
store.dispatch(fetchCustomEmojis());
export default class TimelineContainer extends React.PureComponent { export default class TimelineContainer extends React.PureComponent {
static propTypes = { static propTypes = {

View File

@ -10,8 +10,7 @@ const messages = defineMessages({
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const mapStateToProps = (state, { }) => ({ const mapStateToProps = () => ({});
});
return mapStateToProps; return mapStateToProps;
}; };

View File

@ -6,6 +6,7 @@ import { getLocale } from '../locales';
import MediaGallery from '../components/media_gallery'; import MediaGallery from '../components/media_gallery';
import Video from '../features/video'; import Video from '../features/video';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
import Poll from 'mastodon/components/poll';
import ModalRoot from '../components/modal_root'; import ModalRoot from '../components/modal_root';
import MediaModal from '../features/ui/components/media_modal'; import MediaModal from '../features/ui/components/media_modal';
import { List as ImmutableList, fromJS } from 'immutable'; import { List as ImmutableList, fromJS } from 'immutable';
@ -13,7 +14,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
const MEDIA_COMPONENTS = { MediaGallery, Video, Card }; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
export default class MediaContainer extends PureComponent { export default class MediaContainer extends PureComponent {
@ -54,11 +55,12 @@ export default class MediaContainer extends PureComponent {
{[].map.call(components, (component, i) => { {[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component'); const componentName = component.getAttribute('data-component');
const Component = MEDIA_COMPONENTS[componentName]; const Component = MEDIA_COMPONENTS[componentName];
const { media, card, ...props } = JSON.parse(component.getAttribute('data-props')); const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
Object.assign(props, { Object.assign(props, {
...(media ? { media: fromJS(media) } : {}), ...(media ? { media: fromJS(media) } : {}),
...(card ? { card: fromJS(card) } : {}), ...(card ? { card: fromJS(card) } : {}),
...(poll ? { poll: fromJS(poll) } : {}),
...(componentName === 'Video' ? { ...(componentName === 'Video' ? {
onOpenVideo: this.handleOpenVideo, onOpenVideo: this.handleOpenVideo,

View File

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import Poll from 'mastodon/components/poll';
const mapStateToProps = (state, { pollId }) => ({
poll: state.getIn(['polls', pollId]),
});
export default connect(mapStateToProps)(Poll);

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state'; import { me, isStaff } from '../../../initial_state';
import { shortNumberFormat } from '../../../utils/numbers'; import { shortNumberFormat } from '../../../utils/numbers';
const messages = defineMessages({ const messages = defineMessages({
@ -35,6 +35,7 @@ const messages = defineMessages({
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
}); });
export default @injectIntl export default @injectIntl
@ -151,6 +152,11 @@ class ActionBar extends React.PureComponent {
} }
} }
if (account.get('id') !== me && isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
return ( return (
<div> <div>
{extraInfo} {extraInfo}

View File

@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me } from '../../../initial_state'; import { autoPlayGif, me } from '../../../initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -132,7 +133,7 @@ class Header extends ImmutablePureComponent {
} else if (account.getIn(['relationship', 'blocking'])) { } else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = ( actionBtn = (
<div className='account--action-button'> <div className='account--action-button'>
<IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> <IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
</div> </div>
); );
} }
@ -149,7 +150,7 @@ class Header extends ImmutablePureComponent {
} }
if (account.get('locked')) { if (account.get('locked')) {
lockedIcon = <i className='fa fa-lock' title={intl.formatMessage(messages.account_locked)} />; lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
} }
const content = { __html: account.get('note_emojified') }; const content = { __html: account.get('note_emojified') };
@ -176,7 +177,7 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd> </dd>
</dl> </dl>
))} ))}

View File

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink'; import Permalink from '../../../components/permalink';
import { displayMedia } from '../../../initial_state'; import { displayMedia } from '../../../initial_state';
import Icon from 'mastodon/components/icon';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
@ -45,7 +46,7 @@ export default class MediaItem extends ImmutablePureComponent {
} else { } else {
icon = ( icon = (
<span className='account-gallery__item__icons'> <span className='account-gallery__item__icons'>
<i className='fa fa-eye-slash' /> <Icon id='eye-slash' />
</span> </span>
); );
} }

View File

@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from '../../../components/avatar_overlay'; import AvatarOverlay from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import Icon from 'mastodon/components/icon';
export default class MovedNote extends ImmutablePureComponent { export default class MovedNote extends ImmutablePureComponent {
@ -33,7 +34,7 @@ export default class MovedNote extends ImmutablePureComponent {
return ( return (
<div className='account__moved-note'> <div className='account__moved-note'>
<div className='account__moved-note__message'> <div className='account__moved-note__message'>
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div> <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} /> <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
</div> </div>

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']), accountIds: state.getIn(['user_lists', 'blocks', 'items']),
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { intl, accountIds, shouldUpdateScroll } = this.props; const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='blocks' scrollKey='blocks'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -17,6 +17,7 @@ import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { countableText } from '../util/counter'; import { countableText } from '../util/counter';
import Icon from 'mastodon/components/icon';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -165,7 +166,7 @@ class ComposeForm extends ImmutablePureComponent {
let publishText = ''; let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') { if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
} else { } else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
} }
@ -179,7 +180,7 @@ class ComposeForm extends ImmutablePureComponent {
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
</label> </label>
</div> </div>

View File

@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@ -132,7 +133,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
{items.map(item => ( {items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'> <div className='privacy-dropdown__option__icon'>
<i className={`fa fa-fw fa-${item.icon}`} /> <Icon id={item.icon} fixedWidth />
</div> </div>
<div className='privacy-dropdown__option__content'> <div className='privacy-dropdown__option__content'>
@ -214,7 +215,7 @@ class PrivacyDropdown extends React.PureComponent {
this.options = [ this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
]; ];

View File

@ -5,6 +5,7 @@ import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../../ui/util/optional_motion'; import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import { searchEnabled } from '../../../initial_state'; import { searchEnabled } from '../../../initial_state';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@ -116,8 +117,8 @@ class Search extends React.PureComponent {
</label> </label>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> <Icon id='search' className={hasValue ? '' : 'active'} />
<i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> <Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
</div> </div>
<Overlay show={expanded && !hasValue} placement='bottom' target={this}> <Overlay show={expanded && !hasValue} placement='bottom' target={this}>

View File

@ -6,6 +6,7 @@ import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container'; import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag'; import Hashtag from '../../../components/hashtag';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
@ -34,7 +35,7 @@ class SearchResults extends ImmutablePureComponent {
<div className='search-results'> <div className='search-results'>
<div className='trends'> <div className='trends'>
<div className='trends__header'> <div className='trends__header'>
<i className='fa fa-user-plus fa-fw' /> <Icon id='user-plus' fixedWidth />
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' /> <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
</div> </div>
@ -59,7 +60,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('accounts').size; count += results.get('accounts').size;
accounts = ( accounts = (
<div className='search-results__section'> <div className='search-results__section'>
<h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
</div> </div>
@ -70,7 +71,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('statuses').size; count += results.get('statuses').size;
statuses = ( statuses = (
<div className='search-results__section'> <div className='search-results__section'>
<h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
</div> </div>
@ -81,7 +82,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('hashtags').size; count += results.get('hashtags').size;
hashtags = ( hashtags = (
<div className='search-results__section'> <div className='search-results__section'>
<h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div> </div>
@ -91,7 +92,7 @@ class SearchResults extends ImmutablePureComponent {
return ( return (
<div className='search-results'> <div className='search-results'>
<div className='search-results__header'> <div className='search-results__header'>
<i className='fa fa-search fa-fw' /> <Icon id='search' fixedWidth />
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
</div> </div>

View File

@ -6,6 +6,7 @@ import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
@ -99,17 +100,16 @@ class Upload extends ImmutablePureComponent {
{({ scale }) => ( {({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className={classNames('compose-form__upload__actions', { active })}> <div className={classNames('compose-form__upload__actions', { active })}>
<button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
</div> </div>
<div className={classNames('compose-form__upload-description', { active })}> <div className={classNames('compose-form__upload-description', { active })}>
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
<input <textarea
placeholder={intl.formatMessage(messages.description)} placeholder={intl.formatMessage(messages.description)}
type='text'
value={description} value={description}
maxLength={420} maxLength={420}
onFocus={this.handleInputFocus} onFocus={this.handleInputFocus}

View File

@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent {
key={resetFileKey} key={resetFileKey}
ref={this.setRef} ref={this.setRef}
type='file' type='file'
multiple={false} multiple
accept={acceptContentTypes.toArray().join(',')} accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange} onChange={this.handleChange}
disabled={disabled} disabled={disabled}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Motion from '../../ui/util/optional_motion'; import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
export default class UploadProgress extends React.PureComponent { export default class UploadProgress extends React.PureComponent {
@ -21,7 +22,7 @@ export default class UploadProgress extends React.PureComponent {
return ( return (
<div className='upload-progress'> <div className='upload-progress'>
<div className='upload-progress__icon'> <div className='upload-progress__icon'>
<i className='fa fa-upload' /> <Icon id='upload' />
</div> </div>
<div className='upload-progress__message'> <div className='upload-progress__message'>

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