Merge pull request #1876 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
968f343006
14
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
|
@ -31,6 +31,11 @@ body:
|
||||||
description: What happened?
|
description: What happened?
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Detailed description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Specifications
|
label: Specifications
|
||||||
|
@ -38,5 +43,14 @@ body:
|
||||||
What version or commit hash of Mastodon did you find this bug in?
|
What version or commit hash of Mastodon did you find this bug in?
|
||||||
|
|
||||||
If a front-end issue, what browser and operating systems were you using?
|
If a front-end issue, what browser and operating systems were you using?
|
||||||
|
placeholder: |
|
||||||
|
Mastodon 3.5.3 (or Edge)
|
||||||
|
Ruby 2.7.6 (or v3.1.2)
|
||||||
|
Node.js 16.18.0
|
||||||
|
|
||||||
|
Google Chrome 106.0.5249.119
|
||||||
|
Firefox 105.0.3
|
||||||
|
|
||||||
|
etc...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
5
.github/workflows/build-image.yml
vendored
5
.github/workflows/build-image.yml
vendored
|
@ -34,7 +34,8 @@ jobs:
|
||||||
latest=auto
|
latest=auto
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=main
|
type=edge,branch=main
|
||||||
type=match,pattern=v(.*),group=0
|
type=pep440,pattern={{raw}}
|
||||||
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
- uses: docker/build-push-action@v3
|
- uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -42,5 +43,5 @@ jobs:
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:edge
|
||||||
cache-to: type=inline
|
cache-to: type=inline
|
||||||
|
|
128
CHANGELOG.md
128
CHANGELOG.md
|
@ -3,6 +3,132 @@ 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]
|
||||||
|
|
||||||
|
Some of the features in this release have been funded through the [NGI0 Discovery](https://nlnet.nl/discovery) Fund, a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825322.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add ability to filter followed accounts' posts by language ([Gargron](https://github.com/mastodon/mastodon/pull/19095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19268))
|
||||||
|
- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924))
|
||||||
|
- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945))
|
||||||
|
- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245))
|
||||||
|
- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398))
|
||||||
|
- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335))
|
||||||
|
- Previously, you could only see trends in your current language
|
||||||
|
- For less popular languages, that meant empty trends
|
||||||
|
- Now, trends in your preferred languages' are shown on top, with others beneath
|
||||||
|
- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296))
|
||||||
|
- Add privacy icons to report modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19190))
|
||||||
|
- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014))
|
||||||
|
- Add 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 meta tag for official iOS app ([Gargron](https://github.com/mastodon/mastodon/pull/16599))
|
||||||
|
- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506))
|
||||||
|
- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737))
|
||||||
|
- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209))
|
||||||
|
- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248))
|
||||||
|
- Set for how long remote posts or media should be cached on your server
|
||||||
|
- Hands-off alternative to `tootctl` commands
|
||||||
|
- **Add customizable user roles** ([Gargron](https://github.com/mastodon/mastodon/pull/18641), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18812), [Gargron](https://github.com/mastodon/mastodon/pull/19040), [tribela](https://github.com/mastodon/mastodon/pull/18825), [tribela](https://github.com/mastodon/mastodon/pull/18826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18776), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18777), [unextro](https://github.com/mastodon/mastodon/pull/18786), [tribela](https://github.com/mastodon/mastodon/pull/18824), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19436))
|
||||||
|
- Previously, there were 3 hard-coded roles, user, moderator, and admin
|
||||||
|
- Create your own roles and decide which permissions they should have
|
||||||
|
- Add notifications for new reports ([Gargron](https://github.com/mastodon/mastodon/pull/18697), [Gargron](https://github.com/mastodon/mastodon/pull/19475))
|
||||||
|
- Add ability to select all accounts matching search for batch actions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19053), [Gargron](https://github.com/mastodon/mastodon/pull/19054))
|
||||||
|
- Add ability to view previous edits of a status in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19462))
|
||||||
|
- Add ability to block sign-ups from IP ([Gargron](https://github.com/mastodon/mastodon/pull/19037))
|
||||||
|
- **Add webhooks to admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18510))
|
||||||
|
- Add admin API for managing domain allows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18668))
|
||||||
|
- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247))
|
||||||
|
- Add admin API for managing e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19066))
|
||||||
|
- Add admin API for managing canonical e-mail blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19067))
|
||||||
|
- Add admin API for managing IP blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19065))
|
||||||
|
- Add `services` and `metadata` to the NodeInfo endpoint ([MFTabriz](https://github.com/mastodon/mastodon/pull/18563))
|
||||||
|
- Add `--remove-role` option to `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/19477))
|
||||||
|
- Add `--days` option to `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/18425))
|
||||||
|
- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642))
|
||||||
|
- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757))
|
||||||
|
- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710))
|
||||||
|
- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103))
|
||||||
|
- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273))
|
||||||
|
- The web app can now be accessed without being logged in
|
||||||
|
- No more `/web` prefix on web app paths
|
||||||
|
- Profiles, posts, and other public pages now use the same interface for logged in and logged out users
|
||||||
|
- The web app displays a server information banner
|
||||||
|
- Pop-up windows for remote interaction have been replaced with a modal window
|
||||||
|
- No need to type in your username for remote interaction, copy-paste-to-search method explained
|
||||||
|
- Various hints throughout the app explain what the different timelines are
|
||||||
|
- New about page design
|
||||||
|
- New privacy policy page design shows when the policy was last updated
|
||||||
|
- All sections of the web app now have appropriate window titles
|
||||||
|
- The layout of the interface has been streamlined between different screen sizes
|
||||||
|
- Posts now use more horizontal space
|
||||||
|
- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583))
|
||||||
|
- Change language to be carried over on reply in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18557))
|
||||||
|
- Change "Unfollow" to "Cancel follow request" when request still pending in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/19363))
|
||||||
|
- **Change post filtering system** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18058), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19050), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18894), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19051), [noellabo](https://github.com/mastodon/mastodon/pull/18923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18744))
|
||||||
|
- Filtered keywords and phrases can now be grouped into named categories
|
||||||
|
- Filtered posts show which exact filter was hit
|
||||||
|
- Individual posts can be added to a filter
|
||||||
|
- You can peek inside filtered posts anyway
|
||||||
|
- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249))
|
||||||
|
- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854))
|
||||||
|
- Change public timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291))
|
||||||
|
- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407))
|
||||||
|
- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356))
|
||||||
|
- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979))
|
||||||
|
- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788))
|
||||||
|
- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977))
|
||||||
|
- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326))
|
||||||
|
- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964))
|
||||||
|
- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove setting that disables account deletes ([Gargron](https://github.com/mastodon/mastodon/pull/17683))
|
||||||
|
- Remove digest e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/17985))
|
||||||
|
- Remove unnecessary sections from welcome e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/19299))
|
||||||
|
- Remove item titles from RSS feeds ([Gargron](https://github.com/mastodon/mastodon/pull/18640))
|
||||||
|
- Remove volume number from hashtags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19253))
|
||||||
|
- Remove Nanobox configuration ([tonyjiang](https://github.com/mastodon/mastodon/pull/17881))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817))
|
||||||
|
- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455))
|
||||||
|
- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428))
|
||||||
|
- Fix button to dismiss suggestions not showing up in search results in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19325))
|
||||||
|
- Fix language dropdown sometimes not appearing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19246))
|
||||||
|
- Fix quickly switching notification filters resulting in empty or incorrect list in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18960))
|
||||||
|
- Fix media modal link button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18877))
|
||||||
|
- Fix error upon successful account migration ([Gargron](https://github.com/mastodon/mastodon/pull/19386))
|
||||||
|
- Fix negatives values in search index causing queries to fail ([Gargron](https://github.com/mastodon/mastodon/pull/19464), [Gargron](https://github.com/mastodon/mastodon/pull/19481))
|
||||||
|
- Fix error when searching for invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18580))
|
||||||
|
- Fix IP blocks not having a unique index ([Gargron](https://github.com/mastodon/mastodon/pull/19456))
|
||||||
|
- Fix remote account in contact account setting not being used ([Gargron](https://github.com/mastodon/mastodon/pull/19351))
|
||||||
|
- Fix swallowing mentions of unconfirmed/unapproved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19191))
|
||||||
|
- Fix incorrect and slow cache invalidation when blocking domain and removing media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19062))
|
||||||
|
- Fix HTTPs redirect behaviour when running as I2P service ([gi-yt](https://github.com/mastodon/mastodon/pull/18929))
|
||||||
|
- Fix deleted pinned posts potentially counting towards the pinned posts limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19005))
|
||||||
|
- Fix compatibility with OpenSSL 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18449))
|
||||||
|
- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760))
|
||||||
|
- Fix suspicious sign-in mails never being sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18599))
|
||||||
|
- Fix fallback locale when somehow user's locale is an empty string ([tribela](https://github.com/mastodon/mastodon/pull/18543))
|
||||||
|
- Fix avatar/header not being deleted locally when deleted on remote account ([tribela](https://github.com/mastodon/mastodon/pull/18973))
|
||||||
|
- Fix missing `,` in Blurhash validation ([noellabo](https://github.com/mastodon/mastodon/pull/18660))
|
||||||
|
- Fix order by most recent not working for relationships page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/18996))
|
||||||
|
- Fix uncaught error when invalid date is supplied to API ([Gargron](https://github.com/mastodon/mastodon/pull/19480))
|
||||||
|
- Fix REST API sometimes returning HTML on error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19135))
|
||||||
|
- Fix ambiguous column names in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/19206))
|
||||||
|
- Fix ambiguous column names in `tootctl search deploy` ([mashirozx](https://github.com/mastodon/mastodon/pull/18993))
|
||||||
|
- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662))
|
||||||
|
- Fix `CAS_DISPLAY_NAME`, `SAML_DISPLAY_NAME` and `OIDC_DISPLAY_NAME` being ignored ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18568))
|
||||||
|
- Fix various typos in comments throughout the codebase ([luzpaz](https://github.com/mastodon/mastodon/pull/18604))
|
||||||
|
|
||||||
## [3.5.3] - 2022-05-26
|
## [3.5.3] - 2022-05-26
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -75,7 +201,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190))
|
- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190))
|
||||||
- The IPs of the blocked e-mail domain or its MX records are no longer checked
|
- The IPs of the blocked e-mail domain or its MX records are no longer checked
|
||||||
- Previously it was too easy to block e-mail providers by mistake
|
- Previously it was too easy to block e-mail providers by mistake
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260))
|
- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260))
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -72,6 +72,7 @@ gem 'rack-attack', '~> 6.6'
|
||||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 6.0'
|
gem 'rails-i18n', '~> 6.0'
|
||||||
gem 'rails-settings-cached', '~> 0.6'
|
gem 'rails-settings-cached', '~> 0.6'
|
||||||
|
gem 'redcarpet', '~> 3.5'
|
||||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 2.1'
|
gem 'rqrcode', '~> 2.1'
|
||||||
|
@ -98,8 +99,6 @@ gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.2'
|
gem 'json-ld-preloaded', '~> 3.2'
|
||||||
gem 'rdf-normalize', '~> 0.5'
|
gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
gem 'redcarpet', '~> 3.5'
|
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.30'
|
gem 'fabrication', '~> 2.30'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -272,7 +272,7 @@ GEM
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.5.3)
|
fugit (1.7.1)
|
||||||
et-orbi (~> 1, >= 1.2.7)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
|
@ -600,7 +600,7 @@ GEM
|
||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.8.1)
|
rufus-scheduler (3.8.2)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
|
@ -617,10 +617,10 @@ GEM
|
||||||
redis (>= 4.5.0, < 5)
|
redis (>= 4.5.0, < 5)
|
||||||
sidekiq-bulk (0.2.0)
|
sidekiq-bulk (0.2.0)
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (4.0.2)
|
sidekiq-scheduler (4.0.3)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 4)
|
sidekiq (>= 4, < 7)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.27)
|
sidekiq-unique-jobs (7.1.27)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
|
@ -649,7 +649,7 @@ GEM
|
||||||
sshkit (1.21.2)
|
sshkit (1.21.2)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.21)
|
stackprof (0.2.22)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (3.0.0)
|
stoplight (3.0.0)
|
||||||
strong_migrations (0.7.9)
|
strong_migrations (0.7.9)
|
||||||
|
@ -664,7 +664,7 @@ GEM
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.2.1)
|
thor (1.2.1)
|
||||||
tilt (2.0.10)
|
tilt (2.0.11)
|
||||||
tpm-key_attestation (0.11.0)
|
tpm-key_attestation (0.11.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
openssl (> 2.0, < 3.1)
|
openssl (> 2.0, < 3.1)
|
||||||
|
|
|
@ -1,72 +1,19 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AboutController < ApplicationController
|
class AboutController < ApplicationController
|
||||||
include RegistrationSpamConcern
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_pack
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :require_open_federation!, only: [:show, :more]
|
|
||||||
before_action :set_body_classes, only: :show
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_expires_in, only: [:more]
|
|
||||||
before_action :set_registration_form_time, only: :show
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, only: [:more]
|
def show
|
||||||
|
expires_in 0, public: true unless user_signed_in?
|
||||||
def show; end
|
|
||||||
|
|
||||||
def more
|
|
||||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
|
||||||
|
|
||||||
toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
|
|
||||||
|
|
||||||
@rules = Rule.ordered
|
|
||||||
@contents = toc_generator.html
|
|
||||||
@table_of_contents = toc_generator.toc
|
|
||||||
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :display_blocks?
|
|
||||||
helper_method :display_blocks_rationale?
|
|
||||||
helper_method :public_fetch_mode?
|
|
||||||
helper_method :new_user
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def require_open_federation!
|
|
||||||
not_found if whitelist_mode?
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_blocks?
|
|
||||||
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_blocks_rationale?
|
|
||||||
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_user
|
|
||||||
User.new.tap do |user|
|
|
||||||
user.build_account
|
|
||||||
user.build_invite_request
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@hide_navbar = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_expires_in
|
|
||||||
expires_in 0, public: true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AccountFollowController < ApplicationController
|
|
||||||
include AccountControllerConcern
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
|
|
||||||
def create
|
|
||||||
FollowService.new.call(current_user.account, @account, with_rate_limit: true)
|
|
||||||
redirect_to account_path(@account)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,12 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AccountUnfollowController < ApplicationController
|
|
||||||
include AccountControllerConcern
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
|
|
||||||
def create
|
|
||||||
UnfollowService.new.call(current_user.account, @account)
|
|
||||||
redirect_to account_path(@account)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -9,7 +9,6 @@ class AccountsController < ApplicationController
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
before_action :set_body_classes
|
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
|
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||||
|
@ -17,26 +16,7 @@ class AccountsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
|
||||||
expires_in 0, public: true unless user_signed_in?
|
expires_in 0, public: true unless user_signed_in?
|
||||||
|
|
||||||
@pinned_statuses = []
|
|
||||||
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
|
|
||||||
@featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
|
|
||||||
|
|
||||||
if current_account && @account.blocking?(current_account)
|
|
||||||
@statuses = []
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
@pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
|
|
||||||
@statuses = cached_filtered_status_page
|
|
||||||
@rss_url = rss_url
|
|
||||||
|
|
||||||
unless @statuses.empty?
|
|
||||||
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id
|
|
||||||
@newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
|
@ -56,18 +36,6 @@ class AccountsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'with-modals'
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_pinned_statuses?
|
|
||||||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_pinned_statuses
|
|
||||||
@account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted])
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_statuses
|
def filtered_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
statuses.merge!(hashtag_scope) if tag_requested?
|
statuses.merge!(hashtag_scope) if tag_requested?
|
||||||
|
@ -114,26 +82,6 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def older_url
|
|
||||||
pagination_url(max_id: @statuses.last.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def newer_url
|
|
||||||
pagination_url(min_id: @statuses.first.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_url(max_id: nil, min_id: nil)
|
|
||||||
if tag_requested?
|
|
||||||
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
|
|
||||||
elsif media_requested?
|
|
||||||
short_account_media_url(@account, max_id: max_id, min_id: min_id)
|
|
||||||
elsif replies_requested?
|
|
||||||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
|
||||||
else
|
|
||||||
short_account_url(@account, max_id: max_id, min_id: min_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def media_requested?
|
def media_requested?
|
||||||
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
||||||
end
|
end
|
||||||
|
@ -146,13 +94,6 @@ class AccountsController < ApplicationController
|
||||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_filtered_status_pins
|
|
||||||
cache_collection(
|
|
||||||
filtered_pinned_statuses,
|
|
||||||
Status
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cached_filtered_status_page
|
def cached_filtered_status_page
|
||||||
cache_collection_paginated_by_id(
|
cache_collection_paginated_by_id(
|
||||||
filtered_statuses,
|
filtered_statuses,
|
||||||
|
|
|
@ -34,7 +34,7 @@ module Admin
|
||||||
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.custom_emojis.no_emoji_selected')
|
||||||
rescue Mastodon::NotPermittedError
|
rescue Mastodon::NotPermittedError
|
||||||
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
|
|
@ -62,7 +62,7 @@ module Admin
|
||||||
|
|
||||||
def export_data
|
def export_data
|
||||||
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
||||||
DomainBlock.with_user_facing_limitations.each do |instance|
|
DomainBlock.with_limitations.each do |instance|
|
||||||
content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate]
|
content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module Admin
|
||||||
def index
|
def index
|
||||||
authorize :ip_block, :index?
|
authorize :ip_block, :index?
|
||||||
|
|
||||||
@ip_blocks = IpBlock.page(params[:page])
|
@ip_blocks = IpBlock.order(ip: :asc).page(params[:page])
|
||||||
@form = Form::IpBlockBatch.new
|
@form = Form::IpBlockBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
9
app/controllers/admin/settings/about_controller.rb
Normal file
9
app/controllers/admin/settings/about_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::AboutController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_about_path
|
||||||
|
end
|
||||||
|
end
|
9
app/controllers/admin/settings/appearance_controller.rb
Normal file
9
app/controllers/admin/settings/appearance_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::AppearanceController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_appearance_path
|
||||||
|
end
|
||||||
|
end
|
9
app/controllers/admin/settings/branding_controller.rb
Normal file
9
app/controllers/admin/settings/branding_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::BrandingController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_branding_path
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::ContentRetentionController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_content_retention_path
|
||||||
|
end
|
||||||
|
end
|
9
app/controllers/admin/settings/discovery_controller.rb
Normal file
9
app/controllers/admin/settings/discovery_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::DiscoveryController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_discovery_path
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_registrations_path
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class SettingsController < BaseController
|
class SettingsController < BaseController
|
||||||
def edit
|
def show
|
||||||
authorize :settings, :show?
|
authorize :settings, :show?
|
||||||
|
|
||||||
@admin_settings = Form::AdminSettings.new
|
@admin_settings = Form::AdminSettings.new
|
||||||
|
@ -15,14 +15,18 @@ module Admin
|
||||||
|
|
||||||
if @admin_settings.save
|
if @admin_settings.save
|
||||||
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
||||||
redirect_to edit_admin_settings_path
|
redirect_to after_update_redirect_path
|
||||||
else
|
else
|
||||||
render :edit
|
render :show
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
def settings_params
|
def settings_params
|
||||||
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
|
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Admin
|
||||||
|
|
||||||
@site_upload.destroy!
|
@site_upload.destroy!
|
||||||
|
|
||||||
redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -3,18 +3,23 @@
|
||||||
module Admin
|
module Admin
|
||||||
class StatusesController < BaseController
|
class StatusesController < BaseController
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
before_action :set_statuses
|
before_action :set_statuses, except: :show
|
||||||
|
before_action :set_status, only: :show
|
||||||
|
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :status, :index?
|
authorize [:admin, :status], :index?
|
||||||
|
|
||||||
@status_batch_action = Admin::StatusBatchAction.new
|
@status_batch_action = Admin::StatusBatchAction.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize [:admin, @status], :show?
|
||||||
|
end
|
||||||
|
|
||||||
def batch
|
def batch
|
||||||
authorize :status, :index?
|
authorize [:admin, :status], :index?
|
||||||
|
|
||||||
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
|
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
|
||||||
@status_batch_action.save!
|
@status_batch_action.save!
|
||||||
|
@ -32,6 +37,7 @@ module Admin
|
||||||
|
|
||||||
def after_create_redirect_path
|
def after_create_redirect_path
|
||||||
report_id = @status_batch_action&.report_id || params[:report_id]
|
report_id = @status_batch_action&.report_id || params[:report_id]
|
||||||
|
|
||||||
if report_id.present?
|
if report_id.present?
|
||||||
admin_report_path(report_id)
|
admin_report_path(report_id)
|
||||||
else
|
else
|
||||||
|
@ -43,6 +49,10 @@ module Admin
|
||||||
@account = Account.find(params[:account_id])
|
@account = Account.find(params[:account_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = @account.statuses.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def set_statuses
|
def set_statuses
|
||||||
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
||||||
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.trends.links.publishers.no_publisher_selected')
|
||||||
ensure
|
ensure
|
||||||
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
|
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
authorize :preview_card, :review?
|
authorize :preview_card, :review?
|
||||||
|
|
||||||
|
@locales = PreviewCardTrend.pluck('distinct language')
|
||||||
@preview_cards = filtered_preview_cards.page(params[:page])
|
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||||
@form = Trends::PreviewCardBatch.new
|
@form = Trends::PreviewCardBatch.new
|
||||||
end
|
end
|
||||||
|
@ -14,7 +15,7 @@ class Admin::Trends::LinksController < Admin::BaseController
|
||||||
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.trends.links.no_link_selected')
|
||||||
ensure
|
ensure
|
||||||
redirect_to admin_trends_links_path(filter_params)
|
redirect_to admin_trends_links_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
class Admin::Trends::StatusesController < Admin::BaseController
|
class Admin::Trends::StatusesController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
authorize :status, :review?
|
authorize [:admin, :status], :review?
|
||||||
|
|
||||||
|
@locales = StatusTrend.pluck('distinct language')
|
||||||
@statuses = filtered_statuses.page(params[:page])
|
@statuses = filtered_statuses.page(params[:page])
|
||||||
@form = Trends::StatusBatch.new
|
@form = Trends::StatusBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch
|
def batch
|
||||||
authorize :status, :review?
|
authorize [:admin, :status], :review?
|
||||||
|
|
||||||
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.trends.statuses.no_status_selected')
|
||||||
ensure
|
ensure
|
||||||
redirect_to admin_trends_statuses_path(filter_params)
|
redirect_to admin_trends_statuses_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Admin::Trends::TagsController < Admin::BaseController
|
||||||
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.trends.tags.no_tag_selected')
|
||||||
ensure
|
ensure
|
||||||
redirect_to admin_trends_tags_path(filter_params)
|
redirect_to admin_trends_tags_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,6 +24,10 @@ class Api::BaseController < ApplicationController
|
||||||
render json: { error: 'Duplicate record' }, status: 422
|
render json: { error: 'Duplicate record' }, status: 422
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from Date::Error do
|
||||||
|
render json: { error: 'Invalid date supplied' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
render json: { error: 'Record not found' }, status: 404
|
render json: { error: 'Record not found' }, status: 404
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,14 +60,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @account, :destroy?
|
authorize @account, :destroy?
|
||||||
json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer
|
|
||||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
render json: json
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsensitive
|
def unsensitive
|
||||||
|
|
|
@ -35,20 +35,16 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize :canonical_email_block, :create?
|
authorize :canonical_email_block, :create?
|
||||||
|
|
||||||
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
|
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
|
||||||
log_action :create, @canonical_email_block
|
log_action :create, @canonical_email_block
|
||||||
|
|
||||||
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @canonical_email_block, :destroy?
|
authorize @canonical_email_block, :destroy?
|
||||||
|
|
||||||
@canonical_email_block.destroy!
|
@canonical_email_block.destroy!
|
||||||
log_action :destroy, @canonical_email_block
|
log_action :destroy, @canonical_email_block
|
||||||
|
render_empty
|
||||||
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -43,7 +43,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
authorize @domain_allow, :destroy?
|
authorize @domain_allow, :destroy?
|
||||||
UnallowDomainService.new.call(@domain_allow)
|
UnallowDomainService.new.call(@domain_allow)
|
||||||
log_action :destroy, @domain_allow
|
log_action :destroy, @domain_allow
|
||||||
render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -40,7 +40,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @domain_block, :update?
|
authorize @domain_block, :update?
|
||||||
|
|
||||||
@domain_block.update(domain_block_params)
|
@domain_block.update(domain_block_params)
|
||||||
severity_changed = @domain_block.severity_changed?
|
severity_changed = @domain_block.severity_changed?
|
||||||
@domain_block.save!
|
@domain_block.save!
|
||||||
|
@ -53,7 +52,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
authorize @domain_block, :destroy?
|
authorize @domain_block, :destroy?
|
||||||
UnblockDomainService.new.call(@domain_block)
|
UnblockDomainService.new.call(@domain_block)
|
||||||
log_action :destroy, @domain_block
|
log_action :destroy, @domain_block
|
||||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -39,11 +39,9 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @email_domain_block, :destroy?
|
authorize @email_domain_block, :destroy?
|
||||||
|
|
||||||
@email_domain_block.destroy!
|
@email_domain_block.destroy!
|
||||||
log_action :destroy, @email_domain_block
|
log_action :destroy, @email_domain_block
|
||||||
|
render_empty
|
||||||
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -20,10 +20,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize :ip_block, :create?
|
authorize :ip_block, :create?
|
||||||
|
|
||||||
@ip_block = IpBlock.create!(resource_params)
|
@ip_block = IpBlock.create!(resource_params)
|
||||||
log_action :create, @ip_block
|
log_action :create, @ip_block
|
||||||
|
|
||||||
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,20 +37,16 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @ip_block, :update?
|
authorize @ip_block, :update?
|
||||||
|
|
||||||
@ip_block.update(resource_params)
|
@ip_block.update(resource_params)
|
||||||
log_action :update, @ip_block
|
log_action :update, @ip_block
|
||||||
|
|
||||||
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @ip_block, :destroy?
|
authorize @ip_block, :destroy?
|
||||||
|
|
||||||
@ip_block.destroy!
|
@ip_block.destroy!
|
||||||
log_action :destroy, @ip_block
|
log_action :destroy, @ip_block
|
||||||
|
render_empty
|
||||||
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -13,12 +13,12 @@ class Api::V1::FeaturedTagsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@featured_tag = current_account.featured_tags.create!(featured_tag_params)
|
featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name])
|
||||||
render json: @featured_tag, serializer: REST::FeaturedTagSerializer
|
render json: featured_tag, serializer: REST::FeaturedTagSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@featured_tag.destroy!
|
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
23
app/controllers/api/v1/instances/domain_blocks_controller.rb
Normal file
23
app/controllers/api/v1/instances/domain_blocks_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :require_enabled_api!
|
||||||
|
before_action :set_domain_blocks
|
||||||
|
|
||||||
|
def index
|
||||||
|
expires_in 3.minutes, public: true
|
||||||
|
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_enabled_api!
|
||||||
|
head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_domain_blocks
|
||||||
|
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :set_extended_description
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 3.minutes, public: true
|
||||||
|
render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_extended_description
|
||||||
|
@extended_description = ExtendedDescription.current
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
|
||||||
|
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||||
|
|
||||||
|
before_action :set_privacy_policy
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 1.day, public: true
|
||||||
|
render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_privacy_policy
|
||||||
|
@privacy_policy = PrivacyPolicy.current
|
||||||
|
end
|
||||||
|
end
|
|
@ -66,6 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
|
language: status_params[:language],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
content_type: status_params[:content_type]
|
content_type: status_params[:content_type]
|
||||||
|
@ -79,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
|
StatusPin.find_by(status: @status)&.destroy
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ 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),
|
||||||
|
|
|
@ -28,7 +28,9 @@ class Api::V1::Trends::LinksController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def links_from_trends
|
def links_from_trends
|
||||||
Trends.links.query.allowed.in_locale(content_locale)
|
scope = Trends.links.query.allowed.in_locale(content_locale)
|
||||||
|
scope = scope.filtered_for(current_account) if user_signed_in?
|
||||||
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
|
|
@ -5,8 +5,8 @@ class Api::V2::SearchController < Api::BaseController
|
||||||
|
|
||||||
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i
|
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:search' }
|
before_action -> { authorize_if_got_token! :read, :'read:search' }
|
||||||
before_action :require_user!
|
before_action :validate_search_params!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@search = Search.new(search_results)
|
@search = Search.new(search_results)
|
||||||
|
@ -19,6 +19,16 @@ class Api::V2::SearchController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def validate_search_params!
|
||||||
|
params.require(:q)
|
||||||
|
|
||||||
|
return if user_signed_in?
|
||||||
|
|
||||||
|
return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present?
|
||||||
|
|
||||||
|
render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve)
|
||||||
|
end
|
||||||
|
|
||||||
def search_results
|
def search_results
|
||||||
SearchService.new.call(
|
SearchService.new.call(
|
||||||
params[:q],
|
params[:q],
|
||||||
|
|
|
@ -15,6 +15,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||||
before_action :require_not_suspended!, only: [:update]
|
before_action :require_not_suspended!, only: [:update]
|
||||||
before_action :set_cache_headers, only: [:edit, :update]
|
before_action :set_cache_headers, only: [:edit, :update]
|
||||||
|
before_action :set_rules, only: :new
|
||||||
|
before_action :require_rules_acceptance!, only: :new
|
||||||
before_action :set_registration_form_time, only: :new
|
before_action :set_registration_form_time, only: :new
|
||||||
|
|
||||||
skip_before_action :require_functional!, only: [:edit, :update]
|
skip_before_action :require_functional!, only: [:edit, :update]
|
||||||
|
@ -56,7 +58,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
def configure_sign_up_params
|
def configure_sign_up_params
|
||||||
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
||||||
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
u.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -143,6 +145,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
forbidden if current_account.suspended?
|
forbidden if current_account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_rules
|
||||||
|
@rules = Rule.ordered
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_rules_acceptance!
|
||||||
|
return if @rules.empty? || (session[:accept_token].present? && params[:accept] == session[:accept_token])
|
||||||
|
|
||||||
|
@accept_token = session[:accept_token] = SecureRandom.hex
|
||||||
|
@invite_code = invite_code
|
||||||
|
|
||||||
|
set_locale { render :rules }
|
||||||
|
end
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,13 +3,12 @@
|
||||||
module AccountControllerConcern
|
module AccountControllerConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
include WebAppControllerConcern
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
||||||
FOLLOW_PER_PAGE = 12
|
FOLLOW_PER_PAGE = 12
|
||||||
|
|
||||||
included do
|
included do
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
||||||
end
|
end
|
||||||
|
|
32
app/controllers/concerns/web_app_controller_concern.rb
Normal file
32
app/controllers/concerns/web_app_controller_concern.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WebAppControllerConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_action :set_pack
|
||||||
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
|
before_action :set_app_body_class
|
||||||
|
before_action :set_referrer_policy_header
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_app_body_class
|
||||||
|
@body_classes = 'app-body'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_referrer_policy_header
|
||||||
|
response.headers['Referrer-Policy'] = 'origin'
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_unauthenticated_to_permalinks!
|
||||||
|
return if user_signed_in?
|
||||||
|
|
||||||
|
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||||
|
|
||||||
|
redirect_to(redirect_path) if redirect_path.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_pack
|
||||||
|
use_pack 'home'
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,37 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class DirectoriesController < ApplicationController
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
|
||||||
before_action :require_enabled!
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
before_action :set_accounts
|
|
||||||
before_action :set_pack
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
|
||||||
|
|
||||||
def index
|
|
||||||
render :index
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'share'
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_enabled!
|
|
||||||
return not_found unless Setting.profile_directory
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_accounts
|
|
||||||
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
|
|
||||||
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,6 +3,7 @@
|
||||||
class FollowerAccountsController < ApplicationController
|
class FollowerAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
@ -13,12 +14,7 @@ class FollowerAccountsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
|
||||||
expires_in 0, public: true unless user_signed_in?
|
expires_in 0, public: true unless user_signed_in?
|
||||||
|
|
||||||
next if @account.hide_collections?
|
|
||||||
|
|
||||||
follows
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class FollowingAccountsController < ApplicationController
|
class FollowingAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
@ -13,12 +14,7 @@ class FollowingAccountsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
|
||||||
expires_in 0, public: true unless user_signed_in?
|
expires_in 0, public: true unless user_signed_in?
|
||||||
|
|
||||||
next if @account.hide_collections?
|
|
||||||
|
|
||||||
follows
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -1,47 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
before_action :redirect_unauthenticated_to_permalinks!
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_referrer_policy_header
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
expires_in 0, public: true unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redirect_unauthenticated_to_permalinks!
|
|
||||||
return if user_signed_in?
|
|
||||||
|
|
||||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
|
||||||
redirect_path ||= default_redirect_path
|
|
||||||
|
|
||||||
redirect_to(redirect_path) if redirect_path.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'home'
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_redirect_path
|
|
||||||
if whitelist_mode?
|
|
||||||
new_user_session_path
|
|
||||||
elsif request.path.start_with?('/web')
|
|
||||||
nil
|
|
||||||
elsif single_user_mode?
|
|
||||||
short_account_path(Account.local.without_suspended.where('id > 0').first)
|
|
||||||
else
|
|
||||||
about_path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_referrer_policy_header
|
|
||||||
response.headers['Referrer-Policy'] = 'origin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,28 +1,19 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PrivacyController < ApplicationController
|
class PrivacyController < ApplicationController
|
||||||
layout 'public'
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
before_action :set_expires_in
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show; end
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 0, public: true if current_account.nil?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_expires_in
|
|
||||||
expires_in 0, public: true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PublicTimelinesController < ApplicationController
|
|
||||||
before_action :set_pack
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
|
||||||
before_action :require_enabled!
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def require_enabled!
|
|
||||||
not_found unless Setting.timeline_preview
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'with-modals'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'about'
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,46 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RemoteFollowController < ApplicationController
|
|
||||||
include AccountOwnedConcern
|
|
||||||
|
|
||||||
layout 'modal'
|
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def new
|
|
||||||
@remote_follow = RemoteFollow.new(session_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@remote_follow = RemoteFollow.new(resource_params)
|
|
||||||
|
|
||||||
if @remote_follow.valid?
|
|
||||||
session[:remote_follow] = @remote_follow.acct
|
|
||||||
redirect_to @remote_follow.subscribe_address_for(@account)
|
|
||||||
else
|
|
||||||
render :new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:remote_follow).permit(:acct)
|
|
||||||
end
|
|
||||||
|
|
||||||
def session_params
|
|
||||||
{ acct: session[:remote_follow] || current_account&.username }
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'modal'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'modal-layout'
|
|
||||||
@hide_header = true
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,60 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RemoteInteractionController < ApplicationController
|
|
||||||
include Authorization
|
|
||||||
|
|
||||||
layout 'modal'
|
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
|
||||||
before_action :set_interaction_type
|
|
||||||
before_action :set_status
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_pack
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
|
||||||
|
|
||||||
def new
|
|
||||||
@remote_follow = RemoteFollow.new(session_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@remote_follow = RemoteFollow.new(resource_params)
|
|
||||||
|
|
||||||
if @remote_follow.valid?
|
|
||||||
session[:remote_follow] = @remote_follow.acct
|
|
||||||
redirect_to @remote_follow.interact_address_for(@status)
|
|
||||||
else
|
|
||||||
render :new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:remote_follow).permit(:acct)
|
|
||||||
end
|
|
||||||
|
|
||||||
def session_params
|
|
||||||
{ acct: session[:remote_follow] || current_account&.username }
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_status
|
|
||||||
@status = Status.find(params[:id])
|
|
||||||
authorize @status, :show?
|
|
||||||
rescue Mastodon::NotPermittedError
|
|
||||||
not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'modal-layout'
|
|
||||||
@hide_header = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'modal'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_interaction_type
|
|
||||||
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,7 +4,6 @@ class Settings::DeletesController < Settings::BaseController
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :require_not_suspended!
|
before_action :require_not_suspended!
|
||||||
before_action :check_enabled_deletion
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::DeleteConfirmation.new
|
@confirmation = Form::DeleteConfirmation.new
|
||||||
|
@ -21,10 +20,6 @@ class Settings::DeletesController < Settings::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_enabled_deletion
|
|
||||||
redirect_to root_path unless Setting.open_deletion
|
|
||||||
end
|
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:form_delete_confirmation).permit(:password, :username)
|
params.require(:form_delete_confirmation).permit(:password, :username)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,9 +10,9 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@featured_tag = current_account.featured_tags.new(featured_tag_params)
|
@featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], force: false)
|
||||||
|
|
||||||
if @featured_tag.save
|
if @featured_tag.valid?
|
||||||
redirect_to settings_featured_tags_path
|
redirect_to settings_featured_tags_path
|
||||||
else
|
else
|
||||||
set_featured_tags
|
set_featured_tags
|
||||||
|
@ -23,7 +23,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@featured_tag.destroy!
|
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
|
||||||
redirect_to settings_featured_tags_path
|
redirect_to settings_featured_tags_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class StatusesController < ApplicationController
|
class StatusesController < ApplicationController
|
||||||
|
include WebAppControllerConcern
|
||||||
include StatusControllerConcern
|
include StatusControllerConcern
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
before_action :redirect_to_original, only: :show
|
before_action :redirect_to_original, only: :show
|
||||||
before_action :set_referrer_policy_header, only: :show
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes, only: :embed
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
||||||
|
@ -27,11 +25,7 @@ class StatusesController < ApplicationController
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'public'
|
|
||||||
|
|
||||||
expires_in 10.seconds, public: true if current_account.nil?
|
expires_in 10.seconds, public: true if current_account.nil?
|
||||||
set_ancestors
|
|
||||||
set_descendants
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
@ -80,8 +74,4 @@ class StatusesController < ApplicationController
|
||||||
def redirect_to_original
|
def redirect_to_original
|
||||||
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
|
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_referrer_policy_header
|
|
||||||
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,18 +2,16 @@
|
||||||
|
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
include WebAppControllerConcern
|
||||||
|
|
||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
PAGE_SIZE_MAX = 200
|
PAGE_SIZE_MAX = 200
|
||||||
|
|
||||||
layout 'public'
|
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
before_action :authenticate_user!, if: :whitelist_mode?
|
||||||
before_action :set_local
|
before_action :set_local
|
||||||
before_action :set_tag
|
before_action :set_tag
|
||||||
before_action :set_statuses
|
before_action :set_statuses
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||||
|
@ -21,8 +19,7 @@ class TagsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
use_pack 'about'
|
expires_in 0, public: true unless user_signed_in?
|
||||||
expires_in 0, public: true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
|
@ -55,10 +52,6 @@ class TagsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'with-modals'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,54 +20,10 @@ module AccountsHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_action_button(account)
|
def account_action_button(account)
|
||||||
if user_signed_in?
|
return if account.memorial? || account.moved?
|
||||||
if account.id == current_user.account_id
|
|
||||||
link_to settings_profile_url, class: 'button logo-button' do
|
|
||||||
safe_join([logo_as_symbol, t('settings.edit_profile')])
|
|
||||||
end
|
|
||||||
elsif current_account.following?(account) || current_account.requested?(account)
|
|
||||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
|
||||||
safe_join([logo_as_symbol, t('accounts.unfollow')])
|
|
||||||
end
|
|
||||||
elsif !(account.memorial? || account.moved?)
|
|
||||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
|
||||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elsif !(account.memorial? || account.moved?)
|
|
||||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
|
||||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def minimal_account_action_button(account)
|
link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
|
||||||
if user_signed_in?
|
safe_join([logo_as_symbol, t('accounts.follow')])
|
||||||
return if account.id == current_user.account_id
|
|
||||||
|
|
||||||
if current_account.following?(account) || current_account.requested?(account)
|
|
||||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
|
||||||
fa_icon('user-times fw')
|
|
||||||
end
|
|
||||||
elsif !(account.memorial? || account.moved?)
|
|
||||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
|
||||||
fa_icon('user-plus fw')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elsif !(account.memorial? || account.moved?)
|
|
||||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
|
||||||
fa_icon('user-plus fw')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_badge(account)
|
|
||||||
if account.bot?
|
|
||||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
|
||||||
elsif account.group?
|
|
||||||
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
|
|
||||||
elsif account.user_role&.highlighted?
|
|
||||||
content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::SettingsHelper
|
module Admin::SettingsHelper
|
||||||
def site_upload_delete_hint(hint, var)
|
|
||||||
upload = SiteUpload.find_by(var: var.to_s)
|
|
||||||
return hint unless upload
|
|
||||||
|
|
||||||
link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
|
|
||||||
safe_join([hint, link], '<br/>'.html_safe)
|
|
||||||
end
|
|
||||||
|
|
||||||
def captcha_available?
|
def captcha_available?
|
||||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -87,10 +87,6 @@ module ApplicationHelper
|
||||||
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_deletion?
|
|
||||||
Setting.open_deletion
|
|
||||||
end
|
|
||||||
|
|
||||||
def locale_direction
|
def locale_direction
|
||||||
if RTL_LOCALES.include?(I18n.locale)
|
if RTL_LOCALES.include?(I18n.locale)
|
||||||
'rtl'
|
'rtl'
|
||||||
|
@ -199,10 +195,7 @@ module ApplicationHelper
|
||||||
|
|
||||||
def render_initial_state
|
def render_initial_state
|
||||||
state_params = {
|
state_params = {
|
||||||
settings: {
|
settings: {},
|
||||||
known_fediverse: Setting.show_known_fediverse_at_about_page,
|
|
||||||
},
|
|
||||||
|
|
||||||
text: [params[:title], params[:text], params[:url]].compact.join(' '),
|
text: [params[:title], params[:text], params[:url]].compact.join(' '),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +212,10 @@ module ApplicationHelper
|
||||||
state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
|
state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if single_user_mode?
|
||||||
|
state_params[:owner] = Account.local.without_suspended.where('id > 0').first
|
||||||
|
end
|
||||||
|
|
||||||
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
||||||
# rubocop:disable Rails/OutputSafety
|
# rubocop:disable Rails/OutputSafety
|
||||||
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
||||||
|
|
|
@ -23,7 +23,7 @@ module HomeHelper
|
||||||
else
|
else
|
||||||
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
|
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
|
||||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||||
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
|
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46)
|
||||||
end +
|
end +
|
||||||
content_tag(:span, class: 'display-name') do
|
content_tag(:span, class: 'display-name') do
|
||||||
content_tag(:bdi) do
|
content_tag(:bdi) do
|
||||||
|
|
|
@ -97,7 +97,7 @@ module LanguagesHelper
|
||||||
lg: ['Ganda', 'Luganda'].freeze,
|
lg: ['Ganda', 'Luganda'].freeze,
|
||||||
li: ['Limburgish', 'Limburgs'].freeze,
|
li: ['Limburgish', 'Limburgs'].freeze,
|
||||||
ln: ['Lingala', 'Lingála'].freeze,
|
ln: ['Lingala', 'Lingála'].freeze,
|
||||||
lo: ['Lao', 'ພາສາ'].freeze,
|
lo: ['Lao', 'ລາວ'].freeze,
|
||||||
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
||||||
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
||||||
lv: ['Latvian', 'latviešu valoda'].freeze,
|
lv: ['Latvian', 'latviešu valoda'].freeze,
|
||||||
|
|
|
@ -4,6 +4,24 @@ import 'packs/public-path';
|
||||||
import { delegate } from '@rails/ujs';
|
import { delegate } from '@rails/ujs';
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
const setAnnouncementEndsAttributes = (target) => {
|
||||||
|
const valid = target?.value && target?.validity?.valid;
|
||||||
|
const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
|
||||||
|
if (valid) {
|
||||||
|
element.classList.remove('optional');
|
||||||
|
element.required = true;
|
||||||
|
element.min = target.value;
|
||||||
|
} else {
|
||||||
|
element.classList.add('optional');
|
||||||
|
element.removeAttribute('required');
|
||||||
|
element.removeAttribute('min');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
|
||||||
|
setAnnouncementEndsAttributes(target);
|
||||||
|
});
|
||||||
|
|
||||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||||
|
|
||||||
const showSelectAll = () => {
|
const showSelectAll = () => {
|
||||||
|
@ -143,6 +161,20 @@ const onChangeRegistrationMode = (target) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertUTCDateTimeToLocal = (value) => {
|
||||||
|
const date = new Date(value + 'Z');
|
||||||
|
const twoChars = (x) => (x.toString().padStart(2, '0'));
|
||||||
|
return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertLocalDatetimeToUTC = (value) => {
|
||||||
|
const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
|
||||||
|
const match = re.exec(value);
|
||||||
|
const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
|
||||||
|
const fullISO8601 = date.toISOString();
|
||||||
|
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||||
|
};
|
||||||
|
|
||||||
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
|
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
|
@ -170,4 +202,26 @@ ready(() => {
|
||||||
e.target.href = url;
|
e.target.href = url;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => {
|
||||||
|
if (element.value) {
|
||||||
|
element.value = convertUTCDateTimeToLocal(element.value);
|
||||||
|
}
|
||||||
|
if (element.placeholder) {
|
||||||
|
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate(document, 'form', 'submit', ({ target }) => {
|
||||||
|
[].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
|
||||||
|
if (element.value && element.validity.valid) {
|
||||||
|
element.value = convertLocalDatetimeToUTC(element.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
|
||||||
|
if (announcementStartsAt) {
|
||||||
|
setAnnouncementEndsAttributes(announcementStartsAt);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,28 +6,6 @@ import ready from '../mastodon/ready';
|
||||||
const { delegate } = require('@rails/ujs');
|
const { delegate } = require('@rails/ujs');
|
||||||
const { length } = require('stringz');
|
const { length } = require('stringz');
|
||||||
|
|
||||||
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
|
|
||||||
if (button !== 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
window.location.href = target.href;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
delegate(document, '.modal-button', 'click', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let href;
|
|
||||||
|
|
||||||
if (e.target.nodeName !== 'A') {
|
|
||||||
href = e.target.parentNode.href;
|
|
||||||
} else {
|
|
||||||
href = e.target.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProfileAvatarAnimationHandler = (swapTo) => {
|
const getProfileAvatarAnimationHandler = (swapTo) => {
|
||||||
//animate avatar gifs on the profile page when moused over
|
//animate avatar gifs on the profile page when moused over
|
||||||
return ({ target }) => {
|
return ({ target }) => {
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import api from '../api';
|
import axios from 'axios';
|
||||||
import { CancelToken, isCancel } from 'axios';
|
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
import api from 'flavours/glitch/api';
|
||||||
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
||||||
import { useEmoji } from './emojis';
|
import { tagHistory } from 'flavours/glitch/settings';
|
||||||
import { tagHistory } from '../settings';
|
|
||||||
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
||||||
import resizeImage from 'flavours/glitch/utils/resize_image';
|
import resizeImage from 'flavours/glitch/utils/resize_image';
|
||||||
|
import { showAlert, showAlertForError } from './alerts';
|
||||||
|
import { useEmoji } from './emojis';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
import { updateTimeline } from './timelines';
|
|
||||||
import { showAlertForError } from './alerts';
|
|
||||||
import { showAlert } from './alerts';
|
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
import { defineMessages } from 'react-intl';
|
import { updateTimeline } from './timelines';
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
|
/** @type {AbortController | undefined} */
|
||||||
|
let fetchComposeSuggestionsAccountsController;
|
||||||
|
/** @type {AbortController | undefined} */
|
||||||
|
let fetchComposeSuggestionsTagsController;
|
||||||
|
|
||||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||||
export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
|
export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
|
||||||
|
@ -25,11 +27,13 @@ export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||||
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||||
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||||
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
|
||||||
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
||||||
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||||
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||||
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||||
|
export const COMPOSE_UPLOAD_PROCESSING = 'COMPOSE_UPLOAD_PROCESSING';
|
||||||
|
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||||
|
|
||||||
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
|
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
|
||||||
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
|
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
|
||||||
|
@ -83,10 +87,8 @@ const messages = defineMessages({
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
|
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
|
if (!getState().getIn(['compose', 'mounted'])) {
|
||||||
routerHistory.push('/publish');
|
routerHistory.push('/publish');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -307,13 +309,16 @@ export function uploadCompose(files) {
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
dispatch(uploadComposeSuccess(data, f));
|
dispatch(uploadComposeSuccess(data, f));
|
||||||
} else if (status === 202) {
|
} else if (status === 202) {
|
||||||
|
dispatch(uploadComposeProcessing());
|
||||||
|
|
||||||
let tryCount = 1;
|
let tryCount = 1;
|
||||||
|
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
dispatch(uploadComposeSuccess(response.data, f));
|
dispatch(uploadComposeSuccess(response.data, f));
|
||||||
} else if (response.status === 206) {
|
} else if (response.status === 206) {
|
||||||
let retryAfter = (Math.log2(tryCount) || 1) * 1000;
|
const retryAfter = (Math.log2(tryCount) || 1) * 1000;
|
||||||
tryCount += 1;
|
tryCount += 1;
|
||||||
setTimeout(() => poll(), retryAfter);
|
setTimeout(() => poll(), retryAfter);
|
||||||
}
|
}
|
||||||
|
@ -328,6 +333,10 @@ export function uploadCompose(files) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadComposeProcessing = () => ({
|
||||||
|
type: COMPOSE_UPLOAD_PROCESSING,
|
||||||
|
});
|
||||||
|
|
||||||
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
|
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
|
||||||
dispatch(uploadThumbnailRequest());
|
dispatch(uploadThumbnailRequest());
|
||||||
|
|
||||||
|
@ -472,8 +481,8 @@ export function undoUploadCompose(media_id) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearComposeSuggestions() {
|
export function clearComposeSuggestions() {
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
if (fetchComposeSuggestionsAccountsController) {
|
||||||
cancelFetchComposeSuggestionsAccounts();
|
fetchComposeSuggestionsAccountsController.abort();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
|
@ -481,14 +490,14 @@ export function clearComposeSuggestions() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
|
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
if (fetchComposeSuggestionsAccountsController) {
|
||||||
cancelFetchComposeSuggestionsAccounts();
|
fetchComposeSuggestionsAccountsController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchComposeSuggestionsAccountsController = new AbortController();
|
||||||
|
|
||||||
api(getState).get('/api/v1/accounts/search', {
|
api(getState).get('/api/v1/accounts/search', {
|
||||||
cancelToken: new CancelToken(cancel => {
|
signal: fetchComposeSuggestionsAccountsController.signal,
|
||||||
cancelFetchComposeSuggestionsAccounts = cancel;
|
|
||||||
}),
|
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
q: token.slice(1),
|
q: token.slice(1),
|
||||||
|
@ -499,9 +508,11 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (!isCancel(error)) {
|
if (!axios.isCancel(error)) {
|
||||||
dispatch(showAlertForError(error));
|
dispatch(showAlertForError(error));
|
||||||
}
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
fetchComposeSuggestionsAccountsController = undefined;
|
||||||
});
|
});
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
@ -511,16 +522,16 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
||||||
if (cancelFetchComposeSuggestionsTags) {
|
if (fetchComposeSuggestionsTagsController) {
|
||||||
cancelFetchComposeSuggestionsTags();
|
fetchComposeSuggestionsTagsController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateSuggestionTags(token));
|
dispatch(updateSuggestionTags(token));
|
||||||
|
|
||||||
|
fetchComposeSuggestionsTagsController = new AbortController();
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
cancelToken: new CancelToken(cancel => {
|
signal: fetchComposeSuggestionsTagsController.signal,
|
||||||
cancelFetchComposeSuggestionsTags = cancel;
|
|
||||||
}),
|
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
type: 'hashtags',
|
type: 'hashtags',
|
||||||
|
@ -531,9 +542,11 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
|
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (!isCancel(error)) {
|
if (!axios.isCancel(error)) {
|
||||||
dispatch(showAlertForError(error));
|
dispatch(showAlertForError(error));
|
||||||
}
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
fetchComposeSuggestionsTagsController = undefined;
|
||||||
});
|
});
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
|
34
app/javascript/flavours/glitch/actions/featured_tags.js
Normal file
34
app/javascript/flavours/glitch/actions/featured_tags.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
|
||||||
|
export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
|
||||||
|
export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchFeaturedTagsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
|
||||||
|
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
|
||||||
|
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsRequest = (id) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsSuccess = (id, tags) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchFeaturedTagsFail = (id, error) => ({
|
||||||
|
type: FEATURED_TAGS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
|
@ -29,7 +29,8 @@ export function clearSearch() {
|
||||||
|
|
||||||
export function submitSearch() {
|
export function submitSearch() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const value = getState().getIn(['search', 'value']);
|
const value = getState().getIn(['search', 'value']);
|
||||||
|
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
||||||
|
@ -41,7 +42,7 @@ export function submitSearch() {
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: true,
|
resolve: signedIn,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
|
|
@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
|
||||||
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
||||||
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
|
||||||
|
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
|
||||||
|
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
|
||||||
|
|
||||||
|
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
|
||||||
|
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
|
||||||
|
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchServer = () => (dispatch, getState) => {
|
export const fetchServer = () => (dispatch, getState) => {
|
||||||
dispatch(fetchServerRequest());
|
dispatch(fetchServerRequest());
|
||||||
|
|
||||||
|
@ -28,3 +36,56 @@ const fetchServerFail = error => ({
|
||||||
type: SERVER_FETCH_FAIL,
|
type: SERVER_FETCH_FAIL,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchExtendedDescriptionRequest());
|
||||||
|
|
||||||
|
api(getState)
|
||||||
|
.get('/api/v1/instance/extended_description')
|
||||||
|
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
|
||||||
|
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchExtendedDescriptionRequest = () => ({
|
||||||
|
type: EXTENDED_DESCRIPTION_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchExtendedDescriptionSuccess = description => ({
|
||||||
|
type: EXTENDED_DESCRIPTION_SUCCESS,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchExtendedDescriptionFail = error => ({
|
||||||
|
type: EXTENDED_DESCRIPTION_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchDomainBlocksRequest());
|
||||||
|
|
||||||
|
api(getState)
|
||||||
|
.get('/api/v1/instance/domain_blocks')
|
||||||
|
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response.status === 404) {
|
||||||
|
dispatch(fetchDomainBlocksSuccess(false, []));
|
||||||
|
} else {
|
||||||
|
dispatch(fetchDomainBlocksFail(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDomainBlocksRequest = () => ({
|
||||||
|
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
|
||||||
|
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
|
||||||
|
isAvailable,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDomainBlocksFail = error => ({
|
||||||
|
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
|
@ -156,8 +156,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
|
||||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
|
||||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LinkHeader from 'http-link-header';
|
import LinkHeader from 'http-link-header';
|
||||||
import ready from './ready';
|
import ready from './ready';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('axios').AxiosResponse} response
|
||||||
|
* @returns {LinkHeader}
|
||||||
|
*/
|
||||||
export const getLinks = response => {
|
export const getLinks = response => {
|
||||||
const value = response.headers.link;
|
const value = response.headers.link;
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return { refs: [] };
|
return new LinkHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
return LinkHeader.parse(value);
|
return LinkHeader.parse(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @type {import('axios').RawAxiosRequestHeaders} */
|
||||||
const csrfHeader = {};
|
const csrfHeader = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
const setCSRFHeader = () => {
|
const setCSRFHeader = () => {
|
||||||
|
/** @type {HTMLMetaElement | null} */
|
||||||
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
||||||
|
|
||||||
if (csrfToken) {
|
if (csrfToken) {
|
||||||
|
@ -24,6 +35,10 @@ const setCSRFHeader = () => {
|
||||||
|
|
||||||
ready(setCSRFHeader);
|
ready(setCSRFHeader);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => import('immutable').Map} getState
|
||||||
|
* @returns {import('axios').RawAxiosRequestHeaders}
|
||||||
|
*/
|
||||||
const authorizationHeaderFromState = getState => {
|
const authorizationHeaderFromState = getState => {
|
||||||
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
|
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
|
||||||
|
|
||||||
|
@ -36,17 +51,25 @@ const authorizationHeaderFromState = getState => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getState => axios.create({
|
/**
|
||||||
headers: {
|
* @param {() => import('immutable').Map} getState
|
||||||
...csrfHeader,
|
* @returns {import('axios').AxiosInstance}
|
||||||
...authorizationHeaderFromState(getState),
|
*/
|
||||||
},
|
export default function api(getState) {
|
||||||
|
return axios.create({
|
||||||
|
headers: {
|
||||||
|
...csrfHeader,
|
||||||
|
...authorizationHeaderFromState(getState),
|
||||||
|
},
|
||||||
|
|
||||||
transformResponse: [function (data) {
|
transformResponse: [
|
||||||
try {
|
function (data) {
|
||||||
return JSON.parse(data);
|
try {
|
||||||
} catch(Exception) {
|
return JSON.parse(data);
|
||||||
return data;
|
} catch {
|
||||||
}
|
return data;
|
||||||
}],
|
}
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -70,6 +70,8 @@ export default class Avatar extends React.PureComponent {
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
data-avatar-of={account && `@${account.get('acct')}`}
|
data-avatar-of={account && `@${account.get('acct')}`}
|
||||||
|
role='img'
|
||||||
|
aria-label={account?.get('acct')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import IconButton from './icon_button';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import { bannerSettings } from 'flavours/glitch/settings';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class DismissableBanner extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
visible: !bannerSettings.get(this.props.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDismiss = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
this.setState({ visible: false }, () => bannerSettings.set(id, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { visible } = this.state;
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dismissable-banner'>
|
||||||
|
<div className='dismissable-banner__message'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__action'>
|
||||||
|
<IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { source_url } from 'flavours/glitch/initial_state';
|
import { source_url } from 'flavours/glitch/initial_state';
|
||||||
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
|
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
|
||||||
import StackTrace from 'stacktrace-js';
|
import StackTrace from 'stacktrace-js';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
export default class ErrorBoundary extends React.PureComponent {
|
export default class ErrorBoundary extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -122,6 +123,10 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } 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 Permalink from './permalink';
|
||||||
|
@ -9,10 +9,6 @@ import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
totalVolume: { id: 'hashtag.total_volume', defaultMessage: 'Total volume in the last {days, plural, one {day} other {{days} days}}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class SilentErrorBoundary extends React.Component {
|
class SilentErrorBoundary extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -60,7 +56,6 @@ export const ImmutableHashtag = ({ hashtag }) => (
|
||||||
href={hashtag.get('url')}
|
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}
|
||||||
uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
|
|
||||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -69,39 +64,52 @@ ImmutableHashtag.propTypes = {
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
hashtag: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Hashtag = injectIntl(({ name, href, to, people, uses, history, className, intl }) => (
|
const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
|
||||||
<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}>
|
<Permalink href={href} 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>
|
</Permalink>
|
||||||
|
|
||||||
{typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
|
{description ? (
|
||||||
|
<span>{description}</span>
|
||||||
|
) : (
|
||||||
|
typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<abbr className='trends__item__current' title={intl.formatMessage(messages.totalVolume, { days: 2 })}>
|
{typeof uses !== 'undefined' && (
|
||||||
{typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
|
<div className='trends__item__current'>
|
||||||
<span className='trends__item__current__asterisk'>*</span>
|
<ShortNumber value={uses} />
|
||||||
</abbr>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
{withGraph && (
|
||||||
<SilentErrorBoundary>
|
<div className='trends__item__sparkline'>
|
||||||
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
<SilentErrorBoundary>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
||||||
</Sparklines>
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</SilentErrorBoundary>
|
</Sparklines>
|
||||||
</div>
|
</SilentErrorBoundary>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
|
|
||||||
Hashtag.propTypes = {
|
Hashtag.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
people: PropTypes.number,
|
people: PropTypes.number,
|
||||||
|
description: PropTypes.node,
|
||||||
uses: PropTypes.number,
|
uses: PropTypes.number,
|
||||||
history: PropTypes.arrayOf(PropTypes.number),
|
history: PropTypes.arrayOf(PropTypes.number),
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
withGraph: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
Hashtag.defaultProps = {
|
||||||
|
withGraph: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Hashtag;
|
export default Hashtag;
|
||||||
|
|
33
app/javascript/flavours/glitch/components/image.js
Normal file
33
app/javascript/flavours/glitch/components/image.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Blurhash from './blurhash';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class Image extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
src: PropTypes.string,
|
||||||
|
srcSet: PropTypes.string,
|
||||||
|
blurhash: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoad = () => this.setState({ loaded: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { src, srcSet, blurhash, className } = this.props;
|
||||||
|
const { loaded } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('image', { loaded }, className)} role='presentation'>
|
||||||
|
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
||||||
|
<img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Logo = () => (
|
const Logo = () => (
|
||||||
<svg viewBox='0 0 261 66' className='logo'>
|
<svg viewBox='0 0 261 66' className='logo' role='img'>
|
||||||
|
<title>Mastodon</title>
|
||||||
<use xlinkHref='#logo-symbol-wordmark' />
|
<use xlinkHref='#logo-symbol-wordmark' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg';
|
import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const MissingIndicator = ({ fullPage }) => (
|
const MissingIndicator = ({ fullPage }) => (
|
||||||
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
|
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
|
||||||
|
@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => (
|
||||||
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
||||||
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||||
|
import { showTrends } from 'flavours/glitch/initial_state';
|
||||||
|
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
|
||||||
|
import AccountNavigation from 'flavours/glitch/features/account/navigation';
|
||||||
|
|
||||||
|
const DefaultNavigation = () => (
|
||||||
|
<>
|
||||||
|
{showTrends && (
|
||||||
|
<>
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
<Trends />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default @withRouter
|
||||||
|
class NavigationPortal extends React.PureComponent {
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} />
|
||||||
|
<Route component={DefaultNavigation} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,19 +1,21 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { domain } from 'flavours/glitch/initial_state';
|
import React from 'react';
|
||||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Account from 'flavours/glitch/containers/account_container';
|
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import Account from 'flavours/glitch/containers/account_container';
|
||||||
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
import Image from 'flavours/glitch/components/image';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
server: state.get('server'),
|
server: state.getIn(['server', 'server']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -41,7 +43,7 @@ class ServerBanner extends React.PureComponent {
|
||||||
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' />
|
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
|
||||||
|
|
||||||
<div className='server-banner__description'>
|
<div className='server-banner__description'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -83,7 +85,7 @@ class ServerBanner extends React.PureComponent {
|
||||||
|
|
||||||
<hr className='spacer' />
|
<hr className='spacer' />
|
||||||
|
|
||||||
<a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a>
|
<Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||||
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
|
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
|
||||||
|
|
||||||
Skeleton.propTypes = {
|
Skeleton.propTypes = {
|
||||||
width: PropTypes.number,
|
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
height: PropTypes.number,
|
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Skeleton;
|
export default Skeleton;
|
||||||
|
|
|
@ -242,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -124,6 +124,9 @@ export default class StatusContent extends React.PureComponent {
|
||||||
link.setAttribute('title', link.href);
|
link.setAttribute('title', link.href);
|
||||||
link.classList.add('unhandled-link');
|
link.classList.add('unhandled-link');
|
||||||
|
|
||||||
|
link.setAttribute('target', '_blank');
|
||||||
|
link.setAttribute('rel', 'noopener nofollow noreferrer');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tagLinks && isLinkMisleading(link)) {
|
if (tagLinks && isLinkMisleading(link)) {
|
||||||
// Add a tag besides the link to display its origin
|
// Add a tag besides the link to display its origin
|
||||||
|
@ -149,9 +152,6 @@ export default class StatusContent extends React.PureComponent {
|
||||||
if (tagLinks && e instanceof TypeError) link.removeAttribute('href');
|
if (tagLinks && e instanceof TypeError) link.removeAttribute('href');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
link.setAttribute('rel', 'noopener noreferrer');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
import React from 'react';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import configureStore from 'flavours/glitch/store/configureStore';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
import { BrowserRouter, Route } from 'react-router-dom';
|
import { BrowserRouter, Route } from 'react-router-dom';
|
||||||
import { ScrollContext } from 'react-router-scroll-4';
|
import { ScrollContext } from 'react-router-scroll-4';
|
||||||
|
import configureStore from 'flavours/glitch/store/configureStore';
|
||||||
import UI from 'flavours/glitch/features/ui';
|
import UI from 'flavours/glitch/features/ui';
|
||||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
|
import { hydrateStore } from 'flavours/glitch/actions/store';
|
||||||
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
|
||||||
import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings';
|
import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { getLocale } from 'locales';
|
|
||||||
import initialState from 'flavours/glitch/initial_state';
|
|
||||||
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
||||||
|
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
|
||||||
|
import { getLocale } from 'locales';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
||||||
|
|
||||||
export const store = configureStore();
|
export const store = configureStore();
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
|
@ -78,15 +81,17 @@ export default class Mastodon extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<Provider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BrowserRouter basename='/web'>
|
<BrowserRouter>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<Route path='/' component={UI} />
|
<Route path='/' component={UI} />
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
||||||
|
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Provider>
|
</ReduxProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||||
import 'intersection-observer';
|
import 'intersection-observer';
|
||||||
import 'requestidlecallback';
|
import 'requestidlecallback';
|
||||||
import objectFitImages from 'object-fit-images';
|
import objectFitImages from 'object-fit-images';
|
||||||
|
|
222
app/javascript/flavours/glitch/features/about/index.js
Normal file
222
app/javascript/flavours/glitch/features/about/index.js
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
|
||||||
|
import Account from 'flavours/glitch/containers/account_container';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Image from 'flavours/glitch/components/image';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.about', defaultMessage: 'About' },
|
||||||
|
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
|
||||||
|
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
|
||||||
|
silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
|
||||||
|
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
|
||||||
|
suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' },
|
||||||
|
suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const severityMessages = {
|
||||||
|
silence: {
|
||||||
|
title: messages.silenced,
|
||||||
|
explanation: messages.silencedExplanation,
|
||||||
|
},
|
||||||
|
|
||||||
|
suspend: {
|
||||||
|
title: messages.suspended,
|
||||||
|
explanation: messages.suspendedExplanation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
server: state.getIn(['server', 'server']),
|
||||||
|
extendedDescription: state.getIn(['server', 'extendedDescription']),
|
||||||
|
domainBlocks: state.getIn(['server', 'domainBlocks']),
|
||||||
|
});
|
||||||
|
|
||||||
|
class Section extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
open: PropTypes.bool,
|
||||||
|
onOpen: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
collapsed: !this.props.open,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { onOpen } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { title, children } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('about__section', { active: !collapsed })}>
|
||||||
|
<div className='about__section__title' role='button' tabIndex='0' onClick={this.handleClick}>
|
||||||
|
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!collapsed && (
|
||||||
|
<div className='about__section__body'>{children}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class About extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
server: ImmutablePropTypes.map,
|
||||||
|
extendedDescription: ImmutablePropTypes.map,
|
||||||
|
domainBlocks: ImmutablePropTypes.contains({
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
isAvailable: PropTypes.bool,
|
||||||
|
items: ImmutablePropTypes.list,
|
||||||
|
}),
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchServer());
|
||||||
|
dispatch(fetchExtendedDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDomainBlocksOpen = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchDomainBlocks());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
|
||||||
|
const isLoading = server.get('isLoading');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||||
|
<div className='scrollable about'>
|
||||||
|
<div className='about__header'>
|
||||||
|
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
|
||||||
|
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
||||||
|
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='about__meta'>
|
||||||
|
<div className='about__meta__column'>
|
||||||
|
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||||
|
|
||||||
|
<Account id={server.getIn(['contact', 'account', 'id'])} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className='about__meta__divider' />
|
||||||
|
|
||||||
|
<div className='about__meta__column'>
|
||||||
|
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
|
||||||
|
|
||||||
|
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section open title={intl.formatMessage(messages.title)}>
|
||||||
|
{extendedDescription.get('isLoading') ? (
|
||||||
|
<>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='70%' />
|
||||||
|
</>
|
||||||
|
) : (extendedDescription.get('content')?.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={intl.formatMessage(messages.rules)}>
|
||||||
|
{!isLoading && (server.get('rules').isEmpty() ? (
|
||||||
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
) : (
|
||||||
|
<ol className='rules-list'>
|
||||||
|
{server.get('rules').map(rule => (
|
||||||
|
<li key={rule.get('id')}>
|
||||||
|
<span className='rules-list__text'>{rule.get('text')}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||||
|
{domainBlocks.get('isLoading') ? (
|
||||||
|
<>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<br />
|
||||||
|
<Skeleton width='70%' />
|
||||||
|
</>
|
||||||
|
) : (domainBlocks.get('isAvailable') ? (
|
||||||
|
<>
|
||||||
|
<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'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th>
|
||||||
|
<th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{domainBlocks.get('items').map(block => (
|
||||||
|
<tr key={block.get('domain')}>
|
||||||
|
<td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td>
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<LinkFooter />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='all' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
||||||
|
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class FeaturedTags extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
featuredTags: ImmutablePropTypes.list,
|
||||||
|
tagged: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, featuredTags, intl } = this.props;
|
||||||
|
|
||||||
|
if (!account || account.get('suspended') || featuredTags.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='getting-started__trends'>
|
||||||
|
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
|
||||||
|
|
||||||
|
{featuredTags.take(3).map(featuredTag => (
|
||||||
|
<Hashtag
|
||||||
|
key={featuredTag.get('name')}
|
||||||
|
name={featuredTag.get('name')}
|
||||||
|
href={featuredTag.get('url')}
|
||||||
|
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
||||||
|
uses={featuredTag.get('statuses_count')}
|
||||||
|
withGraph={false}
|
||||||
|
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { autoPlayGif, me, title, domain } from 'flavours/glitch/initial_state';
|
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
|
||||||
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
|
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
@ -19,7 +19,7 @@ import { Helmet } from 'react-helmet';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
|
@ -273,7 +273,9 @@ class Header extends ImmutablePureComponent {
|
||||||
const content = { __html: account.get('note_emojified') };
|
const content = { __html: account.get('note_emojified') };
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const fields = account.get('fields');
|
const fields = account.get('fields');
|
||||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
const isLocal = account.get('acct').indexOf('@') === -1;
|
||||||
|
const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||||
|
const isIndexable = !account.get('noindex');
|
||||||
|
|
||||||
let badge;
|
let badge;
|
||||||
|
|
||||||
|
@ -352,7 +354,8 @@ class Header extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{titleFromAccount(account)} - {title}</title>
|
<title>{titleFromAccount(account)}</title>
|
||||||
|
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FeaturedTags from '../components/featured_tags';
|
||||||
|
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
const mapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
return (state, { accountId }) => ({
|
||||||
|
account: getAccount(state, accountId),
|
||||||
|
featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(FeaturedTags);
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container';
|
||||||
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { match: { params: { acct } } }) => {
|
||||||
|
const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
|
||||||
|
|
||||||
|
if (!accountId) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountId,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class AccountNavigation extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
match: PropTypes.shape({
|
||||||
|
params: PropTypes.shape({
|
||||||
|
acct: PropTypes.string,
|
||||||
|
tagged: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accountId, isLoading, match: { params: { tagged } } } = this.props;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
<FeaturedTags accountId={accountId} tagged={tagged} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,9 +15,10 @@ import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||||
import LoadMore from 'flavours/glitch/components/load_more';
|
import LoadMore from 'flavours/glitch/components/load_more';
|
||||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { unfollowModal } from 'flavours/glitch/initial_state';
|
import { unfollowModal } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
});
|
});
|
||||||
|
@ -43,7 +44,7 @@ const makeMapStateToProps = () => {
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
if (account.getIn(['relationship', 'following'])) {
|
||||||
if (unfollowModal) {
|
if (unfollowModal) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
|
@ -53,6 +54,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
} else {
|
} else {
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
|
if (unfollowModal) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
|
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
||||||
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
|
}));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,22 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||||
import LimitedAccountHint from './components/limited_account_hint';
|
import LimitedAccountHint from './components/limited_account_hint';
|
||||||
import { getAccountHidden } from 'flavours/glitch/selectors';
|
import { getAccountHidden } from 'flavours/glitch/selectors';
|
||||||
|
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
|
||||||
|
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => {
|
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
return {
|
return {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
statusIds: emptyList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountId,
|
accountId,
|
||||||
|
@ -37,7 +40,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
||||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||||
isAccount: !!state.getIn(['accounts', accountId]),
|
isAccount: !!state.getIn(['accounts', accountId]),
|
||||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
|
||||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
|
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], ImmutableList()),
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
@ -60,6 +63,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
acct: PropTypes.string,
|
acct: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
tagged: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
accountId: PropTypes.string,
|
accountId: PropTypes.string,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
@ -77,14 +81,16 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
_load () {
|
_load () {
|
||||||
const { accountId, withReplies, dispatch } = this.props;
|
const { accountId, withReplies, params: { tagged }, dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(fetchAccount(accountId));
|
dispatch(fetchAccount(accountId));
|
||||||
|
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
dispatch(expandAccountFeaturedTimeline(accountId));
|
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
|
||||||
}
|
}
|
||||||
dispatch(expandAccountTimeline(accountId, { withReplies }));
|
|
||||||
|
dispatch(fetchFeaturedTags(accountId));
|
||||||
|
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -98,12 +104,17 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { params: { acct }, accountId, dispatch } = this.props;
|
const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props;
|
||||||
|
|
||||||
if (prevProps.accountId !== accountId && accountId) {
|
if (prevProps.accountId !== accountId && accountId) {
|
||||||
this._load();
|
this._load();
|
||||||
} else if (prevProps.params.acct !== acct) {
|
} else if (prevProps.params.acct !== acct) {
|
||||||
dispatch(lookupAccount(acct));
|
dispatch(lookupAccount(acct));
|
||||||
|
} else if (prevProps.params.tagged !== tagged) {
|
||||||
|
if (!withReplies) {
|
||||||
|
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
|
||||||
|
}
|
||||||
|
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +137,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleLoadMore = maxId => {
|
||||||
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
|
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged }));
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
@ -136,7 +147,13 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (isLoading && statusIds.isEmpty()) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
} else if (!isLoading && !isAccount) {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton multiColumn={multiColumn} />
|
||||||
|
@ -145,14 +162,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statusIds && isLoading) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
const forceEmptyState = suspended || hidden;
|
const forceEmptyState = suspended || hidden;
|
||||||
|
@ -174,7 +183,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks';
|
import PropTypes from 'prop-types';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import React from 'react';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import { Helmet } from 'react-helmet';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
|
||||||
import StatusList from 'flavours/glitch/components/status_list';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
|
import StatusList from 'flavours/glitch/components/status_list';
|
||||||
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -95,6 +96,11 @@ class Bookmarks extends ImmutablePureComponent {
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.heading)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
message: state.getIn(['server', 'server', 'registrations', 'message']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class ClosedRegistrationsModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let closedRegistrationsMessage;
|
||||||
|
|
||||||
|
if (this.props.message) {
|
||||||
|
closedRegistrationsMessage = (
|
||||||
|
<p
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: this.props.message }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
closedRegistrationsMessage = (
|
||||||
|
<p className='prose'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations_modal.description'
|
||||||
|
defaultMessage='Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.'
|
||||||
|
values={{ domain: <strong>{domain}</strong> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal interaction-modal'>
|
||||||
|
<div className='interaction-modal__lead'>
|
||||||
|
<h3><FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' /></h3>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations_modal.preamble'
|
||||||
|
defaultMessage='Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='interaction-modal__choices'>
|
||||||
|
<div className='interaction-modal__choices__choice'>
|
||||||
|
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
|
||||||
|
{closedRegistrationsMessage}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='interaction-modal__choices__choice'>
|
||||||
|
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||||
|
<p className='prose'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='closed_registrations.other_server_instructions'
|
||||||
|
defaultMessage='Since Mastodon is decentralized, you can create an account on another server and still interact with this one.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<a href='https://joinmastodon.org/servers' className='button button--block'><FormattedMessage id='closed_registrations_modal.find_another_server' defaultMessage='Find another server' /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -10,7 +10,8 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { title } from 'flavours/glitch/initial_state';
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||||
|
@ -138,6 +139,10 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
|
<DismissableBanner id='community_timeline'>
|
||||||
|
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
|
||||||
|
</DismissableBanner>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
|
@ -149,7 +154,8 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import Motion from '../../ui/util/optional_motion';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||||
|
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class SearchPopout extends React.PureComponent {
|
class SearchPopout extends React.PureComponent {
|
||||||
|
@ -62,6 +63,7 @@ class Search extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object.isRequired,
|
router: PropTypes.object.isRequired,
|
||||||
|
identity: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -137,6 +139,7 @@ class Search extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { intl, value, submitted } = this.props;
|
const { intl, value, submitted } = this.props;
|
||||||
const { expanded } = this.state;
|
const { expanded } = this.state;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
const hasValue = value.length > 0 || submitted;
|
const hasValue = value.length > 0 || submitted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -147,7 +150,7 @@ class Search extends React.PureComponent {
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
className='search__input'
|
className='search__input'
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onKeyUp={this.handleKeyUp}
|
onKeyUp={this.handleKeyUp}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import UploadProgressContainer from '../containers/upload_progress_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import UploadContainer from '../containers/upload_container';
|
import UploadContainer from '../containers/upload_container';
|
||||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export default class UploadForm extends ImmutablePureComponent {
|
export default class UploadForm extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -16,7 +15,7 @@ export default class UploadForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='composer--upload_form'>
|
<div className='composer--upload_form'>
|
||||||
<UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} />
|
<UploadProgressContainer />
|
||||||
|
|
||||||
{mediaIds.size > 0 && (
|
{mediaIds.size > 0 && (
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
|
|
|
@ -3,26 +3,34 @@ import PropTypes from 'prop-types';
|
||||||
import Motion from '../../ui/util/optional_motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class UploadProgress extends React.PureComponent {
|
export default class UploadProgress extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
icon: PropTypes.string.isRequired,
|
isProcessing: PropTypes.bool,
|
||||||
message: PropTypes.node.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { active, progress, icon, message } = this.props;
|
const { active, progress, isProcessing } = this.props;
|
||||||
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (isProcessing) {
|
||||||
|
message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />;
|
||||||
|
} else {
|
||||||
|
message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='composer--upload_form--progress'>
|
<div className='composer--upload_form--progress'>
|
||||||
<Icon id={icon} />
|
<Icon id='upload' />
|
||||||
|
|
||||||
<div className='message'>
|
<div className='message'>
|
||||||
{message}
|
{message}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import UploadProgress from '../components/upload_progress';
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
active: state.getIn(['compose', 'is_uploading']),
|
active: state.getIn(['compose', 'is_uploading']),
|
||||||
progress: state.getIn(['compose', 'progress']),
|
progress: state.getIn(['compose', 'progress']),
|
||||||
|
isProcessing: state.getIn(['compose', 'is_processing']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UploadProgress);
|
export default connect(mapStateToProps)(UploadProgress);
|
||||||
|
|
|
@ -14,6 +14,8 @@ import SearchResultsContainer from './containers/search_results_container';
|
||||||
import { me, mascot } from 'flavours/glitch/initial_state';
|
import { me, mascot } from 'flavours/glitch/initial_state';
|
||||||
import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
|
import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
|
||||||
import HeaderContainer from './containers/header_container';
|
import HeaderContainer from './containers/header_container';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||||
|
@ -21,7 +23,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
elefriend: state.getIn(['compose', 'elefriend']),
|
elefriend: state.getIn(['compose', 'elefriend']),
|
||||||
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
|
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
@ -44,7 +46,6 @@ class Compose extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
isSearchPage: PropTypes.bool,
|
|
||||||
elefriend: PropTypes.number,
|
elefriend: PropTypes.number,
|
||||||
onClickElefriend: PropTypes.func,
|
onClickElefriend: PropTypes.func,
|
||||||
onMount: PropTypes.func,
|
onMount: PropTypes.func,
|
||||||
|
@ -53,19 +54,11 @@ class Compose extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { isSearchPage } = this.props;
|
this.props.onMount();
|
||||||
|
|
||||||
if (!isSearchPage) {
|
|
||||||
this.props.onMount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
const { isSearchPage } = this.props;
|
this.props.onUnmount();
|
||||||
|
|
||||||
if (!isSearchPage) {
|
|
||||||
this.props.onUnmount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -74,37 +67,49 @@ class Compose extends React.PureComponent {
|
||||||
intl,
|
intl,
|
||||||
multiColumn,
|
multiColumn,
|
||||||
onClickElefriend,
|
onClickElefriend,
|
||||||
isSearchPage,
|
|
||||||
showSearch,
|
showSearch,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
|
const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
|
||||||
|
|
||||||
return (
|
if (multiColumn) {
|
||||||
<div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
|
return (
|
||||||
{multiColumn && <HeaderContainer />}
|
<div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
|
||||||
|
<HeaderContainer />
|
||||||
|
|
||||||
{(multiColumn || isSearchPage) && <SearchContainer />}
|
{multiColumn && <SearchContainer />}
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
<div className='drawer__pager'>
|
||||||
{!isSearchPage && <div className='drawer__inner'>
|
<div className='drawer__inner'>
|
||||||
<NavigationContainer />
|
<NavigationContainer />
|
||||||
|
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
|
|
||||||
<div className='drawer__inner__mastodon'>
|
<div className='drawer__inner__mastodon'>
|
||||||
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
|
||||||
{({ x }) => (
|
|
||||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
|
||||||
<SearchResultsContainer />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</Motion>
|
|
||||||
|
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||||
|
{({ x }) => (
|
||||||
|
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||||
|
<SearchResultsContainer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<NavigationContainer />
|
||||||
|
<ComposeFormContainer />
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
|
import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations';
|
||||||
|
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
|
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||||
import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations';
|
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
|
||||||
import ConversationsListContainer from './containers/conversations_list_container';
|
import ConversationsListContainer from './containers/conversations_list_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -143,6 +144,11 @@ class DirectTimeline extends React.PureComponent {
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
{contents}
|
{contents}
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import classNames from 'classnames';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue