th: Merge remote-tracking branch 'glitch/main'
This commit is contained in:
commit
a590a2a093
446 changed files with 8384 additions and 4621 deletions
1
.github/workflows/build-image.yml
vendored
1
.github/workflows/build-image.yml
vendored
|
@ -12,6 +12,7 @@ on:
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
|
|
138
.github/workflows/test-chart.yml
vendored
Normal file
138
.github/workflows/test-chart.yml
vendored
Normal file
|
@ -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
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -44,6 +44,9 @@
|
||||||
/redis
|
/redis
|
||||||
/elasticsearch
|
/elasticsearch
|
||||||
|
|
||||||
|
# ignore Helm charts
|
||||||
|
/chart/*.tgz
|
||||||
|
|
||||||
# ignore Helm dependency charts
|
# ignore Helm dependency charts
|
||||||
/chart/charts/*.tgz
|
/chart/charts/*.tgz
|
||||||
|
|
||||||
|
|
1121
AUTHORS.md
1121
AUTHORS.md
File diff suppressed because it is too large
Load diff
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -3,7 +3,12 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [Unreleased]
|
## [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.
|
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.
|
||||||
|
|
||||||
|
@ -13,7 +18,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- **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 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 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 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))
|
- 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))
|
- **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
|
- Previously, you could only see trends in your current language
|
||||||
- For less popular languages, that meant empty trends
|
- For less popular languages, that meant empty trends
|
||||||
|
@ -21,6 +26,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296))
|
- 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 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 `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 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 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 WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506))
|
||||||
|
@ -43,22 +49,27 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247))
|
- 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 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 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))
|
- 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 `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 `--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 `--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 `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 `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 `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427))
|
||||||
- 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))
|
- 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 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 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
|
### 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 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 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))
|
- **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
|
- The web app can now be accessed without being logged in
|
||||||
- No more `/web` prefix on web app paths
|
- 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
|
- Profiles, posts, and other public pages now use the same interface for logged in and logged out users
|
||||||
|
@ -74,14 +85,13 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583))
|
- 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 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 "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))
|
- **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 keywords and phrases can now be grouped into named categories
|
||||||
- Filtered posts show which exact filter was hit
|
- Filtered posts show which exact filter was hit
|
||||||
- Individual posts can be added to a filter
|
- Individual posts can be added to a filter
|
||||||
- You can peek inside filtered posts anyway
|
- 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 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 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 public (but not hashtag) timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291), [Gargron](https://github.com/mastodon/mastodon/pull/19563))
|
|
||||||
- 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 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 "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 e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979))
|
||||||
|
@ -95,6 +105,14 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Change mentions of blocked users to not be processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19725))
|
- 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 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 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
|
### Removed
|
||||||
|
|
||||||
|
@ -107,6 +125,28 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
|
|
||||||
### Fixed
|
### 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 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 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 not being able to input featured tag with hash symbol ([Gargron](https://github.com/mastodon/mastodon/pull/19535))
|
||||||
|
@ -118,7 +158,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Fix account action type validation ([Gargron](https://github.com/mastodon/mastodon/pull/19476))
|
- 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 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 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))
|
- 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 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 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 handling of duplicate and out-of-order notifications in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19693))
|
||||||
|
@ -156,6 +196,15 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662))
|
- 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 `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 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
|
## [3.5.3] - 2022-05-26
|
||||||
### Added
|
### Added
|
||||||
|
@ -276,7 +325,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
|
|
||||||
### Fixed
|
### 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 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 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))
|
- Fix trends returning less results per page when filtered in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17996))
|
||||||
|
@ -411,7 +460,7 @@ Some of the features in this release have been funded through the [NGI0 Discover
|
||||||
- Remove profile directory link from main navigation panel in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17688))
|
- 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))
|
- **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
|
- 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
|
- 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))
|
- Remove support for `OAUTH_REDIRECT_AT_SIGN_IN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17287))
|
||||||
- Use `OMNIAUTH_ONLY` instead
|
- Use `OMNIAUTH_ONLY` instead
|
||||||
|
|
|
@ -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)
|
- Code style rules (rubocop, eslint)
|
||||||
- Normalization of locale files (i18n-tasks)
|
- 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
|
## 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).
|
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).
|
||||||
|
|
4
Gemfile
4
Gemfile
|
@ -92,7 +92,7 @@ gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2022'
|
gem 'tzinfo-data', '~> 1.2022'
|
||||||
gem 'webpacker', '~> 5.4'
|
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 'webauthn', '~> 2.5'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
|
@ -122,6 +122,7 @@ group :test do
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.18'
|
gem 'webmock', '~> 3.18'
|
||||||
gem 'rspec_junit_formatter', '~> 0.6'
|
gem 'rspec_junit_formatter', '~> 0.6'
|
||||||
|
gem 'rack-test', '~> 2.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
@ -152,7 +153,6 @@ end
|
||||||
|
|
||||||
gem 'concurrent-ruby', require: false
|
gem 'concurrent-ruby', require: false
|
||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
|
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
|
@ -818,6 +818,7 @@ DEPENDENCIES
|
||||||
rack (~> 2.2.4)
|
rack (~> 2.2.4)
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
|
rack-test (~> 2.0)
|
||||||
rails (~> 6.1.7)
|
rails (~> 6.1.7)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 6.0)
|
rails-i18n (~> 6.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Security Policy
|
# 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 can reach us at <hello@joinmastodon.org>.
|
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 <security@joinmastodon.org>.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
7
app.json
7
app.json
|
@ -79,8 +79,13 @@
|
||||||
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
|
"SMTP_ENABLE_STARTTLS": {
|
||||||
|
"description": "Enable STARTTLS? Default is 'auto'.",
|
||||||
|
"value": "auto",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"SMTP_ENABLE_STARTTLS_AUTO": {
|
"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
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
9
app/controllers/admin/settings/other_controller.rb
Normal file
9
app/controllers/admin/settings/other_controller.rb
Normal file
|
@ -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
|
|
@ -57,7 +57,7 @@ class Api::BaseController < ApplicationController
|
||||||
render json: { error: I18n.t('errors.429') }, status: 429
|
render json: { error: I18n.t('errors.429') }, status: 429
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from ActionController::ParameterMissing do |e|
|
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
|
||||||
render json: { error: e.to_s }, status: 400
|
render json: { error: e.to_s }, status: 400
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Api::V1::Accounts::PinsController < Api::BaseController
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
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
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,10 @@ class Api::V1::ListsController < Api::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_list, except: [:index, :create]
|
before_action :set_list, except: [:index, :create]
|
||||||
|
|
||||||
|
rescue_from ArgumentError do |e|
|
||||||
|
render json: { error: e.to_s }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@lists = List.where(account: current_account).all
|
@lists = List.where(account: current_account).all
|
||||||
render json: @lists, each_serializer: REST::ListSerializer
|
render json: @lists, each_serializer: REST::ListSerializer
|
||||||
|
|
|
@ -18,14 +18,29 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
# than this anyway
|
# than this anyway
|
||||||
CONTEXT_LIMIT = 4_096
|
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
|
def show
|
||||||
@status = cache_collection([@status], Status).first
|
@status = cache_collection([@status], Status).first
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def context
|
def context
|
||||||
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
|
ancestors_limit = CONTEXT_LIMIT
|
||||||
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
|
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_ancestors = cache_collection(ancestors_results, Status)
|
||||||
loaded_descendants = cache_collection(descendants_results, Status)
|
loaded_descendants = cache_collection(descendants_results, Status)
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Api::V1::TagsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_or_create_tag
|
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])
|
@tag = Tag.find_normalized(params[:id]) || Tag.new(name: Tag.normalize(params[:id]), display_name: params[:id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||||
def public_feed
|
def public_feed
|
||||||
PublicFeed.new(
|
PublicFeed.new(
|
||||||
current_account,
|
current_account,
|
||||||
locale: content_locale,
|
|
||||||
local: truthy_param?(:local),
|
local: truthy_param?(:local),
|
||||||
remote: truthy_param?(:remote),
|
remote: truthy_param?(:remote),
|
||||||
only_media: truthy_param?(:only_media),
|
only_media: truthy_param?(:only_media),
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.permit(*FILTER_PARAMS)
|
params.permit(*FILTER_PARAMS, role_ids: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# 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! :read, :'read:filters' }, only: [:index, :show]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# 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! :read, :'read:filters' }, only: [:index, :show]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
|
@ -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
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
class StatusesController < ApplicationController
|
class StatusesController < ApplicationController
|
||||||
include WebAppControllerConcern
|
include WebAppControllerConcern
|
||||||
include StatusControllerConcern
|
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# rubocop:disable Metrics/ModuleLength, Style/WordArray
|
||||||
|
|
||||||
module LanguagesHelper
|
module LanguagesHelper
|
||||||
ISO_639_1 = {
|
ISO_639_1 = {
|
||||||
|
@ -189,8 +190,14 @@ module LanguagesHelper
|
||||||
ISO_639_3 = {
|
ISO_639_3 = {
|
||||||
ast: ['Asturian', 'Asturianu'].freeze,
|
ast: ['Asturian', 'Asturianu'].freeze,
|
||||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||||
|
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||||
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].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,
|
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
@ -259,3 +266,5 @@ module LanguagesHelper
|
||||||
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
|
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:enable Metrics/ModuleLength, Style/WordArray
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const fetchFilters = () => (dispatch, getState) => {
|
||||||
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
|
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||||
dispatch(createFilterStatusRequest());
|
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));
|
dispatch(createFilterStatusSuccess(response.data));
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { reduceMotion } from 'flavours/glitch/initial_state';
|
import { reduceMotion } from 'flavours/glitch/initial_state';
|
||||||
|
@ -51,7 +51,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||||
const { direction } = this.state;
|
const { direction } = this.state;
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
|
return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = [{
|
const styles = [{
|
||||||
|
@ -65,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||||
{items => (
|
{items => (
|
||||||
<span className='animated-number'>
|
<span className='animated-number'>
|
||||||
{items.map(({ key, data, style }) => (
|
{items.map(({ key, data, style }) => (
|
||||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -157,7 +157,6 @@ class ColumnHeader extends React.PureComponent {
|
||||||
className={collapsibleButtonClassName}
|
className={collapsibleButtonClassName}
|
||||||
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
aria-pressed={collapsed ? 'false' : 'true'}
|
|
||||||
onClick={this.handleToggleClick}
|
onClick={this.handleToggleClick}
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
|
|
|
@ -18,7 +18,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
onKeyPress: PropTypes.func,
|
onKeyPress: PropTypes.func,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
pressed: PropTypes.bool,
|
|
||||||
expanded: PropTypes.bool,
|
expanded: PropTypes.bool,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
activeStyle: PropTypes.object,
|
activeStyle: PropTypes.object,
|
||||||
|
@ -111,7 +110,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
icon,
|
icon,
|
||||||
inverted,
|
inverted,
|
||||||
overlay,
|
overlay,
|
||||||
pressed,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
title,
|
title,
|
||||||
counter,
|
counter,
|
||||||
|
@ -156,7 +154,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
aria-pressed={pressed}
|
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
title={title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
|
|
|
@ -687,7 +687,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
||||||
background = attachments.getIn([0, 'preview_url']);
|
background = attachments.getIn([0, 'preview_url']);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && settings.get('inline_preview_cards')) {
|
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
|
||||||
media.push(
|
media.push(
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
|
|
@ -42,6 +42,7 @@ const messages = defineMessages({
|
||||||
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
||||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -182,22 +183,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCopy = () => {
|
handleCopy = () => {
|
||||||
const url = this.props.status.get('url');
|
const url = this.props.status.get('url');
|
||||||
const textarea = document.createElement('textarea');
|
navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
textarea.textContent = url;
|
|
||||||
textarea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
try {
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHideClick = () => {
|
handleHideClick = () => {
|
||||||
|
@ -216,6 +203,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
|
@ -225,6 +213,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
|
}
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
@ -315,10 +306,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
counter={showReplyCount ? status.get('replies_count') : undefined}
|
counter={showReplyCount ? status.get('replies_count') : undefined}
|
||||||
obfuscateCount
|
obfuscateCount
|
||||||
/>
|
/>
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
{filterButton}
|
{filterButton}
|
||||||
|
|
||||||
|
|
|
@ -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 = <HashtagTimeline hashtag={hashtag} local={local} />;
|
|
||||||
} else {
|
|
||||||
timeline = <PublicTimeline local={local} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
|
||||||
<Provider store={store}>
|
|
||||||
<Fragment>
|
|
||||||
{timeline}
|
|
||||||
|
|
||||||
{ReactDOM.createPortal(
|
|
||||||
<ModalContainer />,
|
|
||||||
document.getElementById('modal-container'),
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -183,25 +183,18 @@ class About extends React.PureComponent {
|
||||||
<>
|
<>
|
||||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||||
|
|
||||||
<table className='about__domain-blocks'>
|
<div className='about__domain-blocks'>
|
||||||
<thead>
|
{domainBlocks.get('items').map(block => (
|
||||||
<tr>
|
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||||
<th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th>
|
<div className='about__domain-blocks__domain__header'>
|
||||||
<th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th>
|
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||||
<th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th>
|
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||||
{domainBlocks.get('items').map(block => (
|
</div>
|
||||||
<tr key={block.get('domain')}>
|
))}
|
||||||
<td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td>
|
</div>
|
||||||
<td><span title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span></td>
|
|
||||||
<td>{block.get('comment')}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
|
|
@ -53,6 +53,7 @@ const messages = defineMessages({
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
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}' },
|
add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
|
||||||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const titleFromAccount = account => {
|
const titleFromAccount = account => {
|
||||||
|
@ -97,6 +98,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote: PropTypes.func.isRequired,
|
onEditAccountNote: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
|
onOpenAvatar: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
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 () {
|
render () {
|
||||||
const { account, hidden, intl, domain } = this.props;
|
const { account, hidden, intl, domain } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
@ -142,7 +151,9 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
const accountNote = account.getIn(['relationship', 'note']);
|
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 info = [];
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
|
@ -199,6 +210,11 @@ class Header extends ImmutablePureComponent {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
||||||
|
menu.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
if ('share' in navigator && !suspended) {
|
if ('share' in navigator && !suspended) {
|
||||||
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||||
menu.push(null);
|
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 });
|
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signedIn && account.get('acct') !== account.get('username')) {
|
if (signedIn && isRemote) {
|
||||||
const domain = account.get('acct').split('@')[1];
|
|
||||||
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
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 {
|
} 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,12 +313,10 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
<div className='account__header__bar'>
|
||||||
<div className='account__header__tabs'>
|
<div className='account__header__tabs'>
|
||||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
<a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}>
|
||||||
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='spacer' />
|
|
||||||
|
|
||||||
{!suspended && (
|
{!suspended && (
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{!hidden && (
|
{!hidden && (
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
|
onOpenAvatar: PropTypes.func.isRequired,
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
this.props.onInteractionModal(this.props.account);
|
this.props.onInteractionModal(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOpenAvatar = () => {
|
||||||
|
this.props.onOpenAvatar(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hidden, hideTabs } = this.props;
|
const { account, hidden, hideTabs } = this.props;
|
||||||
|
|
||||||
|
@ -130,6 +135,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
onEditAccountNote={this.handleEditAccountNote}
|
||||||
onChangeLanguages={this.handleChangeLanguages}
|
onChangeLanguages={this.handleChangeLanguages}
|
||||||
onInteractionModal={this.handleInteractionModal}
|
onInteractionModal={this.handleInteractionModal}
|
||||||
|
onOpenAvatar={this.handleOpenAvatar}
|
||||||
domain={this.props.domain}
|
domain={this.props.domain}
|
||||||
hidden={hidden}
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -161,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));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||||
|
|
|
@ -103,7 +103,7 @@ class ToggleOption extends ImmutablePureComponent {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Toggle checked={checked} onChange={this.handleChange} />
|
<Toggle checked={checked} onChange={this.handleChange} />
|
||||||
|
|
||||||
<div className='content'>
|
<div className='privacy-dropdown__option__content'>
|
||||||
<strong>{text}</strong>
|
<strong>{text}</strong>
|
||||||
{meta}
|
{meta}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,6 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||||
isEditingStatus: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUndoClick = e => {
|
handleUndoClick = e => {
|
||||||
|
@ -32,7 +31,7 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, media, isEditingStatus } = this.props;
|
const { media } = this.props;
|
||||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
|
@ -45,10 +44,10 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
<div className='compose-form__upload__actions'>
|
<div className='compose-form__upload__actions'>
|
||||||
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||||
{!isEditingStatus && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
{!!media.get('unattached') && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(media.get('description') || '').length === 0 && (
|
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
|
||||||
<div className='compose-form__upload__warning'>
|
<div className='compose-form__upload__warning'>
|
||||||
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { submitCompose } from 'flavours/glitch/actions/compose';
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
isEditingStatus: state.getIn(['compose', 'id']) !== null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -19,15 +19,26 @@ const emojiFilename = (filename) => {
|
||||||
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}) => {
|
const domParser = new DOMParser();
|
||||||
const tagCharsWithoutEmojis = '<&';
|
|
||||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
const emojifyTextNode = (node, customEmojis) => {
|
||||||
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
|
let str = node.textContent;
|
||||||
|
|
||||||
|
const fragment = new DocumentFragment();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
let match, i = 0, tag;
|
let match, i = 0;
|
||||||
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;
|
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 = '';
|
let rend, replacement = '';
|
||||||
if (i === str.length) {
|
if (i === str.length) {
|
||||||
break;
|
break;
|
||||||
|
@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
if (!(() => {
|
if (!(() => {
|
||||||
rend = str.indexOf(':', i + 1) + 1;
|
rend = str.indexOf(':', i + 1) + 1;
|
||||||
if (!rend) return false; // no pair of ':'
|
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);
|
const shortname = str.slice(i, rend);
|
||||||
// now got a replacee as ':shortname:'
|
// now got a replacee as ':shortname:'
|
||||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
// 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;
|
return false;
|
||||||
})()) rend = ++i;
|
})()) 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('<span class="invisible">', i)) {
|
|
||||||
// avoid emojifying on invisible text
|
|
||||||
invisible = 1;
|
|
||||||
tagChars = tagCharsWithoutEmojis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i = rend;
|
|
||||||
} else if (!useSystemEmojiFont) { // matched to unicode emoji
|
} else if (!useSystemEmojiFont) { // matched to unicode emoji
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
const { filename, shortCode } = unicodeMapping[match];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
@ -80,10 +66,43 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
rend += 1;
|
rend += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rtn += str.slice(0, i) + replacement;
|
|
||||||
|
fragment.append(document.createTextNode(str.slice(0, i)));
|
||||||
|
if (replacement) {
|
||||||
|
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
|
||||||
|
}
|
||||||
|
node.textContent = str.slice(0, i);
|
||||||
str = str.slice(rend);
|
str = str.slice(rend);
|
||||||
}
|
}
|
||||||
return rtn + str;
|
|
||||||
|
fragment.append(document.createTextNode(str));
|
||||||
|
node.parentElement.replaceChild(fragment, node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojifyNode = (node, customEmojis) => {
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
switch(child.nodeType) {
|
||||||
|
case Node.TEXT_NODE:
|
||||||
|
emojifyTextNode(child, customEmojis);
|
||||||
|
break;
|
||||||
|
case Node.ELEMENT_NODE:
|
||||||
|
if (!child.classList.contains('invisible'))
|
||||||
|
emojifyNode(child, customEmojis);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojify = (str, customEmojis = {}) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = str;
|
||||||
|
|
||||||
|
if (!Object.keys(customEmojis).length)
|
||||||
|
customEmojis = null;
|
||||||
|
|
||||||
|
emojifyNode(wrapper, customEmojis);
|
||||||
|
|
||||||
|
return wrapper.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default emojify;
|
export default emojify;
|
||||||
|
|
|
@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const following = tag.get('following');
|
const following = tag.get('following');
|
||||||
|
|
||||||
followButton = (
|
followButton = (
|
||||||
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}>
|
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
|
||||||
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -131,7 +131,6 @@ class HomeTimeline extends React.PureComponent {
|
||||||
className={classNames('column-header__button', { 'active': showAnnouncements })}
|
className={classNames('column-header__button', { 'active': showAnnouncements })}
|
||||||
title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
||||||
aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
||||||
aria-pressed={showAnnouncements ? 'true' : 'false'}
|
|
||||||
onClick={this.handleToggleAnnouncementsClick}
|
onClick={this.handleToggleAnnouncementsClick}
|
||||||
>
|
>
|
||||||
<IconWithBadge id='bullhorn' count={unreadAnnouncements} />
|
<IconWithBadge id='bullhorn' count={unreadAnnouncements} />
|
||||||
|
|
|
@ -150,7 +150,7 @@ class InteractionModal extends React.PureComponent {
|
||||||
|
|
||||||
<div className='interaction-modal__choices__choice'>
|
<div className='interaction-modal__choices__choice'>
|
||||||
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||||
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p>
|
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.' /></p>
|
||||||
<Copypaste value={url} />
|
<Copypaste value={url} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -207,8 +207,8 @@ class Footer extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='picture-in-picture__footer'>
|
<div className='picture-in-picture__footer'>
|
||||||
{replyButton}
|
{replyButton}
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
|
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,6 +35,7 @@ const messages = defineMessages({
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -132,22 +133,8 @@ class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCopy = () => {
|
handleCopy = () => {
|
||||||
const url = this.props.status.get('url');
|
const url = this.props.status.get('url');
|
||||||
const textarea = document.createElement('textarea');
|
navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
textarea.textContent = url;
|
|
||||||
textarea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
try {
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -158,10 +145,15 @@ class ActionBar extends React.PureComponent {
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
|
@ -648,7 +648,7 @@ class Status extends ImmutablePureComponent {
|
||||||
showBackButton
|
showBackButton
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={(
|
extraButton={(
|
||||||
<button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
|
<button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
|
import ImageLoader from './image_loader';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class ImageModal extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
src: PropTypes.string.isRequired,
|
||||||
|
alt: PropTypes.string.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
navigationHidden: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleNavigation = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
navigationHidden: !prevState.navigationHidden,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, src, alt, onClose } = this.props;
|
||||||
|
const { navigationHidden } = this.state;
|
||||||
|
|
||||||
|
const navigationClassName = classNames('media-modal__navigation', {
|
||||||
|
'media-modal__navigation--hidden': navigationHidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal media-modal'>
|
||||||
|
<div className='media-modal__closer' role='presentation' onClick={onClose} >
|
||||||
|
<ImageLoader
|
||||||
|
src={src}
|
||||||
|
width={400}
|
||||||
|
height={400}
|
||||||
|
alt={alt}
|
||||||
|
onClick={this.toggleNavigation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={navigationClassName}>
|
||||||
|
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import DoodleModal from './doodle_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
import FocalPointModal from './focal_point_modal';
|
import FocalPointModal from './focal_point_modal';
|
||||||
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||||
|
import ImageModal from './image_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
|
@ -38,6 +39,7 @@ const MODAL_COMPONENTS = {
|
||||||
'ONBOARDING': OnboardingModal,
|
'ONBOARDING': OnboardingModal,
|
||||||
'VIDEO': () => Promise.resolve({ default: VideoModal }),
|
'VIDEO': () => Promise.resolve({ default: VideoModal }),
|
||||||
'AUDIO': () => Promise.resolve({ default: AudioModal }),
|
'AUDIO': () => Promise.resolve({ default: AudioModal }),
|
||||||
|
'IMAGE': () => Promise.resolve({ default: ImageModal }),
|
||||||
'BOOST': () => Promise.resolve({ default: BoostModal }),
|
'BOOST': () => Promise.resolve({ default: BoostModal }),
|
||||||
'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
|
'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
|
||||||
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
|
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
|
||||||
|
|
|
@ -303,7 +303,7 @@ class UI extends React.Component {
|
||||||
this.dragTargets.push(e.target);
|
this.dragTargets.push(e.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files') && this.props.canUploadMore) {
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) {
|
||||||
this.setState({ draggingOver: true });
|
this.setState({ draggingOver: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -330,7 +330,7 @@ class UI extends React.Component {
|
||||||
this.setState({ draggingOver: false });
|
this.setState({ draggingOver: false });
|
||||||
this.dragTargets = [];
|
this.dragTargets = [];
|
||||||
|
|
||||||
if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) {
|
if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.context.identity.signedIn) {
|
||||||
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
|
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
|
||||||
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
|
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
import ready from 'flavours/glitch/ready';
|
import ready from 'flavours/glitch/ready';
|
||||||
|
|
||||||
const perf = require('flavours/glitch/performance');
|
const perf = require('flavours/glitch/performance');
|
||||||
|
@ -19,23 +20,19 @@ function main() {
|
||||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||||
store.dispatch(setupBrowserNotifications());
|
store.dispatch(setupBrowserNotifications());
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
|
||||||
const [{ Workbox }, { me }] = await Promise.all([
|
const { Workbox } = await import('workbox-window');
|
||||||
import('workbox-window'),
|
|
||||||
import('flavours/glitch/initial_state'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const wb = new Workbox('/sw.js');
|
const wb = new Workbox('/sw.js');
|
||||||
|
/** @type {ServiceWorkerRegistration} */
|
||||||
|
let registration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wb.register();
|
registration = await wb.register();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me) {
|
if (registration) {
|
||||||
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
||||||
|
|
||||||
store.dispatch(registerPushNotifications.register());
|
store.dispatch(registerPushNotifications.register());
|
||||||
|
|
|
@ -21,3 +21,12 @@ es:
|
||||||
skins:
|
skins:
|
||||||
glitch:
|
glitch:
|
||||||
default: Predeterminado
|
default: Predeterminado
|
||||||
|
|
||||||
|
ja:
|
||||||
|
flavours:
|
||||||
|
glitch:
|
||||||
|
description: GlitchSocインスタンスのデフォルトフレーバーです。
|
||||||
|
name: Glitch Edition
|
||||||
|
skins:
|
||||||
|
glitch:
|
||||||
|
default: デフォルト
|
||||||
|
|
|
@ -222,7 +222,7 @@ function appendMedia(state, media, file) {
|
||||||
if (media.get('type') === 'image') {
|
if (media.get('type') === 'image') {
|
||||||
media = media.set('file', file);
|
media = media.set('file', file);
|
||||||
}
|
}
|
||||||
map.update('media_attachments', list => list.push(media));
|
map.update('media_attachments', list => list.push(media.set('unattached', true)));
|
||||||
map.set('is_uploading', false);
|
map.set('is_uploading', false);
|
||||||
map.set('is_processing', false);
|
map.set('is_processing', false);
|
||||||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||||
|
@ -547,7 +547,7 @@ export default function compose(state = initialState, action) {
|
||||||
.setIn(['media_modal', 'dirty'], false)
|
.setIn(['media_modal', 'dirty'], false)
|
||||||
.update('media_attachments', list => list.map(item => {
|
.update('media_attachments', list => list.map(item => {
|
||||||
if (item.get('id') === action.media.id) {
|
if (item.get('id') === action.media.id) {
|
||||||
return fromJS(action.media);
|
return fromJS(action.media).set('unattached', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
@ -563,7 +563,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('content_type', action.content_type || 'text/plain');
|
map.set('content_type', action.content_type || 'text/plain');
|
||||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||||
map.set('privacy', action.status.get('visibility'));
|
map.set('privacy', action.status.get('visibility'));
|
||||||
map.set('media_attachments', action.status.get('media_attachments'));
|
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
|
|
@ -204,7 +204,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-role,
|
.account-role,
|
||||||
.simple_form .recommended {
|
.simple_form .recommended,
|
||||||
|
.simple_form .not_recommended,
|
||||||
|
.simple_form .glitch_only {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -229,6 +231,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.simple_form .not_recommended {
|
||||||
|
color: lighten($error-red, 12%);
|
||||||
|
background-color: rgba(lighten($error-red, 12%), 0.1);
|
||||||
|
border-color: rgba(lighten($error-red, 12%), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple_form .glitch_only {
|
||||||
|
color: lighten($warning-red, 12%);
|
||||||
|
background-color: rgba(lighten($warning-red, 12%), 0.1);
|
||||||
|
border-color: rgba(lighten($warning-red, 12%), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.account__header__fields {
|
.account__header__fields {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -247,28 +247,45 @@
|
||||||
|
|
||||||
&__domain-blocks {
|
&__domain-blocks {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
width: 100%;
|
background: darken($ui-base-color, 4%);
|
||||||
border-collapse: collapse;
|
border: 1px solid lighten($ui-base-color, 4%);
|
||||||
break-inside: auto;
|
border-radius: 4px;
|
||||||
|
|
||||||
th {
|
&__domain {
|
||||||
text-align: left;
|
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||||
font-weight: 500;
|
padding: 10px;
|
||||||
|
font-size: 15px;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
|
||||||
|
|
||||||
thead tr,
|
&:nth-child(2n) {
|
||||||
tbody tr {
|
background: darken($ui-base-color, 2%);
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:last-child {
|
&:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
&__header {
|
||||||
td {
|
display: flex;
|
||||||
padding: 8px;
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
color: $secondary-text-color;
|
||||||
|
font-size: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -535,8 +535,11 @@
|
||||||
&__tabs {
|
&__tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
margin-top: -55px;
|
margin-top: -55px;
|
||||||
|
gap: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -545,6 +548,15 @@
|
||||||
padding-top: 55px;
|
padding-top: 55px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
flex-shrink: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-gap-breakpoint) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
border: 1px solid lighten($ui-base-color, 12%);
|
border: 1px solid lighten($ui-base-color, 12%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -659,6 +659,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 17px;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,9 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommended {
|
.recommended,
|
||||||
|
.not_recommended,
|
||||||
|
.glitch_only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
|
@ -1058,11 +1060,18 @@ code {
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only remove padding when listing applications, to prevent styling issues on
|
||||||
|
// the Authorization page.
|
||||||
|
.applications-list {
|
||||||
|
.permissions-list__item:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.keywords-table {
|
.keywords-table {
|
||||||
thead {
|
thead {
|
||||||
th {
|
th {
|
||||||
|
|
|
@ -78,7 +78,7 @@ html {
|
||||||
.column-header__back-button,
|
.column-header__back-button,
|
||||||
.column-header__button,
|
.column-header__button,
|
||||||
.column-header__button.active,
|
.column-header__button.active,
|
||||||
.account__header__bar {
|
.account__header {
|
||||||
background: $white;
|
background: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +203,8 @@ html {
|
||||||
// Change the colors used in compose-form
|
// Change the colors used in compose-form
|
||||||
.compose-form {
|
.compose-form {
|
||||||
.compose-form__modifiers {
|
.compose-form__modifiers {
|
||||||
.compose-form__upload__actions .icon-button {
|
.compose-form__upload__actions .icon-button,
|
||||||
|
.compose-form__upload__warning .icon-button {
|
||||||
color: lighten($white, 7%);
|
color: lighten($white, 7%);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
|
@ -212,14 +213,6 @@ html {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__upload-description input {
|
|
||||||
color: lighten($white, 7%);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: lighten($white, 7%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__buttons-wrapper {
|
.compose-form__buttons-wrapper {
|
||||||
|
|
|
@ -178,6 +178,9 @@ a.table-action-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__toolbar {
|
&__toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
border: 1px solid darken($ui-base-color, 8%);
|
border: 1px solid darken($ui-base-color, 8%);
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
border-radius: 4px 0 0;
|
border-radius: 4px 0 0;
|
||||||
|
|
|
@ -22,3 +22,12 @@ es:
|
||||||
skins:
|
skins:
|
||||||
vanilla:
|
vanilla:
|
||||||
default: Predeterminado
|
default: Predeterminado
|
||||||
|
|
||||||
|
ja:
|
||||||
|
flavours:
|
||||||
|
vanilla:
|
||||||
|
description: バニラのMastodonインスタンスで使われるテーマです。このテーマはGlitchSocのすべての機能をサポートしない可能性があります。
|
||||||
|
name: Vanilla Mastodon
|
||||||
|
skins:
|
||||||
|
vanilla:
|
||||||
|
default: デフォルト
|
||||||
|
|
1
app/javascript/images/logo_full.svg
Normal file
1
app/javascript/images/logo_full.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.6 KiB |
1
app/javascript/images/logo_transparent.svg
Normal file
1
app/javascript/images/logo_transparent.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -43,7 +43,7 @@ export const fetchFilters = () => (dispatch, getState) => {
|
||||||
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
|
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||||
dispatch(createFilterStatusRequest());
|
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));
|
dispatch(createFilterStatusSuccess(response.data));
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -3,13 +3,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import DisplayName from './display_name';
|
import DisplayName from './display_name';
|
||||||
import Permalink from './permalink';
|
|
||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
import Skeleton from 'mastodon/components/skeleton';
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
@ -140,11 +140,11 @@ class Account extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='account'>
|
<div className='account'>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||||
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
|
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
|
||||||
{mute_expires_at}
|
{mute_expires_at}
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<div className='account__relationship'>
|
<div className='account__relationship'>
|
||||||
{buttons}
|
{buttons}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
|
||||||
<Hashtag
|
<Hashtag
|
||||||
key={hashtag.name}
|
key={hashtag.name}
|
||||||
name={hashtag.name}
|
name={hashtag.name}
|
||||||
href={`/admin/tags/${hashtag.id}`}
|
to={`/admin/tags/${hashtag.id}`}
|
||||||
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||||
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||||
history={hashtag.history.reverse().map(day => day.uses)}
|
history={hashtag.history.reverse().map(day => day.uses)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { reduceMotion } from 'mastodon/initial_state';
|
import { reduceMotion } from 'mastodon/initial_state';
|
||||||
|
@ -51,7 +51,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||||
const { direction } = this.state;
|
const { direction } = this.state;
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
|
return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = [{
|
const styles = [{
|
||||||
|
@ -65,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||||
{items => (
|
{items => (
|
||||||
<span className='animated-number'>
|
<span className='animated-number'>
|
||||||
{items.map(({ key, data, style }) => (
|
{items.map(({ key, data, style }) => (
|
||||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -152,7 +152,6 @@ class ColumnHeader extends React.PureComponent {
|
||||||
className={collapsibleButtonClassName}
|
className={collapsibleButtonClassName}
|
||||||
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
title={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
aria-pressed={collapsed ? 'false' : 'true'}
|
|
||||||
onClick={this.handleToggleClick}
|
onClick={this.handleToggleClick}
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from './permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import Skeleton from 'mastodon/components/skeleton';
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -53,7 +53,6 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
export const ImmutableHashtag = ({ hashtag }) => (
|
export const ImmutableHashtag = ({ hashtag }) => (
|
||||||
<Hashtag
|
<Hashtag
|
||||||
name={hashtag.get('name')}
|
name={hashtag.get('name')}
|
||||||
href={hashtag.get('url')}
|
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
to={`/tags/${hashtag.get('name')}`}
|
||||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||||
|
@ -64,12 +63,12 @@ ImmutableHashtag.propTypes = {
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
hashtag: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
|
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
|
||||||
<div className={classNames('trends__item', className)}>
|
<div className={classNames('trends__item', className)}>
|
||||||
<div className='trends__item__name'>
|
<div className='trends__item__name'>
|
||||||
<Permalink href={href} to={to}>
|
<Link to={to}>
|
||||||
{name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
|
{name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
{description ? (
|
{description ? (
|
||||||
<span>{description}</span>
|
<span>{description}</span>
|
||||||
|
@ -98,7 +97,6 @@ const Hashtag = ({ name, href, to, people, uses, history, className, description
|
||||||
|
|
||||||
Hashtag.propTypes = {
|
Hashtag.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
href: PropTypes.string,
|
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
people: PropTypes.number,
|
people: PropTypes.number,
|
||||||
description: PropTypes.node,
|
description: PropTypes.node,
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class Icon extends React.PureComponent {
|
||||||
const { id, className, fixedWidth, ...other } = this.props;
|
const { id, className, fixedWidth, ...other } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
|
<i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
onKeyPress: PropTypes.func,
|
onKeyPress: PropTypes.func,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
pressed: PropTypes.bool,
|
|
||||||
expanded: PropTypes.bool,
|
expanded: PropTypes.bool,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
activeStyle: PropTypes.object,
|
activeStyle: PropTypes.object,
|
||||||
|
@ -98,7 +97,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
icon,
|
icon,
|
||||||
inverted,
|
inverted,
|
||||||
overlay,
|
overlay,
|
||||||
pressed,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
title,
|
title,
|
||||||
counter,
|
counter,
|
||||||
|
@ -143,7 +141,6 @@ export default class IconButton extends React.PureComponent {
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
aria-pressed={pressed}
|
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
title={title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default class Permalink extends React.PureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
href: PropTypes.string.isRequired,
|
|
||||||
to: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onInterceptClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = e => {
|
|
||||||
if (this.props.onInterceptClick && this.props.onInterceptClick()) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.context.router.history.push(this.props.to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { href, children, className, onInterceptClick, ...other } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -378,7 +378,7 @@ class Status extends ImmutablePureComponent {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
|
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@ class Status extends ImmutablePureComponent {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
|
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
|
||||||
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -476,7 +476,7 @@ class Status extends ImmutablePureComponent {
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
|
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
@ -511,12 +511,12 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
|
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
|
||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
<a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a onClick={this.handleClick} href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
{statusAvatar}
|
{statusAvatar}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,6 +45,7 @@ const messages = defineMessages({
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
const mapStateToProps = (state, { status }) => ({
|
||||||
|
@ -221,25 +222,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCopy = () => {
|
handleCopy = () => {
|
||||||
const url = this.props.status.get('url');
|
const url = this.props.status.get('url');
|
||||||
const textarea = document.createElement('textarea');
|
navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
textarea.textContent = url;
|
|
||||||
textarea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
try {
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleHideClick = () => {
|
handleHideClick = () => {
|
||||||
this.props.onFilter();
|
this.props.onFilter();
|
||||||
}
|
}
|
||||||
|
@ -254,12 +240,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
@ -361,8 +352,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PollContainer from 'mastodon/containers/poll_container';
|
import PollContainer from 'mastodon/containers/poll_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
|
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
|
||||||
|
|
||||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
class TranslateButton extends React.PureComponent {
|
class TranslateButton extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -77,38 +77,45 @@ class StatusContent extends React.PureComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { status, onCollapsedToggle } = this.props;
|
||||||
const links = node.querySelectorAll('a');
|
const links = node.querySelectorAll('a');
|
||||||
|
|
||||||
|
let link, mention;
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
for (var i = 0; i < links.length; ++i) {
|
||||||
let link = links[i];
|
link = links[i];
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
if (link.classList.contains('status-link')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
link.classList.add('status-link');
|
link.classList.add('status-link');
|
||||||
|
|
||||||
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
link.setAttribute('title', mention.get('acct'));
|
link.setAttribute('title', mention.get('acct'));
|
||||||
|
link.setAttribute('href', `/@${mention.get('acct')}`);
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
|
link.setAttribute('href', `/tags/${link.text.slice(1)}`);
|
||||||
} else {
|
} else {
|
||||||
link.setAttribute('title', link.href);
|
link.setAttribute('title', link.href);
|
||||||
link.classList.add('unhandled-link');
|
link.classList.add('unhandled-link');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.status.get('collapsed', null) === null) {
|
if (status.get('collapsed', null) === null && onCollapsedToggle) {
|
||||||
let collapsed =
|
const { collapsable, onClick } = this.props;
|
||||||
this.props.collapsable
|
|
||||||
&& this.props.onClick
|
const collapsed =
|
||||||
|
collapsable
|
||||||
|
&& onClick
|
||||||
&& node.clientHeight > MAX_HEIGHT
|
&& node.clientHeight > MAX_HEIGHT
|
||||||
&& this.props.status.get('spoiler_text').length === 0;
|
&& status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
|
|
||||||
this.props.status.set('collapsed', collapsed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,9 +249,9 @@ class StatusContent extends React.PureComponent {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
const mentionLinks = status.get('mentions').map(item => (
|
const mentionLinks = status.get('mentions').map(item => (
|
||||||
<Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention'>
|
<Link to={`/@${item.get('acct')}`} key={item.get('id')} className='mention'>
|
||||||
@<span>{item.get('username')}</span>
|
@<span>{item.get('username')}</span>
|
||||||
</Permalink>
|
</Link>
|
||||||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
||||||
|
|
||||||
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
|
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
|
||||||
|
|
|
@ -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 '../store/configureStore';
|
|
||||||
import { hydrateStore } from '../actions/store';
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
|
||||||
import { getLocale } from '../locales';
|
|
||||||
import PublicTimeline from '../features/standalone/public_timeline';
|
|
||||||
import HashtagTimeline from '../features/standalone/hashtag_timeline';
|
|
||||||
import ModalContainer from '../features/ui/containers/modal_container';
|
|
||||||
import initialState from '../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 = <HashtagTimeline hashtag={hashtag} local={local} />;
|
|
||||||
} else {
|
|
||||||
timeline = <PublicTimeline local={local} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
|
||||||
<Provider store={store}>
|
|
||||||
<Fragment>
|
|
||||||
{timeline}
|
|
||||||
|
|
||||||
{ReactDOM.createPortal(
|
|
||||||
<ModalContainer />,
|
|
||||||
document.getElementById('modal-container'),
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -183,25 +183,18 @@ class About extends React.PureComponent {
|
||||||
<>
|
<>
|
||||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||||
|
|
||||||
<table className='about__domain-blocks'>
|
<div className='about__domain-blocks'>
|
||||||
<thead>
|
{domainBlocks.get('items').map(block => (
|
||||||
<tr>
|
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||||
<th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th>
|
<div className='about__domain-blocks__domain__header'>
|
||||||
<th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th>
|
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||||
<th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th>
|
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||||
{domainBlocks.get('items').map(block => (
|
</div>
|
||||||
<tr key={block.get('domain')}>
|
))}
|
||||||
<td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td>
|
</div>
|
||||||
<td><span title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span></td>
|
|
||||||
<td>{block.get('comment')}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
|
|
@ -39,7 +39,6 @@ class FeaturedTags extends ImmutablePureComponent {
|
||||||
<Hashtag
|
<Hashtag
|
||||||
key={featuredTag.get('name')}
|
key={featuredTag.get('name')}
|
||||||
name={featuredTag.get('name')}
|
name={featuredTag.get('name')}
|
||||||
href={featuredTag.get('url')}
|
|
||||||
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
||||||
uses={featuredTag.get('statuses_count') * 1}
|
uses={featuredTag.get('statuses_count') * 1}
|
||||||
withGraph={false}
|
withGraph={false}
|
||||||
|
|
|
@ -53,6 +53,7 @@ const messages = defineMessages({
|
||||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const titleFromAccount = account => {
|
const titleFromAccount = account => {
|
||||||
|
@ -97,6 +98,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote: PropTypes.func.isRequired,
|
onEditAccountNote: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
|
onOpenAvatar: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -140,6 +142,13 @@ class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAvatarClick = e => {
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onOpenAvatar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hidden, intl, domain } = this.props;
|
const { account, hidden, intl, domain } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
@ -148,7 +157,9 @@ class Header extends ImmutablePureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 info = [];
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
|
@ -200,6 +211,11 @@ class Header extends ImmutablePureComponent {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
||||||
|
menu.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
if ('share' in navigator) {
|
if ('share' in navigator) {
|
||||||
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -250,15 +266,13 @@ class Header extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signedIn && account.get('acct') !== account.get('username')) {
|
if (signedIn && isRemote) {
|
||||||
const domain = account.get('acct').split('@')[1];
|
|
||||||
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
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 {
|
} 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,12 +310,10 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
<div className='account__header__bar'>
|
||||||
<div className='account__header__tabs'>
|
<div className='account__header__tabs'>
|
||||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
<a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}>
|
||||||
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='spacer' />
|
|
||||||
|
|
||||||
{!suspended && (
|
{!suspended && (
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{!hidden && (
|
{!hidden && (
|
||||||
|
|
|
@ -129,7 +129,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-gallery__item' style={{ width, height }}>
|
<div className='account-gallery__item' style={{ width, height }}>
|
||||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
|
<a className='media-gallery__item-thumbnail' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
|
||||||
<Blurhash
|
<Blurhash
|
||||||
hash={attachment.get('blurhash')}
|
hash={attachment.get('blurhash')}
|
||||||
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
|
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
onInteractionModal: PropTypes.func.isRequired,
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
|
onOpenAvatar: PropTypes.func.isRequired,
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -101,6 +102,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
this.props.onInteractionModal(this.props.account);
|
this.props.onInteractionModal(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOpenAvatar = () => {
|
||||||
|
this.props.onOpenAvatar(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hidden, hideTabs } = this.props;
|
const { account, hidden, hideTabs } = this.props;
|
||||||
|
|
||||||
|
@ -129,6 +134,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
onEditAccountNote={this.handleEditAccountNote}
|
||||||
onChangeLanguages={this.handleChangeLanguages}
|
onChangeLanguages={this.handleChangeLanguages}
|
||||||
onInteractionModal={this.handleInteractionModal}
|
onInteractionModal={this.handleInteractionModal}
|
||||||
|
onOpenAvatar={this.handleOpenAvatar}
|
||||||
domain={this.props.domain}
|
domain={this.props.domain}
|
||||||
hidden={hidden}
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import AvatarOverlay from '../../../components/avatar_overlay';
|
import AvatarOverlay from '../../../components/avatar_overlay';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default class MovedNote extends ImmutablePureComponent {
|
export default class MovedNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -23,12 +23,12 @@ export default class MovedNote extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='moved-account-banner__action'>
|
<div className='moved-account-banner__action'>
|
||||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
|
<Link to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
|
||||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
||||||
<DisplayName account={to} />
|
<DisplayName account={to} />
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink>
|
<Link to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -152,6 +152,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));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ActionBar from './action_bar';
|
import ActionBar from './action_bar';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import Permalink from '../../../components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
@ -19,15 +19,15 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='navigation-bar'>
|
<div className='navigation-bar'>
|
||||||
<Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
|
<Link to={`/@${this.props.account.get('acct')}`}>
|
||||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||||
<Avatar account={this.props.account} size={46} />
|
<Avatar account={this.props.account} size={46} />
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<div className='navigation-bar__profile'>
|
<div className='navigation-bar__profile'>
|
||||||
<Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
|
<Link to={`/@${this.props.account.get('acct')}`}>
|
||||||
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
|
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||||
<div className='reply-indicator__header'>
|
<div className='reply-indicator__header'>
|
||||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||||
|
|
||||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -17,7 +17,6 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||||
isEditingStatus: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUndoClick = e => {
|
handleUndoClick = e => {
|
||||||
|
@ -31,7 +30,7 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, isEditingStatus } = this.props;
|
const { media } = this.props;
|
||||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
|
@ -44,10 +43,10 @@ export default class Upload extends ImmutablePureComponent {
|
||||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
<div className='compose-form__upload__actions'>
|
<div className='compose-form__upload__actions'>
|
||||||
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||||
{!isEditingStatus && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
{!!media.get('unattached') && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(media.get('description') || '').length === 0 && (
|
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
|
||||||
<div className='compose-form__upload__warning'>
|
<div className='compose-form__upload__warning'>
|
||||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { submitCompose } from '../../../actions/compose';
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
isEditingStatus: state.getIn(['compose', 'id']) !== null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -7,7 +7,7 @@ import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import AvatarComposite from 'mastodon/components/avatar_composite';
|
import AvatarComposite from 'mastodon/components/avatar_composite';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
@ -133,7 +133,7 @@ class Conversation extends ImmutablePureComponent {
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
||||||
|
|
||||||
const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
|
const names = accounts.map(a => <Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Link>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
reply: this.handleReply,
|
reply: this.handleReply,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import DisplayName from 'mastodon/components/display_name';
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||||
|
@ -169,7 +169,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-card'>
|
<div className='account-card'>
|
||||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
<Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||||
<div className='account-card__header'>
|
<div className='account-card__header'>
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
|
@ -183,7 +183,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</div>
|
</div>
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
{account.get('note').length > 0 && (
|
{account.get('note').length > 0 && (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -11,8 +11,8 @@ describe('emoji', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works with unclosed tags', () => {
|
it('works with unclosed tags', () => {
|
||||||
expect(emojify('hello>')).toEqual('hello>');
|
expect(emojify('hello>')).toEqual('hello>');
|
||||||
expect(emojify('<hello')).toEqual('<hello');
|
expect(emojify('<hello')).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works with unclosed shortcodes', () => {
|
it('works with unclosed shortcodes', () => {
|
||||||
|
@ -22,23 +22,23 @@ describe('emoji', () => {
|
||||||
|
|
||||||
it('does unicode', () => {
|
it('does unicode', () => {
|
||||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
|
||||||
expect(emojify('👨👩👧👧')).toEqual(
|
expect(emojify('👨👩👧👧')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
|
||||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
|
||||||
expect(emojify('\u2757')).toEqual(
|
expect(emojify('\u2757')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does multiple unicode', () => {
|
it('does multiple unicode', () => {
|
||||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
|
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores unicode inside of tags', () => {
|
it('ignores unicode inside of tags', () => {
|
||||||
|
@ -46,16 +46,16 @@ describe('emoji', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does multiple emoji properly (issue 5188)', () => {
|
it('does multiple emoji properly (issue 5188)', () => {
|
||||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji that has no shortcode', () => {
|
it('does an emoji that has no shortcode', () => {
|
||||||
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />');
|
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji whose filename is irregular', () => {
|
it('does an emoji whose filename is irregular', () => {
|
||||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('avoid emojifying on invisible text', () => {
|
it('avoid emojifying on invisible text', () => {
|
||||||
|
@ -67,26 +67,26 @@ describe('emoji', () => {
|
||||||
|
|
||||||
it('avoid emojifying on invisible text with nested tags', () => {
|
it('avoid emojifying on invisible text with nested tags', () => {
|
||||||
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips the textual presentation VS15 character', () => {
|
it('skips the textual presentation VS15 character', () => {
|
||||||
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
||||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
|
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an simple emoji properly', () => {
|
it('does an simple emoji properly', () => {
|
||||||
expect(emojify('♀♂'))
|
expect(emojify('♀♂'))
|
||||||
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg" /><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg" />');
|
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji containing ZWJ properly', () => {
|
it('does an emoji containing ZWJ properly', () => {
|
||||||
expect(emojify('💂♀️💂♂️'))
|
expect(emojify('💂♀️💂♂️'))
|
||||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg" /><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg" />');
|
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,15 +19,26 @@ const emojiFilename = (filename) => {
|
||||||
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}) => {
|
const domParser = new DOMParser();
|
||||||
const tagCharsWithoutEmojis = '<&';
|
|
||||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
const emojifyTextNode = (node, customEmojis) => {
|
||||||
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
|
let str = node.textContent;
|
||||||
|
|
||||||
|
const fragment = new DocumentFragment();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
let match, i = 0, tag;
|
let match, i = 0;
|
||||||
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
|
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
if (customEmojis === null) {
|
||||||
|
while (i < str.length && !(match = trie.search(str.slice(i)))) {
|
||||||
|
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
|
||||||
|
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rend, replacement = '';
|
let rend, replacement = '';
|
||||||
if (i === str.length) {
|
if (i === str.length) {
|
||||||
break;
|
break;
|
||||||
|
@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
if (!(() => {
|
if (!(() => {
|
||||||
rend = str.indexOf(':', i + 1) + 1;
|
rend = str.indexOf(':', i + 1) + 1;
|
||||||
if (!rend) return false; // no pair of ':'
|
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);
|
const shortname = str.slice(i, rend);
|
||||||
// now got a replacee as ':shortname:'
|
// now got a replacee as ':shortname:'
|
||||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
// 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;
|
return false;
|
||||||
})()) rend = ++i;
|
})()) 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('<span class="invisible">', i)) {
|
|
||||||
// avoid emojifying on invisible text
|
|
||||||
invisible = 1;
|
|
||||||
tagChars = tagCharsWithoutEmojis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i = rend;
|
|
||||||
} else { // matched to unicode emoji
|
} else { // matched to unicode emoji
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
const { filename, shortCode } = unicodeMapping[match];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
@ -80,10 +66,43 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
rend += 1;
|
rend += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rtn += str.slice(0, i) + replacement;
|
|
||||||
|
fragment.append(document.createTextNode(str.slice(0, i)));
|
||||||
|
if (replacement) {
|
||||||
|
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
|
||||||
|
}
|
||||||
|
node.textContent = str.slice(0, i);
|
||||||
str = str.slice(rend);
|
str = str.slice(rend);
|
||||||
}
|
}
|
||||||
return rtn + str;
|
|
||||||
|
fragment.append(document.createTextNode(str));
|
||||||
|
node.parentElement.replaceChild(fragment, node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojifyNode = (node, customEmojis) => {
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
switch(child.nodeType) {
|
||||||
|
case Node.TEXT_NODE:
|
||||||
|
emojifyTextNode(child, customEmojis);
|
||||||
|
break;
|
||||||
|
case Node.ELEMENT_NODE:
|
||||||
|
if (!child.classList.contains('invisible'))
|
||||||
|
emojifyNode(child, customEmojis);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojify = (str, customEmojis = {}) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = str;
|
||||||
|
|
||||||
|
if (!Object.keys(customEmojis).length)
|
||||||
|
customEmojis = null;
|
||||||
|
|
||||||
|
emojifyNode(wrapper, customEmojis);
|
||||||
|
|
||||||
|
return wrapper.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default emojify;
|
export default emojify;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import DisplayName from 'mastodon/components/display_name';
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
|
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
|
||||||
|
@ -66,13 +66,13 @@ class Account extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='account follow-recommendations-account'>
|
<div className='account follow-recommendations-account'>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Link className='account__display-name account__display-name--with-note' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
|
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
|
|
||||||
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
|
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<div className='account__relationship'>
|
<div className='account__relationship'>
|
||||||
{button}
|
{button}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from '../../../components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
|
@ -30,10 +30,10 @@ class AccountAuthorize extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='account-authorize__wrapper'>
|
<div className='account-authorize__wrapper'>
|
||||||
<div className='account-authorize'>
|
<div className='account-authorize'>
|
||||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<div className='account__header__content translate' dangerouslySetInnerHTML={content} />
|
<div className='account__header__content translate' dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const following = tag.get('following');
|
const following = tag.get('following');
|
||||||
|
|
||||||
followButton = (
|
followButton = (
|
||||||
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}>
|
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
|
||||||
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -130,7 +130,6 @@ class HomeTimeline extends React.PureComponent {
|
||||||
className={classNames('column-header__button', { 'active': showAnnouncements })}
|
className={classNames('column-header__button', { 'active': showAnnouncements })}
|
||||||
title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
||||||
aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
|
||||||
aria-pressed={showAnnouncements ? 'true' : 'false'}
|
|
||||||
onClick={this.handleToggleAnnouncementsClick}
|
onClick={this.handleToggleAnnouncementsClick}
|
||||||
>
|
>
|
||||||
<IconWithBadge id='bullhorn' count={unreadAnnouncements} />
|
<IconWithBadge id='bullhorn' count={unreadAnnouncements} />
|
||||||
|
|
|
@ -150,7 +150,7 @@ class InteractionModal extends React.PureComponent {
|
||||||
|
|
||||||
<div className='interaction-modal__choices__choice'>
|
<div className='interaction-modal__choices__choice'>
|
||||||
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||||
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p>
|
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.' /></p>
|
||||||
<Copypaste value={url} />
|
<Copypaste value={url} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import DisplayName from 'mastodon/components/display_name';
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
@ -42,10 +42,10 @@ class FollowRequest extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='account'>
|
<div className='account'>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Link>
|
||||||
|
|
||||||
<div className='account__relationship'>
|
<div className='account__relationship'>
|
||||||
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
||||||
|
|
|
@ -10,7 +10,7 @@ import AccountContainer from 'mastodon/containers/account_container';
|
||||||
import Report from './report';
|
import Report from './report';
|
||||||
import FollowRequestContainer from '../containers/follow_request_container';
|
import FollowRequestContainer from '../containers/follow_request_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import { Link } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -378,7 +378,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
|
|
||||||
const targetAccount = report.get('target_account');
|
const targetAccount = report.get('target_account');
|
||||||
const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
|
const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
|
||||||
const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
@ -403,7 +403,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
|
const link = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
|
||||||
|
|
||||||
switch(notification.get('type')) {
|
switch(notification.get('type')) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
|
|
@ -182,9 +182,9 @@ class Footer extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='picture-in-picture__footer'>
|
<div className='picture-in-picture__footer'>
|
||||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
|
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ const messages = defineMessages({
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
|
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
const mapStateToProps = (state, { status }) => ({
|
||||||
|
@ -174,22 +175,8 @@ class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCopy = () => {
|
handleCopy = () => {
|
||||||
const url = this.props.status.get('url');
|
const url = this.props.status.get('url');
|
||||||
const textarea = document.createElement('textarea');
|
navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
textarea.textContent = url;
|
|
||||||
textarea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
try {
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -201,10 +188,15 @@ class ActionBar extends React.PureComponent {
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
|
if (isRemote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
|
@ -261,7 +261,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
||||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
|
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
|
||||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||||
</a>
|
</a>
|
||||||
|
@ -276,7 +276,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -619,7 +619,7 @@ class Status extends ImmutablePureComponent {
|
||||||
showBackButton
|
showBackButton
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={(
|
extraButton={(
|
||||||
<button type='button' className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
|
<button type='button' className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -98,12 +98,12 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
<div className='boost-modal__container'>
|
<div className='boost-modal__container'>
|
||||||
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
|
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
|
||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
<Avatar account={status.get('account')} size={48} />
|
<Avatar account={status.get('account')} size={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue