mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-13 06:35:08 +01:00
Merge pull request #1224 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
65db9df011
@ -115,6 +115,20 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
||||
# S3_ENDPOINT=
|
||||
# S3_SIGNATURE_VERSION=
|
||||
|
||||
# Google Cloud Storage (optional)
|
||||
# Use S3 compatible API. Since GCS does not support Multipart Upload,
|
||||
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
|
||||
# The attachment host must allow cross origin request - see the description
|
||||
# above.
|
||||
# S3_ENABLED=true
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_PROTOCOL=https
|
||||
# S3_HOSTNAME=storage.googleapis.com
|
||||
# S3_ENDPOINT=https://storage.googleapis.com
|
||||
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
|
||||
|
||||
# Swift (optional)
|
||||
# The attachment host must allow cross origin request - see the description
|
||||
# above.
|
||||
|
@ -1 +1 @@
|
||||
2.6.1
|
||||
2.6.4
|
||||
|
233
CHANGELOG.md
233
CHANGELOG.md
@ -3,6 +3,239 @@ Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11715), [Gargron](https://github.com/tootsuite/mastodon/pull/11745))
|
||||
- **Add profile directory to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11688), [mayaeh](https://github.com/tootsuite/mastodon/pull/11872))
|
||||
- Add profile directory opt-in federation
|
||||
- Add profile directory REST API
|
||||
- Add special alert for throttled requests in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11677))
|
||||
- Add confirmation modal when logging out from the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11671))
|
||||
- **Add audio player in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11644), [Gargron](https://github.com/tootsuite/mastodon/pull/11652), [Gargron](https://github.com/tootsuite/mastodon/pull/11654), [ThibG](https://github.com/tootsuite/mastodon/pull/11629))
|
||||
- **Add autosuggestions for hashtags in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11422), [ThibG](https://github.com/tootsuite/mastodon/pull/11632), [Gargron](https://github.com/tootsuite/mastodon/pull/11764), [Gargron](https://github.com/tootsuite/mastodon/pull/11588), [Gargron](https://github.com/tootsuite/mastodon/pull/11442))
|
||||
- **Add media editing modal with OCR tool in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11563), [Gargron](https://github.com/tootsuite/mastodon/pull/11566), [ThibG](https://github.com/tootsuite/mastodon/pull/11575), [ThibG](https://github.com/tootsuite/mastodon/pull/11576), [Gargron](https://github.com/tootsuite/mastodon/pull/11577), [Gargron](https://github.com/tootsuite/mastodon/pull/11573), [Gargron](https://github.com/tootsuite/mastodon/pull/11571))
|
||||
- Add indicator of unread notifications to window title when web UI is out of focus ([Gargron](https://github.com/tootsuite/mastodon/pull/11560), [Gargron](https://github.com/tootsuite/mastodon/pull/11572))
|
||||
- Add indicator for which options you voted for in a poll in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11195))
|
||||
- **Add search results pagination to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11409), [ThibG](https://github.com/tootsuite/mastodon/pull/11447))
|
||||
- **Add option to disable real-time updates in web UI ("slow mode")** ([Gargron](https://github.com/tootsuite/mastodon/pull/9984), [ykzts](https://github.com/tootsuite/mastodon/pull/11880), [ThibG](https://github.com/tootsuite/mastodon/pull/11883), [Gargron](https://github.com/tootsuite/mastodon/pull/11898), [ThibG](https://github.com/tootsuite/mastodon/pull/11859))
|
||||
- Add option to disable blurhash previews in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11188))
|
||||
- Add native smooth scrolling when supported in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11207))
|
||||
- Add search and sort functions to hashtag admin UI ([mayaeh](https://github.com/tootsuite/mastodon/pull/11829), [Gargron](https://github.com/tootsuite/mastodon/pull/11897), [mayaeh](https://github.com/tootsuite/mastodon/pull/11875))
|
||||
- Add setting for default search engine indexing in admin UI ([brortao](https://github.com/tootsuite/mastodon/pull/11804))
|
||||
- Add account bio to account view in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11473))
|
||||
- **Add option to include reported statuses in warning e-mail from admin UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11639), [Gargron](https://github.com/tootsuite/mastodon/pull/11812), [Gargron](https://github.com/tootsuite/mastodon/pull/11741), [Gargron](https://github.com/tootsuite/mastodon/pull/11698), [mayaeh](https://github.com/tootsuite/mastodon/pull/11765))
|
||||
- Add number of pending accounts and pending hashtags to dashboard in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11514))
|
||||
- **Add account migration UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11846), [noellabo](https://github.com/tootsuite/mastodon/pull/11905), [noellabo](https://github.com/tootsuite/mastodon/pull/11907), [noellabo](https://github.com/tootsuite/mastodon/pull/11906), [noellabo](https://github.com/tootsuite/mastodon/pull/11902))
|
||||
- **Add table of contents to about page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11885), [ykzts](https://github.com/tootsuite/mastodon/pull/11941), [ykzts](https://github.com/tootsuite/mastodon/pull/11895), [Kjwon15](https://github.com/tootsuite/mastodon/pull/11916))
|
||||
- **Add password challenge to 2FA settings, e-mail notifications** ([Gargron](https://github.com/tootsuite/mastodon/pull/11878))
|
||||
- Add optional invite comments ([ThibG](https://github.com/tootsuite/mastodon/pull/10465))
|
||||
- **Add optional public list of domain blocks with comments** ([ThibG](https://github.com/tootsuite/mastodon/pull/11298), [ThibG](https://github.com/tootsuite/mastodon/pull/11515), [Gargron](https://github.com/tootsuite/mastodon/pull/11908))
|
||||
- Add an RSS feed for featured hashtags ([noellabo](https://github.com/tootsuite/mastodon/pull/10502))
|
||||
- Add explanations to featured hashtags UI and profile ([Gargron](https://github.com/tootsuite/mastodon/pull/11586))
|
||||
- **Add hashtag trends with admin and user settings** ([Gargron](https://github.com/tootsuite/mastodon/pull/11490), [Gargron](https://github.com/tootsuite/mastodon/pull/11502), [Gargron](https://github.com/tootsuite/mastodon/pull/11641), [Gargron](https://github.com/tootsuite/mastodon/pull/11594), [Gargron](https://github.com/tootsuite/mastodon/pull/11517), [mayaeh](https://github.com/tootsuite/mastodon/pull/11845), [Gargron](https://github.com/tootsuite/mastodon/pull/11774), [Gargron](https://github.com/tootsuite/mastodon/pull/11712), [Gargron](https://github.com/tootsuite/mastodon/pull/11791), [Gargron](https://github.com/tootsuite/mastodon/pull/11743), [Gargron](https://github.com/tootsuite/mastodon/pull/11740), [Gargron](https://github.com/tootsuite/mastodon/pull/11714), [ThibG](https://github.com/tootsuite/mastodon/pull/11631), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/11569), [Gargron](https://github.com/tootsuite/mastodon/pull/11524), [Gargron](https://github.com/tootsuite/mastodon/pull/11513))
|
||||
- Add hashtag usage breakdown to admin UI
|
||||
- Add batch actions for hashtags to admin UI
|
||||
- Add trends to web UI
|
||||
- Add trends to public pages
|
||||
- Add user preference to hide trends
|
||||
- Add admin setting to disable trends
|
||||
- **Add categories for custom emojis** ([Gargron](https://github.com/tootsuite/mastodon/pull/11196), [Gargron](https://github.com/tootsuite/mastodon/pull/11793), [Gargron](https://github.com/tootsuite/mastodon/pull/11920), [highemerly](https://github.com/tootsuite/mastodon/pull/11876))
|
||||
- Add custom emoji categories to emoji picker in web UI
|
||||
- Add `category` to custom emojis in REST API
|
||||
- Add batch actions for custom emojis in admin UI
|
||||
- Add max image dimensions to error message ([raboof](https://github.com/tootsuite/mastodon/pull/11552))
|
||||
- Add aac, m4a, 3gp, amr, wma to allowed audio formats ([Gargron](https://github.com/tootsuite/mastodon/pull/11342), [umonaca](https://github.com/tootsuite/mastodon/pull/11687))
|
||||
- **Add search syntax for operators and phrases** ([Gargron](https://github.com/tootsuite/mastodon/pull/11411))
|
||||
- **Add REST API for managing featured hashtags** ([noellabo](https://github.com/tootsuite/mastodon/pull/11778))
|
||||
- **Add REST API for managing timeline read markers** ([Gargron](https://github.com/tootsuite/mastodon/pull/11762))
|
||||
- Add `exclude_unreviewed` param to `GET /api/v2/search` REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/11977))
|
||||
- **Add ActivityPub secure mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11269), [ThibG](https://github.com/tootsuite/mastodon/pull/11332), [ThibG](https://github.com/tootsuite/mastodon/pull/11295))
|
||||
- Add HTTP signatures to all outgoing ActivityPub GET requests ([Gargron](https://github.com/tootsuite/mastodon/pull/11284), [ThibG](https://github.com/tootsuite/mastodon/pull/11300))
|
||||
- Add support for ActivityPub Audio activities ([ThibG](https://github.com/tootsuite/mastodon/pull/11189))
|
||||
- Add ActivityPub actor representing the entire server ([ThibG](https://github.com/tootsuite/mastodon/pull/11321), [rtucker](https://github.com/tootsuite/mastodon/pull/11400), [ThibG](https://github.com/tootsuite/mastodon/pull/11561), [Gargron](https://github.com/tootsuite/mastodon/pull/11798))
|
||||
- **Add whitelist mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11291), [mayaeh](https://github.com/tootsuite/mastodon/pull/11634))
|
||||
- Add config of multipart threshold for S3 ([ykzts](https://github.com/tootsuite/mastodon/pull/11924), [ykzts](https://github.com/tootsuite/mastodon/pull/11944))
|
||||
- Add health check endpoint for web ([ykzts](https://github.com/tootsuite/mastodon/pull/11770), [ykzts](https://github.com/tootsuite/mastodon/pull/11947))
|
||||
- Add HTTP signature keyId to request log ([Gargron](https://github.com/tootsuite/mastodon/pull/11591))
|
||||
- Add `SMTP_REPLY_TO` environment variable ([hugogameiro](https://github.com/tootsuite/mastodon/pull/11718))
|
||||
- Add `tootctl preview_cards remove` command ([mayaeh](https://github.com/tootsuite/mastodon/pull/11320))
|
||||
- Add `tootctl media refresh` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11775))
|
||||
- Add `tootctl cache recount` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11597))
|
||||
- Add option to exclude suspended domains from `tootctl domains crawl` ([dariusk](https://github.com/tootsuite/mastodon/pull/11454))
|
||||
- Add soft delete for statuses for instant deletes through API ([Gargron](https://github.com/tootsuite/mastodon/pull/11623), [Gargron](https://github.com/tootsuite/mastodon/pull/11648))
|
||||
- Add rails-level JSON caching ([Gargron](https://github.com/tootsuite/mastodon/pull/11333), [Gargron](https://github.com/tootsuite/mastodon/pull/11271))
|
||||
- **Add request pool to improve delivery performance** ([Gargron](https://github.com/tootsuite/mastodon/pull/10353), [ykzts](https://github.com/tootsuite/mastodon/pull/11756))
|
||||
- Add concurrent connection attempts to resolved IP addresses ([ThibG](https://github.com/tootsuite/mastodon/pull/11757))
|
||||
- Add index for remember_token to improve login performance ([abcang](https://github.com/tootsuite/mastodon/pull/11881))
|
||||
- **Add more accurate hashtag search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11579), [Gargron](https://github.com/tootsuite/mastodon/pull/11427), [Gargron](https://github.com/tootsuite/mastodon/pull/11448))
|
||||
- **Add more accurate account search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11537), [Gargron](https://github.com/tootsuite/mastodon/pull/11580))
|
||||
- **Add a spam check** ([Gargron](https://github.com/tootsuite/mastodon/pull/11217), [Gargron](https://github.com/tootsuite/mastodon/pull/11806), [ThibG](https://github.com/tootsuite/mastodon/pull/11296))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Change conversations UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11896))
|
||||
- Change dashboard to short number notation ([noellabo](https://github.com/tootsuite/mastodon/pull/11847), [noellabo](https://github.com/tootsuite/mastodon/pull/11911))
|
||||
- Change REST API `GET /api/v1/timelines/public` to require authentication when public preview is off ([ThibG](https://github.com/tootsuite/mastodon/pull/11802))
|
||||
- Change REST API `POST /api/v1/follow_requests/:id/(approve|reject)` to return relationship ([ThibG](https://github.com/tootsuite/mastodon/pull/11800))
|
||||
- Change rate limit for media proxy ([ykzts](https://github.com/tootsuite/mastodon/pull/11814))
|
||||
- Change unlisted custom emoji to not appear in autosuggestions ([Gargron](https://github.com/tootsuite/mastodon/pull/11818))
|
||||
- Change max length of media descriptions from 420 to 1500 characters ([Gargron](https://github.com/tootsuite/mastodon/pull/11819), [ThibG](https://github.com/tootsuite/mastodon/pull/11836))
|
||||
- **Change deletes to preserve soft-deleted statuses in unresolved reports** ([Gargron](https://github.com/tootsuite/mastodon/pull/11805))
|
||||
- **Change tootctl to use inline parallelization instead of Sidekiq** ([Gargron](https://github.com/tootsuite/mastodon/pull/11776))
|
||||
- **Change account deletion page to have better explanations** ([Gargron](https://github.com/tootsuite/mastodon/pull/11753), [Gargron](https://github.com/tootsuite/mastodon/pull/11763))
|
||||
- Change hashtag component in web UI to show numbers for 2 last days ([Gargron](https://github.com/tootsuite/mastodon/pull/11742), [Gargron](https://github.com/tootsuite/mastodon/pull/11755), [Gargron](https://github.com/tootsuite/mastodon/pull/11754))
|
||||
- Change OpenGraph description on sign-up page to reflect invite ([Gargron](https://github.com/tootsuite/mastodon/pull/11744))
|
||||
- Change layout of public profile directory to be the same as in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11705))
|
||||
- Change detailed status child ordering to sort self-replies on top ([ThibG](https://github.com/tootsuite/mastodon/pull/11686))
|
||||
- Change window resize handler to switch to/from mobile layout as soon as needed ([ThibG](https://github.com/tootsuite/mastodon/pull/11656))
|
||||
- Change icon button styles to make hover/focus states more obvious ([ThibG](https://github.com/tootsuite/mastodon/pull/11474))
|
||||
- Change contrast of status links that are not mentions or hashtags ([ThibG](https://github.com/tootsuite/mastodon/pull/11406))
|
||||
- **Change hashtags to preserve first-used casing** ([Gargron](https://github.com/tootsuite/mastodon/pull/11416), [Gargron](https://github.com/tootsuite/mastodon/pull/11508), [Gargron](https://github.com/tootsuite/mastodon/pull/11504), [Gargron](https://github.com/tootsuite/mastodon/pull/11507), [Gargron](https://github.com/tootsuite/mastodon/pull/11441))
|
||||
- **Change unconfirmed user login behaviour** ([Gargron](https://github.com/tootsuite/mastodon/pull/11375), [ThibG](https://github.com/tootsuite/mastodon/pull/11394), [Gargron](https://github.com/tootsuite/mastodon/pull/11860))
|
||||
- **Change single-column mode to scroll the whole page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11359), [Gargron](https://github.com/tootsuite/mastodon/pull/11894), [Gargron](https://github.com/tootsuite/mastodon/pull/11891), [ThibG](https://github.com/tootsuite/mastodon/pull/11655), [Gargron](https://github.com/tootsuite/mastodon/pull/11463), [Gargron](https://github.com/tootsuite/mastodon/pull/11458), [ThibG](https://github.com/tootsuite/mastodon/pull/11395), [Gargron](https://github.com/tootsuite/mastodon/pull/11418))
|
||||
- Change `tootctl accounts follow` to only work with local accounts ([angristan](https://github.com/tootsuite/mastodon/pull/11592))
|
||||
- Change Dockerfile ([Shleeble](https://github.com/tootsuite/mastodon/pull/11710), [ykzts](https://github.com/tootsuite/mastodon/pull/11768), [Shleeble](https://github.com/tootsuite/mastodon/pull/11707))
|
||||
- Change supported Node versions to include v12 ([abcang](https://github.com/tootsuite/mastodon/pull/11706))
|
||||
- Change Portuguese language from `pt` to `pt-PT` ([Gargron](https://github.com/tootsuite/mastodon/pull/11820))
|
||||
- Change domain block silence to always require approval on follow ([ThibG](https://github.com/tootsuite/mastodon/pull/11975))
|
||||
|
||||
### Removed
|
||||
|
||||
- **Remove OStatus support** ([Gargron](https://github.com/tootsuite/mastodon/pull/11205), [Gargron](https://github.com/tootsuite/mastodon/pull/11303), [Gargron](https://github.com/tootsuite/mastodon/pull/11460), [ThibG](https://github.com/tootsuite/mastodon/pull/11280), [ThibG](https://github.com/tootsuite/mastodon/pull/11278))
|
||||
- Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11247))
|
||||
- Remove WebP support ([angristan](https://github.com/tootsuite/mastodon/pull/11589))
|
||||
- Remove deprecated config options from Heroku and Scalingo ([ykzts](https://github.com/tootsuite/mastodon/pull/11925))
|
||||
- Remove deprecated REST API `GET /api/v1/search` API ([Gargron](https://github.com/tootsuite/mastodon/pull/11823))
|
||||
- Remove deprecated REST API `GET /api/v1/statuses/:id/card` ([Gargron](https://github.com/tootsuite/mastodon/pull/11213))
|
||||
- Remove deprecated REST API `POST /api/v1/notifications/dismiss?id=:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11214))
|
||||
- Remove deprecated REST API `GET /api/v1/timelines/direct` ([Gargron](https://github.com/tootsuite/mastodon/pull/11212))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix manifest warning ([ykzts](https://github.com/tootsuite/mastodon/pull/11767))
|
||||
- Fix admin UI for custom emoji not respecting GIF autoplay preference ([ThibG](https://github.com/tootsuite/mastodon/pull/11801))
|
||||
- Fix page body not being scrollable in admin/settings layout ([Gargron](https://github.com/tootsuite/mastodon/pull/11893))
|
||||
- Fix placeholder colors for inputs not being explicitly defined ([Gargron](https://github.com/tootsuite/mastodon/pull/11890))
|
||||
- Fix incorrect enclosure length in RSS ([tsia](https://github.com/tootsuite/mastodon/pull/11889))
|
||||
- Fix TOTP codes not being filtered from logs during enabling/disabling ([Gargron](https://github.com/tootsuite/mastodon/pull/11877))
|
||||
- Fix webfinger response not returning 410 when account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11869))
|
||||
- Fix ActivityPub Move handler queuing jobs that will fail if account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11864))
|
||||
- Fix SSO login not using existing account when e-mail is verified ([Gargron](https://github.com/tootsuite/mastodon/pull/11862))
|
||||
- Fix web UI allowing uploads past status limit via drag & drop ([Gargron](https://github.com/tootsuite/mastodon/pull/11863))
|
||||
- Fix expiring polls not being displayed as such in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11835))
|
||||
- Fix 2FA challenge and password challenge for non-database users ([Gargron](https://github.com/tootsuite/mastodon/pull/11831), [Gargron](https://github.com/tootsuite/mastodon/pull/11943))
|
||||
- Fix profile fields overflowing page width in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11828))
|
||||
- Fix web push subscriptions being deleted on rate limit or timeout ([Gargron](https://github.com/tootsuite/mastodon/pull/11826))
|
||||
- Fix display of long poll options in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11717), [ThibG](https://github.com/tootsuite/mastodon/pull/11833))
|
||||
- Fix search API not resolving URL when `type` is given ([Gargron](https://github.com/tootsuite/mastodon/pull/11822))
|
||||
- Fix hashtags being split by ZWNJ character ([Gargron](https://github.com/tootsuite/mastodon/pull/11821))
|
||||
- Fix scroll position resetting when opening media modals in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11815))
|
||||
- Fix duplicate HTML IDs on about page ([ThibG](https://github.com/tootsuite/mastodon/pull/11803))
|
||||
- Fix admin UI showing superfluous reject media/reports on suspended domain blocks ([ThibG](https://github.com/tootsuite/mastodon/pull/11749))
|
||||
- Fix ActivityPub context not being dynamically computed ([ThibG](https://github.com/tootsuite/mastodon/pull/11746))
|
||||
- Fix Mastodon logo style on hover on public pages' footer ([ThibG](https://github.com/tootsuite/mastodon/pull/11735))
|
||||
- Fix height of dashboard counters ([ThibG](https://github.com/tootsuite/mastodon/pull/11736))
|
||||
- Fix custom emoji animation on hover in web UI directory bios ([ThibG](https://github.com/tootsuite/mastodon/pull/11716))
|
||||
- Fix non-numbers being passed to Redis and causing an error ([Gargron](https://github.com/tootsuite/mastodon/pull/11697))
|
||||
- Fix error in REST API for an account's statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/11700))
|
||||
- Fix uncaught error when resource param is missing in Webfinger request ([Gargron](https://github.com/tootsuite/mastodon/pull/11701))
|
||||
- Fix uncaught domain normalization error in remote follow ([Gargron](https://github.com/tootsuite/mastodon/pull/11703))
|
||||
- Fix uncaught 422 and 500 errors ([Gargron](https://github.com/tootsuite/mastodon/pull/11590), [Gargron](https://github.com/tootsuite/mastodon/pull/11811))
|
||||
- Fix uncaught parameter missing exceptions and missing error templates ([Gargron](https://github.com/tootsuite/mastodon/pull/11702))
|
||||
- Fix encoding error when checking e-mail MX records ([Gargron](https://github.com/tootsuite/mastodon/pull/11696))
|
||||
- Fix items in StatusContent render list not all having a key ([ThibG](https://github.com/tootsuite/mastodon/pull/11645))
|
||||
- Fix remote and staff-removed statuses leaving media behind for a day ([Gargron](https://github.com/tootsuite/mastodon/pull/11638))
|
||||
- Fix CSP needlessly allowing blob URLs in script-src ([ThibG](https://github.com/tootsuite/mastodon/pull/11620))
|
||||
- Fix ignoring whole status because of one invalid hashtag ([Gargron](https://github.com/tootsuite/mastodon/pull/11621))
|
||||
- Fix hidden statuses losing focus ([ThibG](https://github.com/tootsuite/mastodon/pull/11208))
|
||||
- Fix loading bar being obscured by other elements in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11598))
|
||||
- Fix multiple issues with replies collection for pages further than self-replies ([ThibG](https://github.com/tootsuite/mastodon/pull/11582))
|
||||
- Fix blurhash and autoplay not working on public pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11585))
|
||||
- Fix 422 being returned instead of 404 when POSTing to unmatched routes ([Gargron](https://github.com/tootsuite/mastodon/pull/11574), [Gargron](https://github.com/tootsuite/mastodon/pull/11704))
|
||||
- Fix client-side resizing of image uploads ([ThibG](https://github.com/tootsuite/mastodon/pull/11570))
|
||||
- Fix short number formatting for numbers above million in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11559))
|
||||
- Fix ActivityPub and REST API queries setting cookies and preventing caching ([ThibG](https://github.com/tootsuite/mastodon/pull/11539), [ThibG](https://github.com/tootsuite/mastodon/pull/11557), [ThibG](https://github.com/tootsuite/mastodon/pull/11336), [ThibG](https://github.com/tootsuite/mastodon/pull/11331))
|
||||
- Fix some emojis in profile metadata labels are not emojified. ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/11534))
|
||||
- Fix account search always returning exact match on paginated results ([Gargron](https://github.com/tootsuite/mastodon/pull/11525))
|
||||
- Fix acct URIs with IDN domains not being resolved ([Gargron](https://github.com/tootsuite/mastodon/pull/11520))
|
||||
- Fix admin dashboard missing latest features ([Gargron](https://github.com/tootsuite/mastodon/pull/11505))
|
||||
- Fix jumping of toot date when clicking spoiler button ([ariasuni](https://github.com/tootsuite/mastodon/pull/11449))
|
||||
- Fix boost to original audience not working on mobile in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11371))
|
||||
- Fix handling of webfinger redirects in ResolveAccountService ([ThibG](https://github.com/tootsuite/mastodon/pull/11279))
|
||||
- Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker ([Gargron](https://github.com/tootsuite/mastodon/pull/11231))
|
||||
- Fix support for HTTP proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/11245))
|
||||
- Fix HTTP requests to IPv6 hosts ([ThibG](https://github.com/tootsuite/mastodon/pull/11240))
|
||||
- Fix error in ElasticSearch index import ([mayaeh](https://github.com/tootsuite/mastodon/pull/11192))
|
||||
- Fix duplicate account error when seeding development database ([ysksn](https://github.com/tootsuite/mastodon/pull/11366))
|
||||
- Fix performance of session clean-up scheduler ([abcang](https://github.com/tootsuite/mastodon/pull/11871))
|
||||
- Fix older migrations not running ([zunda](https://github.com/tootsuite/mastodon/pull/11377))
|
||||
- Fix URLs counting towards RTL detection ([ahangarha](https://github.com/tootsuite/mastodon/pull/11759))
|
||||
- Fix unnecessary status re-rendering in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11211))
|
||||
- Fix http_parser.rb gem not being compiled when no network available ([petabyteboy](https://github.com/tootsuite/mastodon/pull/11444))
|
||||
- Fix muted text color not applying to all text ([trwnh](https://github.com/tootsuite/mastodon/pull/11996))
|
||||
- Fix follower/following lists resetting on back-navigation in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11986))
|
||||
|
||||
## [2.9.3] - 2019-08-10
|
||||
### Added
|
||||
|
||||
- Add GIF and WebP support for custom emojis ([Gargron](https://github.com/tootsuite/mastodon/pull/11519))
|
||||
- Add logout link to dropdown menu in web UI ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11353))
|
||||
- Add indication that text search is unavailable in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11112), [ThibG](https://github.com/tootsuite/mastodon/pull/11202))
|
||||
- Add `suffix` to `Mastodon::Version` to help forks ([clarfon](https://github.com/tootsuite/mastodon/pull/11407))
|
||||
- Add on-hover animation to animated custom emoji in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11348), [ThibG](https://github.com/tootsuite/mastodon/pull/11404), [ThibG](https://github.com/tootsuite/mastodon/pull/11522))
|
||||
- Add custom emoji support in profile metadata labels ([ThibG](https://github.com/tootsuite/mastodon/pull/11350))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change default interface of web and streaming from 0.0.0.0 to 127.0.0.1 ([Gargron](https://github.com/tootsuite/mastodon/pull/11302), [zunda](https://github.com/tootsuite/mastodon/pull/11378), [Gargron](https://github.com/tootsuite/mastodon/pull/11351), [zunda](https://github.com/tootsuite/mastodon/pull/11326))
|
||||
- Change the retry limit of web push notifications ([highemerly](https://github.com/tootsuite/mastodon/pull/11292))
|
||||
- Change ActivityPub deliveries to not retry HTTP 501 errors ([Gargron](https://github.com/tootsuite/mastodon/pull/11233))
|
||||
- Change language detection to include hashtags as words ([Gargron](https://github.com/tootsuite/mastodon/pull/11341))
|
||||
- Change terms and privacy policy pages to always be accessible ([Gargron](https://github.com/tootsuite/mastodon/pull/11334))
|
||||
- Change robots tag to include `noarchive` when user opts out of indexing ([Kjwon15](https://github.com/tootsuite/mastodon/pull/11421))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix account domain block not clearing out notifications ([Gargron](https://github.com/tootsuite/mastodon/pull/11393))
|
||||
- Fix incorrect locale sometimes being detected for browser ([Gargron](https://github.com/tootsuite/mastodon/pull/8657))
|
||||
- Fix crash when saving invalid domain name ([Gargron](https://github.com/tootsuite/mastodon/pull/11528))
|
||||
- Fix pinned statuses REST API returning pagination headers ([Gargron](https://github.com/tootsuite/mastodon/pull/11526))
|
||||
- Fix "cancel follow request" button having unreadable text in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11521))
|
||||
- Fix image uploads being blank when canvas read access is blocked ([ThibG](https://github.com/tootsuite/mastodon/pull/11499))
|
||||
- Fix avatars not being animated on hover when not logged in ([ThibG](https://github.com/tootsuite/mastodon/pull/11349))
|
||||
- Fix overzealous sanitization of HTML lists ([ThibG](https://github.com/tootsuite/mastodon/pull/11354))
|
||||
- Fix block crashing when a follow request exists ([ThibG](https://github.com/tootsuite/mastodon/pull/11288))
|
||||
- Fix backup service crashing when an attachment is missing ([ThibG](https://github.com/tootsuite/mastodon/pull/11241))
|
||||
- Fix account moderation action always sending e-mail notification ([Gargron](https://github.com/tootsuite/mastodon/pull/11242))
|
||||
- Fix swiping columns on mobile sometimes failing in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11200))
|
||||
- Fix wrong actor URI being serialized into poll updates ([ThibG](https://github.com/tootsuite/mastodon/pull/11194))
|
||||
- Fix statsd UDP sockets not being cleaned up in Sidekiq ([Gargron](https://github.com/tootsuite/mastodon/pull/11230))
|
||||
- Fix expiration date of filters being set to "never" when editing them ([ThibG](https://github.com/tootsuite/mastodon/pull/11204))
|
||||
- Fix support for MP4 files that are actually M4V files ([Gargron](https://github.com/tootsuite/mastodon/pull/11210))
|
||||
- Fix `alerts` not being typecast correctly in push subscription in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/11343))
|
||||
- Fix some notices staying on unrelated pages ([ThibG](https://github.com/tootsuite/mastodon/pull/11364))
|
||||
- Fix unboosting sometimes preventing a boost from reappearing on feed ([ThibG](https://github.com/tootsuite/mastodon/pull/11405), [Gargron](https://github.com/tootsuite/mastodon/pull/11450))
|
||||
- Fix only one middle dot being recognized in hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/11345), [ThibG](https://github.com/tootsuite/mastodon/pull/11363))
|
||||
- Fix unnecessary SQL query performed on unauthenticated requests ([Gargron](https://github.com/tootsuite/mastodon/pull/11179))
|
||||
- Fix incorrect timestamp displayed on featured tags ([Kjwon15](https://github.com/tootsuite/mastodon/pull/11477))
|
||||
- Fix privacy dropdown active state when dropdown is placed on top of it ([ThibG](https://github.com/tootsuite/mastodon/pull/11495))
|
||||
- Fix filters not being applied to poll options ([ThibG](https://github.com/tootsuite/mastodon/pull/11174))
|
||||
- Fix keyboard navigation on various dropdowns ([ThibG](https://github.com/tootsuite/mastodon/pull/11511), [ThibG](https://github.com/tootsuite/mastodon/pull/11492), [ThibG](https://github.com/tootsuite/mastodon/pull/11491))
|
||||
- Fix keyboard navigation in modals ([ThibG](https://github.com/tootsuite/mastodon/pull/11493))
|
||||
- Fix image conversation being non-deterministic due to timestamps ([Gargron](https://github.com/tootsuite/mastodon/pull/11408))
|
||||
- Fix web UI performance ([ThibG](https://github.com/tootsuite/mastodon/pull/11211), [ThibG](https://github.com/tootsuite/mastodon/pull/11234))
|
||||
- Fix scrolling to compose form when not necessary in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11246), [ThibG](https://github.com/tootsuite/mastodon/pull/11182))
|
||||
- Fix save button being enabled when list title is empty in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11475))
|
||||
- Fix poll expiration not being pre-filled on delete & redraft in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11203))
|
||||
- Fix content warning sometimes being set when not requested in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11206))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix invites not being disabled upon account suspension ([ThibG](https://github.com/tootsuite/mastodon/pull/11412))
|
||||
- Fix blocked domains still being able to fill database with account records ([Gargron](https://github.com/tootsuite/mastodon/pull/11219))
|
||||
|
||||
## [2.9.2] - 2019-06-22
|
||||
### Added
|
||||
|
||||
|
10
Gemfile
10
Gemfile
@ -5,7 +5,7 @@ ruby '>= 2.4.0', '< 2.7.0'
|
||||
|
||||
gem 'pkg-config', '~> 1.3'
|
||||
|
||||
gem 'puma', '~> 4.1'
|
||||
gem 'puma', '~> 4.2'
|
||||
gem 'rails', '~> 5.2.3'
|
||||
gem 'thor', '~> 0.20'
|
||||
|
||||
@ -29,7 +29,7 @@ gem 'bootsnap', '~> 1.4', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.0'
|
||||
gem 'chewy', '~> 5.1'
|
||||
gem 'cld3', '~> 3.2.4'
|
||||
gem 'devise', '~> 4.7'
|
||||
gem 'devise-two-factor', '~> 3.1'
|
||||
@ -44,13 +44,13 @@ gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'discard', '~> 1.1'
|
||||
gem 'doorkeeper', '~> 5.1'
|
||||
gem 'doorkeeper', '~> 5.2'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'redis-namespace', '~> 1.5'
|
||||
gem 'health_check', '~> 3.0'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'html2text'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 3.3'
|
||||
@ -118,7 +118,7 @@ end
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.29'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.3'
|
||||
gem 'faker', '~> 2.4'
|
||||
gem 'microformats', '~> 4.1'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
|
54
Gemfile.lock
54
Gemfile.lock
@ -1,3 +1,11 @@
|
||||
GIT
|
||||
remote: https://github.com/ianheggie/health_check
|
||||
revision: 0b799ead604f900ed50685e9b2d469cd2befba5b
|
||||
ref: 0b799ead604f900ed50685e9b2d469cd2befba5b
|
||||
specs:
|
||||
health_check (4.0.0.pre)
|
||||
rails (>= 4.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rtomayko/posix-spawn
|
||||
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
|
||||
@ -161,7 +169,7 @@ GEM
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
charlock_holmes (0.7.6)
|
||||
chewy (5.0.0)
|
||||
chewy (5.1.0)
|
||||
activesupport (>= 4.0)
|
||||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch-dsl
|
||||
@ -209,19 +217,19 @@ GEM
|
||||
docile (1.3.2)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.1.0)
|
||||
doorkeeper (5.2.1)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
elasticsearch (6.0.2)
|
||||
elasticsearch-api (= 6.0.2)
|
||||
elasticsearch-transport (= 6.0.2)
|
||||
elasticsearch-api (6.0.2)
|
||||
elasticsearch (7.3.0)
|
||||
elasticsearch-api (= 7.3.0)
|
||||
elasticsearch-transport (= 7.3.0)
|
||||
elasticsearch-api (7.3.0)
|
||||
multi_json
|
||||
elasticsearch-dsl (0.1.5)
|
||||
elasticsearch-transport (6.0.2)
|
||||
elasticsearch-dsl (0.1.8)
|
||||
elasticsearch-transport (7.3.0)
|
||||
faraday
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
@ -231,9 +239,9 @@ GEM
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.2)
|
||||
faker (2.3.0)
|
||||
faker (2.4.0)
|
||||
i18n (~> 1.6.0)
|
||||
faraday (0.15.0)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.7)
|
||||
@ -278,8 +286,6 @@ GEM
|
||||
concurrent-ruby (~> 1.0)
|
||||
hashdiff (1.0.0)
|
||||
hashie (3.6.0)
|
||||
health_check (3.0.0)
|
||||
railties (>= 5.0)
|
||||
heapy (0.1.4)
|
||||
highline (2.0.1)
|
||||
hiredis (0.6.3)
|
||||
@ -372,10 +378,10 @@ GEM
|
||||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
minitest (5.12.0)
|
||||
msgpack (1.3.1)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.1.1)
|
||||
necromancer (0.5.0)
|
||||
net-ldap (0.16.1)
|
||||
net-scp (2.0.0)
|
||||
@ -447,7 +453,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.1)
|
||||
puma (4.1.1)
|
||||
puma (4.2.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@ -503,7 +509,7 @@ GEM
|
||||
rdf-normalize (0.3.3)
|
||||
rdf (>= 2.2, < 4.0)
|
||||
redcarpet (3.4.0)
|
||||
redis (4.1.2)
|
||||
redis (4.1.3)
|
||||
redis-actionpack (5.0.2)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
@ -593,7 +599,7 @@ GEM
|
||||
simple_form (4.1.0)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
simplecov (0.17.0)
|
||||
simplecov (0.17.1)
|
||||
docile (~> 1.1)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
@ -649,7 +655,7 @@ GEM
|
||||
uniform_notifier (1.12.1)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
webmock (3.7.3)
|
||||
webmock (3.7.5)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@ -690,7 +696,7 @@ DEPENDENCIES
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.29)
|
||||
charlock_holmes (~> 0.7.6)
|
||||
chewy (~> 5.0)
|
||||
chewy (~> 5.1)
|
||||
cld3 (~> 3.2.4)
|
||||
climate_control (~> 0.2)
|
||||
concurrent-ruby
|
||||
@ -700,10 +706,10 @@ DEPENDENCIES
|
||||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.1)
|
||||
doorkeeper (~> 5.1)
|
||||
doorkeeper (~> 5.2)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 2.3)
|
||||
faker (~> 2.4)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
@ -711,7 +717,7 @@ DEPENDENCIES
|
||||
fuubar (~> 2.4)
|
||||
goldfinger (~> 2.1)
|
||||
hamlit-rails (~> 0.2)
|
||||
health_check (~> 3.0)
|
||||
health_check!
|
||||
hiredis (~> 0.6)
|
||||
html2text
|
||||
htmlentities (~> 4.3)
|
||||
@ -756,7 +762,7 @@ DEPENDENCIES
|
||||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.7)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.1)
|
||||
puma (~> 4.2)
|
||||
pundit (~> 2.1)
|
||||
rack-attack (~> 6.1)
|
||||
rack-cors (~> 1.0)
|
||||
@ -798,7 +804,7 @@ DEPENDENCIES
|
||||
webpush
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.1p33
|
||||
ruby 2.6.4p104
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
|
9
app.json
9
app.json
@ -13,15 +13,6 @@
|
||||
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
|
||||
"required": true
|
||||
},
|
||||
"LOCAL_HTTPS": {
|
||||
"description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)",
|
||||
"value": "false",
|
||||
"required": true
|
||||
},
|
||||
"PAPERCLIP_SECRET": {
|
||||
"description": "The secret key for storing media files",
|
||||
"generator": "secret"
|
||||
},
|
||||
"SECRET_KEY_BASE": {
|
||||
"description": "The secret key base",
|
||||
"generator": "secret"
|
||||
|
@ -4,9 +4,7 @@ class AboutController < ApplicationController
|
||||
before_action :set_pack
|
||||
layout 'public'
|
||||
|
||||
before_action :require_open_federation!, only: [:show, :more, :blocks]
|
||||
before_action :check_blocklist_enabled, only: [:blocks]
|
||||
before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
|
||||
before_action :require_open_federation!, only: [:show, :more]
|
||||
before_action :set_body_classes, only: :show
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_expires_in, only: [:show, :more, :terms]
|
||||
@ -17,15 +15,20 @@ class AboutController < ApplicationController
|
||||
|
||||
def more
|
||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
||||
|
||||
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
|
||||
|
||||
@contents = toc_generator.html
|
||||
@table_of_contents = toc_generator.toc
|
||||
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
|
||||
end
|
||||
|
||||
def terms; end
|
||||
|
||||
def blocks
|
||||
@show_rationale = Setting.show_domain_blocks_rationale == 'all'
|
||||
@show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional?
|
||||
@blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a
|
||||
end
|
||||
helper_method :display_blocks?
|
||||
helper_method :display_blocks_rationale?
|
||||
helper_method :public_fetch_mode?
|
||||
helper_method :new_user
|
||||
|
||||
private
|
||||
|
||||
@ -33,28 +36,14 @@ class AboutController < ApplicationController
|
||||
not_found if whitelist_mode?
|
||||
end
|
||||
|
||||
def check_blocklist_enabled
|
||||
not_found if Setting.show_domain_blocks == 'disabled'
|
||||
def display_blocks?
|
||||
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||
end
|
||||
|
||||
def blocklist_account_required?
|
||||
Setting.show_domain_blocks == 'users'
|
||||
def display_blocks_rationale?
|
||||
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
|
||||
end
|
||||
|
||||
def block_severity_text(block)
|
||||
if block.severity == 'suspend'
|
||||
I18n.t('domain_blocks.suspension')
|
||||
else
|
||||
limitations = []
|
||||
limitations << I18n.t('domain_blocks.media_block') if block.reject_media?
|
||||
limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence'
|
||||
limitations.join(', ')
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :block_severity_text
|
||||
helper_method :public_fetch_mode?
|
||||
|
||||
def new_user
|
||||
User.new.tap do |user|
|
||||
user.build_account
|
||||
@ -62,8 +51,6 @@ class AboutController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :new_user
|
||||
|
||||
def set_pack
|
||||
use_pack 'public'
|
||||
end
|
||||
|
@ -10,6 +10,7 @@ class AccountsController < ApplicationController
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
|
@ -33,9 +33,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||
def scope_for_collection
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
|
||||
scope.merge!(@account.pinned_statuses)
|
||||
end
|
||||
return Status.none if @account.blocking?(signed_request_account)
|
||||
|
||||
@account.pinned_statuses
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
@ -3,6 +3,7 @@
|
||||
module Admin
|
||||
class RelaysController < BaseController
|
||||
before_action :set_relay, except: [:index, :new, :create]
|
||||
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
|
||||
|
||||
def index
|
||||
authorize :relay, :update?
|
||||
@ -11,7 +12,7 @@ module Admin
|
||||
|
||||
def new
|
||||
authorize :relay, :update?
|
||||
@relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
|
||||
@relay = Relay.new
|
||||
end
|
||||
|
||||
def create
|
||||
@ -54,5 +55,9 @@ module Admin
|
||||
def resource_params
|
||||
params.require(:relay).permit(:inbox_url)
|
||||
end
|
||||
|
||||
def require_signatures_enabled!
|
||||
redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ module Admin
|
||||
authorize @user, :disable_2fa?
|
||||
@user.disable_two_factor!
|
||||
log_action :disable_2fa, @user
|
||||
UserMailer.two_factor_disabled(@user).deliver_later!
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
|
@ -57,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
end
|
||||
|
||||
def pinned_scope
|
||||
return Status.none if @account.blocking?(current_account)
|
||||
|
||||
@account.pinned_statuses
|
||||
end
|
||||
|
||||
|
@ -33,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
def follow
|
||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||
|
||||
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||
end
|
||||
|
@ -22,7 +22,7 @@ class Api::V2::SearchController < Api::BaseController
|
||||
params[:q],
|
||||
current_account,
|
||||
limit_param(RESULTS_LIMIT),
|
||||
search_params.merge(resolve: truthy_param?(:resolve))
|
||||
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
|
||||
)
|
||||
end
|
||||
|
||||
|
29
app/controllers/auth/challenges_controller.rb
Normal file
29
app/controllers/auth/challenges_controller.rb
Normal file
@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Auth::ChallengesController < ApplicationController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'auth'
|
||||
|
||||
before_action :set_pack
|
||||
before_action :authenticate_user!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def create
|
||||
if challenge_passed?
|
||||
session[:challenge_passed_at] = Time.now.utc
|
||||
redirect_to challenge_params[:return_to]
|
||||
else
|
||||
@challenge = Form::Challenge.new(return_to: challenge_params[:return_to])
|
||||
flash.now[:alert] = I18n.t('challenge.invalid_password')
|
||||
render_challenge
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
end
|
@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
skip_before_action :require_functional!
|
||||
|
||||
prepend_before_action :set_pack
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
|
||||
before_action :set_instance_presenter, only: [:new]
|
||||
before_action :set_body_classes
|
||||
@ -22,34 +23,32 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
end
|
||||
|
||||
def create
|
||||
self.resource = begin
|
||||
if user_params[:email].blank? && session[:otp_user_id].present?
|
||||
User.find(session[:otp_user_id])
|
||||
else
|
||||
warden.authenticate!(auth_options)
|
||||
end
|
||||
end
|
||||
|
||||
if resource.otp_required_for_login?
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id].present?
|
||||
authenticate_with_two_factor_via_otp(resource)
|
||||
else
|
||||
prompt_for_two_factor(resource)
|
||||
end
|
||||
else
|
||||
authenticate_and_respond(resource)
|
||||
super do |resource|
|
||||
remember_me(resource)
|
||||
flash.delete(:notice)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
tmp_stored_location = stored_location_for(:user)
|
||||
super
|
||||
session.delete(:challenge_passed_at)
|
||||
flash.delete(:notice)
|
||||
store_location_for(:user, tmp_stored_location) if continue_after?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def find_user
|
||||
if session[:otp_user_id]
|
||||
User.find(session[:otp_user_id])
|
||||
else
|
||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||
user ||= User.find_for_authentication(email: user_params[:email])
|
||||
end
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password, :otp_attempt)
|
||||
end
|
||||
@ -72,6 +71,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
super
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
find_user&.otp_required_for_login?
|
||||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
@ -79,10 +82,24 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
false
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
|
||||
# If encrypted_password is blank, we got the user from LDAP or PAM,
|
||||
# so credentials are already valid
|
||||
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if valid_otp_attempt?(user)
|
||||
session.delete(:otp_user_id)
|
||||
authenticate_and_respond(user)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||
prompt_for_two_factor(user)
|
||||
@ -91,16 +108,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
|
||||
def prompt_for_two_factor(user)
|
||||
session[:otp_user_id] = user.id
|
||||
@body_classes = 'lighter'
|
||||
render :two_factor
|
||||
end
|
||||
|
||||
def authenticate_and_respond(user)
|
||||
sign_in(user)
|
||||
remember_me(user)
|
||||
|
||||
respond_with user, location: after_sign_in_path_for(user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
@ -117,11 +128,9 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
|
||||
def home_paths(resource)
|
||||
paths = [about_path]
|
||||
|
||||
if single_user_mode? && resource.is_a?(User)
|
||||
paths << short_account_path(username: resource.account)
|
||||
end
|
||||
|
||||
paths
|
||||
end
|
||||
|
||||
|
65
app/controllers/concerns/challengable_concern.rb
Normal file
65
app/controllers/concerns/challengable_concern.rb
Normal file
@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This concern is inspired by "sudo mode" on GitHub. It
|
||||
# is a way to re-authenticate a user before allowing them
|
||||
# to see or perform an action.
|
||||
#
|
||||
# Add `before_action :require_challenge!` to actions you
|
||||
# want to protect.
|
||||
#
|
||||
# The user will be shown a page to enter the challenge (which
|
||||
# is either the password, or just the username when no
|
||||
# password exists). Upon passing, there is a grace period
|
||||
# during which no challenge will be asked from the user.
|
||||
#
|
||||
# Accessing challenge-protected resources during the grace
|
||||
# period will refresh the grace period.
|
||||
module ChallengableConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
CHALLENGE_TIMEOUT = 1.hour.freeze
|
||||
|
||||
def require_challenge!
|
||||
return if skip_challenge?
|
||||
|
||||
if challenge_passed_recently?
|
||||
session[:challenge_passed_at] = Time.now.utc
|
||||
return
|
||||
end
|
||||
|
||||
@challenge = Form::Challenge.new(return_to: request.url)
|
||||
|
||||
if params.key?(:form_challenge)
|
||||
if challenge_passed?
|
||||
session[:challenge_passed_at] = Time.now.utc
|
||||
return
|
||||
else
|
||||
flash.now[:alert] = I18n.t('challenge.invalid_password')
|
||||
render_challenge
|
||||
end
|
||||
else
|
||||
render_challenge
|
||||
end
|
||||
end
|
||||
|
||||
def render_challenge
|
||||
@body_classes = 'lighter'
|
||||
render template: 'auth/challenges/new', layout: 'auth'
|
||||
end
|
||||
|
||||
def challenge_passed?
|
||||
current_user.valid_password?(challenge_params[:current_password])
|
||||
end
|
||||
|
||||
def skip_challenge?
|
||||
current_user.encrypted_password.blank?
|
||||
end
|
||||
|
||||
def challenge_passed_recently?
|
||||
session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago
|
||||
end
|
||||
|
||||
def challenge_params
|
||||
params.require(:form_challenge).permit(:current_password, :return_to)
|
||||
end
|
||||
end
|
@ -5,7 +5,10 @@ module ExportControllerConcern
|
||||
|
||||
included do
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :load_export
|
||||
|
||||
skip_before_action :require_functional!
|
||||
end
|
||||
|
||||
private
|
||||
@ -27,4 +30,8 @@ module ExportControllerConcern
|
||||
def export_filename
|
||||
"#{controller_name}.csv"
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class CustomCssController < ApplicationController
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
@ -10,6 +10,8 @@ class DirectoriesController < ApplicationController
|
||||
before_action :set_accounts
|
||||
before_action :set_pack
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def index
|
||||
render :index
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ class FollowerAccountsController < ApplicationController
|
||||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
@ -8,6 +8,7 @@ class FollowingAccountsController < ApplicationController
|
||||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class ManifestsController < ApplicationController
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
expires_in 3.minutes, public: true
|
||||
|
@ -4,6 +4,7 @@ class MediaController < ApplicationController
|
||||
include Authorization
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_media_attachment
|
||||
|
@ -4,6 +4,7 @@ class MediaProxyController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
|
||||
|
@ -8,6 +8,8 @@ class RemoteFollowController < ApplicationController
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
|
@ -11,6 +11,8 @@ class RemoteInteractionController < ApplicationController
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
end
|
||||
|
43
app/controllers/settings/aliases_controller.rb
Normal file
43
app/controllers/settings/aliases_controller.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::AliasesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_aliases, except: :destroy
|
||||
before_action :set_alias, only: :destroy
|
||||
|
||||
def index
|
||||
@alias = current_account.aliases.build
|
||||
end
|
||||
|
||||
def create
|
||||
@alias = current_account.aliases.build(resource_params)
|
||||
|
||||
if @alias.save
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg')
|
||||
else
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@alias.destroy!
|
||||
redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:account_alias).permit(:acct)
|
||||
end
|
||||
|
||||
def set_alias
|
||||
@alias = current_account.aliases.find(params[:id])
|
||||
end
|
||||
|
||||
def set_aliases
|
||||
@aliases = current_account.aliases.order(id: :desc).reject(&:new_record?)
|
||||
end
|
||||
end
|
@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@export = Export.new(current_account)
|
||||
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
45
app/controllers/settings/migration/redirects_controller.rb
Normal file
45
app/controllers/settings/migration/redirects_controller.rb
Normal file
@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::Migration::RedirectsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
@redirect = Form::Redirect.new
|
||||
end
|
||||
|
||||
def create
|
||||
@redirect = Form::Redirect.new(resource_params.merge(account: current_account))
|
||||
|
||||
if @redirect.valid_with_challenge?(current_user)
|
||||
current_account.update!(moved_to_account: @redirect.target_account)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if current_account.moved_to_account_id.present?
|
||||
current_account.update!(moved_to_account: nil)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
end
|
||||
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
@ -4,31 +4,48 @@ class Settings::MigrationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :set_migrations
|
||||
before_action :set_cooldown
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@migration = Form::Migration.new(account: current_account.moved_to_account)
|
||||
@migration = current_account.migrations.build
|
||||
end
|
||||
|
||||
def update
|
||||
@migration = Form::Migration.new(resource_params)
|
||||
def create
|
||||
@migration = current_account.migrations.build(resource_params)
|
||||
|
||||
if @migration.valid? && migration_account_changed?
|
||||
current_account.update!(moved_to_account: @migration.account)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
|
||||
if @migration.save_with_challenge(current_user)
|
||||
MoveService.new.call(@migration)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :on_cooldown?
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:migration).permit(:acct)
|
||||
params.require(:account_migration).permit(:acct, :current_password, :current_username)
|
||||
end
|
||||
|
||||
def migration_account_changed?
|
||||
current_account.moved_to_account_id != @migration.account&.id &&
|
||||
current_account.id != @migration.account&.id
|
||||
def set_migrations
|
||||
@migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
|
||||
end
|
||||
|
||||
def set_cooldown
|
||||
@cooldown = current_account.migrations.within_cooldown.first
|
||||
end
|
||||
|
||||
def on_cooldown?
|
||||
@cooldown.present?
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
@ -3,9 +3,12 @@
|
||||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class ConfirmationsController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_challenge!
|
||||
before_action :ensure_otp_secret
|
||||
|
||||
skip_before_action :require_functional!
|
||||
@ -22,6 +25,8 @@ module Settings
|
||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
|
||||
UserMailer.two_factor_enabled(current_user).deliver_later!
|
||||
|
||||
render 'settings/two_factor_authentication/recovery_codes/index'
|
||||
else
|
||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
||||
|
@ -3,16 +3,22 @@
|
||||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class RecoveryCodesController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_challenge!, on: :create
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def create
|
||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
|
||||
UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
|
||||
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
|
||||
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
module Settings
|
||||
class TwoFactorAuthenticationsController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_otp_required, only: [:create]
|
||||
before_action :require_challenge!, only: [:create]
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
@ -23,6 +26,7 @@ module Settings
|
||||
if acceptable_code?
|
||||
current_user.otp_required_for_login = false
|
||||
current_user.save!
|
||||
UserMailer.two_factor_disabled(current_user).deliver_later!
|
||||
redirect_to settings_two_factor_authentication_path
|
||||
else
|
||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
||||
|
@ -19,6 +19,7 @@ class StatusesController < ApplicationController
|
||||
before_action :set_autoplay, only: :embed
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, only: [:show, :embed]
|
||||
|
||||
content_security_policy only: :embed do |p|
|
||||
p.frame_ancestors(false)
|
||||
|
@ -13,6 +13,8 @@ class TagsController < ApplicationController
|
||||
before_action :set_body_classes
|
||||
before_action :set_instance_presenter
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
19
app/controllers/well_known/nodeinfo_controller.rb
Normal file
19
app/controllers/well_known/nodeinfo_controller.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class NodeInfoController < ActionController::Base
|
||||
include CacheConcern
|
||||
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
|
||||
def index
|
||||
expires_in 3.days, public: true
|
||||
render_with_cache json: {}, serializer: NodeInfo::DiscoverySerializer, adapter: NodeInfo::Adapter, expires_in: 3.days, root: 'nodeinfo'
|
||||
end
|
||||
|
||||
def show
|
||||
expires_in 30.minutes, public: true
|
||||
render_with_cache json: {}, serializer: NodeInfo::Serializer, adapter: NodeInfo::Adapter, expires_in: 30.minutes, root: 'nodeinfo'
|
||||
end
|
||||
end
|
||||
end
|
@ -87,4 +87,12 @@ module SettingsHelper
|
||||
'desktop'
|
||||
end
|
||||
end
|
||||
|
||||
def compact_account_link_to(account)
|
||||
return if account.nil?
|
||||
|
||||
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
|
||||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,7 @@
|
||||
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { openModal } from './modal';
|
||||
|
||||
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
||||
@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
|
||||
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||
|
||||
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
|
||||
|
||||
export function fetchBlocks() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchBlocksRequest());
|
||||
@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function initBlockModal(account) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BLOCKS_INIT_MODAL,
|
||||
account,
|
||||
});
|
||||
|
||||
dispatch(openModal('BLOCK'));
|
||||
};
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ export function uploadCompose(files) {
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||
},
|
||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
|
||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||
};
|
||||
};
|
||||
@ -316,10 +316,11 @@ export function uploadComposeProgress(loaded, total) {
|
||||
};
|
||||
};
|
||||
|
||||
export function uploadComposeSuccess(media) {
|
||||
export function uploadComposeSuccess(media, file) {
|
||||
return {
|
||||
type: COMPOSE_UPLOAD_SUCCESS,
|
||||
media: media,
|
||||
file: file,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||
|
||||
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
||||
|
||||
export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
|
||||
export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
|
||||
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
|
||||
|
||||
export const mountConversations = () => ({
|
||||
type: CONVERSATIONS_MOUNT,
|
||||
});
|
||||
@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
|
||||
conversation,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteConversation = conversationId => (dispatch, getState) => {
|
||||
dispatch(deleteConversationRequest(conversationId));
|
||||
|
||||
api(getState).delete(`/api/v1/conversations/${conversationId}`)
|
||||
.then(() => dispatch(deleteConversationSuccess(conversationId)))
|
||||
.catch(error => dispatch(deleteConversationFail(conversationId, error)));
|
||||
};
|
||||
|
||||
export const deleteConversationRequest = id => ({
|
||||
type: CONVERSATIONS_DELETE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteConversationSuccess = id => ({
|
||||
type: CONVERSATIONS_DELETE_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteConversationFail = (id, error) => ({
|
||||
type: CONVERSATIONS_DELETE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
@ -71,8 +71,9 @@ export function normalizePoll(poll) {
|
||||
|
||||
const emojiMap = makeEmojiMap(normalPoll);
|
||||
|
||||
normalPoll.options = poll.options.map(option => ({
|
||||
normalPoll.options = poll.options.map((option, index) => ({
|
||||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
}));
|
||||
|
||||
|
@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,13 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
|
||||
|
||||
{accounts.size > 4 && (
|
||||
<span className='account__avatar-composite__label'>
|
||||
+{accounts.size - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
export default class ColumnBackButtonSlim extends React.PureComponent {
|
||||
|
||||
|
@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import emojify from 'flavours/glitch/util/emoji';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
||||
});
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
const { poll, disabled } = this.props;
|
||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
|
||||
const { poll, disabled, intl } = this.props;
|
||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
|
||||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||
|
||||
let titleEmojified = option.get('title_emojified');
|
||||
if (!titleEmojified) {
|
||||
@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
|
||||
/>
|
||||
|
||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
||||
{showResults && <span className='poll__number'>
|
||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||
{Math.round(percent)}%
|
||||
</span>}
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||
</label>
|
||||
@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
|
||||
const showResults = poll.get('voted') || expired;
|
||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||
|
||||
let votesCount = null;
|
||||
|
||||
if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
|
||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
||||
} else {
|
||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='poll'>
|
||||
<ul>
|
||||
@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
|
||||
<div className='poll__footer'>
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
|
||||
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
|
||||
{votesCount}
|
||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Status from 'flavours/glitch/components/status';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
@ -18,9 +17,9 @@ import {
|
||||
pin,
|
||||
unpin,
|
||||
} from 'flavours/glitch/actions/interactions';
|
||||
import { blockAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||
@ -37,10 +36,8 @@ const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
||||
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
||||
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
||||
@ -83,6 +80,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
onReply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
@ -186,16 +184,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
|
||||
onBlock (status) {
|
||||
const account = status.get('account');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
},
|
||||
|
||||
onUnfilter (status, onConfirm) {
|
||||
|
@ -5,7 +5,6 @@ import Header from '../components/header';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
pinAccount,
|
||||
@ -16,6 +15,7 @@ import {
|
||||
directCompose
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
|
||||
@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -2,9 +2,28 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
import StatusContent from 'flavours/glitch/components/status_content';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
export default class Conversation extends ImmutablePureComponent {
|
||||
const messages = defineMessages({
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
open: { id: 'conversation.open', defaultMessage: 'View conversation' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
|
||||
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Conversation extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
@ -13,25 +32,61 @@ export default class Conversation extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
conversationId: PropTypes.string.isRequired,
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatusId: PropTypes.string,
|
||||
lastStatus: ImmutablePropTypes.map,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isExpanded: undefined,
|
||||
};
|
||||
|
||||
parseClick = (e, destination) => {
|
||||
const { router } = this.context;
|
||||
const { lastStatus, unread, markRead } = this.props;
|
||||
if (!router) return;
|
||||
|
||||
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
||||
if (destination === undefined) {
|
||||
if (unread) {
|
||||
markRead();
|
||||
}
|
||||
destination = `/statuses/${lastStatus.get('id')}`;
|
||||
}
|
||||
let state = {...router.history.location.state};
|
||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||
router.history.push(destination, state);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.context.router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lastStatusId, unread, markRead } = this.props;
|
||||
const { lastStatus, unread, markRead } = this.props;
|
||||
|
||||
if (unread) {
|
||||
markRead();
|
||||
}
|
||||
|
||||
this.context.router.history.push(`/statuses/${lastStatusId}`);
|
||||
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
|
||||
}
|
||||
|
||||
handleMarkAsRead = () => {
|
||||
this.props.markRead();
|
||||
}
|
||||
|
||||
handleReply = () => {
|
||||
this.props.reply(this.props.lastStatus, this.context.router.history);
|
||||
}
|
||||
|
||||
handleDelete = () => {
|
||||
this.props.delete();
|
||||
}
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
@ -42,22 +97,94 @@ export default class Conversation extends ImmutablePureComponent {
|
||||
this.props.onMoveDown(this.props.conversationId);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatusId, unread } = this.props;
|
||||
handleConversationMute = () => {
|
||||
this.props.onMute(this.props.lastStatus);
|
||||
}
|
||||
|
||||
if (lastStatusId === null) {
|
||||
handleShowMore = () => {
|
||||
if (this.props.lastStatus.get('spoiler_text')) {
|
||||
this.setExpansion(!this.state.isExpanded);
|
||||
}
|
||||
};
|
||||
|
||||
setExpansion = value => {
|
||||
this.setState({ isExpanded: value });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatus, unread, intl } = this.props;
|
||||
const { isExpanded } = this.state;
|
||||
|
||||
if (lastStatus === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.open), action: this.handleClick },
|
||||
null,
|
||||
];
|
||||
|
||||
menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
|
||||
|
||||
if (unread) {
|
||||
menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
||||
|
||||
const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
const handlers = {
|
||||
reply: this.handleReply,
|
||||
open: this.handleClick,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleShowMore,
|
||||
};
|
||||
|
||||
let media = null;
|
||||
if (lastStatus.get('media_attachments').size > 0) {
|
||||
media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusContainer
|
||||
id={lastStatusId}
|
||||
unread={unread}
|
||||
otherAccounts={accounts}
|
||||
onMoveUp={this.handleHotkeyMoveUp}
|
||||
onMoveDown={this.handleHotkeyMoveDown}
|
||||
onClick={this.handleClick}
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className='conversation focusable muted' tabIndex='0'>
|
||||
<div className='conversation__avatar'>
|
||||
<AvatarComposite accounts={accounts} size={48} />
|
||||
</div>
|
||||
|
||||
<div className='conversation__content'>
|
||||
<div className='conversation__content__info'>
|
||||
<div className='conversation__content__relative-time'>
|
||||
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||
</div>
|
||||
|
||||
<div className='conversation__content__names'>
|
||||
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
status={lastStatus}
|
||||
parseClick={this.parseClick}
|
||||
expanded={isExpanded}
|
||||
onExpandedToggle={this.handleShowMore}
|
||||
collapsable
|
||||
media={media}
|
||||
/>
|
||||
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,74 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Conversation from '../components/conversation';
|
||||
import { markConversationRead } from '../../../actions/conversations';
|
||||
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const mapStateToProps = (state, { conversationId }) => {
|
||||
const messages = defineMessages({
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const mapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
return (state, { conversationId }) => {
|
||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||
const lastStatusId = conversation.get('last_status', null);
|
||||
|
||||
return {
|
||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||
unread: conversation.get('unread'),
|
||||
lastStatusId: conversation.get('last_status', null),
|
||||
lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
||||
markRead: () => dispatch(markConversationRead(conversationId)),
|
||||
const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
|
||||
|
||||
markRead () {
|
||||
dispatch(markConversationRead(conversationId));
|
||||
},
|
||||
|
||||
reply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status, router));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
delete () {
|
||||
dispatch(deleteConversation(conversationId));
|
||||
},
|
||||
|
||||
onMute (status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleHidden (status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
|
||||
|
@ -31,8 +31,10 @@ class Favourites extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
|
@ -36,9 +36,11 @@ class Followers extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(fetchFollowers(this.props.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
|
@ -36,9 +36,11 @@ class Following extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(fetchFollowing(this.props.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
|
@ -104,17 +104,15 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { myAccount, fetchFollowRequests, multiColumn } = this.props;
|
||||
const { fetchFollowRequests, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
fetchFollowRequests();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
|
||||
@ -148,7 +146,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||
navItems.push(<ColumnLink key='5' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
|
||||
}
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
||||
navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ class FilterBar extends React.PureComponent {
|
||||
onClick={this.onClick('mention')}
|
||||
title={intl.formatMessage(tooltips.mentions)}
|
||||
>
|
||||
<Icon id='at' fixedWidth />
|
||||
<Icon id='reply-all' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'favourite' ? 'active' : ''}
|
||||
|
@ -31,8 +31,10 @@ class Reblogs extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import DetailedStatus from '../components/detailed_status';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
@ -15,7 +14,6 @@ import {
|
||||
pin,
|
||||
unpin,
|
||||
} from 'flavours/glitch/actions/interactions';
|
||||
import { blockAccount } from 'flavours/glitch/actions/accounts';
|
||||
import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
@ -24,9 +22,10 @@ import {
|
||||
revealStatus,
|
||||
} from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||
import { showAlertForError } from 'flavours/glitch/actions/alerts';
|
||||
|
||||
@ -35,10 +34,8 @@ const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -139,16 +136,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onBlock (status) {
|
||||
const account = status.get('account');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
},
|
||||
|
||||
onReport (status) {
|
||||
|
@ -26,9 +26,9 @@ import {
|
||||
directCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||
import { blockAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
@ -36,7 +36,7 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||
@ -50,13 +50,11 @@ const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
|
||||
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
|
||||
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' },
|
||||
});
|
||||
|
||||
@ -339,19 +337,9 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
handleBlockClick = (status) => {
|
||||
const { dispatch, intl } = this.props;
|
||||
const { dispatch } = this.props;
|
||||
const account = status.get('account');
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
}
|
||||
|
||||
handleReport = (status) => {
|
||||
|
@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import Button from '../../../components/button';
|
||||
import { closeModal } from '../../../actions/modal';
|
||||
import { blockAccount } from '../../../actions/accounts';
|
||||
import { initReport } from '../../../actions/reports';
|
||||
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onConfirm(account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onBlockAndReport(account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
|
||||
onClose() {
|
||||
dispatch(closeModal());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class BlockModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onBlockAndReport: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClose();
|
||||
this.props.onConfirm(this.props.account);
|
||||
}
|
||||
|
||||
handleSecondary = () => {
|
||||
this.props.onClose();
|
||||
this.props.onBlockAndReport(this.props.account);
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.button = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal block-modal'>
|
||||
<div className='block-modal__container'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='confirmations.block.message'
|
||||
defaultMessage='Are you sure you want to block {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='block-modal__action-bar'>
|
||||
<Button onClick={this.handleCancel} className='block-modal__cancel-button'>
|
||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
|
||||
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
|
||||
</Button>
|
||||
<Button onClick={this.handleClick} ref={this.setRef}>
|
||||
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -173,7 +173,17 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
langPath: `${assetHost}/ocr/lang-data`,
|
||||
});
|
||||
|
||||
worker.recognize(media.get('url'))
|
||||
let media_url = media.get('file');
|
||||
|
||||
if (window.URL && URL.createObjectURL) {
|
||||
try {
|
||||
media_url = URL.createObjectURL(media.get('file'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
worker.recognize(media_url)
|
||||
.progress(({ progress }) => this.setState({ progress }))
|
||||
.finally(() => worker.terminate())
|
||||
.then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
|
||||
|
@ -15,6 +15,7 @@ import FocalPointModal from './focal_point_modal';
|
||||
import {
|
||||
OnboardingModal,
|
||||
MuteModal,
|
||||
BlockModal,
|
||||
ReportModal,
|
||||
SettingsModal,
|
||||
EmbedModal,
|
||||
@ -32,6 +33,7 @@ const MODAL_COMPONENTS = {
|
||||
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
|
||||
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
||||
'MUTE': MuteModal,
|
||||
'BLOCK': BlockModal,
|
||||
'REPORT': ReportModal,
|
||||
'SETTINGS': SettingsModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
|
@ -11,7 +11,6 @@ import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||
account: state.getIn(['mutes', 'new', 'account']),
|
||||
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
||||
};
|
||||
@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
class MuteModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isSubmitting: PropTypes.bool.isRequired,
|
||||
account: PropTypes.object.isRequired,
|
||||
notifications: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent {
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
{' '}
|
||||
<p className='mute-modal__explanation'>
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.explanation'
|
||||
defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts follow you.'
|
||||
/>
|
||||
</p>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||
<label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -114,6 +114,16 @@ function main() {
|
||||
this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
delegate(document, '.sidebar__toggle__icon', 'click', () => {
|
||||
const target = document.querySelector('.sidebar ul');
|
||||
|
||||
if (target.style.display === 'block') {
|
||||
target.style.display = 'none';
|
||||
} else {
|
||||
target.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills().then(main).catch(error => {
|
||||
|
20
app/javascript/flavours/glitch/packs/settings.js
Normal file
20
app/javascript/flavours/glitch/packs/settings.js
Normal file
@ -0,0 +1,20 @@
|
||||
import loadPolyfills from 'flavours/glitch/util/load_polyfills';
|
||||
import ready from 'flavours/glitch/util/ready';
|
||||
|
||||
function main() {
|
||||
const { delegate } = require('rails-ujs');
|
||||
|
||||
delegate(document, '.sidebar__toggle__icon', 'click', () => {
|
||||
const target = document.querySelector('.sidebar ul');
|
||||
|
||||
if (target.style.display === 'block') {
|
||||
target.style.display = 'none';
|
||||
} else {
|
||||
target.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills().then(main).catch(error => {
|
||||
console.error(error);
|
||||
});
|
22
app/javascript/flavours/glitch/reducers/blocks.js
Normal file
22
app/javascript/flavours/glitch/reducers/blocks.js
Normal file
@ -0,0 +1,22 @@
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import {
|
||||
BLOCKS_INIT_MODAL,
|
||||
} from '../actions/blocks';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
new: Immutable.Map({
|
||||
account_id: null,
|
||||
}),
|
||||
});
|
||||
|
||||
export default function mutes(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case BLOCKS_INIT_MODAL:
|
||||
return state.withMutations((state) => {
|
||||
state.setIn(['new', 'account_id'], action.account.get('id'));
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -190,10 +190,13 @@ function continueThread (state, status) {
|
||||
});
|
||||
}
|
||||
|
||||
function appendMedia(state, media) {
|
||||
function appendMedia(state, media, file) {
|
||||
const prevSize = state.get('media_attachments').size;
|
||||
|
||||
return state.withMutations(map => {
|
||||
if (media.get('type') === 'image') {
|
||||
media = media.set('file', file);
|
||||
}
|
||||
map.update('media_attachments', list => list.push(media));
|
||||
map.set('is_uploading', false);
|
||||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||
@ -422,7 +425,7 @@ export default function compose(state = initialState, action) {
|
||||
case COMPOSE_UPLOAD_REQUEST:
|
||||
return state.set('is_uploading', true);
|
||||
case COMPOSE_UPLOAD_SUCCESS:
|
||||
return appendMedia(state, fromJS(action.media));
|
||||
return appendMedia(state, fromJS(action.media), action.file);
|
||||
case COMPOSE_UPLOAD_FAIL:
|
||||
return state.set('is_uploading', false);
|
||||
case COMPOSE_UPLOAD_UNDO:
|
||||
|
@ -16,6 +16,7 @@ import local_settings from './local_settings';
|
||||
import push_notifications from './push_notifications';
|
||||
import status_lists from './status_lists';
|
||||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import reports from './reports';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
@ -53,6 +54,7 @@ const reducers = {
|
||||
local_settings,
|
||||
push_notifications,
|
||||
mutes,
|
||||
blocks,
|
||||
reports,
|
||||
contexts,
|
||||
compose,
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
new: Immutable.Map({
|
||||
isSubmitting: false,
|
||||
account: null,
|
||||
notifications: true,
|
||||
}),
|
||||
@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case MUTES_INIT_MODAL:
|
||||
return state.withMutations((state) => {
|
||||
state.setIn(['new', 'isSubmitting'], false);
|
||||
state.setIn(['new', 'account'], action.account);
|
||||
state.setIn(['new', 'notifications'], true);
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ const notificationToMap = (state, notification) => ImmutableMap({
|
||||
const normalizeNotification = (state, notification, usePendingItems) => {
|
||||
const top = !shouldCountUnreadNotifications(state);
|
||||
|
||||
if (usePendingItems || !top || !state.get('pendingItems').isEmpty()) {
|
||||
if (usePendingItems || !state.get('pendingItems').isEmpty()) {
|
||||
return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1);
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
|
||||
|
||||
return state.withMutations(mutable => {
|
||||
if (!items.isEmpty()) {
|
||||
usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('top') || !mutable.get('pendingItems').isEmpty());
|
||||
usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
|
||||
|
||||
mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
|
||||
const lastIndex = 1 + list.findLastIndex(
|
||||
|
@ -40,7 +40,8 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||
if (timeline.endsWith(':pinned')) {
|
||||
mMap.set('items', statuses.map(status => status.get('id')));
|
||||
} else if (!statuses.isEmpty()) {
|
||||
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty());
|
||||
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
|
||||
|
||||
mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
|
||||
const newIds = statuses.map(status => status.get('id'));
|
||||
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
|
||||
@ -62,7 +63,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||
const updateTimeline = (state, timeline, status, usePendingItems) => {
|
||||
const top = state.getIn([timeline, 'top']);
|
||||
|
||||
if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
|
||||
if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
|
||||
if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
|
||||
return state;
|
||||
}
|
||||
|
@ -62,24 +62,6 @@
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin search-popout() {
|
||||
|
@ -17,117 +17,86 @@ $small-breakpoint: 960px;
|
||||
|
||||
.rich-formatting {
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
line-height: 1.7;
|
||||
word-wrap: break-word;
|
||||
color: $darker-text-color;
|
||||
padding-right: 10px;
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
margin-bottom: 12px;
|
||||
color: $darker-text-color;
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: .85em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong,
|
||||
em {
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: lighten($darker-text-color, 10%);
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.85em;
|
||||
background: darken($ui-base-color, 8%);
|
||||
border-radius: 4px;
|
||||
padding: 0.2em 0.3em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: $font-display, sans-serif;
|
||||
margin-top: 1.275em;
|
||||
margin-bottom: .85em;
|
||||
font-weight: 500;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 26px;
|
||||
line-height: 30px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
|
||||
small {
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: lighten($darker-text-color, 10%);
|
||||
}
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 22px;
|
||||
line-height: 26px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h5,
|
||||
h6 {
|
||||
font-family: $font-display, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 20px;
|
||||
|
||||
&[type='a'] {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
&[type='i'] {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
@ -138,23 +107,79 @@ $small-breakpoint: 960px;
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
li > ol,
|
||||
li > ul {
|
||||
margin-top: 6px;
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: 2em;
|
||||
margin-bottom: 0.85em;
|
||||
|
||||
&[type='a'] {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
&[type='i'] {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
margin: 1.7em 0;
|
||||
|
||||
&.spacer {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
break-inside: auto;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
thead tr,
|
||||
tbody tr {
|
||||
break-after: auto;
|
||||
break-inside: avoid;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
font-size: 1em;
|
||||
line-height: 1.625;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
border-bottom-width: 2px;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
color: $dark-text-color;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
align-self: start;
|
||||
align-items: start;
|
||||
|
||||
&.nowrap {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.information-board {
|
||||
@ -418,7 +443,7 @@ $small-breakpoint: 960px;
|
||||
}
|
||||
|
||||
&__call-to-action {
|
||||
background: darken($ui-base-color, 4%);
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px;
|
||||
padding: 25px 40px;
|
||||
overflow: hidden;
|
||||
|
@ -5,21 +5,66 @@ $content-width: 840px;
|
||||
.admin-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
.sidebar-wrapper {
|
||||
flex: 1 1 $sidebar-width;
|
||||
height: 100%;
|
||||
background: $ui-base-color;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background: $ui-base-color;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: $sidebar-width;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
pointer-events: auto;
|
||||
|
||||
&__toggle {
|
||||
display: none;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
height: 48px;
|
||||
|
||||
&__logo {
|
||||
flex: 1 1 auto;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $primary-text-color;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
flex: 0 0 auto;
|
||||
font-size: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
@ -52,6 +97,9 @@ $content-width: 840px;
|
||||
transition: all 200ms linear;
|
||||
transition-property: color, background-color;
|
||||
border-radius: 4px 0 0 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
i.fa {
|
||||
margin-right: 5px;
|
||||
@ -99,12 +147,30 @@ $content-width: 840px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 2 1 $content-width;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: $content-width;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $content-width + $sidebar-width) {
|
||||
.sidebar-wrapper--empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar-width;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
.sidebar-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: $content-width;
|
||||
padding: 20px 15px;
|
||||
padding-top: 60px;
|
||||
padding-left: 25px;
|
||||
@ -123,6 +189,12 @@ $content-width: 840px;
|
||||
padding-bottom: 40px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
margin-bottom: 40px;
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@ -147,7 +219,7 @@ $content-width: 840px;
|
||||
font-size: 16px;
|
||||
color: $secondary-text-color;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fields-group h6 {
|
||||
@ -176,7 +248,7 @@ $content-width: 840px;
|
||||
|
||||
& > p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
line-height: 21px;
|
||||
color: $secondary-text-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@ -204,7 +276,59 @@ $content-width: 840px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
display: block;
|
||||
|
||||
.sidebar-wrapper {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
&__toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul a,
|
||||
ul ul a {
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul ul {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
ul .simple-navigation-active-leaf a {
|
||||
border-bottom-color: $ui-highlight-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr.spacer {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 20px 0;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
body,
|
||||
.admin-wrapper .content {
|
||||
.muted-hint {
|
||||
color: $darker-text-color;
|
||||
|
||||
@ -227,25 +351,10 @@ $content-width: 840px;
|
||||
color: $dark-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
.sidebar-wrapper,
|
||||
.content-wrapper {
|
||||
flex: 0 0 auto;
|
||||
height: auto;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
.warning-hint {
|
||||
color: $gold-star;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,10 +364,10 @@ $content-width: 840px;
|
||||
|
||||
.filter-subset {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 40px 10px 0;
|
||||
margin: 0 40px 20px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
@ -74,9 +74,6 @@ body {
|
||||
|
||||
&.admin {
|
||||
background: darken($ui-base-color, 4%);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,8 @@
|
||||
&-composite {
|
||||
@include avatar-radius;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
|
||||
& div {
|
||||
@include avatar-radius;
|
||||
@ -57,6 +59,18 @@
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: $primary-text-color;
|
||||
text-shadow: 1px 1px 2px $base-shadow-color;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +259,28 @@
|
||||
.column-select {
|
||||
&__control {
|
||||
@include search-input();
|
||||
|
||||
&::placeholder {
|
||||
color: lighten($darker-text-color, 4%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
|
@ -44,6 +44,10 @@
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
|
||||
&::placeholder {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
|
||||
&:focus { outline: 0 }
|
||||
@include single-column('screen and (max-width: 630px)') { font-size: 16px }
|
||||
}
|
||||
@ -263,6 +267,10 @@
|
||||
resize: none;
|
||||
scrollbar-color: initial;
|
||||
|
||||
&::placeholder {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
all: unset;
|
||||
}
|
||||
|
@ -1433,49 +1433,68 @@
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.layout-toggle {
|
||||
.conversation {
|
||||
display: flex;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 5px;
|
||||
padding-bottom: 0;
|
||||
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 50%;
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
border: 0;
|
||||
position: relative;
|
||||
&:focus {
|
||||
background: lighten($ui-base-color, 2%);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
padding: 10px 5px;
|
||||
padding-right: 15px;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
|
||||
&__info {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__relative-time {
|
||||
font-size: 15px;
|
||||
color: $darker-text-color;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&__names {
|
||||
color: $darker-text-color;
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
flex-basis: 170px;
|
||||
flex-shrink: 1000;
|
||||
|
||||
a {
|
||||
color: $primary-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
svg path:first-child {
|
||||
fill: lighten($ui-base-color, 16%);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
path:first-child {
|
||||
fill: lighten($ui-base-color, 12%);
|
||||
.status__content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
path:last-child {
|
||||
fill: darken($ui-base-color, 14%);
|
||||
}
|
||||
}
|
||||
|
||||
&__active {
|
||||
color: $ui-highlight-color;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: lighten($ui-base-color, 12%);
|
||||
border-radius: 50%;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +405,8 @@
|
||||
.confirmation-modal,
|
||||
.report-modal,
|
||||
.actions-modal,
|
||||
.mute-modal {
|
||||
.mute-modal,
|
||||
.block-modal {
|
||||
background: lighten($ui-secondary-color, 8%);
|
||||
color: $inverted-text-color;
|
||||
border-radius: 8px;
|
||||
@ -465,7 +466,8 @@
|
||||
.boost-modal__action-bar,
|
||||
.favourite-modal__action-bar,
|
||||
.confirmation-modal__action-bar,
|
||||
.mute-modal__action-bar {
|
||||
.mute-modal__action-bar,
|
||||
.block-modal__action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: $ui-secondary-color;
|
||||
@ -495,11 +497,13 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mute-modal {
|
||||
.mute-modal,
|
||||
.block-modal {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.mute-modal .react-toggle {
|
||||
.mute-modal .react-toggle,
|
||||
.block-modal .react-toggle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -712,10 +716,17 @@
|
||||
}
|
||||
|
||||
.confirmation-modal__action-bar,
|
||||
.mute-modal__action-bar {
|
||||
.confirmation-modal__secondary-button,
|
||||
.confirmation-modal__cancel-button,
|
||||
.mute-modal__cancel-button {
|
||||
.mute-modal__action-bar,
|
||||
.block-modal__action-bar {
|
||||
.confirmation-modal__secondary-button {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__secondary-button,
|
||||
.confirmation-modal__cancel-button,
|
||||
.mute-modal__cancel-button,
|
||||
.block-modal__cancel-button {
|
||||
background-color: transparent;
|
||||
color: $lighter-text-color;
|
||||
font-size: 14px;
|
||||
@ -726,11 +737,6 @@
|
||||
&:active {
|
||||
color: darken($lighter-text-color, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__secondary-button {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__do_not_ask_again {
|
||||
@ -747,10 +753,10 @@
|
||||
|
||||
.confirmation-modal__container,
|
||||
.mute-modal__container,
|
||||
.block-modal__container,
|
||||
.report-modal__target {
|
||||
padding: 30px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
@ -763,6 +769,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__container,
|
||||
.report-modal__target {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.block-modal,
|
||||
.mute-modal {
|
||||
&__explanation {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
color: $inverted-text-color;
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-modal__target {
|
||||
padding: 15px;
|
||||
|
||||
|
@ -10,6 +10,28 @@
|
||||
padding-right: 30px;
|
||||
line-height: 18px;
|
||||
font-size: 16px;
|
||||
|
||||
&::placeholder {
|
||||
color: lighten($darker-text-color, 4%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.search__icon {
|
||||
|
@ -673,6 +673,7 @@ a.status__display-name,
|
||||
}
|
||||
|
||||
.muted {
|
||||
.status__content,
|
||||
.status__content p,
|
||||
.status__content a,
|
||||
.status__content__text {
|
||||
|
@ -143,6 +143,63 @@
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
grid-gap: 0;
|
||||
grid-template-columns: minmax(0, 100%);
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-columns: 25%;
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
.column-0 {
|
||||
grid-column: 1 / 5;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.column-1 {
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-2 {
|
||||
grid-column: 4;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 2 / 5;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.landing-page__call-to-action {
|
||||
min-height: 100%;
|
||||
}
|
||||
@ -191,6 +248,11 @@
|
||||
}
|
||||
|
||||
.column-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 5;
|
||||
}
|
||||
|
||||
.column-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
@ -245,6 +245,10 @@ code {
|
||||
&-6 {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
.fields-group:last-child,
|
||||
@ -300,6 +304,13 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
.input.static .label_input__wrapper {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
border: 1px solid $dark-text-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
input[type=email],
|
||||
@ -318,6 +329,10 @@ code {
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
&::placeholder {
|
||||
color: lighten($darker-text-color, 4%);
|
||||
}
|
||||
|
||||
&:invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -226,6 +226,7 @@
|
||||
.boost-modal,
|
||||
.confirmation-modal,
|
||||
.mute-modal,
|
||||
.block-modal,
|
||||
.report-modal,
|
||||
.embed-modal,
|
||||
.error-modal,
|
||||
@ -236,6 +237,7 @@
|
||||
.boost-modal__action-bar,
|
||||
.confirmation-modal__action-bar,
|
||||
.mute-modal__action-bar,
|
||||
.block-modal__action-bar,
|
||||
.onboarding-modal__paginator,
|
||||
.error-modal__footer {
|
||||
background: darken($ui-base-color, 6%);
|
||||
|
@ -102,13 +102,19 @@
|
||||
|
||||
&__number {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
width: 52px;
|
||||
font-weight: 700;
|
||||
padding: 0 10px;
|
||||
padding-left: 8px;
|
||||
text-align: right;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
flex: 0 0 36px;
|
||||
flex: 0 0 52px;
|
||||
}
|
||||
|
||||
&__vote__mark {
|
||||
float: left;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
@ -288,70 +288,3 @@ a.table-action-link {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blocks-table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
thead {
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
background: darken($ui-base-color, 4%);
|
||||
font-weight: 500;
|
||||
|
||||
th.severity-column {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
th.button-column {
|
||||
width: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody > tr {
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
border-bottom: 0;
|
||||
background: darken($ui-base-color, 4%);
|
||||
|
||||
&:hover {
|
||||
background: darken($ui-base-color, 2%);
|
||||
}
|
||||
|
||||
&.even {
|
||||
background: $ui-base-color;
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 2%);
|
||||
}
|
||||
}
|
||||
|
||||
&.rationale {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-top: 0;
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 6%);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
line-height: 18px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
@ -128,41 +128,43 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.contact-widget,
|
||||
.landing-page__information.contact-widget {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
min-height: 100%;
|
||||
border-radius: 4px;
|
||||
background: $ui-base-color;
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
}
|
||||
|
||||
.contact-widget {
|
||||
min-height: 100%;
|
||||
font-size: 15px;
|
||||
color: $darker-text-color;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
h4 {
|
||||
padding: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.account {
|
||||
border-bottom: 0;
|
||||
padding: 10px 0;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
&__mail {
|
||||
margin-top: 10px;
|
||||
|
||||
a {
|
||||
color: $primary-text-color;
|
||||
& > a {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -557,3 +559,38 @@ $fluid-breakpoint: $maximum-width + 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-of-contents {
|
||||
background: darken($ui-base-color, 4%);
|
||||
min-height: 100%;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
|
||||
li a {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none;
|
||||
color: $primary-text-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
li:last-child a {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
li ul {
|
||||
padding-left: 20px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ pack:
|
||||
mailer:
|
||||
modal:
|
||||
public: packs/public.js
|
||||
settings:
|
||||
settings: packs/settings.js
|
||||
share: packs/share.js
|
||||
|
||||
# (OPTIONAL) The directory which contains localization files for
|
||||
|
@ -122,6 +122,10 @@ export function MuteModal () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal');
|
||||
}
|
||||
|
||||
export function BlockModal () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'flavours/glitch/features/ui/components/block_modal');
|
||||
}
|
||||
|
||||
export function ReportModal () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
|
||||
}
|
||||
|
@ -100,4 +100,4 @@ export const buildCustomEmojis = (customEmojis) => {
|
||||
return emojis;
|
||||
};
|
||||
|
||||
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set());
|
||||
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import api, { getLinks } from '../api';
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { openModal } from './modal';
|
||||
|
||||
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
||||
@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
|
||||
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||
|
||||
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
|
||||
|
||||
export function fetchBlocks() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchBlocksRequest());
|
||||
@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function initBlockModal(account) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BLOCKS_INIT_MODAL,
|
||||
account,
|
||||
});
|
||||
|
||||
dispatch(openModal('BLOCK'));
|
||||
};
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ export function uploadCompose(files) {
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||
},
|
||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
|
||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||
};
|
||||
};
|
||||
@ -289,10 +289,11 @@ export function uploadComposeProgress(loaded, total) {
|
||||
};
|
||||
};
|
||||
|
||||
export function uploadComposeSuccess(media) {
|
||||
export function uploadComposeSuccess(media, file) {
|
||||
return {
|
||||
type: COMPOSE_UPLOAD_SUCCESS,
|
||||
media: media,
|
||||
file: file,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
@ -368,6 +369,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
||||
q: token.slice(1),
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
exclude_unreviewed: true,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
|
||||
|
@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||
|
||||
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
||||
|
||||
export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
|
||||
export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
|
||||
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
|
||||
|
||||
export const mountConversations = () => ({
|
||||
type: CONVERSATIONS_MOUNT,
|
||||
});
|
||||
@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
|
||||
conversation,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteConversation = conversationId => (dispatch, getState) => {
|
||||
dispatch(deleteConversationRequest(conversationId));
|
||||
|
||||
api(getState).delete(`/api/v1/conversations/${conversationId}`)
|
||||
.then(() => dispatch(deleteConversationSuccess(conversationId)))
|
||||
.catch(error => dispatch(deleteConversationFail(conversationId, error)));
|
||||
};
|
||||
|
||||
export const deleteConversationRequest = id => ({
|
||||
type: CONVERSATIONS_DELETE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteConversationSuccess = id => ({
|
||||
type: CONVERSATIONS_DELETE_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteConversationFail = (id, error) => ({
|
||||
type: CONVERSATIONS_DELETE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
@ -73,8 +73,9 @@ export function normalizePoll(poll) {
|
||||
|
||||
const emojiMap = makeEmojiMap(normalPoll);
|
||||
|
||||
normalPoll.options = poll.options.map(option => ({
|
||||
normalPoll.options = poll.options.map((option, index) => ({
|
||||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
}));
|
||||
|
||||
|
@ -28,6 +28,9 @@ export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||
|
||||
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||
|
||||
defineMessages({
|
||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||
@ -215,3 +218,11 @@ export function setFilter (filterType) {
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
};
|
||||
|
||||
export const mountNotifications = () => ({
|
||||
type: NOTIFICATIONS_MOUNT,
|
||||
});
|
||||
|
||||
export const unmountNotifications = () => ({
|
||||
type: NOTIFICATIONS_UNMOUNT,
|
||||
});
|
||||
|
@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +88,13 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
|
||||
|
||||
{accounts.size > 4 && (
|
||||
<span className='account__avatar-composite__label'>
|
||||
+{accounts.size - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn) {
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
@ -142,7 +142,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
collapsedContent.push(pinButton);
|
||||
}
|
||||
|
||||
if (children || multiColumn) {
|
||||
if (children || (multiColumn && this.props.onPin)) {
|
||||
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import emojify from 'mastodon/features/emoji/emoji';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
||||
});
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
const { poll, disabled } = this.props;
|
||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
|
||||
const { poll, disabled, intl } = this.props;
|
||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
|
||||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||
|
||||
let titleEmojified = option.get('title_emojified');
|
||||
if (!titleEmojified) {
|
||||
@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
|
||||
/>
|
||||
|
||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
||||
{showResults && <span className='poll__number'>
|
||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||
{Math.round(percent)}%
|
||||
</span>}
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||
</label>
|
||||
@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
|
||||
const showResults = poll.get('voted') || expired;
|
||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||
|
||||
let votesCount = null;
|
||||
|
||||
if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
|
||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
||||
} else {
|
||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='poll'>
|
||||
<ul>
|
||||
@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
|
||||
<div className='poll__footer'>
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
|
||||
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
|
||||
{votesCount}
|
||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -199,6 +199,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.clearMouseIdleTimer();
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
|
||||
|
@ -2,17 +2,17 @@ import React, { PureComponent, Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import MediaGallery from '../components/media_gallery';
|
||||
import Video from '../features/video';
|
||||
import Card from '../features/status/components/card';
|
||||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||
import MediaGallery from 'mastodon/components/media_gallery';
|
||||
import Poll from 'mastodon/components/poll';
|
||||
import Hashtag from 'mastodon/components/hashtag';
|
||||
import ModalRoot from 'mastodon/components/modal_root';
|
||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
||||
import Video from 'mastodon/features/video';
|
||||
import Card from 'mastodon/features/status/components/card';
|
||||
import Audio from 'mastodon/features/audio';
|
||||
import ModalRoot from '../components/modal_root';
|
||||
import { getScrollbarWidth } from '../features/ui/components/modal_root';
|
||||
import MediaModal from '../features/ui/components/media_modal';
|
||||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Status from '../components/status';
|
||||
import { makeGetStatus } from '../selectors';
|
||||
@ -15,7 +14,6 @@ import {
|
||||
pin,
|
||||
unpin,
|
||||
} from '../actions/interactions';
|
||||
import { blockAccount } from '../actions/accounts';
|
||||
import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
@ -24,9 +22,10 @@ import {
|
||||
revealStatus,
|
||||
} from '../actions/statuses';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import { initBlockModal } from '../actions/blocks';
|
||||
import { initReport } from '../actions/reports';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { boostModal, deleteModal } from '../initial_state';
|
||||
import { showAlertForError } from '../actions/alerts';
|
||||
|
||||
@ -35,10 +34,8 @@ const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -56,6 +53,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onReply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
@ -137,16 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onBlock (status) {
|
||||
const account = status.get('account');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account, status));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
},
|
||||
|
||||
onReport (status) {
|
||||
|
@ -5,7 +5,6 @@ import Header from '../components/header';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
pinAccount,
|
||||
@ -16,6 +15,7 @@ import {
|
||||
directCompose,
|
||||
} from '../../../actions/compose';
|
||||
import { initMuteModal } from '../../../actions/mutes';
|
||||
import { initBlockModal } from '../../../actions/blocks';
|
||||
import { initReport } from '../../../actions/reports';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||
@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
}));
|
||||
dispatch(initBlockModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user