diff --git a/.env.production.sample b/.env.production.sample index ad058b2b79..da4c7fe4c8 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -17,7 +17,7 @@ LOCAL_DOMAIN=example.com # Use this only if you need to run mastodon on a different domain than the one used for federation. -# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md +# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain # DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING. # WEB_DOMAIN=mastodon.example.com @@ -247,7 +247,7 @@ SMTP_FROM_ADDRESS=notifications@example.com # --------------- # Various ways to customize Mastodon's behavior # --------------- - + # Maximum allowed character count MAX_TOOT_CHARS=500 @@ -279,6 +279,10 @@ MAX_POLL_OPTION_CHARS=100 # Only relevant when elasticsearch is installed # MAX_SEARCH_RESULTS=20 +# Maximum hashtags to display +# Customize the number of hashtags shown in 'Explore' +# MAX_TRENDING_TAGS=10 + # Maximum custom emoji file sizes # If undefined or smaller than MAX_EMOJI_SIZE, the value # of MAX_EMOJI_SIZE will be used for MAX_REMOTE_EMOJI_SIZE diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml index 9cdf813f7b..cdd08d2b0d 100644 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -31,6 +31,11 @@ body: description: What happened? validations: required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false - type: textarea attributes: label: Specifications @@ -38,5 +43,14 @@ body: What version or commit hash of Mastodon did you find this bug in? If a front-end issue, what browser and operating systems were you using? + placeholder: | + Mastodon 3.5.3 (or Edge) + Ruby 2.7.6 (or v3.1.2) + Node.js 16.18.0 + + Google Chrome 106.0.5249.119 + Firefox 105.0.3 + + etc... validations: required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7c0dbaf670..fd62889d05 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,4 @@ blank_issues_enabled: false contact_links: - name: GitHub Discussions url: https://github.com/mastodon/mastodon/discussions - about: Please ask and answer questions here. - - name: Bug Bounty Program - url: https://app.intigriti.com/programs/mastodon/mastodonio/detail - about: Please report security vulnerabilities here. + about: Please ask and answer questions here. \ No newline at end of file diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 3d232120be..4c3cade3bb 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -4,14 +4,13 @@ on: push: branches: - 'main' - tags: - - '*' pull_request: paths: - .github/workflows/build-image.yml - Dockerfile permissions: contents: read + packages: write jobs: build-image: @@ -30,17 +29,15 @@ jobs: id: meta with: images: ghcr.io/${{ github.repository_owner }}/mastodon - flavor: | - latest=auto tags: | + type=raw,value=latest,enable={{is_default_branch}} type=edge,branch=main - type=match,pattern=v(.*),group=0 - type=ref,event=pr + type=sha,prefix=,format=long - uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:edge cache-to: type=inline diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml new file mode 100644 index 0000000000..b9ff808559 --- /dev/null +++ b/.github/workflows/test-chart.yml @@ -0,0 +1,138 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Test chart + +on: + pull_request: + paths: + - "chart/**" + - "!**.md" + - ".github/workflows/test-chart.yml" + push: + paths: + - "chart/**" + - "!**.md" + - ".github/workflows/test-chart.yml" + branches-ignore: + - "dependabot/**" + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + working-directory: chart + +jobs: + lint-templates: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install dependencies (yamllint) + run: pip install yamllint + + - run: helm dependency update + + - name: helm lint + run: | + helm lint . \ + --values dev-values.yaml + + - name: helm template + run: | + helm template . \ + --values dev-values.yaml \ + --output-dir rendered-templates + + - name: yamllint (only on templates we manage) + run: | + rm -rf rendered-templates/mastodon/charts + + yamllint rendered-templates \ + --config-data "{rules: {indentation: {spaces: 2}, line-length: disable}}" + + # This job helps us validate that rendered templates are valid k8s resources + # against a k8s api-server, via "helm template --validate", but also that a + # basic configuration can be used to successfully startup mastodon. + # + test-install: + runs-on: ubuntu-22.04 + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + include: + # k3s-channel reference: https://update.k3s.io/v1-release/channels + - k3s-channel: latest + - k3s-channel: stable + + # This represents the oldest configuration we test against. + # + # The k8s version chosen is based on the oldest still supported k8s + # version among two managed k8s services, GKE, EKS. + # - GKE: https://endoflife.date/google-kubernetes-engine + # - EKS: https://endoflife.date/amazon-eks + # + # The helm client's version can influence what helper functions is + # available for use in the templates, currently we need v3.6.0 or + # higher. + # + - k3s-channel: v1.21 + helm-version: v3.6.0 + + steps: + - uses: actions/checkout@v3 + + # This action starts a k8s cluster with NetworkPolicy enforcement and + # installs both kubectl and helm. + # + # ref: https://github.com/jupyterhub/action-k3s-helm#readme + # + - uses: jupyterhub/action-k3s-helm@v3 + with: + k3s-channel: ${{ matrix.k3s-channel }} + helm-version: ${{ matrix.helm-version }} + metrics-enabled: false + traefik-enabled: false + docker-enabled: false + + - run: helm dependency update + + # Validate rendered helm templates against the k8s api-server + - name: helm template --validate + run: | + helm template --validate mastodon . \ + --values dev-values.yaml + + - name: helm install + run: | + helm install mastodon . \ + --values dev-values.yaml \ + --timeout 10m + + # This actions provides a report about the state of the k8s cluster, + # providing logs etc on anything that has failed and workloads marked as + # important. + # + # ref: https://github.com/jupyterhub/action-k8s-namespace-report#readme + # + - name: Kubernetes namespace report + uses: jupyterhub/action-k8s-namespace-report@v1 + if: always() + with: + important-workloads: >- + deploy/mastodon-sidekiq + deploy/mastodon-streaming + deploy/mastodon-web + job/mastodon-assets-precompile + job/mastodon-chewy-upgrade + job/mastodon-create-admin + job/mastodon-db-migrate diff --git a/.gitignore b/.gitignore index 25c8388e16..7d76b82751 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ /redis /elasticsearch +# ignore Helm charts +/chart/*.tgz + # ignore Helm dependency charts /chart/charts/*.tgz diff --git a/AUTHORS.md b/AUTHORS.md index 9fc5f44f18..18b9f2d708 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -15,32 +15,32 @@ and provided thanks to the work of the following contributors: * [noellabo](https://github.com/noellabo) * [abcang](https://github.com/abcang) * [yiskah](https://github.com/yiskah) +* [tribela](https://github.com/tribela) * [mayaeh](https://github.com/mayaeh) * [nolanlawson](https://github.com/nolanlawson) * [ysksn](https://github.com/ysksn) -* [tribela](https://github.com/tribela) * [sorin-davidoi](https://github.com/sorin-davidoi) * [lynlynlynx](https://github.com/lynlynlynx) * [m4sk1n](mailto:me@m4sk.in) * [Marcin Mikołajczak](mailto:me@m4sk.in) -* [renatolond](https://github.com/renatolond) * [shleeable](https://github.com/shleeable) -* [alpaca-tc](https://github.com/alpaca-tc) +* [renatolond](https://github.com/renatolond) * [zunda](https://github.com/zunda) +* [alpaca-tc](https://github.com/alpaca-tc) * [nclm](https://github.com/nclm) * [ineffyble](https://github.com/ineffyble) * [ariasuni](https://github.com/ariasuni) * [Masoud Abkenar](mailto:ampbox@gmail.com) * [blackle](https://github.com/blackle) * [Quent-in](https://github.com/Quent-in) -* [JantsoP](https://github.com/JantsoP) * [Brawaru](https://github.com/Brawaru) +* [JantsoP](https://github.com/JantsoP) +* [trwnh](https://github.com/trwnh) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) * [dunn](https://github.com/dunn) * [Aditoo17](https://github.com/Aditoo17) * [Quenty31](https://github.com/Quenty31) -* [marek-lach](https://github.com/marek-lach) * [shuheiktgw](https://github.com/shuheiktgw) * [ashfurrow](https://github.com/ashfurrow) * [danhunsaker](https://github.com/danhunsaker) @@ -48,7 +48,6 @@ and provided thanks to the work of the following contributors: * [Jeroen](mailto:jeroenpraat@users.noreply.github.com) * [takayamaki](https://github.com/takayamaki) * [masarakki](https://github.com/masarakki) -* [trwnh](https://github.com/trwnh) * [ticky](https://github.com/ticky) * [ThisIsMissEm](https://github.com/ThisIsMissEm) * [hinaloe](https://github.com/hinaloe) @@ -57,16 +56,19 @@ and provided thanks to the work of the following contributors: * [Wonderfall](https://github.com/Wonderfall) * [matteoaquila](https://github.com/matteoaquila) * [yukimochi](https://github.com/yukimochi) -* [palindromordnilap](https://github.com/palindromordnilap) +* [nightpool](https://github.com/nightpool) +* [alixrossi](https://github.com/alixrossi) * [rkarabut](https://github.com/rkarabut) * [jeroenpraat](mailto:jeroenpraat@users.noreply.github.com) -* [nightpool](https://github.com/nightpool) +* [marek-lach](https://github.com/marek-lach) * [Artoria2e5](https://github.com/Artoria2e5) +* [rinsuki](https://github.com/rinsuki) * [marrus-sh](https://github.com/marrus-sh) * [krainboltgreene](https://github.com/krainboltgreene) * [pfigel](https://github.com/pfigel) * [BoFFire](https://github.com/BoFFire) * [Aldarone](https://github.com/Aldarone) +* [deepy](https://github.com/deepy) * [clworld](https://github.com/clworld) * [MasterGroosha](https://github.com/MasterGroosha) * [dracos](https://github.com/dracos) @@ -75,7 +77,6 @@ and provided thanks to the work of the following contributors: * [Sylvhem](https://github.com/Sylvhem) * [koyuawsmbrtn](https://github.com/koyuawsmbrtn) * [MitarashiDango](https://github.com/MitarashiDango) -* [rinsuki](https://github.com/rinsuki) * [angristan](https://github.com/angristan) * [JeanGauthier](https://github.com/JeanGauthier) * [kschaper](https://github.com/kschaper) @@ -87,14 +88,15 @@ and provided thanks to the work of the following contributors: * [MightyPork](https://github.com/MightyPork) * [ashleyhull-versent](https://github.com/ashleyhull-versent) * [yhirano55](https://github.com/yhirano55) +* [mashirozx](https://github.com/mashirozx) * [devkral](https://github.com/devkral) * [camponez](https://github.com/camponez) * [Hugo Gameiro](mailto:hmgameiro@gmail.com) +* [Marek Ľach](mailto:graweeld@googlemail.com) * [SerCom_KC](mailto:szescxz@gmail.com) * [aschmitz](https://github.com/aschmitz) * [mfmfuyu](https://github.com/mfmfuyu) * [kedamaDQ](https://github.com/kedamaDQ) -* [mashirozx](https://github.com/mashirozx) * [fpiesche](https://github.com/fpiesche) * [gandaro](https://github.com/gandaro) * [johnsudaar](https://github.com/johnsudaar) @@ -106,8 +108,10 @@ and provided thanks to the work of the following contributors: * [hikari-no-yume](https://github.com/hikari-no-yume) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) +* [sunny](https://github.com/sunny) * [puckipedia](https://github.com/puckipedia) -* [spla](mailto:spla@mastodont.cat) +* [splaGit](https://github.com/splaGit) +* [tateisu](https://github.com/tateisu) * [walf443](https://github.com/walf443) * [JoelQ](https://github.com/JoelQ) * [mistydemeo](https://github.com/mistydemeo) @@ -118,13 +122,15 @@ and provided thanks to the work of the following contributors: * [tsuwatch](https://github.com/tsuwatch) * [progval](https://github.com/progval) * [victorhck](https://github.com/victorhck) +* [Izorkin](https://github.com/Izorkin) * [manuelviens](mailto:manuelviens@users.noreply.github.com) -* [tateisu](https://github.com/tateisu) * [fvh-P](https://github.com/fvh-P) * [lfuelling](https://github.com/lfuelling) * [rtucker](https://github.com/rtucker) * [Anna e só](mailto:contraexemplos@gmail.com) +* [danieljakots](https://github.com/danieljakots) * [dariusk](https://github.com/dariusk) +* [Gomasy](https://github.com/Gomasy) * [kazu9su](https://github.com/kazu9su) * [komic](https://github.com/komic) * [lmorchard](https://github.com/lmorchard) @@ -137,6 +143,7 @@ and provided thanks to the work of the following contributors: * [goofy-bz](mailto:goofy@babelzilla.org) * [kadiix](https://github.com/kadiix) * [kodacs](https://github.com/kodacs) +* [luzpaz](https://github.com/luzpaz) * [marcin mikołajczak](mailto:me@m4sk.in) * [berkes](https://github.com/berkes) * [KScl](https://github.com/KScl) @@ -145,23 +152,26 @@ and provided thanks to the work of the following contributors: * [AA4ch1](https://github.com/AA4ch1) * [alexgleason](https://github.com/alexgleason) * [cpytel](https://github.com/cpytel) +* [cutls](https://github.com/cutls) * [northerner](https://github.com/northerner) * [weex](https://github.com/weex) +* [erbridge](https://github.com/erbridge) * [fhemberger](https://github.com/fhemberger) -* [Gomasy](https://github.com/Gomasy) * [greysteil](https://github.com/greysteil) * [henrycatalinismith](https://github.com/henrycatalinismith) +* [HolgerHuo](https://github.com/HolgerHuo) * [d6rkaiz](https://github.com/d6rkaiz) * [ladyisatis](https://github.com/ladyisatis) * [JMendyk](https://github.com/JMendyk) +* [kescherCode](https://github.com/kescherCode) * [JohnD28](https://github.com/JohnD28) * [znz](https://github.com/znz) * [saper](https://github.com/saper) * [Naouak](https://github.com/Naouak) * [pawelngei](https://github.com/pawelngei) +* [rgroothuijsen](https://github.com/rgroothuijsen) * [reneklacan](https://github.com/reneklacan) * [ekiru](https://github.com/ekiru) -* [Izorkin](https://github.com/Izorkin) * [unasuke](https://github.com/unasuke) * [geta6](https://github.com/geta6) * [happycoloredbanana](https://github.com/happycoloredbanana) @@ -174,7 +184,6 @@ and provided thanks to the work of the following contributors: * [aji-su](https://github.com/aji-su) * [ikuradon](https://github.com/ikuradon) * [nzws](https://github.com/nzws) -* [duxovni](https://github.com/duxovni) * [SuperSandro2000](https://github.com/SuperSandro2000) * [178inaba](https://github.com/178inaba) * [acid-chicken](https://github.com/acid-chicken) @@ -183,7 +192,6 @@ and provided thanks to the work of the following contributors: * [aablinov](https://github.com/aablinov) * [stalker314314](https://github.com/stalker314314) * [cohosh](https://github.com/cohosh) -* [cutls](https://github.com/cutls) * [huertanix](https://github.com/huertanix) * [eleboucher](https://github.com/eleboucher) * [halkeye](https://github.com/halkeye) @@ -191,6 +199,7 @@ and provided thanks to the work of the following contributors: * [treby](https://github.com/treby) * [jpdevries](https://github.com/jpdevries) * [gdpelican](https://github.com/gdpelican) +* [pbzweihander](https://github.com/pbzweihander) * [MonaLisaOverrdrive](https://github.com/MonaLisaOverrdrive) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [panarom](https://github.com/panarom) @@ -201,7 +210,6 @@ and provided thanks to the work of the following contributors: * [pierreozoux](https://github.com/pierreozoux) * [qguv](https://github.com/qguv) * [Ram Lmn](mailto:ramlmn@users.noreply.github.com) -* [rgroothuijsen](https://github.com/rgroothuijsen) * [Sascha](mailto:sascha@serenitylabs.cloud) * [harukasan](https://github.com/harukasan) * [stamak](https://github.com/stamak) @@ -217,11 +225,14 @@ and provided thanks to the work of the following contributors: * [chr-1x](https://github.com/chr-1x) * [esetomo](https://github.com/esetomo) * [foxiehkins](https://github.com/foxiehkins) +* [gol-cha](https://github.com/gol-cha) * [highemerly](https://github.com/highemerly) * [hoodie](mailto:hoodiekitten@outlook.com) * [kaiyou](https://github.com/kaiyou) * [007lva](https://github.com/007lva) * [luzi82](https://github.com/luzi82) +* [prplecake](https://github.com/prplecake) +* [duxovni](https://github.com/duxovni) * [slice](https://github.com/slice) * [tmm576](https://github.com/tmm576) * [unsmell](mailto:unsmell@users.noreply.github.com) @@ -251,25 +262,27 @@ and provided thanks to the work of the following contributors: * [cdutson](https://github.com/cdutson) * [farlistener](https://github.com/farlistener) * [baby-gnu](https://github.com/baby-gnu) -* [danieljakots](https://github.com/danieljakots) * [divergentdave](https://github.com/divergentdave) * [DavidLibeau](https://github.com/DavidLibeau) * [dmerejkowsky](https://github.com/dmerejkowsky) * [ddevault](https://github.com/ddevault) +* [emilyst](https://github.com/emilyst) +* [consideRatio](https://github.com/consideRatio) * [Fjoerfoks](https://github.com/Fjoerfoks) * [fmauNeko](https://github.com/fmauNeko) * [gloaec](https://github.com/gloaec) * [unstabler](https://github.com/unstabler) * [potato4d](https://github.com/potato4d) * [h-izumi](https://github.com/h-izumi) -* [HolgerHuo](https://github.com/HolgerHuo) * [ErikXXon](https://github.com/ErikXXon) * [ian-kelling](https://github.com/ian-kelling) * [eltociear](https://github.com/eltociear) * [immae](https://github.com/immae) * [J0WI](https://github.com/J0WI) -* [vahnj](https://github.com/vahnj) +* [koboldunderlord](https://github.com/koboldunderlord) * [foozmeat](https://github.com/foozmeat) +* [jgsmith](https://github.com/jgsmith) +* [raggi](https://github.com/raggi) * [jasonrhodes](https://github.com/jasonrhodes) * [Jason Snell](mailto:jason@newrelic.com) * [jviide](https://github.com/jviide) @@ -287,21 +300,25 @@ and provided thanks to the work of the following contributors: * [Markus Amalthea Magnuson](mailto:markus.magnuson@gmail.com) * [madmath03](https://github.com/madmath03) * [mig5](https://github.com/mig5) +* [mohe2015](https://github.com/mohe2015) * [moritzheiber](https://github.com/moritzheiber) * [Nathaniel Suchy](mailto:me@lunorian.is) * [ndarville](https://github.com/ndarville) * [NimaBoscarino](https://github.com/NimaBoscarino) * [aquarla](https://github.com/aquarla) * [Abzol](https://github.com/Abzol) +* [unextro](https://github.com/unextro) * [PatOnTheBack](https://github.com/PatOnTheBack) * [xPaw](https://github.com/xPaw) * [petzah](https://github.com/petzah) * [PeterDaveHello](https://github.com/PeterDaveHello) * [ignisf](https://github.com/ignisf) +* [postmodern](https://github.com/postmodern) * [lumenwrites](https://github.com/lumenwrites) * [remram44](https://github.com/remram44) * [sts10](https://github.com/sts10) * [u1-liquid](https://github.com/u1-liquid) +* [SISheogorath](https://github.com/SISheogorath) * [rosylilly](https://github.com/rosylilly) * [withshubh](https://github.com/withshubh) * [sim6](https://github.com/sim6) @@ -328,23 +345,23 @@ and provided thanks to the work of the following contributors: * [bsky](mailto:me@imbsky.net) * [codl](https://github.com/codl) * [cpsdqs](https://github.com/cpsdqs) +* [dogelover911](https://github.com/dogelover911) * [barzamin](https://github.com/barzamin) -* [gol-cha](https://github.com/gol-cha) * [gunchleoc](https://github.com/gunchleoc) * [fhalna](https://github.com/fhalna) * [haoyayoi](https://github.com/haoyayoi) +* [helloworldstack](https://github.com/helloworldstack) * [ik11235](https://github.com/ik11235) * [kawax](https://github.com/kawax) * [shrft](https://github.com/shrft) * [luigi](mailto:lvargas@rankia.com) -* [luzpaz](https://github.com/luzpaz) * [mbajur](https://github.com/mbajur) * [matsurai25](https://github.com/matsurai25) * [mecab](https://github.com/mecab) * [nicobz25](https://github.com/nicobz25) * [niwatori24](https://github.com/niwatori24) * [noiob](https://github.com/noiob) -* [oliverkeeble](https://github.com/oliverkeeble) +* [oliverkeeble](mailto:oliverkeeble@users.noreply.github.com) * [partev](https://github.com/partev) * [pinfort](https://github.com/pinfort) * [rbaumert](https://github.com/rbaumert) @@ -360,7 +377,7 @@ and provided thanks to the work of the following contributors: * [clarfonthey](https://github.com/clarfonthey) * [cygnan](https://github.com/cygnan) * [Awea](https://github.com/Awea) -* [eai04191](https://github.com/eai04191) +* [single-right-quote](https://github.com/single-right-quote) * [8398a7](https://github.com/8398a7) * [857b](https://github.com/857b) * [insom](https://github.com/insom) @@ -373,9 +390,10 @@ and provided thanks to the work of the following contributors: * [unleashed](https://github.com/unleashed) * [alxrcs](https://github.com/alxrcs) * [console-cowboy](https://github.com/console-cowboy) +* [Saiv46](https://github.com/Saiv46) * [Alkarex](https://github.com/Alkarex) * [a2](https://github.com/a2) -* [alfiedotwtf](https://github.com/alfiedotwtf) +* [Alfie John](mailto:33c6c91f3bb4a391082e8a29642cafaf@alfie.wtf) * [0xa](https://github.com/0xa) * [ashpieboop](https://github.com/ashpieboop) * [virtualpain](https://github.com/virtualpain) @@ -391,24 +409,34 @@ and provided thanks to the work of the following contributors: * [orlea](https://github.com/orlea) * [armandfardeau](https://github.com/armandfardeau) * [raboof](https://github.com/raboof) +* [v-aisac](https://github.com/v-aisac) +* [gi-yt](https://github.com/gi-yt) +* [boahc077](https://github.com/boahc077) * [aldatsa](https://github.com/aldatsa) * [jumbosushi](https://github.com/jumbosushi) * [acuteaura](https://github.com/acuteaura) * [ayumin](https://github.com/ayumin) * [bzg](https://github.com/bzg) * [BastienDurel](https://github.com/BastienDurel) +* [bearice](https://github.com/bearice) * [li-bei](https://github.com/li-bei) +* [hardillb](https://github.com/hardillb) * [Benedikt Geißler](mailto:benedikt@g5r.eu) * [BenisonSebastian](https://github.com/BenisonSebastian) * [Blake](mailto:blake.barnett@postmates.com) * [Brad Janke](mailto:brad.janke@gmail.com) +* [braydofficial](https://github.com/braydofficial) * [bclindner](https://github.com/bclindner) * [brycied00d](https://github.com/brycied00d) * [carlosjs23](https://github.com/carlosjs23) +* [WyriHaximus](https://github.com/WyriHaximus) * [cgxxx](https://github.com/cgxxx) * [kibitan](https://github.com/kibitan) +* [cdzombak](https://github.com/cdzombak) * [chrisheninger](https://github.com/chrisheninger) * [chris-martin](https://github.com/chris-martin) +* [offbyone](https://github.com/offbyone) +* [cclauss](https://github.com/cclauss) * [DoubleMalt](https://github.com/DoubleMalt) * [Moosh-be](https://github.com/Moosh-be) * [cchoi12](https://github.com/cchoi12) @@ -417,6 +445,8 @@ and provided thanks to the work of the following contributors: * [csu](https://github.com/csu) * [kklleemm](https://github.com/kklleemm) * [colindean](https://github.com/colindean) +* [CommanderRoot](https://github.com/CommanderRoot) +* [connorshea](https://github.com/connorshea) * [DeeUnderscore](https://github.com/DeeUnderscore) * [dachinat](https://github.com/dachinat) * [Daggertooth](mailto:dev@monsterpit.net) @@ -428,35 +458,40 @@ and provided thanks to the work of the following contributors: * [dar5hak](https://github.com/dar5hak) * [kant](https://github.com/kant) * [maxolasersquad](https://github.com/maxolasersquad) -* [singingwolfboy](https://github.com/singingwolfboy) -* [caldwell](https://github.com/caldwell) -* [davidcelis](https://github.com/davidcelis) -* [davefp](https://github.com/davefp) -* [hannahwhy](https://github.com/hannahwhy) -* [debanshuk](https://github.com/debanshuk) -* [mascali33](https://github.com/mascali33) -* [DerekNonGeneric](https://github.com/DerekNonGeneric) -* [dblandin](https://github.com/dblandin) -* [Aranaur](https://github.com/Aranaur) -* [dtschust](https://github.com/dtschust) -* [Dryusdan](https://github.com/Dryusdan) -* [d3vgru](https://github.com/d3vgru) -* [Elizafox](https://github.com/Elizafox) -* [enewhuis](https://github.com/enewhuis) -* [ericblade](https://github.com/ericblade) -* [mikoim](https://github.com/mikoim) -* [espenronnevik](https://github.com/espenronnevik) +* [David Baumgold](mailto:david@davidbaumgold.com) +* [David Caldwell](mailto:david+github@porkrind.org) +* [David Celis](mailto:me@davidcel.is) +* [David Hewitt](mailto:davidmhewitt@users.noreply.github.com) +* [David Underwood](mailto:davefp@gmail.com) +* [David Yip](mailto:yipdw@member.fsf.org) +* [Debanshu Kundu](mailto:debanshu.kundu@joshtechnologygroup.com) +* [Denis Teyssier](mailto:admin@mascali.ovh) +* [Derek Lewis](mailto:derekcecillewis@gmail.com) +* [Devon Blandin](mailto:dblandin@gmail.com) +* [Drew Gates](mailto:aranaur@users.noreply.github.com) +* [Drew Schuster](mailto:dtschust@gmail.com) +* [Dryusdan](mailto:dryusdan@dryusdan.fr) +* [Eai](mailto:eai@mizle.net) +* [Ed Knutson](mailto:knutsoned@gmail.com) +* [Effy Elden](mailto:effy@effy.space) +* [Elizabeth Myers](mailto:elizabeth@interlinked.me) +* [Eric](mailto:enewhuis@gmail.com) +* [Eric Blade](mailto:blade.eric@gmail.com) +* [Eshin Kunishima](mailto:mikoim@users.noreply.github.com) +* [Espen Rønnevik](mailto:espen@ronnevik.net) * [Expenses](mailto:expenses@airmail.cc) -* [fabianonline](https://github.com/fabianonline) -* [shello](https://github.com/shello) -* [Finariel](https://github.com/Finariel) -* [siuying](https://github.com/siuying) -* [zoc](https://github.com/zoc) -* [fwenzel](https://github.com/fwenzel) -* [gabrielrumiranda](https://github.com/gabrielrumiranda) -* [GenbuHase](https://github.com/GenbuHase) -* [nilsding](https://github.com/nilsding) -* [hattori6789](https://github.com/hattori6789) +* [Fabian Schlenz](mailto:mail@fabianonline.de) +* [Faye Duxovni](mailto:duxovni@duxovni.org) +* [Filipe Rodrigues](mailto:shello@shello.org) +* [Finariel](mailto:finariel@gmail.com) +* [Francis Chong](mailto:francis@ignition.hk) +* [Franck Zoccolo](mailto:franck@zoccolo.com) +* [Fred Wenzel](mailto:fwenzel@users.noreply.github.com) +* [Gabriel Rubens](mailto:gabrielrumiranda@gmail.com) +* [Gaelan Steele](mailto:gbs@canishe.com) +* [Genbu Hase](mailto:hasegenbu@gmail.com) +* [Georg Gadinger](mailto:nilsding@nilsding.org) +* [George Hattori](mailto:hattori6789@users.noreply.github.com) * [Gergely Nagy](mailto:algernon@users.noreply.github.com) * [Giuseppe Pignataro](mailto:rogepix@gmail.com) * [Greg V](mailto:greg@unrelenting.technology) @@ -466,7 +501,9 @@ and provided thanks to the work of the following contributors: * [György Nádudvari](mailto:reedcourty@users.noreply.github.com) * [HIKARU KOBORI](mailto:hk.uec.univ@gmail.com) * [Haelwenn Monnier](mailto:lanodan@users.noreply.github.com) +* [Hampton Lintorn-Catlin](mailto:hcatlin@gmail.com) * [Harmon](mailto:harmon758@gmail.com) +* [Hayden](mailto:contact@winisreallybored.com) * [HellPie](mailto:hellpie@users.noreply.github.com) * [Herbert Kagumba](mailto:habukagumba@gmail.com) * [Hiroe Jun](mailto:jun.hiroe@gmail.com) @@ -479,6 +516,7 @@ and provided thanks to the work of the following contributors: * [Ian McDowell](mailto:me@ianmcdowell.net) * [Iijima Yasushi](mailto:kurage.cc@gmail.com) * [Ingo Blechschmidt](mailto:iblech@web.de) +* [Irie Aoi](mailto:eai@mizle.net) * [J Yeary](mailto:usbsnowcrash@users.noreply.github.com) * [Jack Michaud](mailto:jack-michaud@users.noreply.github.com) * [Jakub Mendyk](mailto:jakubmendyk.szkola@gmail.com) @@ -493,6 +531,7 @@ and provided thanks to the work of the following contributors: * [Jo Decker](mailto:trolldecker@users.noreply.github.com) * [Joan Montané](mailto:jmontane@users.noreply.github.com) * [Joe](mailto:401283+htmlbyjoe@users.noreply.github.com) +* [Joe Friedl](mailto:stuff@joefriedl.net) * [Jonathan Klee](mailto:klee.jonathan@gmail.com) * [Jordan Guerder](mailto:jguerder@fr.pulseheberg.net) * [Joseph Mingrone](mailto:jehops@users.noreply.github.com) @@ -502,6 +541,7 @@ and provided thanks to the work of the following contributors: * [Julien](mailto:tiwy57@users.noreply.github.com) * [Julien Deswaef](mailto:juego@requiem4tv.com) * [June Sallou](mailto:jnsll@users.noreply.github.com) +* [Justin Thomas](mailto:justin@jdt.io) * [Jérémy Benoist](mailto:j0k3r@users.noreply.github.com) * [KEINOS](mailto:github@keinos.com) * [Kairui Song | 宋恺睿](mailto:ryncsn@gmail.com) @@ -522,6 +562,7 @@ and provided thanks to the work of the following contributors: * [Mantas](mailto:mistermantas@users.noreply.github.com) * [Mareena Kunjachan](mailto:mareenakunjachan@gmail.com) * [Marek Lach](mailto:marek.brohatwack.lach@gmail.com) +* [Markus Petzsch](mailto:markus@petzsch.eu) * [Markus R](mailto:wirehack7@users.noreply.github.com) * [Marty McGuire](mailto:schmartissimo@gmail.com) * [Marvin Kopf](mailto:marvinkopf@posteo.de) @@ -531,12 +572,15 @@ and provided thanks to the work of the following contributors: * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [Mathieu Brunot](mailto:mb.mathieu.brunot@gmail.com) * [Matt](mailto:matt-auckland@users.noreply.github.com) +* [Matt Corallo](mailto:649246+thebluematt@users.noreply.github.com) * [Matt Sweetman](mailto:webroo@gmail.com) +* [Matthias Bethke](mailto:matthias@towiski.de) * [Matthias Beyer](mailto:mail@beyermatthias.de) * [Matthias Jouan](mailto:matthias.jouan@gmail.com) * [Matthieu Paret](mailto:matthieuparet69@gmail.com) * [Maxime BORGES](mailto:maxime.borges@gmail.com) * [Mayu Laierlence](mailto:minacle@live.com) +* [Meisam](mailto:39205857+mftabriz@users.noreply.github.com) * [Michael Deeb](mailto:michaeldeeb@me.com) * [Michael Vieira](mailto:dtox94@gmail.com) * [Michel](mailto:michel@cyweo.com) @@ -558,6 +602,7 @@ and provided thanks to the work of the following contributors: * [Nanamachi](mailto:town7.haruki@gmail.com) * [Nathaniel Ekoniak](mailto:nekoniak@ennate.tech) * [NecroTechno](mailto:necrotechno@riseup.net) +* [Nicholas La Roux](mailto:larouxn@gmail.com) * [Nick Gerakines](mailto:nick@gerakines.net) * [Nicolai von Neudeck](mailto:nicolai@vonneudeck.com) * [Ninetailed](mailto:ninetailed@gmail.com) @@ -575,18 +620,25 @@ and provided thanks to the work of the following contributors: * [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com) * [Paul](mailto:naydex.mc+github@gmail.com) * [Pete Keen](mailto:pete@petekeen.net) +* [Pierre Bourdon](mailto:delroth@gmail.com) * [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com) * [Ratmir Karabut](mailto:rkarabut@sfmodern.ru) * [Reto Kromer](mailto:retokromer@users.noreply.github.com) +* [Rob Petti](mailto:rob.petti@gmail.com) * [Rob Watson](mailto:rfwatson@users.noreply.github.com) +* [Robert Laurenz](mailto:8169746+laurenzcodes@users.noreply.github.com) * [Rohan Sharma](mailto:i.am.lone.survivor@protonmail.com) +* [Roni Laukkarinen](mailto:roni@laukkarinen.info) * [Ryan Freebern](mailto:ryan@freebern.org) * [Ryan Wade](mailto:ryan.wade@protonmail.com) * [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info) * [S.H](mailto:gamelinks007@gmail.com) +* [SJang1](mailto:git@sjang.dev) * [Sadiq Saif](mailto:staticsafe@users.noreply.github.com) * [Sam Hewitt](mailto:hewittsamuel@gmail.com) +* [Samuel Kaiser](mailto:sk22@mailbox.org) * [Sara Aimée Smiseth](mailto:51710585+sarasmiseth@users.noreply.github.com) +* [Sara Golemon](mailto:pollita@php.net) * [Satoshi KOJIMA](mailto:skoji@mac.com) * [ScienJus](mailto:i@scienjus.com) * [Scott Larkin](mailto:scott@codeclimate.com) @@ -607,6 +659,7 @@ and provided thanks to the work of the following contributors: * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Stanislas](mailto:stanislas.lange@pm.me) * [StefOfficiel](mailto:pichard.stephane@free.fr) +* [Stefano Pigozzi](mailto:ste.pigozzi@gmail.com) * [Steven Tappert](mailto:admin@dark-it.net) * [Stéphane Guillou](mailto:stephane.guillou@member.fsf.org) * [Su Yang](mailto:soulteary@users.noreply.github.com) @@ -619,13 +672,16 @@ and provided thanks to the work of the following contributors: * [TakesxiSximada](mailto:takesxi.sximada@gmail.com) * [Tao Bror Bojlén](mailto:brortao@users.noreply.github.com) * [Taras Gogol](mailto:taras2358@gmail.com) +* [The Stranjer](mailto:791672+thestranjer@users.noreply.github.com) * [TheInventrix](mailto:theinventrix@users.noreply.github.com) * [TheMainOne](mailto:50847364+theevilskeleton@users.noreply.github.com) * [Thomas Alberola](mailto:thomas@needacoffee.fr) +* [Thomas Citharel](mailto:github@tcit.fr) * [Toby Deshane](mailto:fortyseven@users.noreply.github.com) * [Toby Pinder](mailto:gigitrix@gmail.com) * [Tomonori Murakami](mailto:crosslife777@gmail.com) * [TomoyaShibata](mailto:wind.of.hometown@gmail.com) +* [Tony Jiang](mailto:yujiang99@gmail.com) * [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com) * [Truong Nguyen](mailto:truongnmt.dev@gmail.com) * [Udo Kramer](mailto:optik@fluffel.io) @@ -634,6 +690,7 @@ and provided thanks to the work of the following contributors: * [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp) * [Valentin Lorentz](mailto:progval+git@progval.net) * [Vladimir Mincev](mailto:vladimir@canicinteractive.com) +* [Vyr Cossont](mailto:vyrcossont@users.noreply.github.com) * [Waldir Pimenta](mailto:waldyrious@gmail.com) * [Wenceslao Páez Chávez](mailto:wcpaez@gmail.com) * [Wesley Ellis](mailto:tahnok@gmail.com) @@ -648,10 +705,12 @@ and provided thanks to the work of the following contributors: * [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp) * [Yanaken](mailto:yanakend@gmail.com) * [Yann Klis](mailto:yann.klis@gmail.com) +* [Yarden Shoham](mailto:hrsi88@gmail.com) * [Yağızhan](mailto:35808275+yagizhan49@users.noreply.github.com) * [Yeechan Lu](mailto:wz.bluesnow@gmail.com) * [Your Name](mailto:lorenzd@gmail.com) * [Yusuke Abe](mailto:moonset20@gmail.com) +* [Zach Flanders](mailto:zachflanders@gmail.com) * [Zach Neill](mailto:neillz@berea.edu) * [Zachary Spector](mailto:logicaldash@gmail.com) * [ZiiX](mailto:ziix@users.noreply.github.com) @@ -666,8 +725,8 @@ and provided thanks to the work of the following contributors: * [chrolis](mailto:chrolis@users.noreply.github.com) * [cormo](mailto:cormorant2+github@gmail.com) * [d0p1](mailto:dopi-sama@hush.com) -* [dogelover911](mailto:84288771+dogelover911@users.noreply.github.com) * [dxwc](mailto:dxwc@users.noreply.github.com) +* [eai04191](mailto:eai@mizle.net) * [evilny0](mailto:evilny0@moomoocamp.net) * [febrezo](mailto:felixbrezo@gmail.com) * [fsubal](mailto:fsubal@users.noreply.github.com) @@ -678,7 +737,6 @@ and provided thanks to the work of the following contributors: * [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [haosbvnker](mailto:github@chaosbunker.com) * [heguro](mailto:65112898+heguro@users.noreply.github.com) -* [helloworldstack](mailto:66512512+helloworldstack@users.noreply.github.com) * [ichi_i](mailto:51489410+ichi-i@users.noreply.github.com) * [isati](mailto:phil@juchnowi.cz) * [jacob](mailto:jacobherringtondeveloper@gmail.com) @@ -688,15 +746,18 @@ and provided thanks to the work of the following contributors: * [jooops](mailto:joops@autistici.org) * [jukper](mailto:jukkaperanto@gmail.com) * [jumoru](mailto:jumoru@mailbox.org) +* [k.bigwheel (kazufumi nishida)](mailto:k.bigwheel+eng@gmail.com) * [kaias1jp](mailto:kaias1jp@gmail.com) * [karlyeurl](mailto:karl.yeurl@gmail.com) * [kawaguchi](mailto:jiikko@users.noreply.github.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com) +* [keiya](mailto:keiya_21@yahoo.co.jp) * [kuro5hin](mailto:rusty@kuro5hin.org) * [leo60228](mailto:leo@60228.dev) * [matildepark](mailto:matilde.park@pm.me) * [maxypy](mailto:maxime@mpigou.fr) * [mhe](mailto:mail@marcus-herrmann.com) +* [mickkael](mailto:19755421+mickkael@users.noreply.github.com) * [mike castleman](mailto:m@mlcastle.net) * [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mohemohe](mailto:mohemohe@users.noreply.github.com) @@ -707,9 +768,11 @@ and provided thanks to the work of the following contributors: * [notozeki](mailto:notozeki@users.noreply.github.com) * [ntl-purism](mailto:57806346+ntl-purism@users.noreply.github.com) * [nzws](mailto:git-yuzu@svk.jp) +* [pea-sys](mailto:49807271+pea-sys@users.noreply.github.com) * [potpro](mailto:pptppctt@gmail.com) * [proxy](mailto:51172302+3n-k1@users.noreply.github.com) * [rch850](mailto:rich850@gmail.com) +* [rcombs](mailto:rcombs@rcombs.me) * [roikale](mailto:roikale@users.noreply.github.com) * [rysiekpl](mailto:rysiek@hackerspace.pl) * [sasanquaneuf](mailto:sasanquaneuf@gmail.com) @@ -726,13 +789,13 @@ and provided thanks to the work of the following contributors: * [tmyt](mailto:shigure@refy.net) * [trevDev()](mailto:trev@trevdev.ca) * [tsia](mailto:github@tsia.de) +* [txt-file](mailto:44214237+txt-file@users.noreply.github.com) * [utam0k](mailto:k0ma@utam0k.jp) * [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com) * [walfie](mailto:walfington@gmail.com) * [y-temp4](mailto:y.temp4@gmail.com) * [ymmtmdk](mailto:ymmtmdk@gmail.com) * [yoshipc](mailto:yoooo@yoshipc.net) -* [zunda](mailto:zundan@gmail.com) * [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com) * [ばん](mailto:detteiu0321@gmail.com) * [ふるふる](mailto:frfs@users.noreply.github.com) @@ -752,599 +815,951 @@ This document is provided for informational purposes only. Since it is only upda Following people have contributed to translation of Mastodon: - GunChleoc (*Scottish Gaelic*) -- ケインツロ 👾 (KNTRO) (*Spanish, Argentina*) -- Sveinn í Felli (sveinki) (*Icelandic*) +- ケインツロ ⚧️👾🛸 (KNTRO) (*Spanish, Argentina*) - Hồ Nhất Duy (honhatduy) (*Vietnamese*) +- Sveinn í Felli (sveinki) (*Icelandic*) +- Kristaps (Kristaps_M) (*Latvian*) +- NCAA (*Danish, French*) - Zoltán Gera (gerazo) (*Hungarian*) -- Kristaps_M (*Latvian*) -- NCAA (*French, Danish*) -- adrmzz (*Sardinian*) -- Xosé M. (XoseM) (*Spanish, Galician*) -- Ramdziana F Y (rafeyu) (*Indonesian*) -- Jeong Arm (Kjwon15) (*Spanish, Japanese, Korean, Esperanto*) +- ghose (XoseM) (*Galician, Spanish*) +- Jeong Arm (Kjwon15) (*Korean, Esperanto, Japanese, Spanish*) - Emanuel Pina (emanuelpina) (*Portuguese*) -- qezwan (*Persian, Sorani (Kurdish)*) -- Besnik_b (*Albanian*) -- ButterflyOfFire (BoFFire) (*French, Arabic, Kabyle*) +- Reyzadren (*Ido, Malay*) - Thai Localization (thl10n) (*Thai*) +- Besnik_b (*Albanian*) +- Joene (joenepraat) (*Dutch*) - Cyax (Cyaxares) (*Kurmanji (Kurdish)*) +- adrmzz (*Sardinian*) +- Ramdziana F Y (rafeyu) (*Indonesian*) +- xatier (*Chinese Traditional, Chinese Traditional, Hong Kong*) +- qezwan (*Sorani (Kurdish), Persian*) +- spla (*Catalan, Spanish*) +- ButterflyOfFire (BoFFire) (*Arabic, French, Kabyle*) +- Martin (miles) (*Slovenian*) +- නාමල් ජයසිංහ (nimnaya) (*Sinhala*) +- Asier Iturralde Sarasola (aldatsa) (*Basque*) +- Ondřej Pokorný (unextro) (*Czech*) +- Roboron (*Spanish*) - taicv (*Vietnamese*) +- koyu (*German*) - Daniele Lira Mereb (danilmereb) (*Portuguese, Brazilian*) -- spla (*Spanish, Catalan*) +- T. E. Kalaycı (tekrei) (*Turkish*) - Evert Prants (IcyDiamond) (*Estonian*) -- koyu (*German*) -- Alix Rossi (palindromordnilap) (*French, Esperanto, Corsican*) -- Joene (joenepraat) (*Dutch*) +- Yair Mahalalel (yairm) (*Hebrew*) +- Ihor Hordiichuk (ihor_ck) (*Ukrainian*) +- Alessandro Levati (Oct326) (*Italian*) +- Kimmo Kujansuu (mrkujansuu) (*Finnish*) +- Alix Rossi (palindromordnilap) (*Corsican, Esperanto, French*) +- Danial Behzadi (danialbehzadi) (*Persian*) - stan ionut (stanionut12) (*Romanian*) - Mastodon 中文译者 (mastodon-linguist) (*Chinese Simplified*) - Kristijan Tkalec (lapor) (*Slovenian*) -- Danial Behzadi (danialbehzadi) (*Persian*) -- Asier Iturralde Sarasola (aldatsa) (*Basque*) +Alexander Sorokin (Brawaru) (*Russian, Vietnamese, Swedish, Portuguese, Tamil, Kabyle, Polish, Italian, Catalan, Armenian, Hungarian, Albanian, Greek, Galician, Korean, Ukrainian, German, Danish, French*) - ManeraKai (*Arabic*) - мачко (ma4ko) (*Bulgarian*) -- Roboron (*Spanish*) -- Alessandro Levati (Oct326) (*Italian*) -- xatier (*Chinese Traditional, Chinese Traditional, Hong Kong*) -- Ondřej Pokorný (unextro) (*Czech*) -- Alexander Sorokin (Brawaru) (*French, Catalan, Danish, German, Greek, Hungarian, Armenian, Korean, Portuguese, Russian, Albanian, Swedish, Ukrainian, Vietnamese, Galician*) - kamee (*Armenian*) -- Michal Stanke (mstanke) (*Czech*) +- Yamagishi Kazutoshi (ykzts) (*Japanese, Icelandic, Sorani (Kurdish), Albanian, Vietnamese, Chinese Simplified*) +- Takeçi (polygoat) (*French, Italian*) +- REMOVED_USER (*Czech*) - borys_sh (*Ukrainian*) - Imre Kristoffer Eilertsen (DandelionSprout) (*Norwegian*) -- yeft (*Chinese Traditional, Chinese Traditional, Hong Kong*) +- Marek Ľach (mareklach) (*Slovak, Polish*) +- yeft (*Chinese Traditional, Hong Kong, Chinese Traditional*) +- D. Cederberg (cederberget) (*Swedish*) - Miguel Mayol (mitcoes) (*Spanish, Catalan*) -- Marek Ľach (mareklach) (*Polish, Slovak*) +- enolp (*Asturian*) - Manuel Viens (manuelviens) (*French*) -- Kimmo Kujansuu (mrkujansuu) (*Finnish*) +- cybergene (*Japanese*) +- REMOVED_USER (*Turkish*) +- xpil (*Polish, Scottish Gaelic*) +- Balázs Meskó (mesko.balazs) (*Hungarian, Czech*) - Koala Yeung (yookoala) (*Chinese Traditional, Hong Kong*) -- enolp (*Asturian*) - Osoitz (*Basque*) -- Peterandre (*Norwegian, Norwegian Nynorsk*) -- tzium (*Sardinian*) +- Amir Rubinstein - TAU (AmirrTAU) (*Hebrew, Indonesian*) - Maya Minatsuki (mayaeh) (*Japanese*) -- Mélanie Chauvel (ariasuni) (*French, Arabic, Czech, German, Greek, Hungarian, Slovenian, Ukrainian, Chinese Simplified, Portuguese, Brazilian, Persian, Norwegian Nynorsk, Esperanto, Breton, Corsican, Sardinian, Kabyle*) -- T. E. Kalaycı (tekrei) (*Turkish*) -- Takeçi (polygoat) (*French, Italian*) +- Peterandre (*Norwegian Nynorsk, Norwegian*) +Mélanie Chauvel (ariasuni) (*French, Esperanto, Norwegian Nynorsk, Persian, Kabyle, Sardinian, Corsican, Breton, Portuguese, Brazilian, Arabic, Chinese Simplified, Ukrainian, Slovenian, Greek, German, Czech, Hungarian*) +- tzium (*Sardinian*) +- Diluns (*Occitan*) - Galician Translator (Galician_translator) (*Galician*) +- Marcin Mikołajczak (mkljczkk) (*Polish, Czech, Russian*) +- Jeff Huang (s8321414) (*Chinese Traditional*) +- Pixelcode (realpixelcode) (*German*) +- Allen Zhong (AstroProfundis) (*Chinese Simplified*) - lamnatos (*Greek*) - Sean Young (assanges) (*Chinese Traditional*) +- retiolus (*Catalan, French, Spanish*) - tolstoevsky (*Russian*) -- Ihor Hordiichuk (ihor_ck) (*Ukrainian*) - Ali Demirtaş (alidemirtas) (*Turkish*) -- Jasmine Cam Andrever (gourmas) (*Cornish*) +- J. Cam Andrever-Wright (gourmas) (*Cornish*) - coxde (*Chinese Simplified*) +- Dremski (*Bulgarian*) - gagik_ (*Armenian*) - Masoud Abkenar (mabkenar) (*Persian*) - arshat (*Kazakh*) -- Marcin Mikołajczak (mkljczkk) (*Czech, Polish, Russian*) -- Jeff Huang (s8321414) (*Chinese Traditional*) +- Ira (seefood) (*Hebrew*) +- Linerly (*Indonesian*) - Blak Ouille (BlakOuille16) (*French*) - e (diveedd) (*Kurmanji (Kurdish)*) - Em St Cenydd (cancennau) (*Welsh*) -- Diluns (*Occitan*) +- Tigran (tigransimonyan) (*Armenian*) +- Draacoun (*Portuguese, Brazilian*) +- REMOVED_USER (*Turkish*) - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48.moe) (mnh48) (*Malay*) -- Tagomago (tagomago) (*French, Spanish*) -- Jurica (ahjk) (*Croatian*) +- Tagomago (tagomago) (*Spanish, French*) +- Ashun (ashune) (*Croatian*) - Aditoo17 (*Czech*) -- Tigran (tigransimonyan) (*Armenian*) - vishnuvaratharajan (*Tamil*) - pulmonarycosignerkindness (*Swedish*) - calypsoopenmail (*French*) -- cybergene (cyber-gene) (*Japanese*) +- REMOVED_USER (*Kabyle*) +- snerk (*Norwegian Nynorsk*) +- Sebastian (SebastianBerlin) (*German*) +- lisawe (*Norwegian*) +- serratrad (*Catalan*) - Bran_Ruz (*Breton*) +- ViktorOn (*Russian, Danish*) - Gearguy (*Finnish*) +- Andi Chandler (andibing) (*English, United Kingdom*) +- Tor Egil Hoftun Kvæstad (Taloran) (*Norwegian Nynorsk*) - GiorgioHerbie (*Italian*) -- Balázs Meskó (mesko.balazs) (*Czech, Hungarian*) -- Martin (miles) (*Slovenian*) +- හෙළබස සමූහය (HelaBasa) (*Sinhala*) +- kat (katktv) (*Ukrainian, Russian*) +- Yi-Jyun Pan (pan93412) (*Chinese Traditional*) +- Fjoerfoks (fryskefirefox) (*Frisian, Dutch*) +- Eshagh (eshagh79) (*Persian*) - regulartranslator (*Portuguese, Brazilian*) - Saederup92 (*Danish*) -- ozzii (*French, Serbian (Cyrillic)*) +- ozzii (Serbian (Cyrillic), French) - Irfan (Irfan_Radz) (*Malay*) -- Yi-Jyun Pan (pan93412) (*Chinese Traditional*) - ClearlyClaire (*French, Icelandic*) +- Sokratis Alichanidis (alichani) (*Greek*) +- Jiří Podhorecký (trendspotter) (*Czech*) - Akarshan Biswas (biswasab) (*Bengali, Sanskrit*) +- Robert Wolniak (Szkodnix) (*Polish*) +- Jan Lindblom (janlindblom) (*Swedish*) +- Dewi (Unkorneg) (*Breton, French*) - Kristoffer Grundström (Umeaboy) (*Swedish*) - Rafael H L Moretti (Moretti) (*Portuguese, Brazilian*) - d5Ziif3K (*Ukrainian*) -- හෙළබස (HelaBasa) (*Sinhala*) -- xpil (*Polish*) -- Rojdayek (*Kurmanji (Kurdish)*) +- Nemu (Dormemulo) (*Esperanto, French, Italian, Ido, Afrikaans*) +- Johan Mynhardt (johanmynhardt) (*Afrikaans*) +- Rojdayek (Kurmanji (Kurdish)) +- REMOVED_USER (*Portuguese, Brazilian*) +- GCardo (*Portuguese, Brazilian*) - christalleras (*Norwegian Nynorsk*) -- Allen Zhong (AstroProfundis) (*Chinese Simplified*) -- Taloran (*Norwegian Nynorsk*) -- Sokratis Alichanidis (alichani) (*Greek*) +- diorama (*Italian*) +- Jaz-Michael King (jazmichaelking) (*Welsh*) - Catalina (catalina.st) (*Romanian*) -- otrapersona (*Spanish, Spanish, Mexico*) - Ryo (DrRyo) (*Korean*) -- Mauzi (*German, Swedish*) +- otrapersona (*Spanish, Mexico, Spanish*) +- Frontier Translation Ltd. (frontier-translation) (*Chinese Simplified*) +- Mauzi (*Swedish, German*) +- Clopsy87 (*Italian*) - atarashiako (*Chinese Simplified*) - erictapen (*German*) +- zhen liao (az0189re) (*Chinese Simplified*) - 101010 (101010pl) (*Polish*) -- Jaz-Michael King (jazmichaelking) (*Welsh*) +- REMOVED_USER (*Norwegian*) - axi (*Finnish*) - silkevicious (*Italian*) - Floxu (fredrikdim1) (*Norwegian Nynorsk*) -- NadieAishi (*Spanish, Spanish, Mexico*) +- Nic Dafis (nicdafis) (*Welsh*) +- NadieAishi (*Spanish, Mexico, Spanish*) +- 戸渡生野 (aomyouza2543) (*Thai*) +- Tjipke van der Heide (vancha) (*Frisian*) +- Erik Mogensen (mogsie) (*Norwegian*) +- pomoch (*Chinese Traditional, Hong Kong*) +- Alexandre Brito (alexbrito) (*Portuguese, Brazilian*) - Bertil Hedkvist (Berrahed) (*Swedish*) - William(ѕ)ⁿ (wmlgr) (*Spanish*) -- Eshagh (eshagh79) (*Persian*) - LNDDYL (*Chinese Traditional*) +- tanketom (*Norwegian Nynorsk*) - norayr (*Armenian*) +- l3ycle (*German*) +- strubbl (*German*) - Satnam S Virdi (pika10singh) (*Punjabi*) - Tiago Epifânio (tfve) (*Portuguese*) - Mentor Gashi (mentorgashi.com) (*Albanian*) +- Sid (autinerd1) (*Dutch, German*) - carolinagiorno (*Portuguese, Brazilian*) +- Em_i (emiliencoss) (*French*) +- Liam O (liamoshan) (*Irish*) - Hayk Khachatryan (brutusromanus123) (*Armenian*) - Roby Thomas (roby.thomas) (*Malayalam*) +- ThonyVezbe (*Breton*) +- Percy (kecrily) (*Chinese Simplified*) - Bharat Kumar (Marwari) (*Hindi*) - Austra Muizniece (aus_m) (*Latvian*) -- ThonyVezbe (*Breton*) +- Urubu Lageano (urubulageano) (*Portuguese, Brazilian*) - Just Spanish (7_7) (*Spanish, Mexico*) - v4vachan (*Malayalam*) - bilfri (*Danish*) +- IamHappy (mrmx2013) (*Ukrainian*) - dkdarshan760 (*Sanskrit*) - Timur Seber (seber) (*Tatar*) - Slimane Selyan AMIRI (SelyanKab) (*Kabyle*) - VaiTon (*Italian*) -- Vik (ViktorOn) (*Danish, Russian*) - tykayn (*French*) -- GCardo (*Portuguese, Brazilian*) +- Abdulaziz Aljaber (kuwaitna) (*Arabic*) - taoxvx (*Danish*) -- Hrach Mkrtchyan (mhrach87) (*Armenian*) +- Hrach Mkrtchyan (hrachmk) (*Armenian*) - sabri (thetomatoisavegetable) (*Spanish, Spanish, Argentina*) -- Dewi (Unkorneg) (*French, Breton*) - CoelacanthusHex (*Chinese Simplified*) - Rhys Harrison (rhedders) (*Esperanto*) -- syncopams (*Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*) +- syncopams (*Chinese Traditional, Hong Kong, Chinese Traditional, Chinese Simplified*) - SteinarK (*Norwegian Nynorsk*) +- REMOVED_USER (*Standard Moroccan Tamazight*) - Maxine B. Vågnes (vagnes) (*Norwegian, Norwegian Nynorsk*) -- Hakim Oubouali (zenata1) (*Standard Moroccan Tamazight*) +- Rikard Linde (rikardlinde) (*Swedish*) - ahangarha (*Persian*) - Lalo Tafolla (lalotafo) (*Spanish, Spanish, Mexico*) -- dashersyed (*Urdu (Pakistan)*) +- Larissa Cruz (larissacruz) (*Portuguese, Brazilian*) +- dashersyed (Urdu (Pakistan)) +- camerongreer21 (*English, United Kingdom*) +- REMOVED_USER (*Ukrainian*) - Conight Wang (xfddwhh) (*Chinese Simplified*) - liffon (*Swedish*) - Damjan Dimitrioski (gnud) (*Macedonian*) -- Rikard Linde (rikardlinde) (*Swedish*) - rondnunes (*Portuguese, Brazilian*) -- strubbl (*German*) - PPNplus (*Thai*) -- Frontier Translation Ltd. (frontier-translation) (*Chinese Simplified*) +- Steven Ritchie (Steaph38) (*Scottish Gaelic*) +- 游荡 (MamaShip) (*Chinese Simplified*) +- Edward Navarro (EdwardNavarro) (*Spanish*) - shioko (*Chinese Simplified*) +- gnu-ewm (*Polish*) - Kahina Mess (K_hina) (*Kabyle*) +- Hexandcube (hexandcube) (*Polish*) +- Scott Starkey (yekrats) (*Esperanto*) - ZiriSut (*Kabyle*) +- FreddyG (*Esperanto*) +- mynameismonkey (*Welsh*) - Groosha (groosha) (*Russian*) -- Hexandcube (hexandcube) (*Polish*) - Gwenn (Belvar) (*Breton*) -- 游荡 (MamaShip) (*Chinese Simplified*) - StanleyFrew (*French*) -- mynameismonkey (*Welsh*) -- Edward Navarro (EdwardNavarro) (*Spanish*) +- cathalgarvey (*Irish*) - Nikita Epifanov (Nikets) (*Russian*) +- REMOVED_USER (*Finnish*) - jaranta (*Finnish*) - Slobodan Simić (Слободан Симић) (slsimic) (*Serbian (Cyrillic)*) -- retiolus (*Catalan*) -- iVampireSP (*Chinese Simplified, Chinese Traditional*) +- iVampireSP (*Chinese Traditional, Chinese Simplified*) - Felicia Jongleur (midsommar) (*Swedish*) - Denys (dector) (*Ukrainian*) - Mo_der Steven (SakuraPuare) (*Chinese Simplified*) +- REMOVED_USER (*German*) +- Kishin Sagume (kishinsagi) (*Chinese Simplified*) +- bennepharaoh (*Chinese Simplified*) - Vanege (*Esperanto*) +- hibiya inemuri (hibiya) (*Korean*) - Jess Rafn (therealyez) (*Danish*) - Stasiek Michalski (hellcp) (*Polish*) - dxwc (*Bengali*) -- Filbert Salim (gamesbert6) (*Indonesian*) +- Heran Membingung (heranmembingung) (*Indonesian*) +- Parodper (*Galician*) +- rbnval (*Catalan*) - Liboide (*Spanish*) +- hemnaren (*Norwegian Nynorsk*) - jmontane (*Catalan*) +- Andy Kleinert (AndyKl) (*German*) - Chris Kay (chriskarasoulis) (*Greek*) +- CrowdinBRUH (*Vietnamese*) +- Rhoslyn Prys (Rhoslyn) (*Welsh*) +- abidin toumi (Zet24) (*Arabic*) - Johan Schiff (schyffel) (*Swedish*) - Rex_sa (rex07) (*Arabic*) +- amedcj (*Kurmanji (Kurdish)*) - Arunmozhi (tecoholic) (*Tamil*) - zer0-x (ZER0-X) (*Arabic*) -- kat (katktv) (*Russian, Ukrainian*) +- staticnoisexyz (*Czech*) - Lauren Liberda (selfisekai) (*Polish*) +- Michael Zeevi (maze88) (*Hebrew*) - oti4500 (*Hungarian, Ukrainian*) - Delta (Delta-Time) (*Japanese*) -- Michael Zeevi (maze88) (*Hebrew*) +- Marc Antoine Thevenet (MATsxm) (*French*) +- AlexKoala (alexkoala) (*Korean*) - SarfarazAhmed (*Urdu (Pakistan)*) +- Ahmad Dakhlallah (MIUIArabia) (*Arabic*) - Mats Gunnar Ahlqvist (goqbi) (*Swedish*) - diazepan (*Spanish, Spanish, Argentina*) +- Tiger:blank (tsagaanbar) (*Chinese Simplified*) +- REMOVED_USER (*Chinese Simplified*) - marzuquccen (*Kabyle*) - atriix (*Swedish*) +- Laur (melaur) (*Romanian*) - VictorCorreia (victorcorreia1984) (*Afrikaans*) - Remito (remitocat) (*Japanese*) -- AlexKoala (alexkoala) (*Korean*) - Juan José Salvador Piedra (JuanjoSalvador) (*Spanish*) -- BurekzFinezt (*Serbian (Cyrillic)*) +- REMOVED_USER (*Norwegian*) - 森の子リスのミーコの大冒険 (Phroneris) (*Japanese*) +- Gim_Garam (*Korean*) +- BurekzFinezt (*Serbian (Cyrillic)*) +- Pēteris Caune (cuu508) (*Latvian*) - asnomgtu (*Hungarian*) +- bendigeidfran (*Welsh*) - SHeija (*Finnish*) - Врабац (Slovorad) (*Serbian (Cyrillic)*) - Dženan (Dzenan) (*Swedish*) -- Jack R (isaac.97_WT) (*Spanish*) +- Gabriel Beecham (lancet) (*Irish*) - antonyho (*Chinese Traditional, Hong Kong*) -- FreddyG (*Esperanto*) -- andruhov (*Russian, Ukrainian*) +- Jack R (isaac.97_WT) (*Spanish*) +- Henrik Mattsson-Mårn (rchk) (*Swedish*) +- Oguzhan Aydin (aoguzhan) (*Turkish*) +- Soran730 (*Chinese Simplified*) +- andruhov (*Ukrainian, Russian*) +- 北䑓如法 (Nyoho) (*Japanese*) - phena109 (*Chinese Traditional, Hong Kong*) -- Aryamik Sharma (Aryamik) (*Swedish, Hindi*) +- Aryamik Sharma (Aryamik) (*Hindi, Swedish*) - Unmual (*Spanish*) +- Tobias Bannert (toba) (*German*) - Adrián Graña (alaris83) (*Spanish*) -- cruz2020 (*Portuguese*) - vpei (*Chinese Simplified*) +- cruz2020 (*Portuguese*) +- papapep (h9f2ycHh-ktOd6_Y) (*Catalan*) +- Roj (roj1512) (*Sorani (Kurdish), Kurmanji (Kurdish)*) - るいーね (ruine) (*Japanese*) +- aujawindar (*Norwegian Nynorsk*) +- irithys (*Chinese Simplified*) - Sam Tux (imahbub) (*Bengali*) - igordrozniak (*Polish*) +- Johannes Nilsson (nlssn) (*Swedish*) - Michał Sidor (michcioperz) (*Polish*) - Isaac Huang (caasih) (*Chinese Traditional*) - AW Unad (awcodify) (*Indonesian*) - 1Alino (*Slovak*) - Cutls (cutls) (*Japanese*) -- Goudarz Jafari (Goudarz) (*Persian*) -- Parodper (*Galician*) +- Goudarz Jafari (GoudarzJafari) (*Persian*) +- Daniel Strömholm (stromholm) (*Swedish*) - 1 (Ipsumry) (*Spanish*) - Falling Snowdin (tghgg) (*Vietnamese*) +- Paulino Michelazzo (pmichelazzo) (*Portuguese, Brazilian*) +- Y.Yamashiro (uist1idrju3i) (*Japanese*) - Rasmus Lindroth (RasmusLindroth) (*Swedish*) - Gianfranco Fronteddu (gianfro.gianfro) (*Sardinian*) - Andrea Lo Iacono (niels0n) (*Italian*) - fucsia (*Italian*) - Vedran Serbu (SerenoXGen) (*Croatian*) +- Raphael Das Gupta (das-g) (*Esperanto, German*) +- yanchan09 (*Estonian*) +- ainmeolai (*Irish*) +- REMOVED_USER (*Norwegian*) +- mian42 (*Bulgarian*) - Kinshuk Sunil (kinshuksunil) (*Hindi*) -- Ullas Joseph (ullasjoseph) (*Malayalam*) - al_._ (*German, Russian*) +- Ullas Joseph (ullasjoseph) (*Malayalam*) +- sanoth (*Swedish*) +- Aftab Alam (iaftabalam) (*Hindi*) +- frumble (*German*) +- juanda097 (juanda-097) (*Spanish*) - Matthías Páll Gissurarson (icetritlo) (*Icelandic*) -- Percy (kecrily) (*Chinese Simplified*) -- Yu-Pai Liu (tedliou) (*Chinese Traditional*) +- Russian Retro (retrograde) (*Russian*) - KcKcZi (*Chinese Simplified*) +- Yu-Pai Liu (tedliou) (*Chinese Traditional*) - Amarin Cemthong (acitmaster) (*Thai*) -- Johannes Nilsson (nlssn) (*Swedish*) -- juanda097 (juanda-097) (*Spanish*) +- Etinew (*Hebrew*) - xsml (*Chinese Simplified*) +- S.J. L. (samijuhanilii) (*Finnish*) - Anunnakey (*Macedonian*) - erikkemp (*Dutch*) +- Tsl (muun) (*Chinese Simplified*) +- Renato "Lond" Cerqueira (renatolond) (*Portuguese, Brazilian*) +- Úna-Minh Kavanagh (yunitex) (*Irish*) +- kongk (*Norwegian Nynorsk*) - erikstl (*Esperanto*) - twpenguin (*Chinese Traditional*) +- JeremyStarTM (*German*) - Po-chiang Chao (bobchao) (*Chinese Traditional*) - Marcus Myge (mygg-priv) (*Norwegian*) - Esther (esthermations) (*Portuguese*) +- Jiri Grönroos (spammemoreplease) (*Finnish*) - MadeInSteak (*Finnish*) +- witoharmuth (*Swedish*) +- MESHAL45 (*Arabic*) +- mcdutchie (*Dutch*) +- Michal Špondr (michalspondr) (*Czech*) - t_aus_m (*German*) -- serapolis (*Japanese, Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*) +- kaki7777 (*Japanese, Chinese Traditional*) - Heimen Stoffels (Vistaus) (*Dutch*) +- serapolis (*Chinese Traditional, Hong Kong, Chinese Traditional, Japanese, Chinese Simplified*) - Rajarshi Guha (rajarshiguha) (*Bengali*) +- Amir Reza (ElAmir) (*Persian*) +- REMOVED_USER (*Norwegian*) +- MohammadSaleh Kamyab (mskf1383) (*Persian*) +- REMOVED_USER (*Romanian*) - Gopal Sharma (gopalvirat) (*Hindi*) +- Вероніка Някшу (pampushkaveronica) (*Russian, Romanian*) - Linnéa (lesbian_subnet) (*Swedish*) -- 北䑓如法 (Nyoho) (*Japanese*) -- abidin toumi (Zet24) (*Arabic*) -- Tofiq Abdula (Xwla) (*Sorani (Kurdish)*) +- Valentin (HDValentin) (*German*) +- dragnucs2 (*Arabic*) - Carlos Solís (csolisr) (*Esperanto*) -- Yamagishi Kazutoshi (ykzts) (*Japanese, Vietnamese, Icelandic, Sorani (Kurdish)*) +- Tofiq Abdula (Xwla) (*Sorani (Kurdish)*) +- halcek (*Slovak*) +- Tobias Kunze (rixxian) (*German*) - Parthan S Ramanujam (parthan) (*Tamil*) - Kasper Nymand (KasperNymand) (*Danish*) -- subram (*Turkish*) - TS (morte) (*Finnish*) -- SensDeViata (*Ukrainian*) +- REMOVED_USER (*German*) +- REMOVED_USER (*Basque*) +- subram (*Turkish*) +- Gudwin (*Spanish, Mexico, Spanish*) - Ptrcmd (ptrcmd) (*Chinese Traditional*) +- shmuelHal (*Hebrew*) +- SensDeViata (*Ukrainian*) - megaleo (*Portuguese, Brazilian*) +- Acursen (*German*) +- NurKai Kai (nurkaiyttv) (*German*) +- Guttorm (ghveem) (*Norwegian Nynorsk*) - SergioFMiranda (*Portuguese, Brazilian*) -- hiroTS (*Chinese Traditional*) +- Danni Lundgren (dannilundgren) (*Danish*) - Vivek K J (Vivekkj) (*Malayalam*) -- arielcostas3 (*Galician*) +- hiroTS (*Chinese Traditional*) +- teadesu (*Portuguese, Brazilian*) +- petartrajkov (*Macedonian*) +- Ariel Costas (arielcostas3) (*Galician*) +- Ch. (sftblw) (*Korean*) +- Rintan (*Japanese*) +- Jair Henrique (jairhenrique) (*Portuguese, Brazilian*) +- sorcun (*Turkish*) +- filippodb (*Italian*) - johne32rus23 (*Russian*) -- AzureNya (*Chinese Simplified*) - OctolinGamer (octolingamer) (*Portuguese, Brazilian*) -- filippodb (*Italian*) +- AzureNya (*Chinese Simplified*) - Ram varma (ram4varma) (*Tamil*) +- REMOVED_USER (Sorani (Kurdish)) +- REMOVED_USER (*Portuguese, Brazilian*) +- seanmhade (*Irish*) - sanser (*Russian*) -- Y.Yamashiro (uist1idrju3i) (*Japanese*) +- Vijay (vijayatmin) (*Tamil*) +- Anomalion (*German*) - Pukima (Pukimaa) (*German*) -- diorama (*Italian*) -- frumble (*German*) +- Curtis Lee (CansCurtis) (*Chinese Traditional*) +- โบโลน่าไวรัส (nullxyz_) (*Thai*) +- ふぁーらんど (farland1717) (*Japanese*) +- 3wen (*Breton*) +- rlafuente (*Portuguese*) +- Ильзира Рахматуллина (rahmatullinailzira53) (*Tatar*) +- Code Man (codemansrc) (*Russian*) +- Philip Gillißen (guerda) (*German*) - Daniel Dimitrov (daniel.dimitrov) (*Bulgarian*) +- Anton (atjn) (*Danish*) - kekkepikkuni (*Tamil*) - MODcraft (*Chinese Simplified*) - oorsutri (*Tamil*) +- wortfeld (*German*) - Neo_Chen (NeoChen1024) (*Chinese Traditional*) +- Stereopolex (*Polish*) +- NxOne14 (*Bulgarian*) +- Juan Ortiz (Kloido) (*Spanish, Catalan*) - Nithin V (Nithin896) (*Tamil*) +- strikeCunny2245 (*Icelandic*) - Miro Rauhala (mirorauhala) (*Finnish*) +- nicoduesing (duconi) (*German, Esperanto*) +- Gnonthgol (*Norwegian Nynorsk*) +- WKobes (*Dutch*) - Oymate (*Bengali*) +- mikwee (*Hebrew*) +- EzigboOmenana (*Igbo, Cornish*) +- yan Wato (janWato) (*Hindi*) +- Papuass (*Latvian*) +- Vincent Orback (vincentorback) (*Swedish*) +- chettoy (*Chinese Simplified*) +- 19 (nineteen) (*Chinese Simplified*) - ಚಿರಾಗ್ ನಟರಾಜ್ (chiraag-nataraj) (*Kannada*) +- Layik Hama (layik) (*Sorani (Kurdish)*) - Guillaume Turchini (orion78fr) (*French*) +- Andri Yngvason (andryng) (*Icelandic*) - Aswin C (officialcjunior) (*Malayalam*) -- Ganesh D (auntgd) (*Marathi*) - Yuval Nehemia (yuvalne) (*Hebrew*) - mawoka-myblock (mawoka) (*German*) -- dragnucs2 (*Arabic*) +- Ganesh D (auntgd) (*Marathi*) +- Lens0021 (lens0021) (*Korean*) +- An Gafraíoch (angafraioch) (*Irish*) +- Michael Smith (michaelshmitty) (*Dutch*) - Ryan Ho (koungho) (*Chinese Traditional*) -- Tejas Harad (h_tejas) (*Marathi*) +- tunisiano187 (*French*) +- Peter van Mever (SpacemanSpiff) (*Dutch*) - Pedro Henrique (exploronauta) (*Portuguese, Brazilian*) -- Amir Reza (ElAmir) (*Persian*) -- Tatsuto "Laminne" Yamamoto (laminne) (*Japanese*) +- REMOVED_USER (*Esperanto, Italian, Japanese*) +- Tejas Harad (h_tejas) (*Marathi*) +- Balázs Meskó (meskobalazs) (*Hungarian*) - Vasanthan (vasanthan) (*Tamil*) +- Tatsuto "Laminne" Yamamoto (laminne) (*Japanese*) +- slbtty (shenlebantongying) (*Chinese Simplified*) - 硫酸鶏 (acid_chicken) (*Japanese*) - programizer (*German*) +- guessimmaterialgrl (*Chinese Simplified*) - clarmin b8 (clarminb8) (*Sorani (Kurdish)*) +- Maria Riegler (riegler3m) (*German*) - manukp (*Malayalam*) -- psymyn (*Hebrew*) - earth dweller (sanethoughtyt) (*Marathi*) +- psymyn (*Hebrew*) +- Aaraon Thomas (aaraon) (*Portuguese, Brazilian*) +- Rafael Viana (rafacnec) (*Portuguese, Brazilian*) - Marek Ľach (marek-lach) (*Slovak*) - meijerivoi (toilet) (*Finnish*) - essaar (*Tamil*) -- Nemuj (Dentrado) (*Japanese, Esperanto*) - serubeena (*Swedish*) -- Rintan (*Japanese*) -- Karol Kosek (krkkPL) (*Polish*) +- RqndomHax (*French*) +- REMOVED_USER (*Polish*) +- ギャラ (gyara) (*Chinese Simplified, Japanese*) - Khó͘ Tiatlêng (khotiatleng) (*Chinese Traditional, Taigi*) -- valarivan (*Tamil*) -- Hernik (hernik27) (*Czech*) - revarioba (*Spanish*) - friedbeans (*Croatian*) +- An (AnTheMaker) (*German*) - kuchengrab (*German*) +- Hernik (hernik27) (*Czech*) +- valarivan (*Tamil*) +- אדם לוין (adamlevin) (*Hebrew*) +- Vít Horčička (legvita123) (*Czech*) - Abi Turi (abi123) (*Georgian*) +- Thomas Munkholt (munkholt) (*Danish*) +- pparescasellas (*Catalan*) - Hinaloe (hinaloe) (*Japanese*) -- Sebastián Andil (Selrond) (*Slovak*) - Ifnuth (*German*) -- Asbjørn Olling (a2) (*Danish*) +- Sebastián Andil (Selrond) (*Slovak*) +- boni777 (*Chinese Simplified*) - KEINOS (*Japanese*) -- Balázs Meskó (meskobalazs) (*Hungarian*) -- Artem Mikhalitsin (artemmikhalitsin) (*Russian*) -- Algustionesa Yoshi (algustionesa) (*Indonesian*) -- Bottle (suryasalem2010) (*Tamil*) +- Asbjørn Olling (a2) (*Danish*) +- REMOVED_USER (*Chinese Traditional, Hong Kong*) +- DarkShy Community (ponyfrost.mc) (*Russian*) +- Dennis Reimund (reimunddennis7) (*German*) +- jocafeli (*Spanish, Mexico*) - Wrya ali (John12) (*Sorani (Kurdish)*) +- Bottle (suryasalem2010) (*Tamil*) +- Algustionesa Yoshi (algustionesa) (*Indonesian*) - JzshAC (*Chinese Simplified*) +- Artem Mikhalitsin (artemmikhalitsin) (*Russian*) - siamano (*Thai, Esperanto*) -- gnu-ewm (*Polish*) -- Antillion (antillion99) (*Spanish*) +- KARARTI44 (kararti44) (*Turkish*) +- c0c (*Irish*) +- Stefano S. (Sting1_JP) (*Italian*) +- tommil (*Finnish*) +- Ignacio Lis (ilis) (*Galician*) - Steven Tappert (sammy8806) (*German*) -- Reg3xp (*Persian*) +- Antillion (antillion99) (*Spanish*) +- K.B.Dharun Krishna (kbdharun) (*Tamil*) - Wassim EL BOUHAMIDI (elbouhamidiw) (*Arabic*) +- Reg3xp (*Persian*) +- florentVgn (*French*) +- Matt (Exbu) (*Dutch*) - Maciej Błędkowski (mble) (*Polish*) - gowthamanb (*Tamil*) - hiphipvargas (*Portuguese*) -- tunisiano187 (*French*) +- GabuVictor (*Portuguese, Brazilian*) +- Pverte (*French*) +- REMOVED_USER (*Spanish*) +- Surindaku (*Chinese Simplified*) - Arttu Ylhävuori (arttu.ylhavuori) (*Finnish*) -- Ch. (sftblw) (*Korean*) -- eorn (*Breton*) +- Pabllo Soares (pabllosoarez) (*Portuguese, Brazilian*) - Jona (88wcJoWl) (*Spanish*) -- Mikkel B. Goldschmidt (mikkelbjoern) (*Danish*) -- Timo Tijhof (Krinkle) (*Dutch*) - Ka2n (kaanmetu) (*Turkish*) - tctovsli (*Norwegian Nynorsk*) -- mecqor labi (mecqorlabi) (*Persian*) +- Timo Tijhof (Krinkle) (*Dutch*) +- SamitiMed (samiti3d) (*Thai*) +- Mikkel B. Goldschmidt (mikkelbjoern) (*Danish*) - Odyssey346 (alexader612) (*Norwegian*) +- mecqor labi (mecqorlabi) (*Persian*) +- Cù Huy Phúc Khang (taamee) (*Vietnamese*) +- Oskari Lavinto (olavinto) (*Finnish*) +- Philippe Lemaire (philippe-lemaire) (*Esperanto*) - vjasiegd (*Polish*) -- Eban (ebanDev) (*French, Esperanto*) -- SamitiMed (samiti3d) (*Thai*) +- Eban (ebanDev) (*Esperanto, French*) - Nícolas Lavinicki (nclavinicki) (*Portuguese, Brazilian*) +- REMOVED_USER (*Portuguese, Brazilian*) - Rekan Adl (rekan-adl1) (*Sorani (Kurdish)*) -- Antara2Cinta (Se7enTime) (*Indonesian*) -- Yassine Aït-El-Mouden (yaitelmouden) (*Standard Moroccan Tamazight*) - VSx86 (*Russian*) - umelard (*Hebrew*) +- Antara2Cinta (Se7enTime) (*Indonesian*) +- Lucas_NL (*Dutch*) +- Yassine Aït-El-Mouden (yaitelmouden) (*Standard Moroccan Tamazight*) +- Mathieu Marquer (slasherfun) (*French*) +- Haerul Fuad (Dokuwiki) (*Indonesian*) - parnikkapore (*Thai*) -- Lagash (lagash) (*Esperanto*) +- Michelle M (MichelleMMM) (*Dutch*) +- malbona (*Esperanto*) - Sherwan Othman (sherwanothman11) (*Sorani (Kurdish)*) -- SKELET (*Danish*) -- Exbu (*Dutch*) +- Lagash (lagash) (*Esperanto*) - Chine Sebastien (chine.sebastien) (*French*) -- Fei Yang (Fei1Yang) (*Chinese Traditional*) +- bgme (*Chinese Simplified*) +- Rafael V. (Rafaeeel) (*Portuguese, Brazilian*) +- SKELET (*Danish*) - A A (sebastien.chine) (*French*) +- Project Z (projectz.1338) (*German*) +- Fei Yang (Fei1Yang) (*Chinese Traditional*) - Ğani (freegnu) (*Tatar*) -- enipra (*Armenian*) -- Renato "Lond" Cerqueira (renatolond) (*Portuguese, Brazilian*) - musix (*Persian*) -- ギャラ (gyara) (*Japanese, Chinese Simplified*) +- REMOVED_USER (*German*) - ALEM FARID (faridatcemlulaqbayli) (*Kabyle*) +- Jean-Pierre MÉRESSE (Jipem) (*French*) +- enipra (*Armenian*) +- Serhiy Dmytryshyn (dies) (*Ukrainian*) +- Eric Brulatout (ebrulato) (*Esperanto*) - Hougo (hougo) (*French*) -- Mordi Sacks (MordiSacks) (*Hebrew*) -- Trinsec (*Dutch*) -- Adrián Lattes (haztecaso) (*Spanish*) +- Sonstwer (*German*) +- Pedro Fernandes (djprmf) (*Portuguese*) +- REMOVED_USER (*Norwegian*) - Tigran's Tips (tigrank08) (*Armenian*) - 亜緯丹穂 (ayiniho) (*Japanese*) +- maisui (*Chinese Simplified*) +- Trinsec (*Dutch*) +- Adrián Lattes (haztecaso) (*Spanish*) +- webkinzfrog (*Polish*) - ybardapurkar (*Marathi*) +- Mordi Sacks (MordiSacks) (*Hebrew*) +- Manuel Tassi (Mannivu) (*Italian*) - Szabolcs Gál (galszabolcs810624) (*Hungarian*) -- Vladislav Săcrieriu (vladislavs14) (*Romanian*) +- rikrise (*Swedish*) +- when_hurts (*German*) +- Wojciech Bigosinski (wbigos2) (*Polish*) +- Vladislav S (vladislavs) (*Romanian*) +- mikslatvis (*Latvian*) +- MartinAlstad (*Norwegian*) - TracyJacks (*Chinese Simplified*) - rasheedgm (*Kannada*) -- danreznik (*Hebrew*) - Cirelli (cirelli94) (*Italian*) +- danreznik (*Hebrew*) +- iraline (*Portuguese, Brazilian*) +- Seán Mór (seanmor3) (*Irish*) +- vianaweb (*Portuguese, Brazilian*) - Siddharastro Doraku (sidharastro) (*Spanish, Mexico*) -- lexxai (*Ukrainian*) +- REMOVED_USER (*Spanish*) - omquylzu (*Latvian*) +- Arthegor (*French*) - Navjot Singh (nspeaks) (*Hindi*) - mkljczk (*Polish*) - Belkacem Mohammed (belkacem77) (*Kabyle*) +- Showfom (*Chinese Simplified*) +- xemyst (*Catalan*) +- lexxai (*Ukrainian*) - c6ristian (*German*) -- damascene (*Arabic*) +- svetlozaurus (*Bulgarian*) - Ozai (*German*) +- damascene (*Arabic*) +- Jan Ainali (Ainali) (*Swedish*) - Sahak Petrosyan (petrosyan) (*Armenian*) +- Metehan Özyürek (MetehanOzyurek) (*Turkish*) +- Сау Рэмсон (sawrams) (*Russian*) +- metehan-arslan (*Turkish*) - Viorel-Cătălin Răpițeanu (rapiteanu) (*Romanian*) -- Siddhartha Sarathi Basu (quinoa_biryani) (*Bengali*) +- Sébastien SERRE (sebastienserre) (*French*) +- Eugen Caruntu (eugencaruntu) (*Romanian*) +- Kevin Scannell (kscanne) (*Irish*) - Pachara Chantawong (pachara2202) (*Thai*) -- Skew (noan.perrot) (*French*) +- bensch.dev (*German*) +- LIZH (*French*) +- Siddhartha Sarathi Basu (quinoa_biryani) (*Bengali*) +- Overflow Cat (OverflowCat) (*Chinese Traditional, Chinese Simplified*) +- Stephan Voeth (svoeth) (*German*) - Zijian Zhao (jobs2512821228) (*Chinese Simplified*) -- Overflow Cat (OverflowCat) (*Chinese Simplified, Chinese Traditional*) +- bugboy-20 (*Esperanto, Italian*) +- SouthFox (*Chinese Simplified*) +- Noan (SkewRam) (*French*) - dbeaver (*German*) -- zordsdavini (*Lithuanian*) -- Guru Prasath Anandapadmanaban (guruprasath) (*Tamil*) - turtle836 (*German*) +- Guru Prasath Anandapadmanaban (guruprasath) (*Tamil*) +- zordsdavini (*Lithuanian*) +- Susanna Ånäs (susanna.anas) (*Finnish*) +- Alessandro (alephoto85) (*Italian*) - Marcepanek_ (thekingmarcepan) (*Polish*) -- Feruz Oripov (FeruzOripov) (*Russian*) +- Choi Younsoo (usagicore) (*Korean*) - Yann Aguettaz (yann-a) (*French*) +- zylosophe (*French*) +- Celso Fernandes (Celsof) (*Portuguese, Brazilian*) +- Feruz Oripov (FeruzOripov) (*Russian*) +- REMOVED_USER (*French*) +- Bui Huy Quang (bhuyquang1) (*Vietnamese*) +- bogomilshopov (*Bulgarian*) +- REMOVED_USER (*Burmese*) +- Kaede (kaedech) (*Japanese*) - Mick Onio (xgc.redes) (*Asturian*) - Malik Mann (dermalikmann) (*German*) - padulafacundo (*Spanish*) +- r3dsp1 (*Chinese Traditional, Hong Kong*) - dadosch (*German*) -- hg6 (*Hindi*) - Tianqi Zhang (tina.zhang040609) (*Chinese Simplified*) -- r3dsp1 (*Chinese Traditional, Hong Kong*) -- johannes hove-henriksen (J0hsHH) (*Norwegian*) +- HybridGlucose (*Chinese Traditional*) +- vmichalak (*French*) +- hg6 (*Hindi*) +- marivisales (*Portuguese, Brazilian*) - Orlando Murcio (Atos20) (*Spanish, Mexico*) -- cenegd (*Chinese Simplified*) +- maa123 (*Japanese*) +- Julian Doser (julian21) (*English, United Kingdom, German*) +- johannes hove-henriksen (J0hsHH) (*Norwegian*) +- Alexander Ivanov (Saiv46) (*Russian*) +- unstable.icu (*Chinese Simplified*) +- Padraic Calpin (padraic-padraic) (*Slovenian*) - Youngeon Lee (YoungeonLee) (*Korean*) +- LeJun (le-jun) (*French*) - shdy (*German*) -- Umi (mtrumi) (*Chinese Simplified, Chinese Traditional, Hong Kong*) -- Padraic Calpin (padraic-padraic) (*Slovenian*) -- Ильзира Рахматуллина (rahmatullinailzira53) (*Tatar*) +- REMOVED_USER (*French*) +- Yonjae Lee (yonjlee) (*Korean*) +- cenegd (*Chinese Simplified*) - piupiupiudiu (*Chinese Simplified*) -- Pixelcode (realpixelcode) (*German*) -- Dennis Reimund (reimunddennis7) (*German*) +- Umi (mtrumi) (*Chinese Traditional, Hong Kong, Chinese Simplified*) - Yogesh K S (yogi) (*Kannada*) +- Ulong32 (*Japanese*) - Adithya K (adithyak04) (*Malayalam*) - DAI JIE (daijie) (*Chinese Simplified*) +- Mihael Budeč (milli.pretili) (*Croatian*) - Hugh Liu (youloveonlymeh) (*Chinese Simplified*) -- Rakino (rakino) (*Chinese Simplified*) - ZQYD (*Chinese Simplified*) - X.M (kimonoki) (*Chinese Simplified*) -- boni777 (*Chinese Simplified*) +- Rakino (rakino) (*Chinese Simplified*) +- paziy Georgi (paziygeorgi4) (*Dutch*) +- Komeil Parseh (mmdbalkhi) (*Persian*) - Jothipazhani Nagarajan (jothipazhani.n) (*Tamil*) -- Miquel Sabaté Solà (mssola) (*Catalan*) +- tikky9 (*Portuguese, Brazilian*) +- horsm (*Finnish*) +- BenJule (*German*) - Stanisław Jelnicki (JelNiSlaw) (*Polish*) +- Yananas (wangyanyan.hy) (*Chinese Simplified*) +- Vivamus (elaaksu) (*Turkish*) +- ihealyou (*Italian*) - AmazighNM (*Kabyle*) +- Miquel Sabaté Solà (mssola) (*Catalan*) +- residuum (*German*) +- nua_kr (*Korean*) +- Andrea Mazzilli (andreamazzilli) (*Italian*) +- Paula SIMON (EncoreEutIlFalluQueJeLeSusse) (*French*) +- hallomaurits (*Dutch*) +- Erfan Kheyrollahi Qaroğlu (ekm507) (*Persian*) +- REMOVED_USER (*Galician, Spanish*) - alnd hezh (alndhezh) (*Sorani (Kurdish)*) -- CloudSet (*Chinese Simplified*) - Clash Clans (KURD12345) (*Sorani (Kurdish)*) -- Metehan Özyürek (MetehanOzyurek) (*Turkish*) -- Paula SIMON (EncoreEutIlFalluQueJeLeSusse) (*French*) +- ruok (*Chinese Simplified*) +- Frederik-FJ (*German*) +- CloudSet (*Chinese Simplified*) - Solid Rhino (SolidRhino) (*Dutch*) -- nua_kr (*Korean*) -- hallomaurits (*Dutch*) -- 林水溶 (shuiRong) (*Chinese Simplified*) -- rikrise (*Swedish*) -- Takeshi Umeda (noellabo) (*Japanese*) +- hussama (*Portuguese, Brazilian*) +- jazzynico (*French*) - k_taka (peaceroad) (*Japanese*) +- 林水溶 (shuiRong) (*Chinese Simplified*) +- Peter Lutz (theellutzo) (*German*) - Sébastien Feugère (smonff) (*French*) +- AnalGoddess770 (*Hebrew*) +- Sven Goller (svengoller) (*German*) +- Ahmet (ahmetlii) (*Turkish*) +- hosted22 (*German*) - Hallo Abdullah (hallo_hamza12) (*Sorani (Kurdish)*) -- hussama (*Portuguese, Brazilian*) -- EzigboOmenana (*Cornish*) +- Karam Hamada (TheKaram) (*Arabic*) +- Takeshi Umeda (noellabo) (*Japanese*) +- SnDer (*Dutch*) - Robert Yano (throwcalmbobaway) (*Spanish, Mexico*) -- Yasin İsa YILDIRIM (redsfyre) (*Turkish*) -- PifyZ (*French*) -- Tagada (Tagadda) (*French*) -- eichkat3r (*German*) -- Ashok314 (ashok314) (*Hindi*) +- Gustav Lindqvist (Reedyn) (*Swedish*) +- Dagur Ammendrup (dagurp) (*Icelandic*) +- shafouz (*Portuguese, Brazilian*) +- Miguel Branco (mglbranco) (*Galician*) +- Sergey Panteleev (saundefined) (*Russian*) +- Tom_ (*Czech*) - Zlr- (cZeler) (*French*) -- SnDer (*Dutch*) -- OminousCry (*Russian*) +- Ashok314 (ashok314) (*Hindi*) +- PifyZ (*French*) +- Zeyi Fan (fanzeyi) (*Chinese Simplified*) +- OminousCry (*Russian, Ukrainian*) - Adam Sapiński (Adamos9898) (*Polish*) -- Tom_ (*Czech*) -- shafouz (*Portuguese, Brazilian*) -- Shrinivasan T (tshrinivasan) (*Tamil*) -- Kk (kishorkumara3) (*Kannada*) -- Swati Sani (swatisani) (*Urdu (Pakistan)*) -- papayaisnotafood (*Chinese Traditional*) -- さっかりんにーさん (saccharin23) (*Japanese*) +- eichkat3r (*German*) +- Yasin İsa YILDIRIM (redsfyre) (*Turkish*) +- Tagada (Tagadda) (*French*) +- gasrios (*Portuguese, Brazilian*) +- 夜楓Yoka (Yoka2627) (*Chinese Simplified*) +- AniCommieDDR (*Russian*) +- Nathaël Noguès (NatNgs) (*French*) - Daniel M. (daniconil) (*Catalan*) - César Daniel Cavanzo Quintero (LeinadCQ) (*Esperanto*) -- Nathaël Noguès (NatNgs) (*French*) -- 夜楓Yoka (Yoka2627) (*Chinese Simplified*) +- Noam Tamim (noamtm) (*Hebrew*) +- papayaisnotafood (*Chinese Traditional*) +- さっかりんにーさん (saccharin23) (*Japanese*) +- Marcin Wolski (martinwolski) (*Polish*) +- REMOVED_USER (*Chinese Simplified*) +- Kk (kishorkumara3) (*Kannada*) +- Shrinivasan T (tshrinivasan) (*Tamil*) +- REMOVED_USER (Urdu (Pakistan)) +- Kakarico Bra (kakarico20) (*Portuguese, Brazilian*) +- Swati Sani (swatisani) (*Urdu (Pakistan)*) +- 快乐的老鼠宝宝 (LaoShuBaby) (*Chinese Simplified, Chinese Traditional*) - Mt Front (mtfront) (*Chinese Simplified*) -- Artem (Artem4ik) (*Russian*) -- Robin van der Vliet (RobinvanderVliet) (*Esperanto*) -- Tradjincal (tradjincal) (*French*) - SusVersiva (*Catalan*) +- REMOVED_USER (*Portuguese, Brazilian*) +- Avinash Mg (hatman290) (*Malayalam*) +- kruijs (*Dutch*) +- Artem (Artem4ik) (*Russian*) - Zinkokooo (*Basque*) -- Marvin (magicmarvman) (*German*) +- 劉昌賢 (twcctz500) (*Chinese Traditional*) - Vikatakavi (*Kannada*) +- Tradjincal (tradjincal) (*French*) +- Robin van der Vliet (RobinvanderVliet) (*Esperanto*) +- Marvin (magicmarvman) (*German*) - pullopen (*Chinese Simplified*) -- sergioaraujo1 (*Portuguese, Brazilian*) -- prabhjot (*Hindi*) -- CyberAmoeba (pseudoobscura) (*Chinese Simplified*) -- mmokhi (*Persian*) +- Tealk (*German*) +- tibequadorian (*German*) +- Henk Bulder (henkbulder) (*Dutch*) +- Edison Lee (edisonlee55) (*Chinese Traditional*) +- mpdude (*German*) +- Rijk van Geijtenbeek (rvangeijtenbeek) (*Dutch*) - Entelekheia-ousia (*Chinese Simplified*) +- REMOVED_USER (*Spanish*) +- sergioaraujo1 (*Portuguese, Brazilian*) - Livingston Samuel (livingston) (*Tamil*) +- mmokhi (*Persian*) - tsundoker (*Malayalam*) -- skaaarrr (*German*) -- Pierre Morvan (Iriep) (*Breton*) +- CyberAmoeba (pseudoobscura) (*Chinese Simplified*) +- prabhjot (*Hindi*) +- Ikka Putri (ikka240290) (*Indonesian, Danish, English, United Kingdom*) - Paz Galindo (paz.almendra.g) (*Spanish*) -- fedot (*Russian*) -- mkljczk (mykylyjczyk) (*Polish*) - Ricardo Colin (rysard) (*Spanish*) -- Philipp Fischbeck (PFischbeck) (*German*) +- Pierre Morvan (Iriep) (*Breton*) - oscfd (*Spanish*) +- Thies Mueller (thies00) (*German*) +- Lyra (teromene) (*French*) +- Kedr (lava20121991) (*Esperanto*) +- mkljczk (mykylyjczyk) (*Polish*) +- fedot (*Russian*) +- Philipp Fischbeck (PFischbeck) (*German*) +- Hasan Berkay Çağır (berkaycagir) (*Turkish*) +- Silvestri Nicola (nick99silver) (*Italian*) +- skaaarrr (*German*) +- Mo Rijndael (mo_rijndael) (*Russian*) +- tsesunnaallun (orezraey) (*Portuguese, Brazilian*) +- Lukas Fülling (lfuelling) (*German*) +- Algo (algovigura) (*Indonesian*) +- REMOVED_USER (*Spanish*) +- setthemfree (*Ukrainian*) +- i fly (ifly3years) (*Chinese Simplified*) +- ralozkolya (*Georgian*) - Zoé Bőle (zoe1337) (*German*) +- Ville Rantanen (vrntnn) (*Finnish*) - GaggiX (*Italian*) - JackXu (Merman-Jack) (*Chinese Simplified*) -- Lukas Fülling (lfuelling) (*German*) -- ralozkolya (*Georgian*) -- Jason Gibson (barberpike606) (*Slovenian, Chinese Simplified*) -- Dremski (*Bulgarian*) -- Kaede (kaedech) (*Japanese*) -- Aymeric (AymBroussier) (*French*) -- mashirozx (*Chinese Simplified*) -- María José Vera (mjverap) (*Spanish*) -- asala4544 (*Basque*) -- ronee (*Kurmanji (Kurdish)*) -- qwerty287 (*German*) -- pezcurrel (*Italian*) -- Anoop (anoopp) (*Malayalam*) +- ceonia (*Chinese Traditional, Hong Kong*) +- Emirhan Yavuz (takomlii) (*Turkish*) +- teezeh (*German*) +- MevLyshkin (Leinnan) (*Polish*) - Apple (blackteaovo) (*Chinese Simplified*) -- Lilian Nabati (Lilounab49) (*French*) -- ru_mactunnag (*Scottish Gaelic*) -- Nocta (*French*) +- qwerty287 (*German*) - Tangcuyu (*Chinese Simplified*) +- Nocta (*French*) +- ru_mactunnag (*Scottish Gaelic*) +- Lilian Nabati (Lilounab49) (*French*) +- lokalisoija (*Finnish*) - Dennis Reimund (reimund_dennis) (*German*) -- Albatroz Jeremias (albjeremias) (*Portuguese*) -- Xurxo Guerra (xguerrap) (*Galician*) +- ronee (*Kurmanji (Kurdish)*) +- EricVogt_ (*Spanish*) +- yu miao (metaxx.dev) (*Chinese Simplified*) +- Anoop (anoopp) (*Malayalam*) - Samir Tighzert (samir_t7) (*Kabyle*) -- lokalisoija (*Finnish*) +- sn02 (*German*) +- Yui Karasuma (yui87) (*Japanese*) +- asala4544 (*Basque*) +- Thibaut Rousseau (thiht44) (*French*) +- Jason Gibson (barberpike606) (*Slovenian, Chinese Simplified*) +- Sugar NO (g1024116707) (*Chinese Simplified*) +- Aymeric (AymBroussier) (*French*) +- pezcurrel (*Italian*) +- Xurxo Guerra (xguerrap) (*Galician*) +- nicosomb (*French*) +- Albatroz Jeremias (albjeremias) (*Portuguese*) +- María José Vera (mjverap) (*Spanish*) +- mashirozx (*Chinese Simplified*) - codl (*French*) -- thisdudeisvegan (braydofficial) (*German*) -- tamaina (*Japanese*) +- Doug (douglasalvespe) (*Portuguese, Brazilian*) - Matias Lavik (matiaslavik) (*Norwegian Nynorsk*) -- Aman Alam (aalam) (*Punjabi*) +- random_person (*Spanish*) +- whoeta (wh0eta) (*Russian*) +- xpac1985 (xpac) (*German*) +- thisdudeisvegan (braydofficial) (*German*) +- Fleva (*Sardinian*) +- Anonymous (Anonymous666) (*Russian*) +- Mohammad Adnan Mahmood (adnanmig) (*Arabic*) +- ÀŘǾŚ PÀŚĦÀÍ (arospashai) (*Sorani (Kurdish)*) +- mikel (mikelalas) (*Spanish*) +- Trond Boksasp (boksasp) (*Norwegian*) +- asretro (*Chinese Traditional, Hong Kong*) - Holger Huo (holgerhuo) (*Chinese Simplified*) -- Amith Raj Shetty (amithraj1989) (*Kannada*) +- Aman Alam (aalam) (*Punjabi*) +- smedvedev (*Russian*) +- Jay Lonnquist (crowkeep) (*Japanese*) - mimikun (*Japanese*) +- Mohd Bilal (mdb571) (*Malayalam*) +- veer66 (*Thai*) +- OpenAlgeria (*Arabic*) +- Rave (nayumi-464812844) (*Vietnamese*) +- ReavedNetwork (*German*) +- Michael (Discostu36) (*German*) +- tamaina (*Japanese*) +- sk22 (*German*) - Ragnars Eggerts (rmegg1933) (*Latvian*) -- ÀŘǾŚ PÀŚĦÀÍ (arospashai) (*Sorani (Kurdish)*) -- smedvedev (*Russian*) - Sais Lakshmanan (Saislakshmanan) (*Tamil*) -- Mohammad Adnan Mahmood (adnanmig) (*Arabic*) -- OpenAlgeria (*Arabic*) -- Trond Boksasp (boksasp) (*Norwegian*) -- Doug (douglasalvespe) (*Portuguese, Brazilian*) -- Mohd Bilal (mdb571) (*Malayalam*) -- Fleva (*Sardinian*) -- xpac1985 (xpac) (*German*) -- mikel (mikelalas) (*Spanish*) -- random_person (*Spanish*) -- asretro (*Chinese Traditional, Hong Kong*) -- Arĝentakato (argxentakato) (*Japanese*) -- Nithya Mary (nithyamary25) (*Tamil*) -- Azad ahmad (dashty) (*Sorani (Kurdish)*) +- Amith Raj Shetty (amithraj1989) (*Kannada*) - Bartek Fijałkowski (brateq) (*Polish*) +- Asbeltrion (*Spanish*) +- Michael Horstmann (mhrstmnn) (*German*) +- Joffrey Abeilard (Abeilard14) (*French*) +- capiscuas (*Spanish*) +- djoerd (*Dutch*) +- REMOVED_USER (*Spanish*) +- NeverMine17 (*Russian*) +- songxianj (songxian_jiang) (*Chinese Simplified*) +- Ács Zoltán (zoli111) (*Hungarian*) +- haaninjo (*Swedish*) +- REMOVED_USER (*Esperanto*) +- Philip Molares (DerMolly) (*German*) +- ChalkPE (amato0617) (*Korean*) - ebrezhoneg (*Breton*) +- 디떱 (diddub) (*Korean*) +- Hans (hansj) (*German*) +- Nithya Mary (nithyamary25) (*Tamil*) +- kavitha129 (*Tamil*) +- waweic (*German*) +- Aries (orlea) (*Japanese*) +- おさ (osapon) (*Japanese*) +- Abijeet Patro (Abijeet) (*Basque*) +- centumix (*Japanese*) +- Martin Müller (muellermartin) (*German*) +- tateisu (*Japanese*) +- Arĝentakato (argxentakato) (*Japanese*) +- Benjamin Cobb (benjamincobb) (*German*) +- deanerschnitzel (*German*) +- Jill H. (kokakiwi) (*French*) - maksutheam (*Finnish*) +- d0p1 (d0p1s4m4) (*French*) - majorblazr (*Danish*) -- Jill H. (kokakiwi) (*French*) - Patrice Boivin (patriceboivin58) (*French*) -- centumix (*Japanese*) - 江尚寒 (jiangshanghan) (*Chinese Simplified*) -- hud5634j (*Spanish*) -- おさ (osapon) (*Japanese*) -- Jiniux (*Italian*) -- Hannah (Aniqueper1) (*Chinese Simplified*) -- Ni Futchi (futchitwo) (*Japanese*) -- dobrado (*Portuguese, Brazilian*) -- dcapillae (*Spanish*) +- HSD Channel (kvdbve34) (*Russian*) +- alwyn joe (iomedivh200) (*Chinese Simplified*) +- ZHY (sheepzh) (*Chinese Simplified*) +- Bei Li (libei) (*Chinese Simplified*) +- Aluo (Aluo_rbszd) (*Chinese Simplified*) +- clarkzjw (*Chinese Simplified*) +- Noah Luppe (noahlup) (*German*) +- araghunde (*Galician*) +- BratishkaErik (*Russian*) +- Bunny9568 (*Chinese Simplified*) +- SamOak (*Portuguese, Brazilian*) - Ranj A Abdulqadir (RanjAhmed) (*Sorani (Kurdish)*) -- Kurdish Translator (Kurdish.boy) (*Sorani (Kurdish)*) - Amir Kurdo (kuraking202) (*Sorani (Kurdish)*) -- umonaca (*Chinese Simplified*) -- Jari Ronkainen (ronchaine) (*Finnish*) -- djoerd (*Dutch*) -- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*) - 于晚霞 (xissshawww) (*Chinese Simplified*) -- tateisu (*Japanese*) -- NeverMine17 (*Russian*) +- Fyuoxyjidyho Moiodyyiodyhi (fyuodchiodmoiidiiduh86) (*Chinese Simplified*) +- RPD0911 (*Hungarian*) +- dcapillae (*Spanish*) +- dobrado (*Portuguese, Brazilian*) +- Hannah (Aniqueper1) (*Chinese Simplified*) +- Azad ahmad (dashty) (*Sorani (Kurdish)*) +- Uri Chachick (urich.404) (*Hebrew*) +- Bnoru (*Portuguese, Brazilian*) +- Jiniux (*Italian*) +- REMOVED_USER (*German*) +- Salh_haji6 (Sorani (Kurdish)) +- Kurdish Translator (*Kurdish.boy) (Sorani (Kurdish)*) +- Beagle (beagleworks) (*Japanese*) +- hud5634j (*Spanish*) +- Kisaragi Hiu (flyingfeather1501) (*Chinese Traditional*) +- Dominik Ziegler (dodomedia) (*German*) - soheilkhanalipur (*Persian*) -- SamOak (*Portuguese, Brazilian*) -- kavitha129 (*Tamil*) -- Salh_haji6 (*Sorani (Kurdish)*) - Brodi (brodi1) (*Dutch*) -- capiscuas (*Spanish*) -- HSD Channel (kvdbve34) (*Russian*) -- Abijeet Patro (Abijeet) (*Basque*) -- Ács Zoltán (zoli111) (*Hungarian*) -- Benjamin Cobb (benjamincobb) (*German*) -- waweic (*German*) -- Aries (orlea) (*Japanese*) +- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*) +- Ni Futchi (futchitwo) (*Japanese*) +- Zois Lee (gcnwm) (*Chinese Simplified*) +- Arnold Marko (atomicmind) (*Slovenian*) +- scholzco (*German*) +- Jari Ronkainen (ronchaine) (*Finnish*) +- umonaca (*Chinese Simplified*) diff --git a/Aptfile b/Aptfile index 9235141ad5..a52eef4e18 100644 --- a/Aptfile +++ b/Aptfile @@ -1,8 +1,8 @@ ffmpeg libicu[0-9][0-9] libicu-dev -libidn11 -libidn11-dev +libidn12 +libidn-dev libpq-dev libxdamage1 libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4cdd881a..b1ad9e5fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,216 @@ Changelog All notable changes to this project will be documented in this file. +## [4.0.2] - 2022-11-15 +### Fixed + +- Fix wrong color on mentions hidden behind content warning in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20724)) +- Fix filters from other users being used in the streaming service ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20719)) +- Fix `unsafe-eval` being used when `wasm-unsafe-eval` is enough in Content Security Policy ([Gargron](https://github.com/mastodon/mastodon/pull/20729), [prplecake](https://github.com/mastodon/mastodon/pull/20606)) + +## [4.0.1] - 2022-11-14 +### Fixed + +- Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677)) + +## [4.0.0] - 2022-11-14 + +Some of the features in this release have been funded through the [NGI0 Discovery](https://nlnet.nl/discovery) Fund, a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825322. + +### Added + +- Add ability to filter followed accounts' posts by language ([Gargron](https://github.com/mastodon/mastodon/pull/19095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19268)) +- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924)) +- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945)) +- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245)) +- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398), [Gargron](https://github.com/mastodon/mastodon/pull/19712), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20018)) +- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335)) + - Previously, you could only see trends in your current language + - For less popular languages, that meant empty trends + - Now, trends in your preferred languages' are shown on top, with others beneath +- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296)) +- Add privacy icons to report modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19190)) +- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014)) +- Add option to open original page in dropdowns of remote content in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20299)) +- Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885)) +- Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544)) +- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506)) +- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737)) +- Add support for uploading AVIF files ([txt-file](https://github.com/mastodon/mastodon/pull/19647)) +- Add support for uploading HEIC files ([Gargron](https://github.com/mastodon/mastodon/pull/19618)) +- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209)) +- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248)) + - Set for how long remote posts or media should be cached on your server + - Hands-off alternative to `tootctl` commands +- **Add customizable user roles** ([Gargron](https://github.com/mastodon/mastodon/pull/18641), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18812), [Gargron](https://github.com/mastodon/mastodon/pull/19040), [tribela](https://github.com/mastodon/mastodon/pull/18825), [tribela](https://github.com/mastodon/mastodon/pull/18826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18776), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18777), [unextro](https://github.com/mastodon/mastodon/pull/18786), [tribela](https://github.com/mastodon/mastodon/pull/18824), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19436)) + - Previously, there were 3 hard-coded roles, user, moderator, and admin + - Create your own roles and decide which permissions they should have +- Add notifications for new reports ([Gargron](https://github.com/mastodon/mastodon/pull/18697), [Gargron](https://github.com/mastodon/mastodon/pull/19475)) +- Add ability to select all accounts matching search for batch actions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19053), [Gargron](https://github.com/mastodon/mastodon/pull/19054)) +- Add ability to view previous edits of a status in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19462)) +- Add ability to block sign-ups from IP ([Gargron](https://github.com/mastodon/mastodon/pull/19037)) +- **Add webhooks to admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18510)) +- Add admin API for managing domain allows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18668)) +- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247)) +- Add admin API for managing e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19066)) +- Add admin API for managing canonical e-mail blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19067)) +- Add admin API for managing IP blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19065), [trwnh](https://github.com/mastodon/mastodon/pull/20207)) +- Add `sensitized` attribute to accounts in admin REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20094)) +- Add `services` and `metadata` to the NodeInfo endpoint ([MFTabriz](https://github.com/mastodon/mastodon/pull/18563)) +- Add `--remove-role` option to `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/19477)) +- Add `--days` option to `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/18425)) +- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642)) +- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757)) +- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427)) +- Add `ENABLE_STARTTLS` environment variable ([erbridge](https://github.com/mastodon/mastodon/pull/20321)) +- Add caching for payload serialization during fan-out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19637), [Gargron](https://github.com/mastodon/mastodon/pull/19642), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19746), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19747), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19963)) +- Add assets from Twemoji 14.0 ([Gargron](https://github.com/mastodon/mastodon/pull/19733)) +- Add reputation and followers score boost to SQL-only account search ([Gargron](https://github.com/mastodon/mastodon/pull/19251)) +- Add Scots, Balaibalan, Láadan, Lingua Franca Nova, Lojban, Toki Pona to languages list ([VyrCossont](https://github.com/mastodon/mastodon/pull/20168)) +- Set autocomplete hints for e-mail, password and OTP fields ([rcombs](https://github.com/mastodon/mastodon/pull/19833), [offbyone](https://github.com/mastodon/mastodon/pull/19946), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20071)) +- Add support for DigitalOcean Spaces in setup wizard ([v-aisac](https://github.com/mastodon/mastodon/pull/20573)) + +### Changed + +- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710)) +- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103)) +- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273), [Gargron](https://github.com/mastodon/mastodon/pull/19801), [Gargron](https://github.com/mastodon/mastodon/pull/19790), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19773), [Gargron](https://github.com/mastodon/mastodon/pull/19798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19724), [Gargron](https://github.com/mastodon/mastodon/pull/19709), [Gargron](https://github.com/mastodon/mastodon/pull/19514), [Gargron](https://github.com/mastodon/mastodon/pull/19562), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19978), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20148), [Gargron](https://github.com/mastodon/mastodon/pull/20302), [cutls](https://github.com/mastodon/mastodon/pull/20400)) + - The web app can now be accessed without being logged in + - No more `/web` prefix on web app paths + - Profiles, posts, and other public pages now use the same interface for logged in and logged out users + - The web app displays a server information banner + - Pop-up windows for remote interaction have been replaced with a modal window + - No need to type in your username for remote interaction, copy-paste-to-search method explained + - Various hints throughout the app explain what the different timelines are + - New about page design + - New privacy policy page design shows when the policy was last updated + - All sections of the web app now have appropriate window titles + - The layout of the interface has been streamlined between different screen sizes + - Posts now use more horizontal space +- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583)) +- Change language to be carried over on reply in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18557)) +- Change "Unfollow" to "Cancel follow request" when request still pending in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/19363)) +- **Change post filtering system** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18058), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19050), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18894), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19051), [noellabo](https://github.com/mastodon/mastodon/pull/18923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18744), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20567)) + - Filtered keywords and phrases can now be grouped into named categories + - Filtered posts show which exact filter was hit + - Individual posts can be added to a filter + - You can peek inside filtered posts anyway +- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249)) +- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854)) +- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407), [Gargron](https://github.com/mastodon/mastodon/pull/19533)) +- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356)) +- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979)) +- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788)) +- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977)) +- Change admin announcements form to use single inputs for date and time in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18321)) +- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326)) +- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964)) +- Change `AUTHORIZED_FETCH` to not block unauthenticated REST API access ([Gargron](https://github.com/mastodon/mastodon/pull/19803)) +- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941)) +- Change mentions of blocked users to not be processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19725)) +- Change max. thumbnail dimensions to 640x360px (360p) ([Gargron](https://github.com/mastodon/mastodon/pull/19619)) +- Change post-processing to be deferred only for large media types ([Gargron](https://github.com/mastodon/mastodon/pull/19617)) +- Change link verification to only work for https links without unicode ([Gargron](https://github.com/mastodon/mastodon/pull/20304), [Gargron](https://github.com/mastodon/mastodon/pull/20295)) +- Change account deletion requests to spread out over time ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20222)) +- Change larger reblogs/favourites numbers to be shortened in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20303)) +- Change incoming activity processing to happen in `ingress` queue ([Gargron](https://github.com/mastodon/mastodon/pull/20264)) +- Change notifications to not link show preview cards in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20335)) +- Change amount of replies returned for logged out users in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20355)) +- Change in-app links to keep you in-app in web UI ([trwnh](https://github.com/mastodon/mastodon/pull/20540), [Gargron](https://github.com/mastodon/mastodon/pull/20628)) +- Change table header to be sticky in admin UI ([sk22](https://github.com/mastodon/mastodon/pull/20442)) + +### Removed + +- Remove setting that disables account deletes ([Gargron](https://github.com/mastodon/mastodon/pull/17683)) +- Remove digest e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/17985)) +- Remove unnecessary sections from welcome e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/19299)) +- Remove item titles from RSS feeds ([Gargron](https://github.com/mastodon/mastodon/pull/18640)) +- Remove volume number from hashtags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19253)) +- Remove Nanobox configuration ([tonyjiang](https://github.com/mastodon/mastodon/pull/17881)) + +### Fixed + +- Fix rules with same priority being sorted non-deterministically ([Gargron](https://github.com/mastodon/mastodon/pull/20623)) +- Fix error when invalid domain name is submitted ([Gargron](https://github.com/mastodon/mastodon/pull/19474)) +- Fix icons having an image role ([Gargron](https://github.com/mastodon/mastodon/pull/20600)) +- Fix connections to IPv6-only servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20108)) +- Fix unnecessary service worker registration and preloading when logged out in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20341)) +- Fix unnecessary and slow regex construction ([raggi](https://github.com/mastodon/mastodon/pull/20215)) +- Fix `mailers` queue not being used for mailers ([Gargron](https://github.com/mastodon/mastodon/pull/20274)) +- Fix error in webfinger redirect handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20260)) +- Fix report category not being set to `violation` if rule IDs are provided ([trwnh](https://github.com/mastodon/mastodon/pull/20137)) +- Fix nodeinfo metadata attribute being an array instead of an object ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20114)) +- Fix account endorsements not being idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20118)) +- Fix status and rule IDs not being strings in admin reports REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20122)) +- Fix error on invalid `replies_policy` in REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20126)) +- Fix redrafting a currently-editing post not leaving edit mode in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20023)) +- Fix performance by avoiding method cache busts ([raggi](https://github.com/mastodon/mastodon/pull/19957)) +- Fix opening the language picker scrolling the single-column view to the top in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19983)) +- Fix content warning button missing `aria-expanded` attribute in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19975)) +- Fix redundant `aria-pressed` attributes in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/19912)) +- Fix crash when external auth provider has no display name set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19962)) +- Fix followers count not being updated when migrating follows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19998)) +- Fix double button to clear emoji search input in web UI ([sunny](https://github.com/mastodon/mastodon/pull/19888)) +- Fix missing null check on applications on strike disputes ([kescherCode](https://github.com/mastodon/mastodon/pull/19851)) +- Fix featured tags not saving preferred casing ([Gargron](https://github.com/mastodon/mastodon/pull/19732)) +- Fix language not being saved when editing status ([Gargron](https://github.com/mastodon/mastodon/pull/19543)) +- Fix not being able to input featured tag with hash symbol ([Gargron](https://github.com/mastodon/mastodon/pull/19535)) +- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19629)) +- Fix being unable to withdraw follow request when confirmation modal is disabled in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19687)) +- Fix inaccurate admin log entry for re-sending confirmation e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19674)) +- Fix edits not being immediately reflected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19673)) +- Fix bookmark import stopping at the first failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19669)) +- Fix account action type validation ([Gargron](https://github.com/mastodon/mastodon/pull/19476)) +- Fix upload progress not communicating processing phase in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19530)) +- Fix wrong host being used for custom.css when asset host configured ([Gargron](https://github.com/mastodon/mastodon/pull/19521)) +- Fix account migration form ever using outdated account data ([Gargron](https://github.com/mastodon/mastodon/pull/18429), [nightpool](https://github.com/mastodon/mastodon/pull/19883)) +- Fix error when uploading malformed CSV import ([Gargron](https://github.com/mastodon/mastodon/pull/19509)) +- Fix avatars not using image tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19488)) +- Fix handling of duplicate and out-of-order notifications in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19693)) +- Fix reblogs being discarded after the reblogged status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19731)) +- Fix indexing scheduler trying to index when Elasticsearch is disabled ([Gargron](https://github.com/mastodon/mastodon/pull/19805)) +- Fix n+1 queries when rendering initial state JSON ([Gargron](https://github.com/mastodon/mastodon/pull/19795)) +- Fix n+1 query during status removal ([Gargron](https://github.com/mastodon/mastodon/pull/19753)) +- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817)) +- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455)) +- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428)) +- Fix button to dismiss suggestions not showing up in search results in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19325)) +- Fix language dropdown sometimes not appearing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19246)) +- Fix quickly switching notification filters resulting in empty or incorrect list in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18960)) +- Fix media modal link button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18877)) +- Fix error upon successful account migration ([Gargron](https://github.com/mastodon/mastodon/pull/19386)) +- Fix negatives values in search index causing queries to fail ([Gargron](https://github.com/mastodon/mastodon/pull/19464), [Gargron](https://github.com/mastodon/mastodon/pull/19481)) +- Fix error when searching for invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18580)) +- Fix IP blocks not having a unique index ([Gargron](https://github.com/mastodon/mastodon/pull/19456)) +- Fix remote account in contact account setting not being used ([Gargron](https://github.com/mastodon/mastodon/pull/19351)) +- Fix swallowing mentions of unconfirmed/unapproved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19191)) +- Fix incorrect and slow cache invalidation when blocking domain and removing media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19062)) +- Fix HTTPs redirect behaviour when running as I2P service ([gi-yt](https://github.com/mastodon/mastodon/pull/18929)) +- Fix deleted pinned posts potentially counting towards the pinned posts limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19005)) +- Fix compatibility with OpenSSL 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18449)) +- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760)) +- Fix suspicious sign-in mails never being sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18599)) +- Fix fallback locale when somehow user's locale is an empty string ([tribela](https://github.com/mastodon/mastodon/pull/18543)) +- Fix avatar/header not being deleted locally when deleted on remote account ([tribela](https://github.com/mastodon/mastodon/pull/18973)) +- Fix missing `,` in Blurhash validation ([noellabo](https://github.com/mastodon/mastodon/pull/18660)) +- Fix order by most recent not working for relationships page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/18996)) +- Fix uncaught error when invalid date is supplied to API ([Gargron](https://github.com/mastodon/mastodon/pull/19480)) +- Fix REST API sometimes returning HTML on error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19135)) +- Fix ambiguous column names in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/19206)) +- Fix ambiguous column names in `tootctl search deploy` ([mashirozx](https://github.com/mastodon/mastodon/pull/18993)) +- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662)) +- Fix `CAS_DISPLAY_NAME`, `SAML_DISPLAY_NAME` and `OIDC_DISPLAY_NAME` being ignored ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18568)) +- Fix various typos in comments throughout the codebase ([luzpaz](https://github.com/mastodon/mastodon/pull/18604)) +- Fix CSV import error when rows include unicode characters ([HamptonMakes](https://github.com/mastodon/mastodon/pull/20592)) + +### Security + +- Fix being able to spoof link verification ([Gargron](https://github.com/mastodon/mastodon/pull/20217)) +- Fix emoji substitution not applying only to text nodes in backend code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20641)) +- Fix emoji substitution not applying only to text nodes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20640)) +- Fix rate limiting for paths with formats ([Gargron](https://github.com/mastodon/mastodon/pull/20675)) +- Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) + ## [3.5.3] - 2022-05-26 ### Added @@ -75,7 +285,7 @@ All notable changes to this project will be documented in this file. - Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190)) - The IPs of the blocked e-mail domain or its MX records are no longer checked - Previously it was too easy to block e-mail providers by mistake - + ## Fixed - Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260)) @@ -122,7 +332,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fix error resposes for `from` search prefix ([single-right-quote](https://github.com/mastodon/mastodon/pull/17963)) +- Fix error responses for `from` search prefix ([single-right-quote](https://github.com/mastodon/mastodon/pull/17963)) - Fix dangling language-specific trends ([Gargron](https://github.com/mastodon/mastodon/pull/17997)) - Fix extremely rare race condition when deleting a status or account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17994)) - Fix trends returning less results per page when filtered in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17996)) @@ -257,7 +467,7 @@ All notable changes to this project will be documented in this file. - Remove profile directory link from main navigation panel in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17688)) - **Remove language detection through cld3** ([Gargron](https://github.com/mastodon/mastodon/pull/17478), [ykzts](https://github.com/mastodon/mastodon/pull/17539), [Gargron](https://github.com/mastodon/mastodon/pull/17496), [Gargron](https://github.com/mastodon/mastodon/pull/17722)) - cld3 is very inaccurate on short-form content even with unique alphabets - - Post language can be overriden individually using `language` param + - Post language can be overridden individually using `language` param - Otherwise, it defaults to the user's interface language - Remove support for `OAUTH_REDIRECT_AT_SIGN_IN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17287)) - Use `OMNIAUTH_ONLY` instead diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5b5513ff1..049cd4e29b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ See the guidelines below. - - - -You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `tootsuite/mastodon`, reproduced below. +You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below.
@@ -76,6 +76,8 @@ It is not always possible to phrase every change in such a manner, but it is des - Code style rules (rubocop, eslint) - Normalization of locale files (i18n-tasks) +**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run. + ## Documentation The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). diff --git a/Gemfile b/Gemfile index dc9b330ca7..1bff6cc7d8 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6' +gem 'redcarpet', '~> 3.5' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.1' @@ -91,15 +92,13 @@ gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2022' gem 'webpacker', '~> 5.4' -gem 'webpush', git: 'https://github.com/ClearlyClaire/webpush.git', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' +gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' gem 'webauthn', '~> 2.5' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'redcarpet', '~> 3.5' - group :development, :test do gem 'fabrication', '~> 2.30' gem 'fuubar', '~> 2.5' @@ -123,6 +122,7 @@ group :test do gem 'simplecov', '~> 0.21', require: false gem 'webmock', '~> 3.18' gem 'rspec_junit_formatter', '~> 0.6' + gem 'rack-test', '~> 2.0' end group :development do @@ -153,7 +153,6 @@ end gem 'concurrent-ruby', require: false gem 'connection_pool', require: false - gem 'xorcist', '~> 1.1' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 939ed55d0f..9121569661 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -272,7 +272,7 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.5.3) + fugit (1.7.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) fuubar (2.5.1) @@ -343,7 +343,7 @@ GEM multi_json (~> 1.15) rack (~> 2.2) rdf (~> 3.2, >= 3.2.9) - json-ld-preloaded (3.2.0) + json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) jsonapi-renderer (0.2.2) @@ -395,7 +395,7 @@ GEM mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) - memory_profiler (1.0.0) + memory_profiler (1.0.1) method_source (1.0.0) microformats (4.4.1) json (~> 2.2) @@ -414,7 +414,7 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-ssh (7.0.1) nio4r (2.5.8) - nokogiri (1.13.8) + nokogiri (1.13.9) mini_portile2 (~> 2.8.0) racc (~> 1.4) nsa (0.2.8) @@ -422,7 +422,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.13.21) + oj (3.13.23) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -457,7 +457,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.3) + pg (1.4.4) pghero (2.8.3) activerecord (>= 5) pkg-config (1.4.9) @@ -600,7 +600,7 @@ GEM nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) - rufus-scheduler (3.8.1) + rufus-scheduler (3.8.2) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) @@ -611,16 +611,16 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.7) - connection_pool (>= 2.2.5) + sidekiq (6.5.8) + connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (4.0.2) + sidekiq-scheduler (4.0.3) redis (>= 4.2.0) rufus-scheduler (~> 3.2) - sidekiq (>= 4) + sidekiq (>= 4, < 7) tilt (>= 1.4.0) sidekiq-unique-jobs (7.1.27) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) @@ -649,7 +649,7 @@ GEM sshkit (1.21.2) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.21) + stackprof (0.2.22) statsd-ruby (1.5.0) stoplight (3.0.0) strong_migrations (0.7.9) @@ -664,7 +664,7 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (1.2.1) - tilt (2.0.10) + tilt (2.0.11) tpm-key_attestation (0.11.0) bindata (~> 2.4) openssl (> 2.0, < 3.1) @@ -684,7 +684,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) - tzinfo-data (1.2022.4) + tzinfo-data (1.2022.6) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -818,6 +818,7 @@ DEPENDENCIES rack (~> 2.2.4) rack-attack (~> 6.6) rack-cors (~> 1.1) + rack-test (~> 2.0) rails (~> 6.1.7) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) diff --git a/SECURITY.md b/SECURITY.md index 62e23f7360..ccc7c10346 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you should submit the report through our [Bug Bounty Program][bug-bounty]. Alternatively, you can reach us at . +If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at . You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. @@ -10,11 +10,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| 3.5.x | Yes | -| 3.4.x | Yes | -| 3.3.x | No | -| < 3.3 | No | - -[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail +| Version | Supported | +| ------- | ----------| +| 4.0.x | Yes | +| 3.5.x | Yes | +| < 3.5 | No | diff --git a/app.json b/app.json index c694908c53..4f05a64f51 100644 --- a/app.json +++ b/app.json @@ -79,8 +79,13 @@ "description": "SMTP server certificate verification mode. Defaults is 'peer'.", "required": false }, + "SMTP_ENABLE_STARTTLS": { + "description": "Enable STARTTLS? Default is 'auto'.", + "value": "auto", + "required": false + }, "SMTP_ENABLE_STARTTLS_AUTO": { - "description": "Enable STARTTLS if SMTP server supports it? Default is true.", + "description": "Enable STARTTLS if SMTP server supports it? Deprecated by SMTP_ENABLE_STARTTLS.", "required": false } }, diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index d3f03374fe..1043486140 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,72 +1,19 @@ # frozen_string_literal: true class AboutController < ApplicationController - include RegistrationSpamConcern + include WebAppControllerConcern - before_action :set_pack + skip_before_action :require_functional! - layout 'public' - - before_action :require_open_federation!, only: [:show, :more] - before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in, only: [:more] - before_action :set_registration_form_time, only: :show - - skip_before_action :require_functional!, only: [:more] - - def show; end - - def more - flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] - - toc_generator = TOCGenerator.new(@instance_presenter.extended_description) - @rules = Rule.ordered - @contents = toc_generator.html - @table_of_contents = toc_generator.toc - @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? + def show + expires_in 0, public: true unless user_signed_in? end - helper_method :display_blocks? - helper_method :display_blocks_rationale? - helper_method :public_fetch_mode? - helper_method :new_user - private - def require_open_federation! - not_found if whitelist_mode? - end - - def display_blocks? - Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) - end - - def display_blocks_rationale? - Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?) - end - - def new_user - User.new.tap do |user| - user.build_account - user.build_invite_request - end - end - - def set_pack - use_pack 'public' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end - - def set_body_classes - @hide_navbar = true - end - - def set_expires_in - expires_in 0, public: true - end end diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb deleted file mode 100644 index 33394074db..0000000000 --- a/app/controllers/account_follow_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class AccountFollowController < ApplicationController - include AccountControllerConcern - - before_action :authenticate_user! - - def create - FollowService.new.call(current_user.account, @account, with_rate_limit: true) - redirect_to account_path(@account) - end -end diff --git a/app/controllers/account_unfollow_controller.rb b/app/controllers/account_unfollow_controller.rb deleted file mode 100644 index 378ec86dc6..0000000000 --- a/app/controllers/account_unfollow_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class AccountUnfollowController < ApplicationController - include AccountControllerConcern - - before_action :authenticate_user! - - def create - UnfollowService.new.call(current_user.account, @account) - redirect_to account_path(@account) - end -end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 02f3c3dd7c..f36a0c8599 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -9,7 +9,6 @@ class AccountsController < ApplicationController before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers - before_action :set_body_classes skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -17,26 +16,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - use_pack 'public' expires_in 0, public: true unless user_signed_in? - - @pinned_statuses = [] - @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) - @featured_hashtags = @account.featured_tags.order(statuses_count: :desc) - - if current_account && @account.blocking?(current_account) - @statuses = [] - return - end - - @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses? - @statuses = cached_filtered_status_page - @rss_url = rss_url - - unless @statuses.empty? - @older_url = older_url if @statuses.last.id > filtered_statuses.last.id - @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id - end end format.rss do @@ -56,18 +36,6 @@ class AccountsController < ApplicationController private - def set_body_classes - @body_classes = 'with-modals' - end - - def show_pinned_statuses? - [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? - end - - def filtered_pinned_statuses - @account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted]) - end - def filtered_statuses default_statuses.tap do |statuses| statuses.merge!(hashtag_scope) if tag_requested? @@ -114,26 +82,6 @@ class AccountsController < ApplicationController end end - def older_url - pagination_url(max_id: @statuses.last.id) - end - - def newer_url - pagination_url(min_id: @statuses.first.id) - end - - def pagination_url(max_id: nil, min_id: nil) - 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) - elsif replies_requested? - short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) - else - short_account_url(@account, max_id: max_id, min_id: min_id) - end - end - def media_requested? request.path.split('.').first.end_with?('/media') && !tag_requested? end @@ -146,13 +94,6 @@ class AccountsController < ApplicationController request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def cached_filtered_status_pins - cache_collection( - filtered_pinned_statuses, - Status - ) - end - def cached_filtered_status_page cache_collection_paginated_by_id( filtered_statuses, diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index efe7dcbd4b..6f4e426797 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -17,7 +17,7 @@ module Admin @user.resend_confirmation_instructions - log_action :confirm, @user + log_action :resend, @user flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') redirect_to admin_accounts_path diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 1fae60f5b0..431dc1524f 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -34,7 +34,7 @@ module Admin @form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') + flash[:alert] = I18n.t('admin.custom_emojis.no_emoji_selected') rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') rescue ActiveRecord::RecordInvalid => e diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index db8863551c..545bd94edd 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -62,7 +62,7 @@ module Admin def export_data CSV.generate(headers: export_headers, write_headers: true) do |content| - DomainBlock.with_user_facing_limitations.each do |instance| + DomainBlock.with_limitations.each do |instance| content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] end end diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb index a87520f4ed..1bd7ec8059 100644 --- a/app/controllers/admin/ip_blocks_controller.rb +++ b/app/controllers/admin/ip_blocks_controller.rb @@ -5,7 +5,7 @@ module Admin def index authorize :ip_block, :index? - @ip_blocks = IpBlock.page(params[:page]) + @ip_blocks = IpBlock.order(ip: :asc).page(params[:page]) @form = Form::IpBlockBatch.new end diff --git a/app/controllers/admin/settings/about_controller.rb b/app/controllers/admin/settings/about_controller.rb new file mode 100644 index 0000000000..86327fe397 --- /dev/null +++ b/app/controllers/admin/settings/about_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::AboutController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_about_path + end +end diff --git a/app/controllers/admin/settings/appearance_controller.rb b/app/controllers/admin/settings/appearance_controller.rb new file mode 100644 index 0000000000..39b2448d80 --- /dev/null +++ b/app/controllers/admin/settings/appearance_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::AppearanceController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_appearance_path + end +end diff --git a/app/controllers/admin/settings/branding_controller.rb b/app/controllers/admin/settings/branding_controller.rb new file mode 100644 index 0000000000..4a4d76f497 --- /dev/null +++ b/app/controllers/admin/settings/branding_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::BrandingController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_branding_path + end +end diff --git a/app/controllers/admin/settings/content_retention_controller.rb b/app/controllers/admin/settings/content_retention_controller.rb new file mode 100644 index 0000000000..b88336a2c4 --- /dev/null +++ b/app/controllers/admin/settings/content_retention_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::ContentRetentionController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_content_retention_path + end +end diff --git a/app/controllers/admin/settings/discovery_controller.rb b/app/controllers/admin/settings/discovery_controller.rb new file mode 100644 index 0000000000..be4b57f79a --- /dev/null +++ b/app/controllers/admin/settings/discovery_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::DiscoveryController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_discovery_path + end +end diff --git a/app/controllers/admin/settings/other_controller.rb b/app/controllers/admin/settings/other_controller.rb new file mode 100644 index 0000000000..c7bfa3d586 --- /dev/null +++ b/app/controllers/admin/settings/other_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::OtherController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_other_path + end +end diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb new file mode 100644 index 0000000000..b4a74349c0 --- /dev/null +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::RegistrationsController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_registrations_path + end +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index dc1c79b7fd..338a3638c4 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -2,7 +2,7 @@ module Admin class SettingsController < BaseController - def edit + def show authorize :settings, :show? @admin_settings = Form::AdminSettings.new @@ -15,14 +15,18 @@ module Admin if @admin_settings.save flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to edit_admin_settings_path + redirect_to after_update_redirect_path else - render :edit + render :show end end private + def after_update_redirect_path + raise NotImplementedError + end + def settings_params params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb index cacecedb0a..a5d2cf41cf 100644 --- a/app/controllers/admin/site_uploads_controller.rb +++ b/app/controllers/admin/site_uploads_controller.rb @@ -9,7 +9,7 @@ module Admin @site_upload.destroy! - redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') + redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') end private diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 084921cebb..b80cd20f56 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -3,18 +3,23 @@ module Admin class StatusesController < BaseController before_action :set_account - before_action :set_statuses + before_action :set_statuses, except: :show + before_action :set_status, only: :show PER_PAGE = 20 def index - authorize :status, :index? + authorize [:admin, :status], :index? @status_batch_action = Admin::StatusBatchAction.new end + def show + authorize [:admin, @status], :show? + end + def batch - authorize :status, :index? + authorize [:admin, :status], :index? @status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button)) @status_batch_action.save! @@ -32,6 +37,7 @@ module Admin def after_create_redirect_path report_id = @status_batch_action&.report_id || params[:report_id] + if report_id.present? admin_report_path(report_id) else @@ -43,6 +49,10 @@ module Admin @account = Account.find(params[:account_id]) end + def set_status + @status = @account.statuses.find(params[:id]) + end + def set_statuses @statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE) end diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 97dee8eca4..768b79f8db 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -14,7 +14,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll @form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') + flash[:alert] = I18n.t('admin.trends.links.publishers.no_publisher_selected') ensure redirect_to admin_trends_links_preview_card_providers_path(filter_params) end diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index a497eae411..83d68eba63 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController def index authorize :preview_card, :review? + @locales = PreviewCardTrend.pluck('distinct language') @preview_cards = filtered_preview_cards.page(params[:page]) @form = Trends::PreviewCardBatch.new end @@ -14,7 +15,7 @@ class Admin::Trends::LinksController < Admin::BaseController @form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') + flash[:alert] = I18n.t('admin.trends.links.no_link_selected') ensure redirect_to admin_trends_links_path(filter_params) end diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index c538962f99..3d8b53ea8a 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -2,19 +2,20 @@ class Admin::Trends::StatusesController < Admin::BaseController def index - authorize :status, :review? + authorize [:admin, :status], :review? + @locales = StatusTrend.pluck('distinct language') @statuses = filtered_statuses.page(params[:page]) @form = Trends::StatusBatch.new end def batch - authorize :status, :review? + authorize [:admin, :status], :review? @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') + flash[:alert] = I18n.t('admin.trends.statuses.no_status_selected') ensure redirect_to admin_trends_statuses_path(filter_params) end diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index 98dd6c8ec2..f5946448ae 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -14,7 +14,7 @@ class Admin::Trends::TagsController < Admin::BaseController @form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') + flash[:alert] = I18n.t('admin.trends.tags.no_tag_selected') ensure redirect_to admin_trends_tags_path(filter_params) end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 7ce6599c52..defef0656f 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -24,6 +24,10 @@ class Api::BaseController < ApplicationController render json: { error: 'Duplicate record' }, status: 422 end + rescue_from Date::Error do + render json: { error: 'Invalid date supplied' }, status: 422 + end + rescue_from ActiveRecord::RecordNotFound do render json: { error: 'Record not found' }, status: 404 end @@ -53,7 +57,7 @@ class Api::BaseController < ApplicationController render json: { error: I18n.t('errors.429') }, status: 429 end - rescue_from ActionController::ParameterMissing do |e| + rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e| render json: { error: e.to_s }, status: 400 end @@ -125,11 +129,11 @@ class Api::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' + response.headers['Cache-Control'] = 'private, no-store' end def disallow_unauthenticated_api_access? - authorized_fetch_mode? + ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode end private diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb index 3915b56693..73f845c614 100644 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Accounts::PinsController < Api::BaseController before_action :set_account def create - AccountPin.create!(account: current_account, target_account: @account) + AccountPin.find_or_create_by!(account: current_account, target_account: @account) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 0dee02e94f..ae7f7d0762 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -60,14 +60,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController def reject authorize @account.user, :reject? DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) - render json: @account, serializer: REST::Admin::AccountSerializer + render_empty end def destroy authorize @account, :destroy? - json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer Admin::AccountDeletionWorker.perform_async(@account.id) - render json: json + render_empty end def unsensitive diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index bf8a6a1311..9ef1b3be71 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -35,20 +35,16 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController def create authorize :canonical_email_block, :create? - @canonical_email_block = CanonicalEmailBlock.create!(resource_params) log_action :create, @canonical_email_block - render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer end def destroy authorize @canonical_email_block, :destroy? - @canonical_email_block.destroy! log_action :destroy, @canonical_email_block - - render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer + render_empty end private diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 59aa807d6f..0658199f0f 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -43,7 +43,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) log_action :destroy, @domain_allow - render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + render_empty end private diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index de8fd9d089..df5b1b3fcb 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -40,7 +40,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController def update authorize @domain_block, :update? - @domain_block.update(domain_block_params) severity_changed = @domain_block.severity_changed? @domain_block.save! @@ -53,7 +52,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block) log_action :destroy, @domain_block - render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + render_empty end private diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index ac16f70b05..e53d0b1573 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -39,11 +39,9 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController def destroy authorize @email_domain_block, :destroy? - @email_domain_block.destroy! log_action :destroy, @email_domain_block - - render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + render_empty end private diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index f13d632671..201ab6b1ff 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -20,10 +20,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController def create authorize :ip_block, :create? - @ip_block = IpBlock.create!(resource_params) log_action :create, @ip_block - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer end @@ -39,20 +37,16 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController def update authorize @ip_block, :update? - @ip_block.update(resource_params) log_action :update, @ip_block - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer end def destroy authorize @ip_block, :destroy? - @ip_block.destroy! log_action :destroy, @ip_block - - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + render_empty end private diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index c1ead4f540..edb42a94ea 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,12 +13,12 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - @featured_tag = current_account.featured_tags.create!(featured_tag_params) - render json: @featured_tag, serializer: REST::FeaturedTagSerializer + featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + render json: featured_tag, serializer: REST::FeaturedTagSerializer end def destroy - @featured_tag.destroy! + RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id) render_empty end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 07cd141478..149139b40a 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -52,7 +52,7 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) + params.permit(:phrase, :expires_in, :irreversible, context: []) end def filter_params diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb new file mode 100644 index 0000000000..37a6906fb6 --- /dev/null +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Api::V1::Instances::DomainBlocksController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :require_enabled_api! + before_action :set_domain_blocks + + def index + expires_in 3.minutes, public: true + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) + end + + private + + def require_enabled_api! + head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) + end + + def set_domain_blocks + @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity + end +end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb new file mode 100644 index 0000000000..c72e16cff2 --- /dev/null +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_extended_description + + def show + expires_in 3.minutes, public: true + render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer + end + + private + + def set_extended_description + @extended_description = ExtendedDescription.current + end +end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb new file mode 100644 index 0000000000..dbd69f54d4 --- /dev/null +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_privacy_policy + + def show + expires_in 1.day, public: true + render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer + end + + private + + def set_privacy_policy + @privacy_policy = PrivacyPolicy.current + end +end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index e5ac45fefb..843ca2ec2b 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -7,6 +7,10 @@ class Api::V1::ListsController < Api::BaseController before_action :require_user! before_action :set_list, except: [:index, :create] + rescue_from ArgumentError do |e| + render json: { error: e.to_s }, status: 422 + end + def index @lists = List.where(account: current_account).all render json: @lists, each_serializer: REST::ListSerializer diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b2cee3e92e..e2e48f6337 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -18,14 +18,29 @@ class Api::V1::StatusesController < Api::BaseController # than this anyway CONTEXT_LIMIT = 4_096 + # This remains expensive and we don't want to show everything to logged-out users + ANCESTORS_LIMIT = 40 + DESCENDANTS_LIMIT = 60 + DESCENDANTS_DEPTH_LIMIT = 20 + def show @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end def context - ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account) - descendants_results = @status.descendants(CONTEXT_LIMIT, current_account) + ancestors_limit = CONTEXT_LIMIT + descendants_limit = CONTEXT_LIMIT + descendants_depth_limit = nil + + if current_account.nil? + ancestors_limit = ANCESTORS_LIMIT + descendants_limit = DESCENDANTS_LIMIT + descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT + end + + ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account) + descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit) loaded_ancestors = cache_collection(ancestors_results, Status) loaded_descendants = cache_collection(descendants_results, Status) @@ -66,6 +81,7 @@ class Api::V1::StatusesController < Api::BaseController text: status_params[:status], media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], + language: status_params[:language], spoiler_text: status_params[:spoiler_text], poll: status_params[:poll], content_type: status_params[:content_type] @@ -78,7 +94,8 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.where(account: current_account).find(params[:id]) authorize @status, :destroy? - @status.discard + @status.discard_with_reblogs + StatusPin.find_by(status: @status)&.destroy @status.account.statuses_count = @status.account.statuses_count - 1 json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 9e5c53330a..32f71bdce2 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -24,7 +24,7 @@ class Api::V1::TagsController < Api::BaseController private def set_or_create_tag - return not_found unless /\A(#{Tag::HASHTAG_NAME_RE})\z/.match?(params[:id]) + return not_found unless Tag::HASHTAG_NAME_RE.match?(params[:id]) @tag = Tag.find_normalized(params[:id]) || Tag.new(name: Tag.normalize(params[:id]), display_name: params[:id]) end end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 1a9f918f29..8ff3b364e2 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -28,7 +28,9 @@ class Api::V1::Trends::LinksController < Api::BaseController end def links_from_trends - Trends.links.query.allowed.in_locale(content_locale) + scope = Trends.links.query.allowed.in_locale(content_locale) + scope = scope.filtered_for(current_account) if user_signed_in? + scope end def insert_pagination_headers diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 21adfa2a1f..885a4ad7e8 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Trends::TagsController < Api::BaseController after_action :insert_pagination_headers - DEFAULT_TAGS_LIMIT = 10 + DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i def index render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index bcc1a07339..b25831aa09 100644 --- a/app/controllers/api/v2/admin/accounts_controller.rb +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -33,7 +33,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController end def filter_params - params.permit(*FILTER_PARAMS) + params.permit(*FILTER_PARAMS, role_ids: []) end def pagination_params(core_params) diff --git a/app/controllers/api/v1/filters/keywords_controller.rb b/app/controllers/api/v2/filters/keywords_controller.rb similarity index 95% rename from app/controllers/api/v1/filters/keywords_controller.rb rename to app/controllers/api/v2/filters/keywords_controller.rb index d3718a1371..c63e1d986b 100644 --- a/app/controllers/api/v1/filters/keywords_controller.rb +++ b/app/controllers/api/v2/filters/keywords_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1::Filters::KeywordsController < Api::BaseController +class Api::V2::Filters::KeywordsController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v1/filters/statuses_controller.rb b/app/controllers/api/v2/filters/statuses_controller.rb similarity index 95% rename from app/controllers/api/v1/filters/statuses_controller.rb rename to app/controllers/api/v2/filters/statuses_controller.rb index b6bed306fc..755c14cffa 100644 --- a/app/controllers/api/v1/filters/statuses_controller.rb +++ b/app/controllers/api/v2/filters/statuses_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1::Filters::StatusesController < Api::BaseController +class Api::V2::Filters::StatusesController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 0c1baf01d0..288f847f17 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -3,7 +3,7 @@ class Api::V2::MediaController < Api::V1::MediaController def create @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params)) - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202 + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 77eeab5b0b..b084eae425 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -5,8 +5,8 @@ class Api::V2::SearchController < Api::BaseController RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i - before_action -> { doorkeeper_authorize! :read, :'read:search' } - before_action :require_user! + before_action -> { authorize_if_got_token! :read, :'read:search' } + before_action :validate_search_params! def index @search = Search.new(search_results) @@ -19,6 +19,16 @@ class Api::V2::SearchController < Api::BaseController private + def validate_search_params! + params.require(:q) + + return if user_signed_in? + + return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present? + + render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve) + end + def search_results SearchService.new.call( params[:q], diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index f9cf6d6551..3d7962de56 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -18,7 +18,8 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController ) sign_in_and_redirect @user, event: :authentication - set_flash_message(:notice, :success, kind: Devise.omniauth_configs[provider].strategy.display_name.capitalize) if is_navigational_format? + label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize) + set_flash_message(:notice, :success, kind: label) if is_navigational_format? else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 486edcdcb8..d363efeee4 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -15,6 +15,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :require_not_suspended!, only: [:update] before_action :set_cache_headers, only: [:edit, :update] + before_action :set_rules, only: :new + before_action :require_rules_acceptance!, only: :new before_action :set_registration_form_time, only: :new skip_before_action :require_functional!, only: [:edit, :update] @@ -56,7 +58,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) + u.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) end end @@ -143,7 +145,20 @@ class Auth::RegistrationsController < Devise::RegistrationsController forbidden if current_account.suspended? end + def set_rules + @rules = Rule.ordered + end + + def require_rules_acceptance! + return if @rules.empty? || (session[:accept_token].present? && params[:accept] == session[:accept_token]) + + @accept_token = session[:accept_token] = SecureRandom.hex + @invite_code = invite_code + + set_locale { render :rules } + end + def set_cache_headers - response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' + response.headers['Cache-Control'] = 'private, no-store' end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 11eac0eb6b..2f7d84df04 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -3,13 +3,12 @@ module AccountControllerConcern extend ActiveSupport::Concern + include WebAppControllerConcern include AccountOwnedConcern FOLLOW_PER_PAGE = 12 included do - layout 'public' - before_action :set_instance_presenter before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end diff --git a/app/controllers/concerns/status_controller_concern.rb b/app/controllers/concerns/status_controller_concern.rb deleted file mode 100644 index 62a7cf5086..0000000000 --- a/app/controllers/concerns/status_controller_concern.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module StatusControllerConcern - extend ActiveSupport::Concern - - ANCESTORS_LIMIT = 40 - DESCENDANTS_LIMIT = 60 - DESCENDANTS_DEPTH_LIMIT = 20 - - def create_descendant_thread(starting_depth, statuses) - depth = starting_depth + statuses.size - - if depth < DESCENDANTS_DEPTH_LIMIT - { - statuses: statuses, - starting_depth: starting_depth, - } - else - next_status = statuses.pop - - { - statuses: statuses, - starting_depth: starting_depth, - next_status: next_status, - } - end - end - - def set_ancestors - @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] - @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift - end - - def set_descendants - @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i - @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i - - descendants = cache_collection( - @status.descendants( - DESCENDANTS_LIMIT, - current_account, - @max_descendant_thread_id, - @since_descendant_thread_id, - DESCENDANTS_DEPTH_LIMIT - ), - Status - ) - - @descendant_threads = [] - - if descendants.present? - statuses = [descendants.first] - starting_depth = 0 - - descendants.drop(1).each_with_index do |descendant, index| - if descendants[index].id == descendant.in_reply_to_id - statuses << descendant - else - @descendant_threads << create_descendant_thread(starting_depth, statuses) - - # The thread is broken, assume it's a reply to the root status - starting_depth = 0 - - # ... unless we can find its ancestor in one of the already-processed threads - @descendant_threads.reverse_each do |descendant_thread| - statuses = descendant_thread[:statuses] - - index = statuses.find_index do |thread_status| - thread_status.id == descendant.in_reply_to_id - end - - if index.present? - starting_depth = descendant_thread[:starting_depth] + index + 1 - break - end - end - - statuses = [descendant] - end - end - - @descendant_threads << create_descendant_thread(starting_depth, statuses) - end - - @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT - end -end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb new file mode 100644 index 0000000000..b6050c9138 --- /dev/null +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module WebAppControllerConcern + extend ActiveSupport::Concern + + included do + before_action :set_pack + before_action :redirect_unauthenticated_to_permalinks! + before_action :set_app_body_class + before_action :set_referrer_policy_header + end + + def set_app_body_class + @body_classes = 'app-body' + end + + def set_referrer_policy_header + response.headers['Referrer-Policy'] = 'origin' + end + + def redirect_unauthenticated_to_permalinks! + return if user_signed_in? + + redirect_path = PermalinkRedirector.new(request.path).redirect_path + + redirect_to(redirect_path) if redirect_path.present? + end + + def set_pack + use_pack 'home' + end +end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb deleted file mode 100644 index 2263f286b3..0000000000 --- a/app/controllers/directories_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class DirectoriesController < ApplicationController - layout 'public' - - before_action :authenticate_user!, if: :whitelist_mode? - before_action :require_enabled! - before_action :set_instance_presenter - before_action :set_accounts - before_action :set_pack - - skip_before_action :require_functional!, unless: :whitelist_mode? - - def index - render :index - end - - private - - def set_pack - use_pack 'share' - end - - def require_enabled! - return not_found unless Setting.profile_directory - end - - def set_accounts - @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query| - query.merge!(Account.not_excluded_by_account(current_account)) if current_account - end - end - - def set_instance_presenter - @instance_presenter = InstancePresenter.new - end -end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 0d9e624ef8..35ce31f806 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -3,6 +3,7 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification + include WebAppControllerConcern before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers @@ -13,12 +14,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - use_pack 'public' expires_in 0, public: true unless user_signed_in? - - next if @account.hide_collections? - - follows end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 68e1a79be8..f84dca1e5a 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -3,6 +3,7 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification + include WebAppControllerConcern before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers @@ -13,12 +14,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - use_pack 'public' expires_in 0, public: true unless user_signed_in? - - next if @account.hide_collections? - - follows end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 61b1690fab..d8ee82a7a2 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,47 +1,16 @@ # frozen_string_literal: true class HomeController < ApplicationController - before_action :redirect_unauthenticated_to_permalinks! + include WebAppControllerConcern - before_action :set_pack - before_action :set_referrer_policy_header before_action :set_instance_presenter def index - @body_classes = 'app-body' + expires_in 0, public: true unless user_signed_in? end private - def redirect_unauthenticated_to_permalinks! - return if user_signed_in? - - redirect_path = PermalinkRedirector.new(request.path).redirect_path - redirect_path ||= default_redirect_path - - redirect_to(redirect_path) if redirect_path.present? - end - - def set_pack - use_pack 'home' - end - - def default_redirect_path - if whitelist_mode? - new_user_session_path - elsif request.path.start_with?('/web') - nil - elsif single_user_mode? - short_account_path(Account.local.without_suspended.where('id > 0').first) - else - about_path - end - end - - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'origin' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 137346ed09..279b680166 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -35,6 +35,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController end def set_cache_headers - response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' + response.headers['Cache-Control'] = 'private, no-store' end end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index 126bb01285..2c98bf3bf4 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -1,28 +1,19 @@ # frozen_string_literal: true class PrivacyController < ApplicationController - layout 'public' + include WebAppControllerConcern - before_action :set_pack + skip_before_action :require_functional! before_action :set_instance_presenter - before_action :set_expires_in - - skip_before_action :require_functional! - def show; end + def show + expires_in 0, public: true if current_account.nil? + end private - def set_pack - use_pack 'public' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end - - def set_expires_in - expires_in 0, public: true - end end diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb deleted file mode 100644 index eb5bb191b9..0000000000 --- a/app/controllers/public_timelines_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class PublicTimelinesController < ApplicationController - before_action :set_pack - layout 'public' - - before_action :authenticate_user!, if: :whitelist_mode? - before_action :require_enabled! - before_action :set_body_classes - before_action :set_instance_presenter - - def show; end - - private - - def require_enabled! - not_found unless Setting.timeline_preview - end - - def set_body_classes - @body_classes = 'with-modals' - end - - def set_instance_presenter - @instance_presenter = InstancePresenter.new - end - - def set_pack - use_pack 'about' - end -end diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb deleted file mode 100644 index 93a0a74764..0000000000 --- a/app/controllers/remote_follow_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class RemoteFollowController < ApplicationController - include AccountOwnedConcern - - layout 'modal' - - before_action :set_pack - before_action :set_body_classes - - skip_before_action :require_functional! - - def new - @remote_follow = RemoteFollow.new(session_params) - end - - def create - @remote_follow = RemoteFollow.new(resource_params) - - if @remote_follow.valid? - session[:remote_follow] = @remote_follow.acct - redirect_to @remote_follow.subscribe_address_for(@account) - else - render :new - end - end - - private - - def resource_params - params.require(:remote_follow).permit(:acct) - end - - def session_params - { acct: session[:remote_follow] || current_account&.username } - end - - def set_pack - use_pack 'modal' - end - - def set_body_classes - @body_classes = 'modal-layout' - @hide_header = true - end -end diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb deleted file mode 100644 index a277bfa103..0000000000 --- a/app/controllers/remote_interaction_controller.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -class RemoteInteractionController < ApplicationController - include Authorization - - layout 'modal' - - before_action :authenticate_user!, if: :whitelist_mode? - before_action :set_interaction_type - before_action :set_status - before_action :set_body_classes - before_action :set_pack - - skip_before_action :require_functional!, unless: :whitelist_mode? - - def new - @remote_follow = RemoteFollow.new(session_params) - end - - def create - @remote_follow = RemoteFollow.new(resource_params) - - if @remote_follow.valid? - session[:remote_follow] = @remote_follow.acct - redirect_to @remote_follow.interact_address_for(@status) - else - render :new - end - end - - private - - def resource_params - params.require(:remote_follow).permit(:acct) - end - - def session_params - { acct: session[:remote_follow] || current_account&.username } - end - - def set_status - @status = Status.find(params[:id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def set_body_classes - @body_classes = 'modal-layout' - @hide_header = true - end - - def set_pack - use_pack 'modal' - end - - def set_interaction_type - @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply' - end -end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index dee3922d80..bf17b918cc 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' + response.headers['Cache-Control'] = 'private, no-store' end def require_not_suspended! diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index e0dd5edcb1..bb096567a9 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -4,7 +4,6 @@ class Settings::DeletesController < Settings::BaseController skip_before_action :require_functional! before_action :require_not_suspended! - before_action :check_enabled_deletion def show @confirmation = Form::DeleteConfirmation.new @@ -21,10 +20,6 @@ class Settings::DeletesController < Settings::BaseController private - def check_enabled_deletion - redirect_to root_path unless Setting.open_deletion - end - def resource_params params.require(:form_delete_confirmation).permit(:password, :username) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index aadff7c835..c384402650 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -10,9 +10,9 @@ class Settings::FeaturedTagsController < Settings::BaseController end def create - @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], force: false) - if @featured_tag.save + if @featured_tag.valid? redirect_to settings_featured_tags_path else set_featured_tags @@ -23,7 +23,7 @@ class Settings::FeaturedTagsController < Settings::BaseController end def destroy - @featured_tag.destroy! + RemoveFeaturedTagService.new.call(current_account, @featured_tag) redirect_to settings_featured_tags_path end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 55cc3790f3..1a835c7268 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,21 +1,18 @@ # frozen_string_literal: true class StatusesController < ApplicationController - include StatusControllerConcern + include WebAppControllerConcern include SignatureAuthentication include Authorization include AccountOwnedConcern - layout 'public' - before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers before_action :redirect_to_original, only: :show - before_action :set_referrer_policy_header, only: :show before_action :set_cache_headers - before_action :set_body_classes + before_action :set_body_classes, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -27,11 +24,7 @@ class StatusesController < ApplicationController def show respond_to do |format| format.html do - use_pack 'public' - expires_in 10.seconds, public: true if current_account.nil? - set_ancestors - set_descendants end format.json do @@ -80,8 +73,4 @@ class StatusesController < ApplicationController def redirect_to_original redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end - - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? - end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 315eabb3d9..f0a0993506 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -2,18 +2,16 @@ class TagsController < ApplicationController include SignatureVerification + include WebAppControllerConcern PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 - layout 'public' - before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local before_action :set_tag before_action :set_statuses - before_action :set_body_classes before_action :set_instance_presenter skip_before_action :require_functional!, unless: :whitelist_mode? @@ -21,8 +19,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - use_pack 'about' - expires_in 0, public: true + expires_in 0, public: true unless user_signed_in? end format.rss do @@ -55,10 +52,6 @@ class TagsController < ApplicationController end end - def set_body_classes - @body_classes = 'with-modals' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 2a17b69e35..e15aee6df1 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -20,54 +20,10 @@ module AccountsHelper end def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([logo_as_symbol, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([logo_as_symbol, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([logo_as_symbol, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([logo_as_symbol, t('accounts.follow')]) - end - end - end - - def minimal_account_action_button(account) - if user_signed_in? - return if account.id == current_user.account_id - - if current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do - fa_icon('user-times fw') - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - end + return if account.memorial? || account.moved? - def account_badge(account) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif account.group? - content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') - elsif account.user_role&.highlighted? - content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles') + link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do + safe_join([logo_as_symbol, t('accounts.follow')]) end end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index fd1977ac53..215ecea0d7 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -4,15 +4,19 @@ module Admin::ActionLogsHelper def log_target(log) case log.target_type when 'Account' - link_to log.human_identifier, admin_account_path(log.target_id) + link_to (log.human_identifier.presence || I18n.t('admin.action_logs.deleted_account')), admin_account_path(log.target_id) when 'User' - link_to log.human_identifier, admin_account_path(log.route_param) + if log.route_param.present? + link_to log.human_identifier, admin_account_path(log.route_param) + else + I18n.t('admin.action_logs.deleted_account') + end when 'UserRole' link_to log.human_identifier, admin_roles_path(log.target_id) when 'Report' - link_to "##{log.human_identifier}", admin_report_path(log.target_id) + link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' - link_to log.human_identifier, "https://#{log.human_identifier}" + link_to log.human_identifier, "https://#{log.human_identifier.presence}" when 'Status' link_to log.human_identifier, log.permalink when 'AccountWarning' @@ -22,9 +26,13 @@ module Admin::ActionLogsHelper when 'IpBlock', 'Instance', 'CustomEmoji' log.human_identifier when 'CanonicalEmailBlock' - content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier) + content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier) when 'Appeal' - link_to log.human_identifier, disputes_strike_path(log.route_param) + if log.route_param.present? + link_to log.human_identifier, disputes_strike_path(log.route_param.presence) + else + I18n.t('admin.action_logs.deleted_account') + end end end end diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index f99a2b8c8a..552a3ee5a8 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -1,14 +1,6 @@ # frozen_string_literal: true module Admin::SettingsHelper - def site_upload_delete_hint(hint, var) - upload = SiteUpload.find_by(var: var.to_s) - return hint unless upload - - link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end - def captcha_available? ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 62739b964b..af453825b3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -87,10 +87,6 @@ module ApplicationHelper link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post end - def open_deletion? - Setting.open_deletion - end - def locale_direction if RTL_LOCALES.include?(I18n.locale) 'rtl' @@ -199,10 +195,7 @@ module ApplicationHelper def render_initial_state state_params = { - settings: { - known_fediverse: Setting.show_known_fediverse_at_about_page, - }, - + settings: {}, text: [params[:title], params[:text], params[:url]].compact.join(' '), } @@ -211,7 +204,7 @@ module ApplicationHelper permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] - if user_signed_in? + if user_signed_in? && current_user.functional? state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) state_params[:current_account] = current_account @@ -219,6 +212,15 @@ module ApplicationHelper state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) end + if user_signed_in? && !current_user.functional? + state_params[:disabled_account] = current_account + state_params[:moved_to_account] = current_account.moved_to_account + end + + if single_user_mode? + state_params[:owner] = Account.local.without_suspended.where('id > 0').first + end + json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json # rubocop:disable Rails/OutputSafety content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 4da68500a1..f41104709e 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -23,7 +23,7 @@ module HomeHelper else link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do content_tag(:div, class: 'account__avatar-wrapper') do - image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar') + image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46) end + content_tag(:span, class: 'display-name') do content_tag(:bdi) do diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 4077e19bdf..fff073cedb 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ModuleLength, Style/WordArray module LanguagesHelper ISO_639_1 = { @@ -97,7 +98,7 @@ module LanguagesHelper lg: ['Ganda', 'Luganda'].freeze, li: ['Limburgish', 'Limburgs'].freeze, ln: ['Lingala', 'Lingála'].freeze, - lo: ['Lao', 'ພາສາ'].freeze, + lo: ['Lao', 'ລາວ'].freeze, lt: ['Lithuanian', 'lietuvių kalba'].freeze, lu: ['Luba-Katanga', 'Tshiluba'].freeze, lv: ['Latvian', 'latviešu valoda'].freeze, @@ -189,8 +190,14 @@ module LanguagesHelper ISO_639_3 = { ast: ['Asturian', 'Asturianu'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, + jbo: ['Lojban', 'la .lojban.'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze, kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze, + ldn: ['Láadan', 'Láadan'].freeze, + lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze, + sco: ['Scots', 'Scots'].freeze, + tok: ['Toki Pona', 'toki pona'].freeze, + zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, }.freeze @@ -259,3 +266,5 @@ module LanguagesHelper locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) end end + +# rubocop:enable Metrics/ModuleLength, Style/WordArray diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index c84f566a3f..3175ff5607 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -4,6 +4,24 @@ import 'packs/public-path'; import { delegate } from '@rails/ujs'; import ready from '../mastodon/ready'; +const setAnnouncementEndsAttributes = (target) => { + const valid = target?.value && target?.validity?.valid; + const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at'); + if (valid) { + element.classList.remove('optional'); + element.required = true; + element.min = target.value; + } else { + element.classList.add('optional'); + element.removeAttribute('required'); + element.removeAttribute('min'); + } +}; + +delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => { + setAnnouncementEndsAttributes(target); +}); + const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; const showSelectAll = () => { @@ -143,6 +161,20 @@ const onChangeRegistrationMode = (target) => { }); }; +const convertUTCDateTimeToLocal = (value) => { + const date = new Date(value + 'Z'); + const twoChars = (x) => (x.toString().padStart(2, '0')); + return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`; +}; + +const convertLocalDatetimeToUTC = (value) => { + const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/; + const match = re.exec(value); + const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]); + const fullISO8601 = date.toISOString(); + return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6); +}; + delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target)); ready(() => { @@ -170,4 +202,26 @@ ready(() => { e.target.href = url; } }); + + [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => { + if (element.value) { + element.value = convertUTCDateTimeToLocal(element.value); + } + if (element.placeholder) { + element.placeholder = convertUTCDateTimeToLocal(element.placeholder); + } + }); + + delegate(document, 'form', 'submit', ({ target }) => { + [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => { + if (element.value && element.validity.valid) { + element.value = convertLocalDatetimeToUTC(element.value); + } + }); + }); + + const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at'); + if (announcementStartsAt) { + setAnnouncementEndsAttributes(announcementStartsAt); + } }); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index b67fb13e54..5c7a51f447 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -6,28 +6,6 @@ import ready from '../mastodon/ready'; const { delegate } = require('@rails/ujs'); const { length } = require('stringz'); -delegate(document, '.webapp-btn', 'click', ({ target, button }) => { - if (button !== 0) { - return true; - } - window.location.href = target.href; - return false; -}); - -delegate(document, '.modal-button', 'click', e => { - e.preventDefault(); - - let href; - - if (e.target.nodeName !== 'A') { - href = e.target.parentNode.href; - } else { - href = e.target.href; - } - - window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); -}); - const getProfileAvatarAnimationHandler = (swapTo) => { //animate avatar gifs on the profile page when moused over return ({ target }) => { diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index defcba9007..0372ea3480 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,19 +1,21 @@ -import api from '../api'; -import { CancelToken, isCancel } from 'axios'; +import axios from 'axios'; import { throttle } from 'lodash'; +import { defineMessages } from 'react-intl'; +import api from 'flavours/glitch/api'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; -import { useEmoji } from './emojis'; -import { tagHistory } from '../settings'; +import { tagHistory } from 'flavours/glitch/settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; -import { importFetchedAccounts } from './importer'; -import { updateTimeline } from './timelines'; -import { showAlertForError } from './alerts'; -import { showAlert } from './alerts'; +import { showAlert, showAlertForError } from './alerts'; +import { useEmoji } from './emojis'; +import { importFetchedAccounts, importFetchedStatus } from './importer'; import { openModal } from './modal'; -import { defineMessages } from 'react-intl'; +import { updateTimeline } from './timelines'; -let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; +/** @type {AbortController | undefined} */ +let fetchComposeSuggestionsAccountsController; +/** @type {AbortController | undefined} */ +let fetchComposeSuggestionsTagsController; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND'; @@ -25,11 +27,13 @@ export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; -export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; -export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; -export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; -export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; -export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; + +export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; +export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; +export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; +export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; +export const COMPOSE_UPLOAD_PROCESSING = 'COMPOSE_UPLOAD_PROCESSING'; +export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST'; export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS'; @@ -83,10 +87,8 @@ const messages = defineMessages({ uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, }); -const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); - export const ensureComposeIsVisible = (getState, routerHistory) => { - if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { + if (!getState().getIn(['compose', 'mounted'])) { routerHistory.push('/publish'); } }; @@ -221,6 +223,10 @@ export function submitCompose(routerHistory) { } }; + if (statusId) { + dispatch(importFetchedStatus({ ...response.data })); + } + if (statusId === null) { insertIfOnline('home'); } @@ -307,13 +313,16 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, f)); } else if (status === 202) { + dispatch(uploadComposeProcessing()); + let tryCount = 1; + const poll = () => { api(getState).get(`/api/v1/media/${data.id}`).then(response => { if (response.status === 200) { dispatch(uploadComposeSuccess(response.data, f)); } else if (response.status === 206) { - let retryAfter = (Math.log2(tryCount) || 1) * 1000; + const retryAfter = (Math.log2(tryCount) || 1) * 1000; tryCount += 1; setTimeout(() => poll(), retryAfter); } @@ -328,6 +337,10 @@ export function uploadCompose(files) { }; }; +export const uploadComposeProcessing = () => ({ + type: COMPOSE_UPLOAD_PROCESSING, +}); + export const uploadThumbnail = (id, file) => (dispatch, getState) => { dispatch(uploadThumbnailRequest()); @@ -472,8 +485,8 @@ export function undoUploadCompose(media_id) { }; export function clearComposeSuggestions() { - if (cancelFetchComposeSuggestionsAccounts) { - cancelFetchComposeSuggestionsAccounts(); + if (fetchComposeSuggestionsAccountsController) { + fetchComposeSuggestionsAccountsController.abort(); } return { type: COMPOSE_SUGGESTIONS_CLEAR, @@ -481,14 +494,14 @@ export function clearComposeSuggestions() { }; const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { - if (cancelFetchComposeSuggestionsAccounts) { - cancelFetchComposeSuggestionsAccounts(); + if (fetchComposeSuggestionsAccountsController) { + fetchComposeSuggestionsAccountsController.abort(); } + fetchComposeSuggestionsAccountsController = new AbortController(); + api(getState).get('/api/v1/accounts/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestionsAccounts = cancel; - }), + signal: fetchComposeSuggestionsAccountsController.signal, params: { q: token.slice(1), @@ -499,9 +512,11 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => dispatch(importFetchedAccounts(response.data)); dispatch(readyComposeSuggestionsAccounts(token, response.data)); }).catch(error => { - if (!isCancel(error)) { + if (!axios.isCancel(error)) { dispatch(showAlertForError(error)); } + }).finally(() => { + fetchComposeSuggestionsAccountsController = undefined; }); }, 200, { leading: true, trailing: true }); @@ -511,16 +526,16 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { }; const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { - if (cancelFetchComposeSuggestionsTags) { - cancelFetchComposeSuggestionsTags(); + if (fetchComposeSuggestionsTagsController) { + fetchComposeSuggestionsTagsController.abort(); } dispatch(updateSuggestionTags(token)); + fetchComposeSuggestionsTagsController = new AbortController(); + api(getState).get('/api/v2/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestionsTags = cancel; - }), + signal: fetchComposeSuggestionsTagsController.signal, params: { type: 'hashtags', @@ -531,9 +546,11 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { }).then(({ data }) => { dispatch(readyComposeSuggestionsTags(token, data.hashtags)); }).catch(error => { - if (!isCancel(error)) { + if (!axios.isCancel(error)) { dispatch(showAlertForError(error)); } + }).finally(() => { + fetchComposeSuggestionsTagsController = undefined; }); }, 200, { leading: true, trailing: true }); diff --git a/app/javascript/flavours/glitch/actions/featured_tags.js b/app/javascript/flavours/glitch/actions/featured_tags.js new file mode 100644 index 0000000000..18bb615394 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/featured_tags.js @@ -0,0 +1,34 @@ +import api from '../api'; + +export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST'; +export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS'; +export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL'; + +export const fetchFeaturedTags = (id) => (dispatch, getState) => { + if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) { + return; + } + + dispatch(fetchFeaturedTagsRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/featured_tags`) + .then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data))) + .catch(err => dispatch(fetchFeaturedTagsFail(id, err))); +}; + +export const fetchFeaturedTagsRequest = (id) => ({ + type: FEATURED_TAGS_FETCH_REQUEST, + id, +}); + +export const fetchFeaturedTagsSuccess = (id, tags) => ({ + type: FEATURED_TAGS_FETCH_SUCCESS, + id, + tags, +}); + +export const fetchFeaturedTagsFail = (id, error) => ({ + type: FEATURED_TAGS_FETCH_FAIL, + id, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js index 76326802ea..e9c609fc87 100644 --- a/app/javascript/flavours/glitch/actions/filters.js +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -43,7 +43,7 @@ export const fetchFilters = () => (dispatch, getState) => { export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => { dispatch(createFilterStatusRequest()); - api(getState).post(`/api/v1/filters/${params.filter_id}/statuses`, params).then(response => { + api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => { dispatch(createFilterStatusSuccess(response.data)); if (onSuccess) onSuccess(); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 7767826933..f21c0058ba 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -29,7 +29,8 @@ export function clearSearch() { export function submitSearch() { return (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); + const value = getState().getIn(['search', 'value']); + const signedIn = !!getState().getIn(['meta', 'me']); if (value.length === 0) { dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '')); @@ -41,7 +42,7 @@ export function submitSearch() { api(getState).get('/api/v2/search', { params: { q: value, - resolve: true, + resolve: signedIn, limit: 10, }, }).then(response => { diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index af8fef780f..31d4aea100 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; +export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; +export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; + +export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST'; +export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; + export const fetchServer = () => (dispatch, getState) => { dispatch(fetchServerRequest()); @@ -28,3 +36,56 @@ const fetchServerFail = error => ({ type: SERVER_FETCH_FAIL, error, }); + +export const fetchExtendedDescription = () => (dispatch, getState) => { + dispatch(fetchExtendedDescriptionRequest()); + + api(getState) + .get('/api/v1/instance/extended_description') + .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data))) + .catch(err => dispatch(fetchExtendedDescriptionFail(err))); +}; + +const fetchExtendedDescriptionRequest = () => ({ + type: EXTENDED_DESCRIPTION_REQUEST, +}); + +const fetchExtendedDescriptionSuccess = description => ({ + type: EXTENDED_DESCRIPTION_SUCCESS, + description, +}); + +const fetchExtendedDescriptionFail = error => ({ + type: EXTENDED_DESCRIPTION_FAIL, + error, +}); + +export const fetchDomainBlocks = () => (dispatch, getState) => { + dispatch(fetchDomainBlocksRequest()); + + api(getState) + .get('/api/v1/instance/domain_blocks') + .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data))) + .catch(err => { + if (err.response.status === 404) { + dispatch(fetchDomainBlocksSuccess(false, [])); + } else { + dispatch(fetchDomainBlocksFail(err)); + } + }); +}; + +const fetchDomainBlocksRequest = () => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, +}); + +const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, + isAvailable, + blocks, +}); + +const fetchDomainBlocksFail = error => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index ef1e4dbbb9..a1c4dd43ad 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -156,8 +156,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); -export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); +export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId }); +export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 645ef65006..6bbddbef66 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -1,20 +1,31 @@ +// @ts-check + import axios from 'axios'; import LinkHeader from 'http-link-header'; import ready from './ready'; +/** + * @param {import('axios').AxiosResponse} response + * @returns {LinkHeader} + */ export const getLinks = response => { const value = response.headers.link; if (!value) { - return { refs: [] }; + return new LinkHeader(); } return LinkHeader.parse(value); }; +/** @type {import('axios').RawAxiosRequestHeaders} */ const csrfHeader = {}; +/** + * @returns {void} + */ const setCSRFHeader = () => { + /** @type {HTMLMetaElement | null} */ const csrfToken = document.querySelector('meta[name=csrf-token]'); if (csrfToken) { @@ -24,6 +35,10 @@ const setCSRFHeader = () => { ready(setCSRFHeader); +/** + * @param {() => import('immutable').Map} getState + * @returns {import('axios').RawAxiosRequestHeaders} + */ const authorizationHeaderFromState = getState => { const accessToken = getState && getState().getIn(['meta', 'access_token'], ''); @@ -36,17 +51,25 @@ const authorizationHeaderFromState = getState => { }; }; -export default getState => axios.create({ - headers: { - ...csrfHeader, - ...authorizationHeaderFromState(getState), - }, - - transformResponse: [function (data) { - try { - return JSON.parse(data); - } catch(Exception) { - return data; - } - }], -}); +/** + * @param {() => import('immutable').Map} getState + * @returns {import('axios').AxiosInstance} + */ +export default function api(getState) { + return axios.create({ + headers: { + ...csrfHeader, + ...authorizationHeaderFromState(getState), + }, + + transformResponse: [ + function (data) { + try { + return JSON.parse(data); + } catch { + return data; + } + }, + ], + }); +} diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 8bfc8e9dce..8e810ce5fb 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -27,6 +27,7 @@ export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { + size: PropTypes.number, account: ImmutablePropTypes.map, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, @@ -41,6 +42,10 @@ class Account extends ImmutablePureComponent { onActionClick: PropTypes.func, }; + static defaultProps = { + size: 36, + }; + handleFollow = () => { this.props.onFollow(this.props.account); } @@ -75,6 +80,7 @@ class Account extends ImmutablePureComponent { actionIcon, actionTitle, defaultAction, + size, } = this.props; if (!account) { @@ -163,7 +169,7 @@ class Account extends ImmutablePureComponent {
-
+
{mute_expires_at}
diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js index 4619aad587..9431c96f7b 100644 --- a/app/javascript/flavours/glitch/components/animated_number.js +++ b/app/javascript/flavours/glitch/components/animated_number.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedNumber } from 'react-intl'; +import ShortNumber from 'mastodon/components/short_number'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; import { reduceMotion } from 'flavours/glitch/initial_state'; @@ -51,7 +51,7 @@ export default class AnimatedNumber extends React.PureComponent { const { direction } = this.state; if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : ; + return obfuscate ? obfuscatedCount(value) : ; } const styles = [{ @@ -65,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } ))} )} diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js index ce91d401d1..38fd99af59 100644 --- a/app/javascript/flavours/glitch/components/avatar.js +++ b/app/javascript/flavours/glitch/components/avatar.js @@ -70,6 +70,8 @@ export default class Avatar extends React.PureComponent { onMouseLeave={this.handleMouseLeave} style={style} data-avatar-of={account && `@${account.get('acct')}`} + role='img' + aria-label={account?.get('acct')} /> ); } diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index 024068c584..0f89b3a97b 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -63,7 +63,7 @@ class ColumnHeader extends React.PureComponent { } handleTitleClick = () => { - this.props.onClick(); + this.props.onClick?.(); } handleMoveLeft = () => { @@ -157,7 +157,6 @@ class ColumnHeader extends React.PureComponent { 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} > diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.js b/app/javascript/flavours/glitch/components/dismissable_banner.js new file mode 100644 index 0000000000..ff52a619d2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/dismissable_banner.js @@ -0,0 +1,51 @@ +import React from 'react'; +import IconButton from './icon_button'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import { bannerSettings } from 'flavours/glitch/settings'; + +const messages = defineMessages({ + dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, +}); + +export default @injectIntl +class DismissableBanner extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node, + intl: PropTypes.object.isRequired, + }; + + state = { + visible: !bannerSettings.get(this.props.id), + }; + + handleDismiss = () => { + const { id } = this.props; + this.setState({ visible: false }, () => bannerSettings.set(id, true)); + } + + render () { + const { visible } = this.state; + + if (!visible) { + return null; + } + + const { children, intl } = this.props; + + return ( +
+
+ {children} +
+ +
+ +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.js index fd3659de7a..e0ca3e2b05 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.js @@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl'; import { source_url } from 'flavours/glitch/initial_state'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import StackTrace from 'stacktrace-js'; +import { Helmet } from 'react-helmet'; export default class ErrorBoundary extends React.PureComponent { @@ -122,6 +123,10 @@ export default class ErrorBoundary extends React.PureComponent { )}
+ + + +
); } diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 2bb7f02f76..422b9a8fa1 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -1,7 +1,7 @@ // @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; @@ -9,10 +9,6 @@ import ShortNumber from 'flavours/glitch/components/short_number'; import Skeleton from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; -const messages = defineMessages({ - totalVolume: { id: 'hashtag.total_volume', defaultMessage: 'Total volume in the last {days, plural, one {day} other {{days} days}}' }, -}); - class SilentErrorBoundary extends React.Component { static propTypes = { @@ -60,7 +56,6 @@ export const ImmutableHashtag = ({ hashtag }) => ( href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`} people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1} - uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1} history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()} /> ); @@ -69,39 +64,52 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; -const Hashtag = injectIntl(({ name, href, to, people, uses, history, className, intl }) => ( +const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
{name ? #{name} : } - {typeof people !== 'undefined' ? : } + {description ? ( + {description} + ) : ( + typeof people !== 'undefined' ? : + )}
- - {typeof uses !== 'undefined' ? : } - * - - -
- - 0)}> - - - -
+ {typeof uses !== 'undefined' && ( +
+ +
+ )} + + {withGraph && ( +
+ + 0)}> + + + +
+ )}
-)); +); Hashtag.propTypes = { name: PropTypes.string, href: PropTypes.string, to: PropTypes.string, people: PropTypes.number, + description: PropTypes.node, uses: PropTypes.number, history: PropTypes.arrayOf(PropTypes.number), className: PropTypes.string, + withGraph: PropTypes.bool, +}; + +Hashtag.defaultProps = { + withGraph: true, }; export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 42f5d4bc35..41a95e92f7 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -18,7 +18,6 @@ export default class IconButton extends React.PureComponent { onKeyPress: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, - pressed: PropTypes.bool, expanded: PropTypes.bool, style: PropTypes.object, activeStyle: PropTypes.object, @@ -111,7 +110,6 @@ export default class IconButton extends React.PureComponent { icon, inverted, overlay, - pressed, tabIndex, title, counter, @@ -156,7 +154,6 @@ export default class IconButton extends React.PureComponent { return (

diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index bc03112936..6c92376d81 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -1,22 +1,25 @@ -import React from 'react'; -import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; +import configureStore from 'flavours/glitch/store/configureStore'; import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; -import { connectUserStream } from 'flavours/glitch/actions/streaming'; import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'locales'; -import initialState from 'flavours/glitch/initial_state'; +import { connectUserStream } from 'flavours/glitch/actions/streaming'; import ErrorBoundary from 'flavours/glitch/components/error_boundary'; +import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; +import { getLocale } from 'locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); +const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; + export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); @@ -30,6 +33,7 @@ store.dispatch(fetchCustomEmojis()); const createIdentityContext = state => ({ signedIn: !!state.meta.me, accountId: state.meta.me, + disabledAccountId: state.meta.disabled_account_id, accessToken: state.meta.access_token, permissions: state.role ? state.role.permissions : 0, }); @@ -44,6 +48,7 @@ export default class Mastodon extends React.PureComponent { identity: PropTypes.shape({ signedIn: PropTypes.bool.isRequired, accountId: PropTypes.string, + disabledAccountId: PropTypes.string, accessToken: PropTypes.string, }).isRequired, }; @@ -78,15 +83,17 @@ export default class Mastodon extends React.PureComponent { return ( - + - + + + - + ); } diff --git a/app/javascript/flavours/glitch/containers/timeline_container.js b/app/javascript/flavours/glitch/containers/timeline_container.js deleted file mode 100644 index 838bcd390d..0000000000 --- a/app/javascript/flavours/glitch/containers/timeline_container.js +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom'; -import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; -import { hydrateStore } from 'flavours/glitch/actions/store'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import PublicTimeline from 'flavours/glitch/features/standalone/public_timeline'; -import HashtagTimeline from 'flavours/glitch/features/standalone/hashtag_timeline'; -import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container'; -import initialState from 'flavours/glitch/initial_state'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -const store = configureStore(); - -if (initialState) { - store.dispatch(hydrateStore(initialState)); -} - -export default class TimelineContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - hashtag: PropTypes.string, - local: PropTypes.bool, - }; - - static defaultProps = { - local: !initialState.settings.known_fediverse, - }; - - render () { - const { locale, hashtag, local } = this.props; - - let timeline; - - if (hashtag) { - timeline = ; - } else { - timeline = ; - } - - return ( - - - - {timeline} - - {ReactDOM.createPortal( - , - document.getElementById('modal-container'), - )} - - - - ); - } - -} diff --git a/app/javascript/flavours/glitch/extra_polyfills.js b/app/javascript/flavours/glitch/extra_polyfills.js index 3acc55abd6..0d45c23b04 100644 --- a/app/javascript/flavours/glitch/extra_polyfills.js +++ b/app/javascript/flavours/glitch/extra_polyfills.js @@ -1,3 +1,4 @@ +import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; import 'intersection-observer'; import 'requestidlecallback'; import objectFitImages from 'object-fit-images'; diff --git a/app/javascript/flavours/glitch/features/about/index.js b/app/javascript/flavours/glitch/features/about/index.js new file mode 100644 index 0000000000..57f295ea97 --- /dev/null +++ b/app/javascript/flavours/glitch/features/about/index.js @@ -0,0 +1,220 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Column from 'flavours/glitch/components/column'; +import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { Helmet } from 'react-helmet'; +import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; +import Account from 'flavours/glitch/containers/account_container'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import Image from 'flavours/glitch/components/image'; + +const messages = defineMessages({ + title: { id: 'column.about', defaultMessage: 'About' }, + rules: { id: 'about.rules', defaultMessage: 'Server rules' }, + blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' }, + silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' }, + silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' }, + suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' }, + suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' }, +}); + +const severityMessages = { + silence: { + title: messages.silenced, + explanation: messages.silencedExplanation, + }, + + suspend: { + title: messages.suspended, + explanation: messages.suspendedExplanation, + }, +}; + +const mapStateToProps = state => ({ + server: state.getIn(['server', 'server']), + extendedDescription: state.getIn(['server', 'extendedDescription']), + domainBlocks: state.getIn(['server', 'domainBlocks']), +}); + +class Section extends React.PureComponent { + + static propTypes = { + title: PropTypes.string, + children: PropTypes.node, + open: PropTypes.bool, + onOpen: PropTypes.func, + }; + + state = { + collapsed: !this.props.open, + }; + + handleClick = () => { + const { onOpen } = this.props; + const { collapsed } = this.state; + + this.setState({ collapsed: !collapsed }, () => onOpen && onOpen()); + } + + render () { + const { title, children } = this.props; + const { collapsed } = this.state; + + return ( +
+
+ {title} +
+ + {!collapsed && ( +
{children}
+ )} +
+ ); + } + +} + +export default @connect(mapStateToProps) +@injectIntl +class About extends React.PureComponent { + + static propTypes = { + server: ImmutablePropTypes.map, + extendedDescription: ImmutablePropTypes.map, + domainBlocks: ImmutablePropTypes.contains({ + isLoading: PropTypes.bool, + isAvailable: PropTypes.bool, + items: ImmutablePropTypes.list, + }), + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + dispatch(fetchExtendedDescription()); + } + + handleDomainBlocksOpen = () => { + const { dispatch } = this.props; + dispatch(fetchDomainBlocks()); + } + + render () { + const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; + const isLoading = server.get('isLoading'); + + return ( + +
+
+ `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> +

{isLoading ? : server.get('domain')}

+

Mastodon }} />

+
+ +
+
+

+ + +
+ +
+ +
+

+ + {isLoading ? : {server.getIn(['contact', 'email'])}} +
+
+ +
+ {extendedDescription.get('isLoading') ? ( + <> + +
+ +
+ +
+ + + ) : (extendedDescription.get('content')?.length > 0 ? ( +
+ ) : ( +

+ ))} +
+ +
+ {!isLoading && (server.get('rules').isEmpty() ? ( +

+ ) : ( +
    + {server.get('rules').map(rule => ( +
  1. + {rule.get('text')} +
  2. + ))} +
+ ))} +
+ +
+ {domainBlocks.get('isLoading') ? ( + <> + +
+ + + ) : (domainBlocks.get('isAvailable') ? ( + <> +

+ +
+ {domainBlocks.get('items').map(block => ( +
+
+
{block.get('domain')}
+ {intl.formatMessage(severityMessages[block.get('severity')].title)} +
+ +

{(block.get('comment') || '').length > 0 ? block.get('comment') : }

+
+ ))} +
+ + ) : ( +

+ ))} +
+ + + +
+

+

+
+
+ + + {intl.formatMessage(messages.title)} + + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.js b/app/javascript/flavours/glitch/features/account/components/featured_tags.js new file mode 100644 index 0000000000..d646b08b29 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.js @@ -0,0 +1,53 @@ +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 Hashtag from 'flavours/glitch/components/hashtag'; + +const messages = defineMessages({ + lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, + empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, +}); + +export default @injectIntl +class FeaturedTags extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + account: ImmutablePropTypes.map, + featuredTags: ImmutablePropTypes.list, + tagged: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + render () { + const { account, featuredTags, intl } = this.props; + + if (!account || account.get('suspended') || featuredTags.isEmpty()) { + return null; + } + + return ( +
+

}} />

+ + {featuredTags.take(3).map(featuredTag => ( + 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)} + /> + ))} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 0f0e40268f..47c074ec3d 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, title, domain } from 'flavours/glitch/initial_state'; +import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; @@ -19,7 +19,7 @@ import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -53,6 +53,7 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, + openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); const titleFromAccount = account => { @@ -97,6 +98,7 @@ class Header extends ImmutablePureComponent { onEditAccountNote: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, + onOpenAvatar: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -132,6 +134,13 @@ class Header extends ImmutablePureComponent { } } + handleAvatarClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.onOpenAvatar(); + } + } + render () { const { account, hidden, intl, domain } = this.props; const { signedIn } = this.context.identity; @@ -142,7 +151,9 @@ class Header extends ImmutablePureComponent { const accountNote = account.getIn(['relationship', 'note']); - const suspended = account.get('suspended'); + const suspended = account.get('suspended'); + const isRemote = account.get('acct') !== account.get('username'); + const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; let info = []; let actionBtn = ''; @@ -199,6 +210,11 @@ class Header extends ImmutablePureComponent { menu.push(null); } + if (isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); + menu.push(null); + } + if ('share' in navigator && !suspended) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push(null); @@ -253,15 +269,13 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } - if (signedIn && account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; - + if (signedIn && isRemote) { menu.push(null); if (account.getIn(['relationship', 'domain_blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); } } @@ -273,7 +287,9 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isLocal = account.get('acct').indexOf('@') === -1; + const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isIndexable = !account.get('noindex'); let badge; @@ -297,12 +313,10 @@ class Header extends ImmutablePureComponent {
- + -
- {!suspended && (
{!hidden && ( @@ -352,7 +366,8 @@ class Header extends ImmutablePureComponent {
- {titleFromAccount(account)} - {title} + {titleFromAccount(account)} +
); diff --git a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js new file mode 100644 index 0000000000..6f0b069419 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FeaturedTags from '../components/featured_tags'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { List as ImmutableList } from 'immutable'; + +const mapStateToProps = () => { + const getAccount = makeGetAccount(); + + return (state, { accountId }) => ({ + account: getAccount(state, accountId), + featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()), + }); +}; + +export default connect(mapStateToProps)(FeaturedTags); diff --git a/app/javascript/flavours/glitch/features/account/navigation.js b/app/javascript/flavours/glitch/features/account/navigation.js new file mode 100644 index 0000000000..edae38ce5f --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/navigation.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; + +const mapStateToProps = (state, { match: { params: { acct } } }) => { + const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + isLoading: false, + }; +}; + +export default @connect(mapStateToProps) +class AccountNavigation extends React.PureComponent { + + static propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + acct: PropTypes.string, + tagged: PropTypes.string, + }).isRequired, + }).isRequired, + + accountId: PropTypes.string, + isLoading: PropTypes.bool, + }; + + render () { + const { accountId, isLoading, match: { params: { tagged } } } = this.props; + + if (isLoading) { + return null; + } + + return ( + <> +
+ + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 8df1bf4ca5..638224bc02 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -15,9 +15,10 @@ import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import { openModal } from 'flavours/glitch/actions/modal'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; const mapStateToProps = (state, { params: { acct, id } }) => { - const accountId = id || state.getIn(['accounts_map', acct]); + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index eb332e296a..90c4c9d513 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -25,6 +25,7 @@ export default class Header extends ImmutablePureComponent { onAddToList: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, + onOpenAvatar: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent { this.props.onInteractionModal(this.props.account); } + handleOpenAvatar = () => { + this.props.onOpenAvatar(this.props.account); + } + render () { const { account, hidden, hideTabs } = this.props; @@ -130,6 +135,7 @@ export default class Header extends ImmutablePureComponent { onEditAccountNote={this.handleEditAccountNote} onChangeLanguages={this.handleChangeLanguages} onInteractionModal={this.handleInteractionModal} + onOpenAvatar={this.handleOpenAvatar} domain={this.props.domain} hidden={hidden} /> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js index e465c83b47..ca0e60672b 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { revealAccount } from 'flavours/glitch/actions/accounts'; import { FormattedMessage } from 'react-intl'; import Button from 'flavours/glitch/components/button'; +import { domain } from 'flavours/glitch/initial_state'; const mapDispatchToProps = (dispatch, { accountId }) => ({ @@ -26,7 +27,7 @@ class LimitedAccountHint extends React.PureComponent { return (
-

+

); diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index c1577e1700..25bcd01190 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -24,6 +24,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { unfollowModal } from 'flavours/glitch/initial_state'; const messages = defineMessages({ + cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); @@ -43,7 +44,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { dispatch(openModal('CONFIRM', { message: @{account.get('acct')} }} />, @@ -53,6 +54,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } else { dispatch(unfollowAccount(account.get('id'))); } + } else if (account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } else { + dispatch(unfollowAccount(account.get('id'))); + } } else { dispatch(followAccount(account.get('id'))); } @@ -150,6 +161,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, + onOpenAvatar (account) { + dispatch(openModal('IMAGE', { + src: account.get('avatar'), + alt: account.get('acct'), + })); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 68d558e662..b735af0ac5 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -17,19 +17,22 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { fetchFeaturedTags } from '../../actions/featured_tags'; const emptyList = ImmutableList(); -const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => { - const accountId = id || state.getIn(['accounts_map', acct]); +const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => { + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { isLoading: true, + statusIds: emptyList, }; } - const path = withReplies ? `${accountId}:with_replies` : accountId; + const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`; return { accountId, @@ -37,7 +40,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) = remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), suspended: state.getIn(['accounts', accountId, 'suspended'], false), @@ -60,6 +63,7 @@ class AccountTimeline extends ImmutablePureComponent { params: PropTypes.shape({ acct: PropTypes.string, id: PropTypes.string, + tagged: PropTypes.string, }).isRequired, accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, @@ -77,14 +81,16 @@ class AccountTimeline extends ImmutablePureComponent { }; _load () { - const { accountId, withReplies, dispatch } = this.props; + const { accountId, withReplies, params: { tagged }, dispatch } = this.props; dispatch(fetchAccount(accountId)); if (!withReplies) { - dispatch(expandAccountFeaturedTimeline(accountId)); + dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); } - dispatch(expandAccountTimeline(accountId, { withReplies })); + + dispatch(fetchFeaturedTags(accountId)); + dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); } componentDidMount () { @@ -98,12 +104,17 @@ class AccountTimeline extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { params: { acct }, accountId, dispatch } = this.props; + const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props; if (prevProps.accountId !== accountId && accountId) { this._load(); } else if (prevProps.params.acct !== acct) { dispatch(lookupAccount(acct)); + } else if (prevProps.params.tagged !== tagged) { + if (!withReplies) { + dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); + } + dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); } } @@ -126,7 +137,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged })); } setRef = c => { @@ -136,19 +147,17 @@ class AccountTimeline extends ImmutablePureComponent { render () { const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; - if (!isAccount) { + if (isLoading && statusIds.isEmpty()) { return ( - - + ); - } - - if (!statusIds && isLoading) { + } else if (!isLoading && !isAccount) { return ( - + + ); } @@ -174,7 +183,7 @@ class AccountTimeline extends ImmutablePureComponent { } + prepend={} alwaysPrepend append={remoteMessage} scrollKey='account_timeline' diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index ada8fb0681..8978ac5fc1 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -1,15 +1,16 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import Column from 'flavours/glitch/features/ui/components/column'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, @@ -95,6 +96,11 @@ class Bookmarks extends ImmutablePureComponent { emptyMessage={emptyMessage} bindToDocument={!multiColumn} /> + + + {intl.formatMessage(messages.heading)} + + ); } diff --git a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js new file mode 100644 index 0000000000..cb91636cb8 --- /dev/null +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { domain } from 'flavours/glitch/initial_state'; +import { fetchServer } from 'flavours/glitch/actions/server'; + +const mapStateToProps = state => ({ + message: state.getIn(['server', 'server', 'registrations', 'message']), +}); + +export default @connect(mapStateToProps) +class ClosedRegistrationsModal extends ImmutablePureComponent { + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + } + + render () { + let closedRegistrationsMessage; + + if (this.props.message) { + closedRegistrationsMessage = ( +

+ ); + } else { + closedRegistrationsMessage = ( +

+ {domain} }} + /> +

+ ); + } + + return ( +
+
+

+

+ +

+
+ +
+
+

+ {closedRegistrationsMessage} +
+ +
+

+

+ +

+ +
+
+
+ ); + } + +}; diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 5ee46ca3bb..67bf548755 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -10,7 +10,8 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import ColumnSettingsContainer from './containers/column_settings_container'; import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; +import { domain } from 'flavours/glitch/initial_state'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -138,6 +139,10 @@ class CommunityTimeline extends React.PureComponent { + + + + - {intl.formatMessage(messages.title)} - {title} + {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 0be14e495b..516648f4b3 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -305,12 +305,12 @@ class ComposeForm extends ImmutablePureComponent { const countText = this.getFulltextForCharacterCounting(); return ( -
+
-
+
-
+
0)} spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} /> -
+
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 829f6d00f2..6b6d3de948 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -16,7 +16,6 @@ import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; export default class ComposerOptionsDropdown extends React.PureComponent { static propTypes = { - active: PropTypes.bool, disabled: PropTypes.bool, icon: PropTypes.string, items: PropTypes.arrayOf(PropTypes.shape({ @@ -162,7 +161,6 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // Rendering. render () { const { - active, disabled, title, icon, @@ -174,35 +172,34 @@ export default class ComposerOptionsDropdown extends React.PureComponent { closeOnChange, } = this.props; const { open, placement } = this.state; - const computedClass = classNames('composer--options--dropdown', { - active, - open, - top: placement === 'top', - }); - // The result. + const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1)); + return (
- +
+ +
+ {icon && } -
+
{text} {meta}
@@ -218,7 +218,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // size will be used to determine the coordinate of the menu by // react-overlays
{ + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); } componentWillUnmount () { @@ -226,7 +235,7 @@ class LanguageDropdownMenu extends React.PureComponent { // react-overlays
- +
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 32a464011f..c6278f4cb5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -103,7 +103,7 @@ class ToggleOption extends ImmutablePureComponent { -
+
{text} {meta}
@@ -228,7 +228,7 @@ class ComposerOptions extends ImmutablePureComponent { // The result. return ( -
+
!!value)} disabled={disabled || isEditing} icon='ellipsis-h' items={advancedOptions ? [ diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index 50baad0658..9d53b7ee3c 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -48,7 +48,7 @@ class Publisher extends ImmutablePureComponent { const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; const diff = maxChars - length(countText || ''); - const computedClass = classNames('composer--publisher', { + const computedClass = classNames('compose-form__publish', { disabled: disabled, over: diff < 0, }); @@ -72,22 +72,26 @@ class Publisher extends ImmutablePureComponent { return (
{sideArm && !isEditing && sideArm !== 'none' ? ( +
+
+ ) : null} +
); }; diff --git a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js index 37ae9cab9b..7ad9e2b644 100644 --- a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js +++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js @@ -49,10 +49,10 @@ class ReplyIndicator extends ImmutablePureComponent { // The result. return ( -
-
+
+
{attachments.size > 0 && ( diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index a59418e46c..326fe5b707 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -21,6 +21,7 @@ import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, + placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, }); class SearchPopout extends React.PureComponent { @@ -62,6 +63,7 @@ class Search extends React.PureComponent { static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, }; static propTypes = { @@ -137,6 +139,7 @@ class Search extends React.PureComponent { render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; + const { signedIn } = this.context.identity; const hasValue = value.length > 0 || submitted; return ( @@ -147,7 +150,7 @@ class Search extends React.PureComponent { ref={this.setRef} className='search__input' type='text' - placeholder={intl.formatMessage(messages.placeholder)} + placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} value={value || ''} onChange={this.handleChange} onKeyUp={this.handleKeyUp} diff --git a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js index b875fb15e3..25c2443b16 100644 --- a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js +++ b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js @@ -38,7 +38,7 @@ class TextareaIcons extends ImmutablePureComponent { render () { const { advancedOptions, intl } = this.props; return ( -
+
{advancedOptions ? iconMap.map( ([key, icon, message]) => advancedOptions.get(key) ? ( { @@ -32,24 +31,24 @@ export default class Upload extends ImmutablePureComponent { } render () { - const { intl, media, isEditingStatus } = this.props; + const { media } = this.props; const focusX = media.getIn(['meta', 'focus', 'x']); const focusY = media.getIn(['meta', 'focus', 'y']); const x = ((focusX / 2) + .5) * 100; const y = ((focusY / -2) + .5) * 100; return ( -
+
{({ scale }) => ( -
-
+
+
- {!isEditingStatus && ()} + {!!media.get('unattached') && ()}
- {(media.get('description') || '').length === 0 && ( -
+ {(media.get('description') || '').length === 0 && !!media.get('unattached') && ( +
)} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js index 43039c674b..7ebbac9632 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -4,7 +4,6 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; -import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { static propTypes = { @@ -15,11 +14,11 @@ export default class UploadForm extends ImmutablePureComponent { const { mediaIds } = this.props; return ( -
- } /> +
+ {mediaIds.size > 0 && ( -
+
{mediaIds.map(id => ( ))} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js index 8896bbffd6..39ac310532 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js @@ -3,35 +3,44 @@ import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import Icon from 'flavours/glitch/components/icon'; +import { FormattedMessage } from 'react-intl'; export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, - icon: PropTypes.string.isRequired, - message: PropTypes.node.isRequired, + isProcessing: PropTypes.bool, }; render () { - const { active, progress, icon, message } = this.props; + const { active, progress, isProcessing } = this.props; if (!active) { return null; } + let message; + + if (isProcessing) { + message = ; + } else { + message = ; + } + return ( -
- +
+
+ +
-
+
{message} -
+
{({ width }) => - (
) +
}
diff --git a/app/javascript/flavours/glitch/features/compose/components/warning.js b/app/javascript/flavours/glitch/features/compose/components/warning.js index 4009be8c6b..803b7f86ab 100644 --- a/app/javascript/flavours/glitch/features/compose/components/warning.js +++ b/app/javascript/flavours/glitch/features/compose/components/warning.js @@ -15,7 +15,7 @@ export default class Warning extends React.PureComponent { return ( {({ opacity, scaleX, scaleY }) => ( -
+
{message}
)} diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js index d6256fe966..f3ca4ce7bb 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -5,7 +5,6 @@ import { submitCompose } from 'flavours/glitch/actions/compose'; const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), - isEditingStatus: state.getIn(['compose', 'id']) !== null, }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js index 0cfee96daa..b18c76a43f 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js @@ -4,6 +4,7 @@ import UploadProgress from '../components/upload_progress'; const mapStateToProps = state => ({ active: state.getIn(['compose', 'is_uploading']), progress: state.getIn(['compose', 'progress']), + isProcessing: state.getIn(['compose', 'is_processing']), }); export default connect(mapStateToProps)(UploadProgress); diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js index 567bb3711a..8ca3786728 100644 --- a/app/javascript/flavours/glitch/features/compose/index.js +++ b/app/javascript/flavours/glitch/features/compose/index.js @@ -14,6 +14,8 @@ import SearchResultsContainer from './containers/search_results_container'; import { me, mascot } from 'flavours/glitch/initial_state'; import { cycleElefriendCompose } from 'flavours/glitch/actions/compose'; import HeaderContainer from './containers/header_container'; +import Column from 'flavours/glitch/components/column'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, @@ -21,7 +23,7 @@ const messages = defineMessages({ const mapStateToProps = (state, ownProps) => ({ elefriend: state.getIn(['compose', 'elefriend']), - showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage, + showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false, }); const mapDispatchToProps = (dispatch, { intl }) => ({ @@ -44,7 +46,6 @@ class Compose extends React.PureComponent { static propTypes = { multiColumn: PropTypes.bool, showSearch: PropTypes.bool, - isSearchPage: PropTypes.bool, elefriend: PropTypes.number, onClickElefriend: PropTypes.func, onMount: PropTypes.func, @@ -53,19 +54,11 @@ class Compose extends React.PureComponent { }; componentDidMount () { - const { isSearchPage } = this.props; - - if (!isSearchPage) { - this.props.onMount(); - } + this.props.onMount(); } componentWillUnmount () { - const { isSearchPage } = this.props; - - if (!isSearchPage) { - this.props.onUnmount(); - } + this.props.onUnmount(); } render () { @@ -74,37 +67,49 @@ class Compose extends React.PureComponent { intl, multiColumn, onClickElefriend, - isSearchPage, showSearch, } = this.props; const computedClass = classNames('drawer', `mbstobon-${elefriend}`); - return ( -
- {multiColumn && } + if (multiColumn) { + return ( +
+ - {(multiColumn || isSearchPage) && } + {multiColumn && } -
- {!isSearchPage &&
- +
+
+ - + -
- {mascot ? :
-
} - - {({ x }) => ( -
- -
- )} -
+ + {({ x }) => ( +
+ +
+ )} +
+
-
+ ); + } + + return ( + + + + + + + + ); } diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index 75ca19d170..d55c63c2b9 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -1,15 +1,16 @@ +import PropTypes from 'prop-types'; import React from 'react'; +import { Helmet } from 'react-helmet'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations'; +import { connectDirectStream } from 'flavours/glitch/actions/streaming'; +import { expandDirectTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandDirectTimeline } from 'flavours/glitch/actions/timelines'; -import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectDirectStream } from 'flavours/glitch/actions/streaming'; import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ @@ -143,6 +144,11 @@ class DirectTimeline extends React.PureComponent { {contents} + + + {intl.formatMessage(messages.title)} + + ); } diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index b6785a5f95..ccc3dd3d2e 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -24,7 +24,8 @@ import classNames from 'classnames'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, + cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, @@ -45,10 +46,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow(account) { - if ( - account.getIn(['relationship', 'following']) || - account.getIn(['relationship', 'requested']) - ) { + if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { dispatch( openModal('CONFIRM', { @@ -66,6 +64,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } else { dispatch(unfollowAccount(account.get('id'))); } + } else if (account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } else { + dispatch(unfollowAccount(account.get('id'))); + } } else { dispatch(followAccount(account.get('id'))); } diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js index dce8fa4e76..94bcd578cf 100644 --- a/app/javascript/flavours/glitch/features/directory/index.js +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -13,7 +13,6 @@ import RadioButton from 'flavours/glitch/components/radio_button'; import LoadMore from 'flavours/glitch/components/load_more'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { title } from 'flavours/glitch/initial_state'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -169,7 +168,8 @@ class Directory extends React.PureComponent { {multiColumn && !pinned ? {scrollableArea} : scrollableArea} - {intl.formatMessage(messages.title)} - {title} + {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index acce87d5a6..cb0b55c637 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -11,6 +11,7 @@ import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_bloc import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' }, @@ -59,6 +60,7 @@ class Blocks extends ImmutablePureComponent { return ( + , )} + + + + ); } diff --git a/app/javascript/flavours/glitch/features/emoji/emoji.js b/app/javascript/flavours/glitch/features/emoji/emoji.js index c4e2c26f28..50a3991147 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji.js +++ b/app/javascript/flavours/glitch/features/emoji/emoji.js @@ -19,15 +19,26 @@ const emojiFilename = (filename) => { return borderedEmoji.includes(filename) ? (filename + '_border') : filename; }; -const emojify = (str, customEmojis = {}) => { - const tagCharsWithoutEmojis = '<&'; - const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; - let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; +const domParser = new DOMParser(); + +const emojifyTextNode = (node, customEmojis) => { + let str = node.textContent; + + const fragment = new DocumentFragment(); + for (;;) { - let match, i = 0, tag; - while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { - i += str.codePointAt(i) < 65536 ? 1 : 2; + let match, i = 0; + + if (customEmojis === null) { + while (i < str.length && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { + i += str.codePointAt(i) < 65536 ? 1 : 2; + } + } else { + while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { + i += str.codePointAt(i) < 65536 ? 1 : 2; + } } + let rend, replacement = ''; if (i === str.length) { break; @@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => { if (!(() => { rend = str.indexOf(':', i + 1) + 1; if (!rend) return false; // no pair of ':' - const lt = str.indexOf('<', i + 1); - if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':' const shortname = str.slice(i, rend); // now got a replacee as ':shortname:' // if you want additional emoji handler, add statements below which set replacement and return true. @@ -47,29 +56,6 @@ const emojify = (str, customEmojis = {}) => { } return false; })()) rend = ++i; - } else if (tag >= 0) { // <, & - rend = str.indexOf('>;'[tag], i + 1) + 1; - if (!rend) { - break; - } - if (tag === 0) { - if (invisible) { - if (str[i + 1] === '/') { // closing tag - if (!--invisible) { - tagChars = tagCharsWithEmojis; - } - } else if (str[rend - 2] !== '/') { // opening tag - invisible++; - } - } else { - if (str.startsWith('