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