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