Merge pull request #1224 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
65db9df011
402 changed files with 8259 additions and 2700 deletions
|
@ -115,6 +115,20 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
||||||
# S3_ENDPOINT=
|
# S3_ENDPOINT=
|
||||||
# S3_SIGNATURE_VERSION=
|
# 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)
|
# Swift (optional)
|
||||||
# The attachment host must allow cross origin request - see the description
|
# The attachment host must allow cross origin request - see the description
|
||||||
# above.
|
# 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.
|
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
|
## [2.9.2] - 2019-06-22
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
10
Gemfile
10
Gemfile
|
@ -5,7 +5,7 @@ ruby '>= 2.4.0', '< 2.7.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.3'
|
gem 'pkg-config', '~> 1.3'
|
||||||
|
|
||||||
gem 'puma', '~> 4.1'
|
gem 'puma', '~> 4.2'
|
||||||
gem 'rails', '~> 5.2.3'
|
gem 'rails', '~> 5.2.3'
|
||||||
gem 'thor', '~> 0.20'
|
gem 'thor', '~> 0.20'
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ gem 'bootsnap', '~> 1.4', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.6'
|
gem 'charlock_holmes', '~> 0.7.6'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.0'
|
gem 'chewy', '~> 5.1'
|
||||||
gem 'cld3', '~> 3.2.4'
|
gem 'cld3', '~> 3.2.4'
|
||||||
gem 'devise', '~> 4.7'
|
gem 'devise', '~> 4.7'
|
||||||
gem 'devise-two-factor', '~> 3.1'
|
gem 'devise-two-factor', '~> 3.1'
|
||||||
|
@ -44,13 +44,13 @@ gem 'omniauth-saml', '~> 1.10'
|
||||||
gem 'omniauth', '~> 1.9'
|
gem 'omniauth', '~> 1.9'
|
||||||
|
|
||||||
gem 'discard', '~> 1.1'
|
gem 'discard', '~> 1.1'
|
||||||
gem 'doorkeeper', '~> 5.1'
|
gem 'doorkeeper', '~> 5.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'goldfinger', '~> 2.1'
|
gem 'goldfinger', '~> 2.1'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.5'
|
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 'html2text'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 3.3'
|
gem 'http', '~> 3.3'
|
||||||
|
@ -118,7 +118,7 @@ end
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.29'
|
gem 'capybara', '~> 3.29'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.3'
|
gem 'faker', '~> 2.4'
|
||||||
gem 'microformats', '~> 4.1'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
|
|
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
|
GIT
|
||||||
remote: https://github.com/rtomayko/posix-spawn
|
remote: https://github.com/rtomayko/posix-spawn
|
||||||
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
|
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
|
||||||
|
@ -161,7 +169,7 @@ GEM
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
charlock_holmes (0.7.6)
|
charlock_holmes (0.7.6)
|
||||||
chewy (5.0.0)
|
chewy (5.1.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
elasticsearch (>= 2.0.0)
|
elasticsearch (>= 2.0.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
|
@ -209,19 +217,19 @@ GEM
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20180417)
|
domain_name (0.5.20180417)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.1.0)
|
doorkeeper (5.2.1)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.5)
|
dotenv (2.7.5)
|
||||||
dotenv-rails (2.7.5)
|
dotenv-rails (2.7.5)
|
||||||
dotenv (= 2.7.5)
|
dotenv (= 2.7.5)
|
||||||
railties (>= 3.2, < 6.1)
|
railties (>= 3.2, < 6.1)
|
||||||
elasticsearch (6.0.2)
|
elasticsearch (7.3.0)
|
||||||
elasticsearch-api (= 6.0.2)
|
elasticsearch-api (= 7.3.0)
|
||||||
elasticsearch-transport (= 6.0.2)
|
elasticsearch-transport (= 7.3.0)
|
||||||
elasticsearch-api (6.0.2)
|
elasticsearch-api (7.3.0)
|
||||||
multi_json
|
multi_json
|
||||||
elasticsearch-dsl (0.1.5)
|
elasticsearch-dsl (0.1.8)
|
||||||
elasticsearch-transport (6.0.2)
|
elasticsearch-transport (7.3.0)
|
||||||
faraday
|
faraday
|
||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
|
@ -231,9 +239,9 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.2)
|
fabrication (2.20.2)
|
||||||
faker (2.3.0)
|
faker (2.4.0)
|
||||||
i18n (~> 1.6.0)
|
i18n (~> 1.6.0)
|
||||||
faraday (0.15.0)
|
faraday (0.15.4)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
fastimage (2.1.7)
|
fastimage (2.1.7)
|
||||||
|
@ -278,8 +286,6 @@ GEM
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
hashdiff (1.0.0)
|
hashdiff (1.0.0)
|
||||||
hashie (3.6.0)
|
hashie (3.6.0)
|
||||||
health_check (3.0.0)
|
|
||||||
railties (>= 5.0)
|
|
||||||
heapy (0.1.4)
|
heapy (0.1.4)
|
||||||
highline (2.0.1)
|
highline (2.0.1)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
|
@ -372,10 +378,10 @@ GEM
|
||||||
mimemagic (0.3.3)
|
mimemagic (0.3.3)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.11.3)
|
minitest (5.12.0)
|
||||||
msgpack (1.3.1)
|
msgpack (1.3.1)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.1.1)
|
||||||
necromancer (0.5.0)
|
necromancer (0.5.0)
|
||||||
net-ldap (0.16.1)
|
net-ldap (0.16.1)
|
||||||
net-scp (2.0.0)
|
net-scp (2.0.0)
|
||||||
|
@ -447,7 +453,7 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.1)
|
public_suffix (4.0.1)
|
||||||
puma (4.1.1)
|
puma (4.2.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -503,7 +509,7 @@ GEM
|
||||||
rdf-normalize (0.3.3)
|
rdf-normalize (0.3.3)
|
||||||
rdf (>= 2.2, < 4.0)
|
rdf (>= 2.2, < 4.0)
|
||||||
redcarpet (3.4.0)
|
redcarpet (3.4.0)
|
||||||
redis (4.1.2)
|
redis (4.1.3)
|
||||||
redis-actionpack (5.0.2)
|
redis-actionpack (5.0.2)
|
||||||
actionpack (>= 4.0, < 6)
|
actionpack (>= 4.0, < 6)
|
||||||
redis-rack (>= 1, < 3)
|
redis-rack (>= 1, < 3)
|
||||||
|
@ -593,7 +599,7 @@ GEM
|
||||||
simple_form (4.1.0)
|
simple_form (4.1.0)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
activemodel (>= 5.0)
|
activemodel (>= 5.0)
|
||||||
simplecov (0.17.0)
|
simplecov (0.17.1)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
|
@ -649,7 +655,7 @@ GEM
|
||||||
uniform_notifier (1.12.1)
|
uniform_notifier (1.12.1)
|
||||||
warden (1.2.8)
|
warden (1.2.8)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.6)
|
||||||
webmock (3.7.3)
|
webmock (3.7.5)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
@ -690,7 +696,7 @@ DEPENDENCIES
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.29)
|
capybara (~> 3.29)
|
||||||
charlock_holmes (~> 0.7.6)
|
charlock_holmes (~> 0.7.6)
|
||||||
chewy (~> 5.0)
|
chewy (~> 5.1)
|
||||||
cld3 (~> 3.2.4)
|
cld3 (~> 3.2.4)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
|
@ -700,10 +706,10 @@ DEPENDENCIES
|
||||||
devise-two-factor (~> 3.1)
|
devise-two-factor (~> 3.1)
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
discard (~> 1.1)
|
discard (~> 1.1)
|
||||||
doorkeeper (~> 5.1)
|
doorkeeper (~> 5.2)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.20)
|
||||||
faker (~> 2.3)
|
faker (~> 2.4)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
@ -711,7 +717,7 @@ DEPENDENCIES
|
||||||
fuubar (~> 2.4)
|
fuubar (~> 2.4)
|
||||||
goldfinger (~> 2.1)
|
goldfinger (~> 2.1)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
health_check (~> 3.0)
|
health_check!
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
html2text
|
html2text
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
|
@ -756,7 +762,7 @@ DEPENDENCIES
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.7)
|
pry-byebug (~> 3.7)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 4.1)
|
puma (~> 4.2)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack-attack (~> 6.1)
|
rack-attack (~> 6.1)
|
||||||
rack-cors (~> 1.0)
|
rack-cors (~> 1.0)
|
||||||
|
@ -798,7 +804,7 @@ DEPENDENCIES
|
||||||
webpush
|
webpush
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.6.1p33
|
ruby 2.6.4p104
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.17.3
|
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)",
|
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
|
||||||
"required": true
|
"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": {
|
"SECRET_KEY_BASE": {
|
||||||
"description": "The secret key base",
|
"description": "The secret key base",
|
||||||
"generator": "secret"
|
"generator": "secret"
|
||||||
|
|
|
@ -4,9 +4,7 @@ class AboutController < ApplicationController
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
layout 'public'
|
layout 'public'
|
||||||
|
|
||||||
before_action :require_open_federation!, only: [:show, :more, :blocks]
|
before_action :require_open_federation!, only: [:show, :more]
|
||||||
before_action :check_blocklist_enabled, only: [:blocks]
|
|
||||||
before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
|
|
||||||
before_action :set_body_classes, only: :show
|
before_action :set_body_classes, only: :show
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_expires_in, only: [:show, :more, :terms]
|
before_action :set_expires_in, only: [:show, :more, :terms]
|
||||||
|
@ -17,15 +15,20 @@ class AboutController < ApplicationController
|
||||||
|
|
||||||
def more
|
def more
|
||||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
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
|
end
|
||||||
|
|
||||||
def terms; end
|
def terms; end
|
||||||
|
|
||||||
def blocks
|
helper_method :display_blocks?
|
||||||
@show_rationale = Setting.show_domain_blocks_rationale == 'all'
|
helper_method :display_blocks_rationale?
|
||||||
@show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional?
|
helper_method :public_fetch_mode?
|
||||||
@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
|
helper_method :new_user
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
@ -33,28 +36,14 @@ class AboutController < ApplicationController
|
||||||
not_found if whitelist_mode?
|
not_found if whitelist_mode?
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_blocklist_enabled
|
def display_blocks?
|
||||||
not_found if Setting.show_domain_blocks == 'disabled'
|
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocklist_account_required?
|
def display_blocks_rationale?
|
||||||
Setting.show_domain_blocks == 'users'
|
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
|
||||||
end
|
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
|
def new_user
|
||||||
User.new.tap do |user|
|
User.new.tap do |user|
|
||||||
user.build_account
|
user.build_account
|
||||||
|
@ -62,8 +51,6 @@ class AboutController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :new_user
|
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
use_pack 'public'
|
use_pack 'public'
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ class AccountsController < ApplicationController
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -33,9 +33,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
def scope_for_collection
|
def scope_for_collection
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
|
return Status.none if @account.blocking?(signed_request_account)
|
||||||
scope.merge!(@account.pinned_statuses)
|
|
||||||
end
|
@account.pinned_statuses
|
||||||
else
|
else
|
||||||
raise ActiveRecord::RecordNotFound
|
raise ActiveRecord::RecordNotFound
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Admin
|
module Admin
|
||||||
class RelaysController < BaseController
|
class RelaysController < BaseController
|
||||||
before_action :set_relay, except: [:index, :new, :create]
|
before_action :set_relay, except: [:index, :new, :create]
|
||||||
|
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :relay, :update?
|
authorize :relay, :update?
|
||||||
|
@ -11,7 +12,7 @@ module Admin
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize :relay, :update?
|
authorize :relay, :update?
|
||||||
@relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
|
@relay = Relay.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -54,5 +55,9 @@ module Admin
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:relay).permit(:inbox_url)
|
params.require(:relay).permit(:inbox_url)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ module Admin
|
||||||
authorize @user, :disable_2fa?
|
authorize @user, :disable_2fa?
|
||||||
@user.disable_two_factor!
|
@user.disable_two_factor!
|
||||||
log_action :disable_2fa, @user
|
log_action :disable_2fa, @user
|
||||||
|
UserMailer.two_factor_disabled(@user).deliver_later!
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned_scope
|
def pinned_scope
|
||||||
|
return Status.none if @account.blocking?(current_account)
|
||||||
|
|
||||||
@account.pinned_statuses
|
@account.pinned_statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||||
|
|
||||||
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)
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Api::V2::SearchController < Api::BaseController
|
||||||
params[:q],
|
params[:q],
|
||||||
current_account,
|
current_account,
|
||||||
limit_param(RESULTS_LIMIT),
|
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
|
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!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
prepend_before_action :set_pack
|
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_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
@ -22,34 +23,32 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
self.resource = begin
|
super do |resource|
|
||||||
if user_params[:email].blank? && session[:otp_user_id].present?
|
remember_me(resource)
|
||||||
User.find(session[:otp_user_id])
|
flash.delete(:notice)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
tmp_stored_location = stored_location_for(:user)
|
tmp_stored_location = stored_location_for(:user)
|
||||||
super
|
super
|
||||||
|
session.delete(:challenge_passed_at)
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
store_location_for(:user, tmp_stored_location) if continue_after?
|
store_location_for(:user, tmp_stored_location) if continue_after?
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
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
|
def user_params
|
||||||
params.require(:user).permit(:email, :password, :otp_attempt)
|
params.require(:user).permit(:email, :password, :otp_attempt)
|
||||||
end
|
end
|
||||||
|
@ -72,6 +71,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def two_factor_enabled?
|
||||||
|
find_user&.otp_required_for_login?
|
||||||
|
end
|
||||||
|
|
||||||
def valid_otp_attempt?(user)
|
def valid_otp_attempt?(user)
|
||||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||||
|
@ -79,10 +82,24 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
false
|
false
|
||||||
end
|
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)
|
def authenticate_with_two_factor_via_otp(user)
|
||||||
if valid_otp_attempt?(user)
|
if valid_otp_attempt?(user)
|
||||||
session.delete(:otp_user_id)
|
session.delete(:otp_user_id)
|
||||||
authenticate_and_respond(user)
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||||
prompt_for_two_factor(user)
|
prompt_for_two_factor(user)
|
||||||
|
@ -91,16 +108,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
def prompt_for_two_factor(user)
|
def prompt_for_two_factor(user)
|
||||||
session[:otp_user_id] = user.id
|
session[:otp_user_id] = user.id
|
||||||
|
@body_classes = 'lighter'
|
||||||
render :two_factor
|
render :two_factor
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_and_respond(user)
|
|
||||||
sign_in(user)
|
|
||||||
remember_me(user)
|
|
||||||
|
|
||||||
respond_with user, location: after_sign_in_path_for(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
|
@ -117,11 +128,9 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
def home_paths(resource)
|
def home_paths(resource)
|
||||||
paths = [about_path]
|
paths = [about_path]
|
||||||
|
|
||||||
if single_user_mode? && resource.is_a?(User)
|
if single_user_mode? && resource.is_a?(User)
|
||||||
paths << short_account_path(username: resource.account)
|
paths << short_account_path(username: resource.account)
|
||||||
end
|
end
|
||||||
|
|
||||||
paths
|
paths
|
||||||
end
|
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
|
included do
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_not_suspended!
|
||||||
before_action :load_export
|
before_action :load_export
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -27,4 +30,8 @@ module ExportControllerConcern
|
||||||
def export_filename
|
def export_filename
|
||||||
"#{controller_name}.csv"
|
"#{controller_name}.csv"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class CustomCssController < ApplicationController
|
class CustomCssController < ApplicationController
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ class DirectoriesController < ApplicationController
|
||||||
before_action :set_accounts
|
before_action :set_accounts
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ class FollowerAccountsController < ApplicationController
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -8,6 +8,7 @@ class FollowingAccountsController < ApplicationController
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class ManifestsController < ApplicationController
|
class ManifestsController < ApplicationController
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 3.minutes, public: true
|
expires_in 3.minutes, public: true
|
||||||
|
|
|
@ -4,6 +4,7 @@ class MediaController < ApplicationController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
before_action :authenticate_user!, if: :whitelist_mode?
|
||||||
before_action :set_media_attachment
|
before_action :set_media_attachment
|
||||||
|
|
|
@ -4,6 +4,7 @@ class MediaProxyController < ApplicationController
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
before_action :authenticate_user!, if: :whitelist_mode?
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ class RemoteFollowController < ApplicationController
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@remote_follow = RemoteFollow.new(session_params)
|
@remote_follow = RemoteFollow.new(session_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,8 @@ class RemoteInteractionController < ApplicationController
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@remote_follow = RemoteFollow.new(session_params)
|
@remote_follow = RemoteFollow.new(session_params)
|
||||||
end
|
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'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@export = Export.new(current_account)
|
@export = Export.new(current_account)
|
||||||
|
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
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'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
before_action :set_migrations
|
||||||
|
before_action :set_cooldown
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@migration = Form::Migration.new(account: current_account.moved_to_account)
|
@migration = current_account.migrations.build
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def create
|
||||||
@migration = Form::Migration.new(resource_params)
|
@migration = current_account.migrations.build(resource_params)
|
||||||
|
|
||||||
if @migration.valid? && migration_account_changed?
|
if @migration.save_with_challenge(current_user)
|
||||||
current_account.update!(moved_to_account: @migration.account)
|
MoveService.new.call(@migration)
|
||||||
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)
|
||||||
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
|
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
helper_method :on_cooldown?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:migration).permit(:acct)
|
params.require(:account_migration).permit(:acct, :current_password, :current_username)
|
||||||
end
|
end
|
||||||
|
|
||||||
def migration_account_changed?
|
def set_migrations
|
||||||
current_account.moved_to_account_id != @migration.account&.id &&
|
@migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
|
||||||
current_account.id != @migration.account&.id
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
module Settings
|
module Settings
|
||||||
module TwoFactorAuthentication
|
module TwoFactorAuthentication
|
||||||
class ConfirmationsController < BaseController
|
class ConfirmationsController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_challenge!
|
||||||
before_action :ensure_otp_secret
|
before_action :ensure_otp_secret
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
@ -22,6 +25,8 @@ module Settings
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
||||||
|
UserMailer.two_factor_enabled(current_user).deliver_later!
|
||||||
|
|
||||||
render 'settings/two_factor_authentication/recovery_codes/index'
|
render 'settings/two_factor_authentication/recovery_codes/index'
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
||||||
|
|
|
@ -3,16 +3,22 @@
|
||||||
module Settings
|
module Settings
|
||||||
module TwoFactorAuthentication
|
module TwoFactorAuthentication
|
||||||
class RecoveryCodesController < BaseController
|
class RecoveryCodesController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :require_challenge!, on: :create
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
||||||
|
UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
|
||||||
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
|
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
|
||||||
|
|
||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,13 @@
|
||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
class TwoFactorAuthenticationsController < BaseController
|
class TwoFactorAuthenticationsController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :verify_otp_required, only: [:create]
|
before_action :verify_otp_required, only: [:create]
|
||||||
|
before_action :require_challenge!, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
@ -23,6 +26,7 @@ module Settings
|
||||||
if acceptable_code?
|
if acceptable_code?
|
||||||
current_user.otp_required_for_login = false
|
current_user.otp_required_for_login = false
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
UserMailer.two_factor_disabled(current_user).deliver_later!
|
||||||
redirect_to settings_two_factor_authentication_path
|
redirect_to settings_two_factor_authentication_path
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
||||||
|
|
|
@ -19,6 +19,7 @@ class StatusesController < ApplicationController
|
||||||
before_action :set_autoplay, only: :embed
|
before_action :set_autoplay, only: :embed
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
|
skip_before_action :require_functional!, only: [:show, :embed]
|
||||||
|
|
||||||
content_security_policy only: :embed do |p|
|
content_security_policy only: :embed do |p|
|
||||||
p.frame_ancestors(false)
|
p.frame_ancestors(false)
|
||||||
|
|
|
@ -13,6 +13,8 @@ class TagsController < ApplicationController
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
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'
|
'desktop'
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import api, { getLinks } from 'flavours/glitch/util/api';
|
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||||
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
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_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
|
||||||
|
|
||||||
export function fetchBlocks() {
|
export function fetchBlocks() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchBlocksRequest());
|
dispatch(fetchBlocksRequest());
|
||||||
|
@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
|
||||||
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;
|
progress[i] = loaded;
|
||||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
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)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -316,10 +316,11 @@ export function uploadComposeProgress(loaded, total) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function uploadComposeSuccess(media) {
|
export function uploadComposeSuccess(media, file) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_SUCCESS,
|
type: COMPOSE_UPLOAD_SUCCESS,
|
||||||
media: media,
|
media: media,
|
||||||
|
file: file,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||||
|
|
||||||
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
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 = () => ({
|
export const mountConversations = () => ({
|
||||||
type: CONVERSATIONS_MOUNT,
|
type: CONVERSATIONS_MOUNT,
|
||||||
});
|
});
|
||||||
|
@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
|
||||||
conversation,
|
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);
|
const emojiMap = makeEmojiMap(normalPoll);
|
||||||
|
|
||||||
normalPoll.options = poll.options.map(option => ({
|
normalPoll.options = poll.options.map((option, index) => ({
|
||||||
...option,
|
...option,
|
||||||
|
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
|
||||||
|
|
||||||
if (size === 2) {
|
if (size === 2) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
} else {
|
} else {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
} else if (size === 3) {
|
} else if (size === 3) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
} else if (index > 0) {
|
} else if (index > 0) {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === 1) {
|
if (index === 1) {
|
||||||
bottom = '2px';
|
bottom = '1px';
|
||||||
} else if (index > 1) {
|
} else if (index > 1) {
|
||||||
top = '2px';
|
top = '1px';
|
||||||
}
|
}
|
||||||
} else if (size === 4) {
|
} else if (size === 4) {
|
||||||
if (index === 0 || index === 2) {
|
if (index === 0 || index === 2) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === 1 || index === 3) {
|
if (index === 1 || index === 3) {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 2) {
|
if (index < 2) {
|
||||||
bottom = '2px';
|
bottom = '1px';
|
||||||
} else {
|
} else {
|
||||||
top = '2px';
|
top = '1px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,13 @@ export default class AvatarComposite extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
export default class ColumnBackButtonSlim extends React.PureComponent {
|
export default class ColumnBackButtonSlim extends React.PureComponent {
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from 'flavours/glitch/util/emoji';
|
import emojify from 'flavours/glitch/util/emoji';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
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) => {
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
|
@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, disabled } = this.props;
|
const { poll, disabled, intl } = this.props;
|
||||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
|
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
|
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
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');
|
let titleEmojified = option.get('title_emojified');
|
||||||
if (!titleEmojified) {
|
if (!titleEmojified) {
|
||||||
|
@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!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 }} />
|
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||||
</label>
|
</label>
|
||||||
|
@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
|
||||||
const showResults = poll.get('voted') || expired;
|
const showResults = poll.get('voted') || expired;
|
||||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
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 (
|
return (
|
||||||
<div className='poll'>
|
<div className='poll'>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
<div className='poll__footer'>
|
<div className='poll__footer'>
|
||||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
{!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>}
|
{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>}
|
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Status from 'flavours/glitch/components/status';
|
import Status from 'flavours/glitch/components/status';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
@ -18,9 +17,9 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import { blockAccount } from 'flavours/glitch/actions/accounts';
|
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
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?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
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.' },
|
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' },
|
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?' },
|
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' },
|
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
||||||
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
||||||
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
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) {
|
onReply (status, router) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
||||||
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
message: intl.formatMessage(messages.replyMessage),
|
||||||
|
@ -186,16 +184,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
|
|
||||||
onBlock (status) {
|
onBlock (status) {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initBlockModal(account));
|
||||||
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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnfilter (status, onConfirm) {
|
onUnfilter (status, onConfirm) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import Header from '../components/header';
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
blockAccount,
|
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
pinAccount,
|
pinAccount,
|
||||||
|
@ -16,6 +15,7 @@ import {
|
||||||
directCompose
|
directCompose
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
|
import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
|
||||||
|
@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
dispatch(unblockAccount(account.get('id')));
|
dispatch(unblockAccount(account.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initBlockModal(account));
|
||||||
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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,28 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
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 = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
@ -13,25 +32,61 @@ export default class Conversation extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
conversationId: PropTypes.string.isRequired,
|
conversationId: PropTypes.string.isRequired,
|
||||||
accounts: ImmutablePropTypes.list.isRequired,
|
accounts: ImmutablePropTypes.list.isRequired,
|
||||||
lastStatusId: PropTypes.string,
|
lastStatus: ImmutablePropTypes.map,
|
||||||
unread:PropTypes.bool.isRequired,
|
unread:PropTypes.bool.isRequired,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
onMoveDown: PropTypes.func,
|
onMoveDown: PropTypes.func,
|
||||||
markRead: PropTypes.func.isRequired,
|
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 = () => {
|
handleClick = () => {
|
||||||
if (!this.context.router) {
|
if (!this.context.router) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lastStatusId, unread, markRead } = this.props;
|
const { lastStatus, unread, markRead } = this.props;
|
||||||
|
|
||||||
if (unread) {
|
if (unread) {
|
||||||
markRead();
|
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 = () => {
|
handleHotkeyMoveUp = () => {
|
||||||
|
@ -42,22 +97,94 @@ export default class Conversation extends ImmutablePureComponent {
|
||||||
this.props.onMoveDown(this.props.conversationId);
|
this.props.onMoveDown(this.props.conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
handleConversationMute = () => {
|
||||||
const { accounts, lastStatusId, unread } = this.props;
|
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;
|
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 (
|
return (
|
||||||
<StatusContainer
|
<HotKeys handlers={handlers}>
|
||||||
id={lastStatusId}
|
<div className='conversation focusable muted' tabIndex='0'>
|
||||||
unread={unread}
|
<div className='conversation__avatar'>
|
||||||
otherAccounts={accounts}
|
<AvatarComposite accounts={accounts} size={48} />
|
||||||
onMoveUp={this.handleHotkeyMoveUp}
|
</div>
|
||||||
onMoveDown={this.handleHotkeyMoveDown}
|
|
||||||
onClick={this.handleClick}
|
<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 { connect } from 'react-redux';
|
||||||
import Conversation from '../components/conversation';
|
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({
|
||||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
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?' },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
const mapStateToProps = () => {
|
||||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
const getStatus = makeGetStatus();
|
||||||
unread: conversation.get('unread'),
|
|
||||||
lastStatusId: conversation.get('last_status', null),
|
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'),
|
||||||
|
lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
|
||||||
markRead: () => dispatch(markConversationRead(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,7 +31,9 @@ class Favourites extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
if (!this.props.accountIds) {
|
||||||
|
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
|
|
@ -36,8 +36,10 @@ class Followers extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
if (!this.props.accountIds) {
|
||||||
this.props.dispatch(fetchFollowers(this.props.params.accountId));
|
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||||
|
this.props.dispatch(fetchFollowers(this.props.params.accountId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
|
|
@ -36,8 +36,10 @@ class Following extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
if (!this.props.accountIds) {
|
||||||
this.props.dispatch(fetchFollowing(this.props.params.accountId));
|
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||||
|
this.props.dispatch(fetchFollowing(this.props.params.accountId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
|
|
@ -104,16 +104,14 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { myAccount, fetchFollowRequests, multiColumn } = this.props;
|
const { fetchFollowRequests, multiColumn } = this.props;
|
||||||
|
|
||||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||||
this.context.router.history.replace('/timelines/home');
|
this.context.router.history.replace('/timelines/home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myAccount.get('locked')) {
|
fetchFollowRequests();
|
||||||
fetchFollowRequests();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -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' />);
|
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' />);
|
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')}
|
onClick={this.onClick('mention')}
|
||||||
title={intl.formatMessage(tooltips.mentions)}
|
title={intl.formatMessage(tooltips.mentions)}
|
||||||
>
|
>
|
||||||
<Icon id='at' fixedWidth />
|
<Icon id='reply-all' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'favourite' ? 'active' : ''}
|
className={selectedFilter === 'favourite' ? 'active' : ''}
|
||||||
|
|
|
@ -31,7 +31,9 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
if (!this.props.accountIds) {
|
||||||
|
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import DetailedStatus from '../components/detailed_status';
|
import DetailedStatus from '../components/detailed_status';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
|
@ -15,7 +14,6 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import { blockAccount } from 'flavours/glitch/actions/accounts';
|
|
||||||
import {
|
import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
|
@ -24,9 +22,10 @@ import {
|
||||||
revealStatus,
|
revealStatus,
|
||||||
} from 'flavours/glitch/actions/statuses';
|
} from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
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 { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||||
import { showAlertForError } from 'flavours/glitch/actions/alerts';
|
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?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
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.' },
|
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' },
|
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?' },
|
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 = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -139,16 +136,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onBlock (status) {
|
onBlock (status) {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initBlockModal(account));
|
||||||
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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onReport (status) {
|
onReport (status) {
|
||||||
|
|
|
@ -26,9 +26,9 @@ import {
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
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 { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import { ScrollContainer } from 'react-router-scroll-4';
|
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 ColumnHeader from '../../components/column_header';
|
||||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
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?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
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.' },
|
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' },
|
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
|
||||||
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
|
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
|
||||||
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
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?' },
|
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' },
|
tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -339,19 +337,9 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlockClick = (status) => {
|
handleBlockClick = (status) => {
|
||||||
const { dispatch, intl } = this.props;
|
const { dispatch } = this.props;
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
dispatch(initBlockModal(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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReport = (status) => {
|
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`,
|
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 }))
|
.progress(({ progress }) => this.setState({ progress }))
|
||||||
.finally(() => worker.terminate())
|
.finally(() => worker.terminate())
|
||||||
.then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
|
.then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
|
||||||
|
|
|
@ -15,6 +15,7 @@ import FocalPointModal from './focal_point_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
|
BlockModal,
|
||||||
ReportModal,
|
ReportModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
|
@ -32,6 +33,7 @@ const MODAL_COMPONENTS = {
|
||||||
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
|
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
|
||||||
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
||||||
'MUTE': MuteModal,
|
'MUTE': MuteModal,
|
||||||
|
'BLOCK': BlockModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'SETTINGS': SettingsModal,
|
'SETTINGS': SettingsModal,
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return {
|
return {
|
||||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
|
||||||
account: state.getIn(['mutes', 'new', 'account']),
|
account: state.getIn(['mutes', 'new', 'account']),
|
||||||
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
||||||
};
|
};
|
||||||
|
@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
class MuteModal extends React.PureComponent {
|
class MuteModal extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isSubmitting: PropTypes.bool.isRequired,
|
|
||||||
account: PropTypes.object.isRequired,
|
account: PropTypes.object.isRequired,
|
||||||
notifications: PropTypes.bool.isRequired,
|
notifications: PropTypes.bool.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent {
|
||||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<p className='mute-modal__explanation'>
|
||||||
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
<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?' />
|
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||||
{' '}
|
|
||||||
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -114,6 +114,16 @@ function main() {
|
||||||
this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
|
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 => {
|
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;
|
const prevSize = state.get('media_attachments').size;
|
||||||
|
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
if (media.get('type') === 'image') {
|
||||||
|
media = media.set('file', file);
|
||||||
|
}
|
||||||
map.update('media_attachments', list => list.push(media));
|
map.update('media_attachments', list => list.push(media));
|
||||||
map.set('is_uploading', false);
|
map.set('is_uploading', false);
|
||||||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||||
|
@ -422,7 +425,7 @@ export default function compose(state = initialState, action) {
|
||||||
case COMPOSE_UPLOAD_REQUEST:
|
case COMPOSE_UPLOAD_REQUEST:
|
||||||
return state.set('is_uploading', true);
|
return state.set('is_uploading', true);
|
||||||
case COMPOSE_UPLOAD_SUCCESS:
|
case COMPOSE_UPLOAD_SUCCESS:
|
||||||
return appendMedia(state, fromJS(action.media));
|
return appendMedia(state, fromJS(action.media), action.file);
|
||||||
case COMPOSE_UPLOAD_FAIL:
|
case COMPOSE_UPLOAD_FAIL:
|
||||||
return state.set('is_uploading', false);
|
return state.set('is_uploading', false);
|
||||||
case COMPOSE_UPLOAD_UNDO:
|
case COMPOSE_UPLOAD_UNDO:
|
||||||
|
|
|
@ -16,6 +16,7 @@ import local_settings from './local_settings';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
|
import blocks from './blocks';
|
||||||
import reports from './reports';
|
import reports from './reports';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
|
@ -53,6 +54,7 @@ const reducers = {
|
||||||
local_settings,
|
local_settings,
|
||||||
push_notifications,
|
push_notifications,
|
||||||
mutes,
|
mutes,
|
||||||
|
blocks,
|
||||||
reports,
|
reports,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
new: Immutable.Map({
|
new: Immutable.Map({
|
||||||
isSubmitting: false,
|
|
||||||
account: null,
|
account: null,
|
||||||
notifications: true,
|
notifications: true,
|
||||||
}),
|
}),
|
||||||
|
@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case MUTES_INIT_MODAL:
|
case MUTES_INIT_MODAL:
|
||||||
return state.withMutations((state) => {
|
return state.withMutations((state) => {
|
||||||
state.setIn(['new', 'isSubmitting'], false);
|
|
||||||
state.setIn(['new', 'account'], action.account);
|
state.setIn(['new', 'account'], action.account);
|
||||||
state.setIn(['new', 'notifications'], true);
|
state.setIn(['new', 'notifications'], true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,7 +52,7 @@ const notificationToMap = (state, notification) => ImmutableMap({
|
||||||
const normalizeNotification = (state, notification, usePendingItems) => {
|
const normalizeNotification = (state, notification, usePendingItems) => {
|
||||||
const top = !shouldCountUnreadNotifications(state);
|
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);
|
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 => {
|
return state.withMutations(mutable => {
|
||||||
if (!items.isEmpty()) {
|
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 => {
|
mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
|
||||||
const lastIndex = 1 + list.findLastIndex(
|
const lastIndex = 1 + list.findLastIndex(
|
||||||
|
|
|
@ -40,7 +40,8 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||||
if (timeline.endsWith(':pinned')) {
|
if (timeline.endsWith(':pinned')) {
|
||||||
mMap.set('items', statuses.map(status => status.get('id')));
|
mMap.set('items', statuses.map(status => status.get('id')));
|
||||||
} else if (!statuses.isEmpty()) {
|
} 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 => {
|
mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
|
||||||
const newIds = statuses.map(status => status.get('id'));
|
const newIds = statuses.map(status => status.get('id'));
|
||||||
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
|
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 updateTimeline = (state, timeline, status, usePendingItems) => {
|
||||||
const top = state.getIn([timeline, 'top']);
|
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'))) {
|
if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,24 +62,6 @@
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
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() {
|
@mixin search-popout() {
|
||||||
|
|
|
@ -17,117 +17,86 @@ $small-breakpoint: 960px;
|
||||||
|
|
||||||
.rich-formatting {
|
.rich-formatting {
|
||||||
font-family: $font-sans-serif, sans-serif;
|
font-family: $font-sans-serif, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 16px;
|
line-height: 1.7;
|
||||||
line-height: 30px;
|
word-wrap: break-word;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
padding-right: 10px;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
p,
|
||||||
li {
|
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;
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
p {
|
||||||
color: $highlight-text-color;
|
margin-top: 0;
|
||||||
text-decoration: underline;
|
margin-bottom: .85em;
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strong,
|
strong {
|
||||||
em {
|
|
||||||
font-weight: 700;
|
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 {
|
h1 {
|
||||||
font-family: $font-display, sans-serif;
|
font-size: 2em;
|
||||||
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%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-family: $font-display, sans-serif;
|
font-size: 1.75em;
|
||||||
font-size: 22px;
|
|
||||||
line-height: 26px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: $secondary-text-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-family: $font-display, sans-serif;
|
font-size: 1.5em;
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: $secondary-text-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-family: $font-display, sans-serif;
|
font-size: 1.25em;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
font-family: $font-display, sans-serif;
|
font-size: 1em;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -138,23 +107,79 @@ $small-breakpoint: 960px;
|
||||||
list-style: decimal;
|
list-style: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
li > ol,
|
ul,
|
||||||
li > ul {
|
ol {
|
||||||
margin-top: 6px;
|
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 {
|
hr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
|
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||||
margin: 20px 0;
|
margin: 1.7em 0;
|
||||||
|
|
||||||
&.spacer {
|
&.spacer {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
border: 0;
|
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 {
|
.information-board {
|
||||||
|
@ -418,7 +443,7 @@ $small-breakpoint: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__call-to-action {
|
&__call-to-action {
|
||||||
background: darken($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 25px 40px;
|
padding: 25px 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -5,21 +5,66 @@ $content-width: 840px;
|
||||||
.admin-wrapper {
|
.admin-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
.sidebar-wrapper {
|
.sidebar-wrapper {
|
||||||
flex: 1 1 $sidebar-width;
|
min-height: 100vh;
|
||||||
height: 100%;
|
overflow: hidden;
|
||||||
background: $ui-base-color;
|
pointer-events: none;
|
||||||
display: flex;
|
flex: 1 1 auto;
|
||||||
justify-content: flex-end;
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background: $ui-base-color;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: $sidebar-width;
|
width: $sidebar-width;
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
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 {
|
.logo {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -52,6 +97,9 @@ $content-width: 840px;
|
||||||
transition: all 200ms linear;
|
transition: all 200ms linear;
|
||||||
transition-property: color, background-color;
|
transition-property: color, background-color;
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
i.fa {
|
i.fa {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
@ -99,12 +147,30 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
flex: 2 1 $content-width;
|
box-sizing: border-box;
|
||||||
overflow: auto;
|
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 {
|
.content {
|
||||||
max-width: $content-width;
|
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
padding-top: 60px;
|
padding-top: 60px;
|
||||||
padding-left: 25px;
|
padding-left: 25px;
|
||||||
|
@ -123,6 +189,12 @@ $content-width: 840px;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-columns-breakpoint) {
|
||||||
|
border-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
@ -147,7 +219,7 @@ $content-width: 840px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields-group h6 {
|
.fields-group h6 {
|
||||||
|
@ -176,7 +248,7 @@ $content-width: 840px;
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 21px;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
@ -204,61 +276,98 @@ $content-width: 840px;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-hint {
|
|
||||||
color: $darker-text-color;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $highlight-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.positive-hint {
|
|
||||||
color: $valid-value-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.negative-hint {
|
|
||||||
color: $error-value-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neutral-hint {
|
|
||||||
color: $dark-text-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $no-columns-breakpoint) {
|
@media screen and (max-width: $no-columns-breakpoint) {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
.sidebar-wrapper,
|
.sidebar-wrapper {
|
||||||
.content-wrapper {
|
min-height: 0;
|
||||||
flex: 0 0 auto;
|
|
||||||
height: auto;
|
|
||||||
overflow: initial;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: auto;
|
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;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive-hint {
|
||||||
|
color: $valid-value-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative-hint {
|
||||||
|
color: $error-value-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral-hint {
|
||||||
|
color: $dark-text-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-hint {
|
||||||
|
color: $gold-star;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.filter-subset {
|
.filter-subset {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 0 40px 10px 0;
|
margin: 0 40px 20px 0;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|
|
@ -74,9 +74,6 @@ body {
|
||||||
|
|
||||||
&.admin {
|
&.admin {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,8 @@
|
||||||
&-composite {
|
&-composite {
|
||||||
@include avatar-radius;
|
@include avatar-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
@include avatar-radius;
|
@include avatar-radius;
|
||||||
|
@ -57,6 +59,18 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
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 {
|
.column-select {
|
||||||
&__control {
|
&__control {
|
||||||
@include search-input();
|
@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 {
|
&__placeholder {
|
||||||
|
|
|
@ -44,6 +44,10 @@
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus { outline: 0 }
|
&:focus { outline: 0 }
|
||||||
@include single-column('screen and (max-width: 630px)') { font-size: 16px }
|
@include single-column('screen and (max-width: 630px)') { font-size: 16px }
|
||||||
}
|
}
|
||||||
|
@ -263,6 +267,10 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
scrollbar-color: initial;
|
scrollbar-color: initial;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
all: unset;
|
all: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1433,49 +1433,68 @@
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-toggle {
|
.conversation {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
button {
|
&:focus {
|
||||||
box-sizing: border-box;
|
background: lighten($ui-base-color, 2%);
|
||||||
flex: 0 0 50%;
|
outline: 0;
|
||||||
background: transparent;
|
}
|
||||||
padding: 5px;
|
|
||||||
border: 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover,
|
&__avatar {
|
||||||
&:focus,
|
flex: 0 0 auto;
|
||||||
&:active {
|
padding: 10px;
|
||||||
svg path:first-child {
|
padding-top: 12px;
|
||||||
fill: lighten($ui-base-color, 16%);
|
}
|
||||||
|
|
||||||
|
&__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 {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
.status__content {
|
||||||
width: 100%;
|
margin: 0;
|
||||||
height: auto;
|
|
||||||
|
|
||||||
path:first-child {
|
|
||||||
fill: lighten($ui-base-color, 12%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
.confirmation-modal,
|
||||||
.report-modal,
|
.report-modal,
|
||||||
.actions-modal,
|
.actions-modal,
|
||||||
.mute-modal {
|
.mute-modal,
|
||||||
|
.block-modal {
|
||||||
background: lighten($ui-secondary-color, 8%);
|
background: lighten($ui-secondary-color, 8%);
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -465,7 +466,8 @@
|
||||||
.boost-modal__action-bar,
|
.boost-modal__action-bar,
|
||||||
.favourite-modal__action-bar,
|
.favourite-modal__action-bar,
|
||||||
.confirmation-modal__action-bar,
|
.confirmation-modal__action-bar,
|
||||||
.mute-modal__action-bar {
|
.mute-modal__action-bar,
|
||||||
|
.block-modal__action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: $ui-secondary-color;
|
background: $ui-secondary-color;
|
||||||
|
@ -495,11 +497,13 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mute-modal {
|
.mute-modal,
|
||||||
|
.block-modal {
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mute-modal .react-toggle {
|
.mute-modal .react-toggle,
|
||||||
|
.block-modal .react-toggle {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,27 +716,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-modal__action-bar,
|
.confirmation-modal__action-bar,
|
||||||
.mute-modal__action-bar {
|
.mute-modal__action-bar,
|
||||||
.confirmation-modal__secondary-button,
|
.block-modal__action-bar {
|
||||||
.confirmation-modal__cancel-button,
|
|
||||||
.mute-modal__cancel-button {
|
|
||||||
background-color: transparent;
|
|
||||||
color: $lighter-text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
color: darken($lighter-text-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-modal__secondary-button {
|
.confirmation-modal__secondary-button {
|
||||||
flex-shrink: 1;
|
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;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: darken($lighter-text-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.confirmation-modal__do_not_ask_again {
|
.confirmation-modal__do_not_ask_again {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
@ -747,10 +753,10 @@
|
||||||
|
|
||||||
.confirmation-modal__container,
|
.confirmation-modal__container,
|
||||||
.mute-modal__container,
|
.mute-modal__container,
|
||||||
|
.block-modal__container,
|
||||||
.report-modal__target {
|
.report-modal__target {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: 500;
|
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 {
|
.report-modal__target {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,28 @@
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-size: 16px;
|
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 {
|
.search__icon {
|
||||||
|
|
|
@ -673,6 +673,7 @@ a.status__display-name,
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
|
.status__content,
|
||||||
.status__content p,
|
.status__content p,
|
||||||
.status__content a,
|
.status__content a,
|
||||||
.status__content__text {
|
.status__content__text {
|
||||||
|
|
|
@ -143,6 +143,63 @@
|
||||||
grid-row: 3;
|
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 {
|
.landing-page__call-to-action {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -191,6 +248,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-3 {
|
.column-3 {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-4 {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 4;
|
grid-row: 4;
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,6 +245,10 @@ code {
|
||||||
&-6 {
|
&-6 {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 27px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields-group:last-child,
|
.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=text],
|
||||||
input[type=number],
|
input[type=number],
|
||||||
input[type=email],
|
input[type=email],
|
||||||
|
@ -318,6 +329,10 @@ code {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: lighten($darker-text-color, 4%);
|
||||||
|
}
|
||||||
|
|
||||||
&:invalid {
|
&:invalid {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,6 +226,7 @@
|
||||||
.boost-modal,
|
.boost-modal,
|
||||||
.confirmation-modal,
|
.confirmation-modal,
|
||||||
.mute-modal,
|
.mute-modal,
|
||||||
|
.block-modal,
|
||||||
.report-modal,
|
.report-modal,
|
||||||
.embed-modal,
|
.embed-modal,
|
||||||
.error-modal,
|
.error-modal,
|
||||||
|
@ -236,6 +237,7 @@
|
||||||
.boost-modal__action-bar,
|
.boost-modal__action-bar,
|
||||||
.confirmation-modal__action-bar,
|
.confirmation-modal__action-bar,
|
||||||
.mute-modal__action-bar,
|
.mute-modal__action-bar,
|
||||||
|
.block-modal__action-bar,
|
||||||
.onboarding-modal__paginator,
|
.onboarding-modal__paginator,
|
||||||
.error-modal__footer {
|
.error-modal__footer {
|
||||||
background: darken($ui-base-color, 6%);
|
background: darken($ui-base-color, 6%);
|
||||||
|
|
|
@ -102,13 +102,19 @@
|
||||||
|
|
||||||
&__number {
|
&__number {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 36px;
|
width: 52px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
padding-left: 8px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
flex: 0 0 36px;
|
flex: 0 0 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__vote__mark {
|
||||||
|
float: left;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__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;
|
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 {
|
.contact-widget {
|
||||||
|
min-height: 100%;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
strong {
|
h4 {
|
||||||
font-weight: 500;
|
padding: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.account {
|
||||||
margin-bottom: 10px;
|
border-bottom: 0;
|
||||||
|
padding: 10px 0;
|
||||||
&:last-child {
|
padding-top: 5px;
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__mail {
|
& > a {
|
||||||
margin-top: 10px;
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 0;
|
||||||
|
color: $darker-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
a {
|
&:hover,
|
||||||
color: $primary-text-color;
|
&:focus,
|
||||||
text-decoration: none;
|
&: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:
|
mailer:
|
||||||
modal:
|
modal:
|
||||||
public: packs/public.js
|
public: packs/public.js
|
||||||
settings:
|
settings: packs/settings.js
|
||||||
share: packs/share.js
|
share: packs/share.js
|
||||||
|
|
||||||
# (OPTIONAL) The directory which contains localization files for
|
# (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');
|
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 () {
|
export function ReportModal () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
|
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;
|
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 api, { getLinks } from '../api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||||
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
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_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||||
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
|
||||||
|
|
||||||
export function fetchBlocks() {
|
export function fetchBlocks() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchBlocksRequest());
|
dispatch(fetchBlocksRequest());
|
||||||
|
@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
|
||||||
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;
|
progress[i] = loaded;
|
||||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
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)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -289,10 +289,11 @@ export function uploadComposeProgress(loaded, total) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function uploadComposeSuccess(media) {
|
export function uploadComposeSuccess(media, file) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_SUCCESS,
|
type: COMPOSE_UPLOAD_SUCCESS,
|
||||||
media: media,
|
media: media,
|
||||||
|
file: file,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -368,6 +369,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
||||||
q: token.slice(1),
|
q: token.slice(1),
|
||||||
resolve: false,
|
resolve: false,
|
||||||
limit: 4,
|
limit: 4,
|
||||||
|
exclude_unreviewed: true,
|
||||||
},
|
},
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
|
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
|
||||||
|
|
|
@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||||
|
|
||||||
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
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 = () => ({
|
export const mountConversations = () => ({
|
||||||
type: CONVERSATIONS_MOUNT,
|
type: CONVERSATIONS_MOUNT,
|
||||||
});
|
});
|
||||||
|
@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
|
||||||
conversation,
|
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);
|
const emojiMap = makeEmojiMap(normalPoll);
|
||||||
|
|
||||||
normalPoll.options = poll.options.map(option => ({
|
normalPoll.options = poll.options.map((option, index) => ({
|
||||||
...option,
|
...option,
|
||||||
|
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
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_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||||
|
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
|
@ -215,3 +218,11 @@ export function setFilter (filterType) {
|
||||||
dispatch(saveSettings());
|
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 (size === 2) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
} else {
|
} else {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
} else if (size === 3) {
|
} else if (size === 3) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
} else if (index > 0) {
|
} else if (index > 0) {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === 1) {
|
if (index === 1) {
|
||||||
bottom = '2px';
|
bottom = '1px';
|
||||||
} else if (index > 1) {
|
} else if (index > 1) {
|
||||||
top = '2px';
|
top = '1px';
|
||||||
}
|
}
|
||||||
} else if (size === 4) {
|
} else if (size === 4) {
|
||||||
if (index === 0 || index === 2) {
|
if (index === 0 || index === 2) {
|
||||||
right = '2px';
|
right = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === 1 || index === 3) {
|
if (index === 1 || index === 3) {
|
||||||
left = '2px';
|
left = '1px';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 2) {
|
if (index < 2) {
|
||||||
bottom = '2px';
|
bottom = '1px';
|
||||||
} else {
|
} else {
|
||||||
top = '2px';
|
top = '1px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,13 @@ export default class AvatarComposite extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
<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>
|
</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>
|
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (multiColumn) {
|
} else if (multiColumn && 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>;
|
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);
|
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>;
|
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 escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
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) => {
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
|
@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, disabled } = this.props;
|
const { poll, disabled, intl } = this.props;
|
||||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
|
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
|
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
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');
|
let titleEmojified = option.get('title_emojified');
|
||||||
if (!titleEmojified) {
|
if (!titleEmojified) {
|
||||||
|
@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!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 }} />
|
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||||
</label>
|
</label>
|
||||||
|
@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
|
||||||
const showResults = poll.get('voted') || expired;
|
const showResults = poll.get('voted') || expired;
|
||||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
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 (
|
return (
|
||||||
<div className='poll'>
|
<div className='poll'>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
<div className='poll__footer'>
|
<div className='poll__footer'>
|
||||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
{!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>}
|
{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>}
|
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -199,6 +199,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
this.clearMouseIdleTimer();
|
this.clearMouseIdleTimer();
|
||||||
this.detachScrollListener();
|
this.detachScrollListener();
|
||||||
this.detachIntersectionObserver();
|
this.detachIntersectionObserver();
|
||||||
|
|
||||||
detachFullscreenListener(this.onFullScreenChange);
|
detachFullscreenListener(this.onFullScreenChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,17 @@ import React, { PureComponent, Fragment } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
import MediaGallery from '../components/media_gallery';
|
import { getLocale } from 'mastodon/locales';
|
||||||
import Video from '../features/video';
|
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||||
import Card from '../features/status/components/card';
|
import MediaGallery from 'mastodon/components/media_gallery';
|
||||||
import Poll from 'mastodon/components/poll';
|
import Poll from 'mastodon/components/poll';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
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 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();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import { makeGetStatus } from '../selectors';
|
import { makeGetStatus } from '../selectors';
|
||||||
|
@ -15,7 +14,6 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import { blockAccount } from '../actions/accounts';
|
|
||||||
import {
|
import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
|
@ -24,9 +22,10 @@ import {
|
||||||
revealStatus,
|
revealStatus,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import { initMuteModal } from '../actions/mutes';
|
import { initMuteModal } from '../actions/mutes';
|
||||||
|
import { initBlockModal } from '../actions/blocks';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
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 { boostModal, deleteModal } from '../initial_state';
|
||||||
import { showAlertForError } from '../actions/alerts';
|
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?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
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.' },
|
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' },
|
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?' },
|
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 = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -56,6 +53,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
onReply (status, router) {
|
onReply (status, router) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
||||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
message: intl.formatMessage(messages.replyMessage),
|
||||||
|
@ -137,16 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onBlock (status) {
|
onBlock (status) {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initBlockModal(account));
|
||||||
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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onReport (status) {
|
onReport (status) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import Header from '../components/header';
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
blockAccount,
|
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
pinAccount,
|
pinAccount,
|
||||||
|
@ -16,6 +15,7 @@ import {
|
||||||
directCompose,
|
directCompose,
|
||||||
} from '../../../actions/compose';
|
} from '../../../actions/compose';
|
||||||
import { initMuteModal } from '../../../actions/mutes';
|
import { initMuteModal } from '../../../actions/mutes';
|
||||||
|
import { initBlockModal } from '../../../actions/blocks';
|
||||||
import { initReport } from '../../../actions/reports';
|
import { initReport } from '../../../actions/reports';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||||
|
@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
dispatch(unblockAccount(account.get('id')));
|
dispatch(unblockAccount(account.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initBlockModal(account));
|
||||||
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));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue