Compare commits

...

59 Commits

Author SHA1 Message Date
khr 1e31148c57 Merge tag 'v2.6.5' into cybrespace 2018-12-21 12:23:13 -08:00
Eugen Rochko 887f9de6dc Bump version to 2.6.5 2018-12-02 17:04:03 +01:00
ThibG e625425c8f Include replies to list owner and replies to list members in list statuses (#9324) 2018-12-02 17:02:44 +01:00
ThibG f13d08314e Preload common JSON-LD contexts (#9412)
Fixes #9411
2018-12-02 16:50:28 +01:00
Eugen Rochko 13979a84f9 Bump version to 2.6.4 2018-11-30 19:54:24 +01:00
Eugen Rochko 82570019ba Remove npm-run-all dependency (#9401)
Fix #9359
2018-11-30 19:51:59 +01:00
Eugen Rochko a1216e6315 Bump version to 2.6.3 2018-11-30 03:08:37 +01:00
Eugen Rochko 34de90c486 Fix TLS handshake timeout not being enforced (#9381)
Follow-up to #9329
2018-11-30 03:02:55 +01:00
Eugen Rochko 442f335504 Skip deliveries to inboxes that have already been marked as unavailable (#9358) 2018-11-30 03:02:55 +01:00
Eugen Rochko 58108b4481 Don't count suspended users in user count (#9380)
Fix #7637
2018-11-30 03:02:55 +01:00
Eugen Rochko cc0c1674f0 Fix nil error when no DNS addresses are found for host (#9379) 2018-11-30 03:02:55 +01:00
ThibG 49f49cf367 Allow hyphens in the middle of remote user names (#9345)
Fixes #9309

This only allows hyphens in the middle of a username, much like dots,
although I don't have a compelling reason to do so other than keeping
the changes minimal.
2018-11-30 03:02:55 +01:00
Hugo Gameiro ec20a5d53a add loglevel to ffmpeg in gif upload (#9368) 2018-11-30 03:02:55 +01:00
Eugen Rochko 404dc97fb0 Bump version to 2.6.2 2018-11-23 22:32:20 +01:00
Eugen Rochko a2cda74ba3 Fix connect timeout not being enforced (#9329)
* Fix connect timeout not being enforced

The loop was catching the timeout exception that should stop execution, so the next IP would no longer be within a timed block, which led to requests taking much longer than 10 seconds.

* Use timeout on each IP attempt, but limit to 2 attempts

* Fix code style issue

* Do not break Request#perform if no block given

* Update method stub in spec for Request

* Move timeout inside the begin/rescue block

* Use Resolv::DNS with timeout of 1 to get IP addresses

* Update Request spec to stub Resolv::DNS instead of Addrinfo

* Fix Resolve::DNS stubs in Request spec
2018-11-23 22:17:04 +01:00
valerauko 12bdd7dc5f Ignore JSON-LD profile in mime type comparison (#9179)
Ignore JSON-LD profile in mime type comparison
2018-11-23 22:16:13 +01:00
Renato "Lond" Cerqueira 15dcb414bf Touch account on successful response, change char shown when culled (#9293)
Just the color is not enough change since not everyone uses colored
terminals.
Touching the account makes it so that the account is not in the
threshold window in case of running again
2018-11-23 22:15:56 +01:00
Alexandre Alapetite 2c36d35784 WebSub: ATOM before RSS (#9302)
Hello,
The ATOM feed contains the hub declaration for WebSub, but the RSS
version does not.
RSS/ATOM readers will typically pick whichever version comes first, and
will thus not see the WebSub feature.
I therefore suggest putting the ATOM version first, as it is more
feature-rich than its RSS counterpart is.

Clients not compatible with ATOM would not pick it anyway due to the
different type attribute.

A more complicated alternative would be to declare the WebSub feature in
the RSS version as well, using something like the following code, and
ensuring that clients subscribed to the RSS version would receive PuSH
updates just like those subscribed to the ATOM version.

````xml
<rss version="2.0" xmlns:webfeeds="http://webfeeds.org/rss/1.0"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link rel="self" type="application/rss+xml"
href="https://diaspodon.fr/users/test.rss"/>
<atom:link rel="hub" href="https://diaspodon.fr/api/push"/>
</channel>
</rss>
```
2018-11-23 22:15:46 +01:00
Dan Hunsaker c0736c466c Update Nginx config for Nanobox apps (#9310)
The Nanobox files have gotten out of sync, a touch, with what Masto needs for Nginx settings. This PR updates them accordingly.
2018-11-23 22:15:41 +01:00
Eugen Rochko fa02f878fc Fix filter ID not being a string in REST API (#9303) 2018-11-23 22:15:22 +01:00
Eugen Rochko ecc58c0f23 Prevent multiple handlers for Delete of Actor from running (#9292) 2018-11-23 22:15:12 +01:00
Eugen Rochko 6d4438a6ae Remove intermediary arrays when creating hash maps from results (#9291) 2018-11-23 22:15:03 +01:00
mayaeh 01a8ab921e Fix "tootctl media remove" can't count the file size (#9288)
* Fixed an issue where "tootctl media remove" can not count the file size.

* Fixed the problem pointed out by codeclimate.
2018-11-23 22:14:51 +01:00
ThibG a3ef076160 Fix race condition causing shallow status with only a "favourited" attribute (#9272)
Fixes #9231
2018-11-23 22:14:42 +01:00
Eugen Rochko cd8575aef6 Fix null error introduced in #9270 (#9275) 2018-11-23 22:14:35 +01:00
ThibG 4ce6ed2021 Perform deep comparison for card data when receiving new props (#9270)
Fixes #9226
2018-11-23 22:14:25 +01:00
ThibG 886ef1cc38 Fix emoji update date processing (#9255) 2018-11-23 22:14:15 +01:00
ThibG d06a724b1c Check that twitter:player is valid before using it (#9254)
Fixes #9251
2018-11-23 22:14:08 +01:00
Eugen Rochko f73b7e77da Improve ActiveRecord connection in on_worker_boot (#9238)
This is how it looks in the example in the Puma README
2018-11-23 22:13:44 +01:00
Eugen Rochko 63f168c3bf Fix nil error regression from #9229 in tootctl media remove (#9239)
Fix #9237
2018-11-23 22:13:34 +01:00
Eugen Rochko 0f436de035 Add "Show thread" link to self-replies (#9228)
Fix #4716
2018-11-23 22:12:56 +01:00
Eugen Rochko 21fd335dd7 Display amount of freed disk space in tootctl media remove (#9229)
* Display amount of freed disk space in tootctl media remove

Fix #9213

* Fix code style issue
2018-11-23 22:12:50 +01:00
Eugen Rochko 4b2f254806 Fix form validation flash message color and input borders (#9235)
* Fix form validation flash message color and input borders

* Fix typo
2018-11-23 22:12:42 +01:00
Eugen Rochko b3c29ece47 Fix follow limit validator reporting lower number past threshold (#9230)
* Fix follow limit validator reporting lower number past threshold

* Avoid floating point follow limit
2018-11-23 22:12:33 +01:00
Eugen Rochko 330401bec0 Optimize the process of following someone (#9220)
* Eliminate extra accounts select query from FollowService

* Optimistically update follow state in web UI and hide loading bar

Fix #6205

* Asynchronize NotifyService in FollowService

And fix failing test

* Skip Webfinger resolve routine when called from FollowService if possible

If an account is ActivityPub, then webfinger re-resolving is not necessary
when called from FollowService. Improve options of ResolveAccountService
2018-11-23 22:12:26 +01:00
Eugen Rochko 5ee4fd4606 Increase default column width from 330px to 350px (#9227) 2018-11-23 22:12:15 +01:00
m.b 430499fbe1 Update resolve_url_service.rb (#9188) 2018-11-23 22:11:31 +01:00
Steven Tappert 449e6e451f Check for empty "last_status" before sorting DM column (#9207)
* Check for empty "last_status" before sorting

* Small touchups for codeclimate
2018-11-23 22:11:05 +01:00
LenPayne d510ef7375 Re-added add_user_automatic rake task (#3) 2018-11-20 07:29:00 +01:00
nightpool b9bab4e477 Fix embed width, light theme embeds (#2) 2018-11-20 07:27:41 +01:00
khr 9a49c503c8 Fix unnecessary scrollbar on compose drawer 2018-11-17 20:00:41 -08:00
khr 7c0624ccfa Merge branch 'theme_win95' into cybrespace 2018-11-17 17:27:22 -08:00
khr 6f6c4f2a50 Merge branch 'theme_light' into cybrespace 2018-11-17 17:26:59 -08:00
khr 42954f329a Merge branch 'theme_cybre' into cybrespace 2018-11-17 17:26:55 -08:00
khr a0ba41a78c Merge branch 'theme_base' into cybrespace 2018-11-17 17:26:47 -08:00
khr 6695882f19 Merge branches 'branding_cybre', 'dependency_whatinput', 'feature_512_char_toots', 'feature_disable_reply_counts', 'feature_hotlink_twitter_mentions', 'feature_cybrespace_locale', 'feature_doodlebox' and 'feature_longer_bios' into cybrespace 2018-11-17 17:26:31 -08:00
khr 35d7ae4974 Cybrespace branding changes 2018-11-17 17:26:07 -08:00
khr f40d8a3c37 Windows 95 theme 2018-11-17 17:21:37 -08:00
khr 9eab8a139f Cybrespace light theme 2018-11-17 16:30:57 -08:00
khr 7abb76fd6f Cybre dark theme 2018-11-17 16:22:42 -08:00
khr a0bce3a23f Fix media height weirdness, handle new status cards 2018-11-17 16:20:49 -08:00
chr 7dae98557c Base SCSS for cybrespace themes
Performs some baseline tweaks to the mastodon application scss that are
shared between all the cybrespace themes
2018-11-12 23:26:12 -08:00
Andrew 87eb1df45a Add whatinput dependency for more responsive focus styling 2018-11-12 23:00:14 -08:00
khr a01c7ce321 Cybrespace locale
An alternative locale that replaces "toot" with "ping", "favorite" with
"florp", and "boost" with "relay", as well as a few other miscellaneous
changes. Also includes alternate settings that make the en-CY locale
default instead of en-US, and fixes the timestamps.
2018-11-12 22:35:38 -08:00
khr ebed61cf88 Doodlebox
A modification to the Mastodon interface that allows you to select a
doodle icon in addition to uploading a media file from your computer.
The doodle interface allows you to draw using your mouse, finger, or
pointing device in several colors. It was originally developed by
Ondřej Hruška <ondra@ondrovo.com> for use on the GlitchSoc fork.
2018-11-12 21:53:37 -08:00
Andrew 99b3c18b28 Hotlink twitter mentions
Differentiate twitter mentions from normal mentions
2018-11-12 21:44:10 -08:00
khr df0855a91b Bio length -> 413 characters
Increase the cybre.space profile bio text length limit to 413
characters. Also modifies the account settings page to automatically
resize the textbox to the size of the contained text, so that it's
easier to type longer bios.
2018-11-12 21:28:25 -08:00
Andrew bf7b956a99 Remove reply counts on non-expanded view 2018-11-12 20:16:48 -08:00
Andrew 5834fc5aa1 Feature: 512-character limit in the toot box on client and serverside 2018-11-12 19:58:20 -08:00
154 changed files with 6722 additions and 321 deletions

View File

@ -3,6 +3,73 @@ Changelog
All notable changes to this project will be documented in this file.
## [2.6.5] - 2018-12-01
### Changed
- Change lists to display replies to others on the list and list owner (#9324)
### Fixed
- Fix failures caused by commonly-used JSON-LD contexts being unavailable (#9412)
## [2.6.4] - 2018-11-30
### Fixed
- Fix yarn dependencies not installing due to yanked event-stream package (#9401)
## [2.6.3] - 2018-11-30
### Added
- Add hyphen to characters allowed in remote usernames (#9345)
### Changed
- Change server user count to exclude suspended accounts (#9380)
### Fixed
- Fix ffmpeg processing sometimes stalling due to overfilled stdout buffer (#9368)
- Fix missing DNS records raising the wrong kind of exception (#9379)
- Fix already queued deliveries still trying to reach inboxes marked as unavailable (#9358)
### Security
- Fix TLS handshake timeout not being enforced (#9381)
## [2.6.2] - 2018-11-23
### Added
- Add Page to whitelisted ActivityPub types (#9188)
- Add 20px to column width in web UI (#9227)
- Add amount of freed disk space in `tootctl media remove` (#9229, #9239, #9288)
- Add "Show thread" link to self-replies (#9228)
### Changed
- Change order of Atom and RSS links so Atom is first (#9302)
- Change Nginx configuration for Nanobox apps (#9310)
- Change the follow action to appear instant in web UI (#9220)
- Change how the ActiveRecord connection is instantiated in on_worker_boot (#9238)
- Change `tootctl accounts cull` to always touch accounts so they can be skipped (#9293)
- Change mime type comparison to ignore JSON-LD profile (#9179)
### Fixed
- Fix web UI crash when conversation has no last status (#9207)
- Fix follow limit validator reporting lower number past threshold (#9230)
- Fix form validation flash message color and input borders (#9235)
- Fix invalid twitter:player cards being displayed (#9254)
- Fix emoji update date being processed incorrectly (#9255)
- Fix playing embed resetting if status is reloaded in web UI (#9270, #9275)
- Fix web UI crash when favouriting a deleted status (#9272)
- Fix intermediary arrays being created for hash maps (#9291)
- Fix filter ID not being a string in REST API (#9303)
### Security
- Fix multiple remote account deletions being able to deadlock the database (#9292)
- Fix HTTP connection timeout of 10s not being enforced (#9329)
## [2.6.1] - 2018-10-30
### Fixed

View File

@ -90,6 +90,7 @@ gem 'webpacker', '~> 3.5'
gem 'webpush'
gem 'json-ld', '~> 2.2'
gem 'json-ld-preloaded', '~> 2.2'
gem 'rdf-normalize', '~> 0.3'
group :development, :test do

View File

@ -291,6 +291,10 @@ GEM
json-ld (2.2.1)
multi_json (~> 1.12)
rdf (>= 2.2.8, < 4.0)
json-ld-preloaded (2.2.3)
json-ld (>= 2.2, < 4.0)
multi_json (~> 1.12)
rdf (>= 2.2, < 4.0)
jsonapi-renderer (0.2.0)
jwt (2.1.0)
kaminari (1.1.1)
@ -696,6 +700,7 @@ DEPENDENCIES
idn-ruby
iso-639
json-ld (~> 2.2)
json-ld-preloaded (~> 2.2)
kaminari (~> 1.1)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)

View File

@ -17,7 +17,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
FollowService.new.call(current_user.account, @account.acct, 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 } }

View File

@ -113,7 +113,7 @@ class ApplicationController < ActionController::Base
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)

View File

@ -20,7 +20,12 @@ module Localized
if ENV['DEFAULT_LOCALE'].present?
I18n.default_locale
else
request_locale || I18n.default_locale
case request_locale
when /en\b/, nil
I18n.default_locale
else
request_locale
end
end
end

View File

@ -3,6 +3,7 @@
module SettingsHelper
HUMAN_LOCALES = {
en: 'English',
'en-CY': 'English (Cybre)',
ar: 'العربية',
ast: 'l\'asturianu',
bg: 'Български',

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
<g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
<g id="strokes">
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path2" d="m 405.991,306.829 v 10 10 10 10 10 10 10 10 6.581 5.978 9.656 3.057" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path4" d="m 405.991,409.044 v 0 -9.656 -5.978 -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path6" d="m 370.692,127.899 v 1.329 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path8" d="m 439.906,283.073 v 0 h -10 -10 -10.303 -3.612 -10 -10 -10 -2.337 l -2.962,-2.901 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path10" d="m 373.654,283.073 -2.962,-2.901 v -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path12" d="m 370.692,127.899 h -7.383 -10 -2.899 -5.311 v 0 l -0.45,0.28 -0.37,0.387 -0.268,0.47 v 0.192 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 l 6.235,6.039 7.183,6.957 2.643,2.56 3.237,3.135 v 0 h 7.383 2.962 10 10 10 2.337 v 0 0 -3.057" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path14" d="m 584.095,303.466 3.074,3.363 v 10 10 10 10 10 10 10 10 6.581 6.183 9.988 10e-4 0.001 2.518 h 6.497 10 2.976 7.534 v 0 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path16" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581 -3.363 -5.906 -10 -4.487 -2.901 -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658 -2.701 -4.955 -10 -0.947 -10 -1.4 -0.061 l -0.299,-0.524 -0.394,-0.413 -0.019,-0.019 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path18" d="m 584.093,129.228 h -10 -10 -10 -10 l -10,-0.001 h -10 -5.476 -10 -10 -10 -10 -10 -10 l -10,-0.001 h -8.828 -10 -10 -10 -0.326 -3.615 l -10,0.001 h -10 l -10,0.001 h -2.205 -2.951 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path20" d="m 615.277,128.211 v 0 l -0.501,-0.312 v 0 0 h -8.134 -10 -2.976 -6.497 v 1.329" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path22" d="m 615.69,128.643 0.299,0.524 v 0.061 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 6.183 9.987 0.626 l -0.447,0.784 -0.617,0.644 -0.749,0.467" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path24" d="m 587.169,156.53 v 0 0 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 l -3.074,2.901 h -10 -10 -10 -10 -10 -10 -5.424" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path26" d="m 370.692,129.228 v -1.329 0 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path28" d="m 584.093,129.228 h 3.076" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path30" d="m 350.41,140.628 h 10 2.899 v 10 0.947 h -3.555 -9.344 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path32" d="m 587.169,129.228 v 10 1.4 10 0.947 4.955" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path34" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path36" d="m 593.666,151.575 v -10 -0.947 0 h 10 2.976" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path38" d="m 606.642,140.628 v 10 0.947 h -10 -2.976 v 0 -10 -0.947" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path40" d="m 615.69,128.643 -0.394,-0.413 -0.019,-0.019 v 0 l -0.501,-0.312" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path42" d="m 615.989,393.41 v 6.183 9.987 0.626 l -0.447,0.783 -0.576,0.603 -0.041,0.042 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path44" d="m 614.925,411.634 v 0 l -0.749,0.467" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path46" d="m 615.69,128.643 -0.009,-0.01 -0.404,-0.422 -0.501,-0.312" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path48" d="m 614.176,412.101 0.749,-0.467 v 0 l 0.041,-0.042 0.576,-0.603 0.447,-0.783 v -0.626 -9.987 -6.183" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path50" d="m 406.223,306.613 -0.232,0.216 v 0 l 0.232,-0.216 3.38,-3.147 h 10 10 10.303 10 10 10 10 10 10 10 8.765 10 10 1.007" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path52" d="m 518.671,283.073 v 0 h -10 -2.247" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path54" d="m 615.277,128.211 0.404,0.422" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path56" d="m 383.445,273.927 h 10 10 10 10 10 6.461" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path58" d="m 576.555,273.927 2.03,-0.051 1.978,-0.302 1.707,-0.727" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path60" d="m 582.27,272.847 1.099,-1.131 0.456,-1.309 0.077,-1.342" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path62" d="m 583.902,269.065 v -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -9.834 0 -10 -7.959" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path64" d="m 583.902,141.272 -0.077,-1.344 -0.457,-1.309 -1.098,-1.13" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path66" d="m 582.27,137.489 -1.709,-0.727 -1.977,-0.301 -2.029,-0.051" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path68" d="m 576.555,136.41 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -3.11 l -2.03,0.051 -1.978,0.302 -1.707,0.726" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path70" d="m 377.73,137.489 -1.099,1.131 -0.456,1.309 -0.077,1.343" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path72" d="m 376.098,269.065 0.077,1.343 0.457,1.309 1.098,1.13" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path74" d="m 377.73,272.847 1.709,0.727 1.977,0.302 2.029,0.051" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path76" d="m 457.882,318.176 v 10 10 10 10 10 10 10 8.996 l -0.375,1.491 -0.542,1.03" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path78" d="m 456.965,399.693 h -10 -10 -8.726 l -0.542,-1.03 -0.375,-1.491" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path80" d="m 539.678,303.466 -0.064,-0.035 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10.198 l -1.889,1.029 -1.304,1.491" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path82" d="m 543.74,411.917 v -2.465 -0.024 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -2.742 l -1.686,-1.925 -2.376,-1.295" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path84" d="m 427.612,316.452 0.42,-0.797 h 10 10 8.933 l 0.542,1.03 0.375,1.491" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path86" d="m 427.322,317.607 0.29,-1.155" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path88" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path90" d="m 370.692,127.899 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path92" d="m 376.098,141.272 v 10 7.954 0.005 10 10 10 10 10 10 10 10 10 10 9.834" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path94" d="m 405.991,409.044 v 3.057" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path96" d="m 406.223,305.951 v 0.662 0 0 0 10 10 10 10 10 10 10 10 10 10 2.333 0.098 2.873" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path98" d="m 427.322,397.172 v -10 -10 -10 -10 -10 -10 -10 -9.565" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path100" d="m 405.991,409.044 h 0.232" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path102" d="m 506.424,283.073 h -3.436 -10 -10 -10 -10 -6.548 -3.543 -10e-4 -10 -2.99" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path104" d="m 406.223,411.917 h 10 10 10 10 10 10 10 10 10 10 10 10 10 7.517" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path106" d="m 518.671,273.927 h 10 10 10 10 10 7.884" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path108" d="m 439.906,273.927 h 6.803 v 0 h 2.704 10 10 10 10 10 10.551 2.65 10e-4 6.056" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path110" d="m 539.678,303.466 h 10 10 10 10 4.417" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path112" d="m 587.169,156.53 v 0" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path114" d="m 543.74,409.452 10,0.029 10,0.03 10,0.03 9.999,0.029 3.43,0.01" />
<path style="fill:none;stroke:#87B97D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path116" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
<g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
<g id="strokes">
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path2" d="m 405.991,306.829 v 10 10 10 10 10 10 10 10 6.581 5.978 9.656 3.057" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path4" d="m 405.991,409.044 v 0 -9.656 -5.978 -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path6" d="m 370.692,127.899 v 1.329 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path8" d="m 439.906,283.073 v 0 h -10 -10 -10.303 -3.612 -10 -10 -10 -2.337 l -2.962,-2.901 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path10" d="m 373.654,283.073 -2.962,-2.901 v -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path12" d="m 370.692,127.899 h -7.383 -10 -2.899 -5.311 v 0 l -0.45,0.28 -0.37,0.387 -0.268,0.47 v 0.192 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 l 6.235,6.039 7.183,6.957 2.643,2.56 3.237,3.135 v 0 h 7.383 2.962 10 10 10 2.337 v 0 0 -3.057" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path14" d="m 584.095,303.466 3.074,3.363 v 10 10 10 10 10 10 10 10 6.581 6.183 9.988 10e-4 0.001 2.518 h 6.497 10 2.976 7.534 v 0 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path16" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581 -3.363 -5.906 -10 -4.487 -2.901 -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658 -2.701 -4.955 -10 -0.947 -10 -1.4 -0.061 l -0.299,-0.524 -0.394,-0.413 -0.019,-0.019 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path18" d="m 584.093,129.228 h -10 -10 -10 -10 l -10,-0.001 h -10 -5.476 -10 -10 -10 -10 -10 -10 l -10,-0.001 h -8.828 -10 -10 -10 -0.326 -3.615 l -10,0.001 h -10 l -10,0.001 h -2.205 -2.951 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path20" d="m 615.277,128.211 v 0 l -0.501,-0.312 v 0 0 h -8.134 -10 -2.976 -6.497 v 1.329" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path22" d="m 615.69,128.643 0.299,0.524 v 0.061 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 6.183 9.987 0.626 l -0.447,0.784 -0.617,0.644 -0.749,0.467" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path24" d="m 587.169,156.53 v 0 0 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 l -3.074,2.901 h -10 -10 -10 -10 -10 -10 -5.424" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path26" d="m 370.692,129.228 v -1.329 0 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path28" d="m 584.093,129.228 h 3.076" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path30" d="m 350.41,140.628 h 10 2.899 v 10 0.947 h -3.555 -9.344 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path32" d="m 587.169,129.228 v 10 1.4 10 0.947 4.955" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path34" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path36" d="m 593.666,151.575 v -10 -0.947 0 h 10 2.976" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path38" d="m 606.642,140.628 v 10 0.947 h -10 -2.976 v 0 -10 -0.947" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path40" d="m 615.69,128.643 -0.394,-0.413 -0.019,-0.019 v 0 l -0.501,-0.312" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path42" d="m 615.989,393.41 v 6.183 9.987 0.626 l -0.447,0.783 -0.576,0.603 -0.041,0.042 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path44" d="m 614.925,411.634 v 0 l -0.749,0.467" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path46" d="m 615.69,128.643 -0.009,-0.01 -0.404,-0.422 -0.501,-0.312" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path48" d="m 614.176,412.101 0.749,-0.467 v 0 l 0.041,-0.042 0.576,-0.603 0.447,-0.783 v -0.626 -9.987 -6.183" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path50" d="m 406.223,306.613 -0.232,0.216 v 0 l 0.232,-0.216 3.38,-3.147 h 10 10 10.303 10 10 10 10 10 10 10 8.765 10 10 1.007" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path52" d="m 518.671,283.073 v 0 h -10 -2.247" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path54" d="m 615.277,128.211 0.404,0.422" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path56" d="m 383.445,273.927 h 10 10 10 10 10 6.461" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path58" d="m 576.555,273.927 2.03,-0.051 1.978,-0.302 1.707,-0.727" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path60" d="m 582.27,272.847 1.099,-1.131 0.456,-1.309 0.077,-1.342" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path62" d="m 583.902,269.065 v -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -9.834 0 -10 -7.959" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path64" d="m 583.902,141.272 -0.077,-1.344 -0.457,-1.309 -1.098,-1.13" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path66" d="m 582.27,137.489 -1.709,-0.727 -1.977,-0.301 -2.029,-0.051" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path68" d="m 576.555,136.41 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -3.11 l -2.03,0.051 -1.978,0.302 -1.707,0.726" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path70" d="m 377.73,137.489 -1.099,1.131 -0.456,1.309 -0.077,1.343" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path72" d="m 376.098,269.065 0.077,1.343 0.457,1.309 1.098,1.13" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path74" d="m 377.73,272.847 1.709,0.727 1.977,0.302 2.029,0.051" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path76" d="m 457.882,318.176 v 10 10 10 10 10 10 10 8.996 l -0.375,1.491 -0.542,1.03" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path78" d="m 456.965,399.693 h -10 -10 -8.726 l -0.542,-1.03 -0.375,-1.491" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path80" d="m 539.678,303.466 -0.064,-0.035 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10.198 l -1.889,1.029 -1.304,1.491" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path82" d="m 543.74,411.917 v -2.465 -0.024 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -2.742 l -1.686,-1.925 -2.376,-1.295" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path84" d="m 427.612,316.452 0.42,-0.797 h 10 10 8.933 l 0.542,1.03 0.375,1.491" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path86" d="m 427.322,317.607 0.29,-1.155" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path88" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path90" d="m 370.692,127.899 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path92" d="m 376.098,141.272 v 10 7.954 0.005 10 10 10 10 10 10 10 10 10 10 9.834" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path94" d="m 405.991,409.044 v 3.057" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path96" d="m 406.223,305.951 v 0.662 0 0 0 10 10 10 10 10 10 10 10 10 10 2.333 0.098 2.873" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path98" d="m 427.322,397.172 v -10 -10 -10 -10 -10 -10 -10 -9.565" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path100" d="m 405.991,409.044 h 0.232" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path102" d="m 506.424,283.073 h -3.436 -10 -10 -10 -10 -6.548 -3.543 -10e-4 -10 -2.99" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path104" d="m 406.223,411.917 h 10 10 10 10 10 10 10 10 10 10 10 10 10 7.517" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path106" d="m 518.671,273.927 h 10 10 10 10 10 7.884" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path108" d="m 439.906,273.927 h 6.803 v 0 h 2.704 10 10 10 10 10 10.551 2.65 10e-4 6.056" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path110" d="m 539.678,303.466 h 10 10 10 10 4.417" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path112" d="m 587.169,156.53 v 0" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path114" d="m 543.74,409.452 10,0.029 10,0.03 10,0.03 9.999,0.029 3.43,0.01" />
<path style="fill:none;stroke:#A9713D;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path116" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
<g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
<g id="strokes">
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path2" d="m 405.991,306.829 v 10 10 10 10 10 10 10 10 6.581 5.978 9.656 3.057" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path4" d="m 405.991,409.044 v 0 -9.656 -5.978 -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path6" d="m 370.692,127.899 v 1.329 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path8" d="m 439.906,283.073 v 0 h -10 -10 -10.303 -3.612 -10 -10 -10 -2.337 l -2.962,-2.901 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path10" d="m 373.654,283.073 -2.962,-2.901 v -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path12" d="m 370.692,127.899 h -7.383 -10 -2.899 -5.311 v 0 l -0.45,0.28 -0.37,0.387 -0.268,0.47 v 0.192 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 l 6.235,6.039 7.183,6.957 2.643,2.56 3.237,3.135 v 0 h 7.383 2.962 10 10 10 2.337 v 0 0 -3.057" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path14" d="m 584.095,303.466 3.074,3.363 v 10 10 10 10 10 10 10 10 6.581 6.183 9.988 10e-4 0.001 2.518 h 6.497 10 2.976 7.534 v 0 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path16" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581 -3.363 -5.906 -10 -4.487 -2.901 -10 -10 -10 -10 -10 -10 -1.283 -10 -10 -10 -10 -10 -9.658 -2.701 -4.955 -10 -0.947 -10 -1.4 -0.061 l -0.299,-0.524 -0.394,-0.413 -0.019,-0.019 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path18" d="m 584.093,129.228 h -10 -10 -10 -10 l -10,-0.001 h -10 -5.476 -10 -10 -10 -10 -10 -10 l -10,-0.001 h -8.828 -10 -10 -10 -0.326 -3.615 l -10,0.001 h -10 l -10,0.001 h -2.205 -2.951 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path20" d="m 615.277,128.211 v 0 l -0.501,-0.312 v 0 0 h -8.134 -10 -2.976 -6.497 v 1.329" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path22" d="m 615.69,128.643 0.299,0.524 v 0.061 10 1.4 10 0.947 4.955 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 2.901 10 4.487 5.906 3.363 10 10 10 10 10 10 10 10 6.581 6.183 9.987 0.626 l -0.447,0.784 -0.617,0.644 -0.749,0.467" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path24" d="m 587.169,156.53 v 0 0 2.701 10 10 10 10 10 9.658 10 10 10 10 10 10 1.283 l -3.074,2.901 h -10 -10 -10 -10 -10 -10 -5.424" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path26" d="m 370.692,129.228 v -1.329 0 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path28" d="m 584.093,129.228 h 3.076" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path30" d="m 350.41,140.628 h 10 2.899 v 10 0.947 h -3.555 -9.344 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path32" d="m 587.169,129.228 v 10 1.4 10 0.947 4.955" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path34" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path36" d="m 593.666,151.575 v -10 -0.947 0 h 10 2.976" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path38" d="m 606.642,140.628 v 10 0.947 h -10 -2.976 v 0 -10 -0.947" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path40" d="m 615.69,128.643 -0.394,-0.413 -0.019,-0.019 v 0 l -0.501,-0.312" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path42" d="m 615.989,393.41 v 6.183 9.987 0.626 l -0.447,0.783 -0.576,0.603 -0.041,0.042 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path44" d="m 614.925,411.634 v 0 l -0.749,0.467" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path46" d="m 615.69,128.643 -0.009,-0.01 -0.404,-0.422 -0.501,-0.312" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path48" d="m 614.176,412.101 0.749,-0.467 v 0 l 0.041,-0.042 0.576,-0.603 0.447,-0.783 v -0.626 -9.987 -6.183" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path50" d="m 406.223,306.613 -0.232,0.216 v 0 l 0.232,-0.216 3.38,-3.147 h 10 10 10.303 10 10 10 10 10 10 10 8.765 10 10 1.007" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path52" d="m 518.671,283.073 v 0 h -10 -2.247" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path54" d="m 615.277,128.211 0.404,0.422" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path56" d="m 383.445,273.927 h 10 10 10 10 10 6.461" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path58" d="m 576.555,273.927 2.03,-0.051 1.978,-0.302 1.707,-0.727" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path60" d="m 582.27,272.847 1.099,-1.131 0.456,-1.309 0.077,-1.342" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path62" d="m 583.902,269.065 v -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -9.834 0 -10 -7.959" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path64" d="m 583.902,141.272 -0.077,-1.344 -0.457,-1.309 -1.098,-1.13" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path66" d="m 582.27,137.489 -1.709,-0.727 -1.977,-0.301 -2.029,-0.051" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path68" d="m 576.555,136.41 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -3.11 l -2.03,0.051 -1.978,0.302 -1.707,0.726" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path70" d="m 377.73,137.489 -1.099,1.131 -0.456,1.309 -0.077,1.343" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path72" d="m 376.098,269.065 0.077,1.343 0.457,1.309 1.098,1.13" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path74" d="m 377.73,272.847 1.709,0.727 1.977,0.302 2.029,0.051" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path76" d="m 457.882,318.176 v 10 10 10 10 10 10 10 8.996 l -0.375,1.491 -0.542,1.03" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path78" d="m 456.965,399.693 h -10 -10 -8.726 l -0.542,-1.03 -0.375,-1.491" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path80" d="m 539.678,303.466 -0.064,-0.035 h -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10.198 l -1.889,1.029 -1.304,1.491" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path82" d="m 543.74,411.917 v -2.465 -0.024 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -2.742 l -1.686,-1.925 -2.376,-1.295" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path84" d="m 427.612,316.452 0.42,-0.797 h 10 10 8.933 l 0.542,1.03 0.375,1.491" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path86" d="m 427.322,317.607 0.29,-1.155" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path88" d="m 350.41,151.575 v -10 -0.947" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path90" d="m 370.692,127.899 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path92" d="m 376.098,141.272 v 10 7.954 0.005 10 10 10 10 10 10 10 10 10 10 9.834" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path94" d="m 405.991,409.044 v 3.057" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path96" d="m 406.223,305.951 v 0.662 0 0 0 10 10 10 10 10 10 10 10 10 10 2.333 0.098 2.873" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path98" d="m 427.322,397.172 v -10 -10 -10 -10 -10 -10 -10 -9.565" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path100" d="m 405.991,409.044 h 0.232" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path102" d="m 506.424,283.073 h -3.436 -10 -10 -10 -10 -6.548 -3.543 -10e-4 -10 -2.99" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path104" d="m 406.223,411.917 h 10 10 10 10 10 10 10 10 10 10 10 10 10 7.517" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path106" d="m 518.671,273.927 h 10 10 10 10 10 7.884" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path108" d="m 439.906,273.927 h 6.803 v 0 h 2.704 10 10 10 10 10 10.551 2.65 10e-4 6.056" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path110" d="m 539.678,303.466 h 10 10 10 10 4.417" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path112" d="m 587.169,156.53 v 0" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path114" d="m 543.74,409.452 10,0.029 10,0.03 10,0.03 9.999,0.029 3.43,0.01" />
<path style="fill:none;stroke:#CB7590;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" id="path116" d="m 615.989,393.41 v -10 -10 -10 -10 -10 -10 -10 -10 -6.581" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

View File

@ -145,12 +145,14 @@ export function fetchAccountFail(id, error) {
export function followAccount(id, reblogs = true) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
dispatch(followAccountRequest(id));
const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(followAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error));
dispatch(followAccountFail(error, locked));
});
};
};
@ -167,10 +169,12 @@ export function unfollowAccount(id) {
};
};
export function followAccountRequest(id) {
export function followAccountRequest(id, locked) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
id,
locked,
skipLoading: true,
};
};
@ -179,13 +183,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
type: ACCOUNT_FOLLOW_SUCCESS,
relationship,
alreadyFollowing,
skipLoading: true,
};
};
export function followAccountFail(error) {
export function followAccountFail(error, locked) {
return {
type: ACCOUNT_FOLLOW_FAIL,
error,
locked,
skipLoading: true,
};
};
@ -193,6 +200,7 @@ export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
id,
skipLoading: true,
};
};
@ -201,6 +209,7 @@ export function unfollowAccountSuccess(relationship, statuses) {
type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
statuses,
skipLoading: true,
};
};
@ -208,6 +217,7 @@ export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
error,
skipLoading: true,
};
};

View File

@ -49,6 +49,8 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@ -178,6 +180,13 @@ export function submitComposeFail(error) {
};
};
export function doodleSet(options) {
return {
type: COMPOSE_DOODLE_SET,
options: options,
};
};
export function uploadCompose(files) {
return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) {

View File

@ -22,6 +22,7 @@ export default class IconButton extends React.PureComponent {
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
label: PropTypes.string,
};
static defaultProps = {
@ -42,14 +43,18 @@ export default class IconButton extends React.PureComponent {
}
render () {
const style = {
let style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
if (!this.props.label) {
style.width = `${this.props.size * 1.28571429}px`;
} else {
style.textAlign = 'left';
}
const {
active,
@ -104,7 +109,8 @@ export default class IconButton extends React.PureComponent {
style={style}
tabIndex={tabIndex}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
{this.props.label}
</button>
)}
</Motion>

View File

@ -60,6 +60,8 @@ const getUnitDelay = units => {
}
};
const fallbackFormat = new Intl.DateTimeFormat('en', shortDateFormatOptions);
export const timeAgoString = (intl, date, now, year) => {
const delta = now - date.getTime();

View File

@ -67,6 +67,7 @@ class Status extends ImmutablePureComponent {
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
};
// Avoid checking props that are functions (and whose equality will always
@ -168,7 +169,7 @@ class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured, otherAccounts, unread } = this.props;
const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
let { status, account, ...other } = this.props;
@ -309,6 +310,12 @@ class Status extends ImmutablePureComponent {
{media}
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
<button className='status__content__read-more-button' onClick={this.handleClick}>
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
</button>
)}
<StatusActionBar status={status} account={account} {...other} />
</div>
</div>

View File

@ -148,7 +148,6 @@ class StatusActionBar extends ImmutablePureComponent {
let menu = [];
let reblogIcon = 'retweet';
let replyIcon;
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
@ -191,10 +190,8 @@ class StatusActionBar extends ImmutablePureComponent {
}
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
@ -204,9 +201,9 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>

View File

@ -104,6 +104,7 @@ export default class StatusList extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType={timelineId}
showThread
/>
))
) : null;
@ -117,6 +118,7 @@ export default class StatusList extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType={timelineId}
showThread
/>
)).concat(scrollableContent);
}

View File

@ -0,0 +1,133 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
// Our imports //
import ComposeDropdown from './compose_dropdown';
import { uploadCompose } from '../../../actions/compose';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { openModal } from '../../../actions/modal';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
upload :
{ id: 'compose.attach.upload', defaultMessage: 'Upload a file' },
doodle :
{ id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
attach :
{ id: 'compose.attach', defaultMessage: 'Attach...' },
});
const mapStateToProps = state => ({
// This horrible expression is copied from vanilla upload_button_container
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
const mapDispatchToProps = dispatch => ({
onSelectFile (files) {
dispatch(uploadCompose(files));
},
onOpenDoodle () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
export default class ComposeAttachOptions extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes.object.isRequired,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
onOpenDoodle: PropTypes.func.isRequired,
};
handleItemClick = bt => {
if (bt === 'upload') {
this.fileElement.click();
}
if (bt === 'doodle') {
this.props.onOpenDoodle();
}
this.dropdown.setState({ open: false });
};
handleFileChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
setFileRef = (c) => {
this.fileElement = c;
}
setDropdownRef = (c) => {
this.dropdown = c;
}
render () {
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
const options = [
{ icon: 'cloud-upload', text: messages.upload, name: 'upload' },
{ icon: 'paint-brush', text: messages.doodle, name: 'doodle' },
];
const optionElems = options.map((item) => {
const hdl = () => this.handleItemClick(item.name);
return (
<div
role='button'
tabIndex='0'
key={item.name}
onClick={hdl}
className='privacy-dropdown__option'
>
<div className='privacy-dropdown__option__icon'>
<i className={`fa fa-fw fa-${item.icon}`} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{intl.formatMessage(item.text)}</strong>
</div>
</div>
);
});
return (
<div>
<ComposeDropdown
title={intl.formatMessage(messages.attach)}
icon='paperclip'
disabled={disabled}
ref={this.setDropdownRef}
>
{optionElems}
</ComposeDropdown>
<input
key={resetFileKey}
ref={this.setFileRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleFileChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</div>
);
}
}

View File

@ -0,0 +1,76 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
// Mastodon imports //
import IconButton from '../../../components/icon_button';
const iconStyle = {
height : null,
lineHeight : '27px',
};
export default class ComposeDropdown extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string,
highlight: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};
state = {
open: false,
};
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
};
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
onToggleDropdown = () => {
if (this.props.disabled) return;
this.setState({ open: !this.state.open });
};
setRef = (c) => {
this.node = c;
};
render () {
const { open } = this.state;
let { highlight, title, icon, disabled } = this.props;
if (!icon) icon = 'ellipsis-h';
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className={'inverted'}
title={title}
icon={icon} active={open || highlight}
size={18}
style={iconStyle}
disabled={disabled}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{this.props.children}
</div>
</div>
);
}
}

View File

@ -5,7 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl } from 'react-intl';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
@ -17,6 +16,7 @@ import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import ComposeAttachOptions from './attach_options';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -84,7 +84,7 @@ class ComposeForm extends ImmutablePureComponent {
const { is_submitting, is_uploading, anyMedia } = this.props;
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
if (is_submitting || is_uploading || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
if (is_submitting || is_uploading || length(fulltext) > 512 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return;
}
@ -160,7 +160,7 @@ class ComposeForm extends ImmutablePureComponent {
const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
const disabledButton = disabled || this.props.is_uploading || length(text) > 512 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@ -207,12 +207,13 @@ class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PrivacyDropdownContainer />
<ComposeAttachOptions />
<SensitiveButtonContainer />
<div className='compose-form__buttons-separator' />
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
</div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='character-counter__wrapper'><CharacterCounter max={512} text={text} /></div>
</div>
<div className='compose-form__publish'>

View File

@ -357,8 +357,8 @@ class EmojiPickerDropdown extends React.PureComponent {
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
<img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={`${assetHost}/emoji/1f602.svg`}
alt='🤔'
src={`${assetHost}/emoji/1f914.svg`}
/>
</div>

View File

@ -12,13 +12,13 @@ const DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'vhs',
'laughing',
'stuck_out_tongue_winking_eye',
'floppy_disk',
'sweat_smile',
'joy',
'yum',
'disappointed',
'computer',
'thinking_face',
'weary',
'sob',

View File

@ -14,20 +14,20 @@ import { Link } from 'react-router-dom';
import NavigationBar from '../compose/components/navigation_bar';
const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
home_timeline: { id: 'tabs_bar.home', defaultMessage: '/timelines/home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: '~/.notifications' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: '/timelines/federated' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: '/timelines/local' },
direct: { id: 'navigation_bar.direct', defaultMessage: '~/.dms' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'edit ~/.config' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: '~/.follow-requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: '~/.florps' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: '~/.blocked' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: '~/.muted/domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: '~/.muted' },
pins: { id: 'navigation_bar.pins', defaultMessage: '~/.pinned' },
lists: { id: 'navigation_bar.lists', defaultMessage: '~/.lists' },
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
@ -136,14 +136,10 @@ class GettingStarted extends ImmutablePureComponent {
<div className='getting-started__footer'>
<ul>
<li><a href='https://bridge.joinmastodon.org/' target='_blank'><FormattedMessage id='getting_started.find_friends' defaultMessage='Find friends from Twitter' /></a> · </li>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
@ -152,7 +148,7 @@ class GettingStarted extends ImmutablePureComponent {
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> (v{version})</span> }}
values={{ github: <a href='https://cybre.tech/cybrespace/mastodon' rel='noopener' target='_blank'>cybrespace/mastodon</a> }}
/>
</p>
</div>

View File

@ -117,7 +117,7 @@ class Notification extends ImmutablePureComponent {
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-star star-icon' />
<i className='fa fa-fw fa-floppy-o star-icon' />
</div>
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
</div>

View File

@ -159,9 +159,9 @@ class ActionBar extends React.PureComponent {
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} /></div>
{shareButton}
<div className='detailed-status__action-bar-dropdown'>

View File

@ -73,7 +73,7 @@ export default class Card extends React.PureComponent {
};
componentWillReceiveProps (nextProps) {
if (this.props.card !== nextProps.card) {
if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false });
}
}

View File

@ -119,7 +119,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<i className='fa fa-star' />
<i className='fa fa-floppy-o' />
<span className='detailed-status__favorites'>
<FormattedNumber value={status.get('favourites_count')} />
</span>

View File

@ -0,0 +1,614 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../../../components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Atrament from 'atrament'; // the doodling library
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { doodleSet, uploadCompose } from '../../../actions/compose';
import IconButton from '../../../components/icon_button';
import { debounce, mapValues } from 'lodash';
import classNames from 'classnames';
// palette nicked from MyPaint, CC0
const palette = [
['rgb( 0, 0, 0)', 'Black'],
['rgb( 38, 38, 38)', 'Gray 15'],
['rgb( 77, 77, 77)', 'Grey 30'],
['rgb(128, 128, 128)', 'Grey 50'],
['rgb(171, 171, 171)', 'Grey 67'],
['rgb(217, 217, 217)', 'Grey 85'],
['rgb(255, 255, 255)', 'White'],
['rgb(128, 0, 0)', 'Maroon'],
['rgb(209, 0, 0)', 'English-red'],
['rgb(255, 54, 34)', 'Tomato'],
['rgb(252, 60, 3)', 'Orange-red'],
['rgb(255, 140, 105)', 'Salmon'],
['rgb(252, 232, 32)', 'Cadium-yellow'],
['rgb(243, 253, 37)', 'Lemon yellow'],
['rgb(121, 5, 35)', 'Dark crimson'],
['rgb(169, 32, 62)', 'Deep carmine'],
['rgb(255, 140, 0)', 'Orange'],
['rgb(255, 168, 18)', 'Dark tangerine'],
['rgb(217, 144, 88)', 'Persian orange'],
['rgb(194, 178, 128)', 'Sand'],
['rgb(255, 229, 180)', 'Peach'],
['rgb(100, 54, 46)', 'Bole'],
['rgb(108, 41, 52)', 'Dark cordovan'],
['rgb(163, 65, 44)', 'Chestnut'],
['rgb(228, 136, 100)', 'Dark salmon'],
['rgb(255, 195, 143)', 'Apricot'],
['rgb(255, 219, 188)', 'Unbleached silk'],
['rgb(242, 227, 198)', 'Straw'],
['rgb( 53, 19, 13)', 'Bistre'],
['rgb( 84, 42, 14)', 'Dark chocolate'],
['rgb(102, 51, 43)', 'Burnt sienna'],
['rgb(184, 66, 0)', 'Sienna'],
['rgb(216, 153, 12)', 'Yellow ochre'],
['rgb(210, 180, 140)', 'Tan'],
['rgb(232, 204, 144)', 'Dark wheat'],
['rgb( 0, 49, 83)', 'Prussian blue'],
['rgb( 48, 69, 119)', 'Dark grey blue'],
['rgb( 0, 71, 171)', 'Cobalt blue'],
['rgb( 31, 117, 254)', 'Blue'],
['rgb(120, 180, 255)', 'Bright french blue'],
['rgb(171, 200, 255)', 'Bright steel blue'],
['rgb(208, 231, 255)', 'Ice blue'],
['rgb( 30, 51, 58)', 'Medium jungle green'],
['rgb( 47, 79, 79)', 'Dark slate grey'],
['rgb( 74, 104, 93)', 'Dark grullo green'],
['rgb( 0, 128, 128)', 'Teal'],
['rgb( 67, 170, 176)', 'Turquoise'],
['rgb(109, 174, 199)', 'Cerulean frost'],
['rgb(173, 217, 186)', 'Tiffany green'],
['rgb( 22, 34, 29)', 'Gray-asparagus'],
['rgb( 36, 48, 45)', 'Medium dark teal'],
['rgb( 74, 104, 93)', 'Xanadu'],
['rgb(119, 198, 121)', 'Mint'],
['rgb(175, 205, 182)', 'Timberwolf'],
['rgb(185, 245, 246)', 'Celeste'],
['rgb(193, 255, 234)', 'Aquamarine'],
['rgb( 29, 52, 35)', 'Cal Poly Pomona'],
['rgb( 1, 68, 33)', 'Forest green'],
['rgb( 42, 128, 0)', 'Napier green'],
['rgb(128, 128, 0)', 'Olive'],
['rgb( 65, 156, 105)', 'Sea green'],
['rgb(189, 246, 29)', 'Green-yellow'],
['rgb(231, 244, 134)', 'Bright chartreuse'],
['rgb(138, 23, 137)', 'Purple'],
['rgb( 78, 39, 138)', 'Violet'],
['rgb(193, 75, 110)', 'Dark thulian pink'],
['rgb(222, 49, 99)', 'Cerise'],
['rgb(255, 20, 147)', 'Deep pink'],
['rgb(255, 102, 204)', 'Rose pink'],
['rgb(255, 203, 219)', 'Pink'],
['rgb(255, 255, 255)', 'White'],
['rgb(229, 17, 1)', 'RGB Red'],
['rgb( 0, 255, 0)', 'RGB Green'],
['rgb( 0, 0, 255)', 'RGB Blue'],
['rgb( 0, 255, 255)', 'CMYK Cyan'],
['rgb(255, 0, 255)', 'CMYK Magenta'],
['rgb(255, 255, 0)', 'CMYK Yellow'],
];
// re-arrange to the right order for display
let palReordered = [];
for (let row = 0; row < 7; row++) {
for (let col = 0; col < 11; col++) {
palReordered.push(palette[col * 7 + row]);
}
palReordered.push(null); // null indicates a <br />
}
// Utility for converting base64 image to binary for upload
// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f
function dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
const DOODLE_SIZES = {
normal: [500, 500, 'Square 500'],
tootbanner: [702, 330, 'Tootbanner'],
s640x480: [640, 480, '640×480 - 480p'],
s800x600: [800, 600, '800×600 - SVGA'],
s720x480: [720, 405, '720x405 - 16:9'],
};
const mapStateToProps = state => ({
options: state.getIn(['compose', 'doodle']),
});
const mapDispatchToProps = dispatch => ({
/** Set options in the redux store */
setOpt: (opts) => dispatch(doodleSet(opts)),
/** Submit doodle for upload */
submit: (file) => dispatch(uploadCompose([file])),
});
/**
* Doodling dialog with drawing canvas
*
* Keyboard shortcuts:
* - Delete: Clear screen, fill with background color
* - Backspace, Ctrl+Z: Undo one step
* - Ctrl held while drawing: Use background color
* - Shift held while clicking screen: Use fill tool
*
* Palette:
* - Left mouse button: pick foreground
* - Ctrl + left mouse button: pick background
* - Right mouse button: pick background
*/
@connect(mapStateToProps, mapDispatchToProps)
export default class DoodleModal extends ImmutablePureComponent {
static propTypes = {
options: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired,
setOpt: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
};
//region Option getters/setters
/** Foreground color */
get fg () {
return this.props.options.get('fg');
}
set fg (value) {
this.props.setOpt({ fg: value });
}
/** Background color */
get bg () {
return this.props.options.get('bg');
}
set bg (value) {
this.props.setOpt({ bg: value });
}
/** Swap Fg and Bg for drawing */
get swapped () {
return this.props.options.get('swapped');
}
set swapped (value) {
this.props.setOpt({ swapped: value });
}
/** Mode - 'draw' or 'fill' */
get mode () {
return this.props.options.get('mode');
}
set mode (value) {
this.props.setOpt({ mode: value });
}
/** Base line weight */
get weight () {
return this.props.options.get('weight');
}
set weight (value) {
this.props.setOpt({ weight: value });
}
/** Drawing opacity */
get opacity () {
return this.props.options.get('opacity');
}
set opacity (value) {
this.props.setOpt({ opacity: value });
}
/** Adaptive stroke - change width with speed */
get adaptiveStroke () {
return this.props.options.get('adaptiveStroke');
}
set adaptiveStroke (value) {
this.props.setOpt({ adaptiveStroke: value });
}
/** Smoothing (for mouse drawing) */
get smoothing () {
return this.props.options.get('smoothing');
}
set smoothing (value) {
this.props.setOpt({ smoothing: value });
}
/** Size preset */
get size () {
return this.props.options.get('size');
}
set size (value) {
this.props.setOpt({ size: value });
}
//endregion
/** Key up handler */
handleKeyUp = (e) => {
if (e.target.nodeName === 'INPUT') return;
if (e.key === 'Delete') {
e.preventDefault();
this.handleClearBtn();
return;
}
if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) {
e.preventDefault();
this.undo();
}
if (e.key === 'Control' || e.key === 'Meta') {
this.controlHeld = false;
this.swapped = false;
}
if (e.key === 'Shift') {
this.shiftHeld = false;
this.mode = 'draw';
}
};
/** Key down handler */
handleKeyDown = (e) => {
if (e.key === 'Control' || e.key === 'Meta') {
this.controlHeld = true;
this.swapped = true;
}
if (e.key === 'Shift') {
this.shiftHeld = true;
this.mode = 'fill';
}
};
/**
* Component installed in the DOM, do some initial set-up
*/
componentDidMount () {
this.controlHeld = false;
this.shiftHeld = false;
this.swapped = false;
window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false);
};
/**
* Tear component down
*/
componentWillUnmount () {
window.removeEventListener('keyup', this.handleKeyUp, false);
window.removeEventListener('keydown', this.handleKeyDown, false);
if (this.sketcher) this.sketcher.destroy();
}
/**
* Set reference to the canvas element.
* This is called during component init
*
* @param elem - canvas element
*/
setCanvasRef = (elem) => {
this.canvas = elem;
if (elem) {
elem.addEventListener('dirty', () => {
this.saveUndo();
this.sketcher._dirty = false;
});
elem.addEventListener('click', () => {
// sketcher bug - does not fire dirty on fill
if (this.mode === 'fill') {
this.saveUndo();
}
});
// prevent context menu
elem.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
elem.addEventListener('mousedown', (e) => {
if (e.button === 2) {
this.swapped = true;
}
});
elem.addEventListener('mouseup', (e) => {
if (e.button === 2) {
this.swapped = this.controlHeld;
}
});
this.initSketcher(elem);
this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
}
};
/**
* Set up the sketcher instance
*
* @param canvas - canvas element. Null if we're just resizing
*/
initSketcher (canvas = null) {
const sizepreset = DOODLE_SIZES[this.size];
if (this.sketcher) this.sketcher.destroy();
this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]);
if (canvas) {
this.ctx = this.sketcher.context;
this.updateSketcherSettings();
}
this.clearScreen();
}
/**
* Done button handler
*/
onDoneButton = () => {
const dataUrl = this.sketcher.toImage();
const file = dataURLtoFile(dataUrl, 'doodle.png');
this.props.submit(file);
this.props.onClose(); // close dialog
};
/**
* Cancel button handler
*/
onCancelButton = () => {
if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) {
return;
}
this.props.onClose(); // close dialog
};
/**
* Update sketcher options based on state
*/
updateSketcherSettings () {
if (!this.sketcher) return;
if (this.oldSize !== this.size) this.initSketcher();
this.sketcher.color = (this.swapped ? this.bg : this.fg);
this.sketcher.opacity = this.opacity;
this.sketcher.weight = this.weight;
this.sketcher.mode = this.mode;
this.sketcher.smoothing = this.smoothing;
this.sketcher.adaptiveStroke = this.adaptiveStroke;
this.oldSize = this.size;
}
/**
* Fill screen with background color
*/
clearScreen = () => {
this.ctx.fillStyle = this.bg;
this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2);
this.undos = [];
this.doSaveUndo();
};
/**
* Undo one step
*/
undo = () => {
if (this.undos.length > 1) {
this.undos.pop();
const buf = this.undos.pop();
this.sketcher.clear();
this.ctx.putImageData(buf, 0, 0);
this.doSaveUndo();
}
};
/**
* Save canvas content into the undo buffer immediately
*/
doSaveUndo = () => {
this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height));
};
/**
* Called on each canvas change.
* Saves canvas content to the undo buffer after some period of inactivity.
*/
saveUndo = debounce(() => {
this.doSaveUndo();
}, 100);
/**
* Palette left click.
* Selects Fg color (or Bg, if Control/Meta is held)
*
* @param e - event
*/
onPaletteClick = (e) => {
const c = e.target.dataset.color;
if (this.controlHeld) {
this.bg = c;
} else {
this.fg = c;
}
e.target.blur();
e.preventDefault();
};
/**
* Palette right click.
* Selects Bg color
*
* @param e - event
*/
onPaletteRClick = (e) => {
this.bg = e.target.dataset.color;
e.target.blur();
e.preventDefault();
};
/**
* Handle click on the Draw mode button
*
* @param e - event
*/
setModeDraw = (e) => {
this.mode = 'draw';
e.target.blur();
};
/**
* Handle click on the Fill mode button
*
* @param e - event
*/
setModeFill = (e) => {
this.mode = 'fill';
e.target.blur();
};
/**
* Handle click on Smooth checkbox
*
* @param e - event
*/
tglSmooth = (e) => {
this.smoothing = !this.smoothing;
e.target.blur();
};
/**
* Handle click on Adaptive checkbox
*
* @param e - event
*/
tglAdaptive = (e) => {
this.adaptiveStroke = !this.adaptiveStroke;
e.target.blur();
};
/**
* Handle change of the Weight input field
*
* @param e - event
*/
setWeight = (e) => {
this.weight = +e.target.value || 1;
};
/**
* Set size - clalback from the select box
*
* @param e - event
*/
changeSize = (e) => {
let newSize = e.target.value;
if (newSize === this.oldSize) return;
if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) {
return;
}
this.size = newSize;
};
handleClearBtn = () => {
if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) {
return;
}
this.clearScreen();
};
/**
* Render the component
*/
render () {
this.updateSketcherSettings();
return (
<div className='modal-root__modal doodle-modal'>
<div className='doodle-modal__container'>
<canvas ref={this.setCanvasRef} />
</div>
<div className='doodle-modal__action-bar'>
<div className='doodle-toolbar'>
<Button text='Done' onClick={this.onDoneButton} />
<Button text='Cancel' onClick={this.onCancelButton} />
</div>
<div className='filler' />
<div className='doodle-toolbar with-inputs'>
<div>
<label htmlFor='dd_smoothing'>Smoothing</label>
<span className='val'>
<input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} />
</span>
</div>
<div>
<label htmlFor='dd_adaptive'>Adaptive</label>
<span className='val'>
<input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} />
</span>
</div>
<div>
<label htmlFor='dd_weight'>Weight</label>
<span className='val'>
<input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
</span>
</div>
<div>
<select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}>
{ Object.values(mapValues(DOODLE_SIZES, (val, k) =>
<option key={k} value={k}>{val[2]}</option>
)) }
</select>
</div>
</div>
<div className='doodle-toolbar'>
<IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
<IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
<IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
<IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
</div>
<div className='doodle-palette'>
{
palReordered.map((c, i) =>
c === null ?
<br key={i} /> :
<button
key={i}
style={{ backgroundColor: c[0] }}
onClick={this.onPaletteClick}
onContextMenu={this.onPaletteRClick}
data-color={c[0]}
title={c[1]}
className={classNames({
'foreground': this.fg === c[0],
'background': this.bg === c[0],
})}
/>
)
}
</div>
</div>
</div>
);
}
}

View File

@ -8,6 +8,7 @@ import ActionsModal from './actions_modal';
import MediaModal from './media_modal';
import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import DoodleModal from './doodle_modal';
import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal';
import {
@ -23,6 +24,7 @@ const MODAL_COMPONENTS = {
'ONBOARDING': OnboardingModal,
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'DOODLE': () => Promise.resolve({ default: DoodleModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal,
'REPORT': ReportModal,
@ -43,6 +45,16 @@ export default class ModalRoot extends React.PureComponent {
getSnapshotBeforeUpdate () {
return { visible: !!this.props.type };
}
state = {
revealed: false,
};
handleKeyUp = (e) => {
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
&& !!this.props.type && !this.props.props.noEsc) {
this.props.onClose();
}
}
componentDidUpdate (prevProps, prevState, { visible }) {
if (visible) {
@ -53,7 +65,7 @@ export default class ModalRoot extends React.PureComponent {
}
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
}
renderError = (props) => {

View File

@ -0,0 +1,293 @@
{
"account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.disclaimer_full": "THESE NUMBERS ARE THE STUFF WHAT YOUR SERVER KNOWS ABOUT AND THERE MIGHT BE MORE THAT IT DONT KNOW ABOUT.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "edit ~/.profile",
"account.follow": "Follow",
"account.followers": "Followers",
"account.follows": "Follows",
"account.follows_you": "Follows you",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Pings",
"account.posts_with_replies": "Pings with replies",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval. Click to cancel follow request",
"account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"column.blocks": "~/.blocked",
"column.community": "/timelines/local",
"column.direct": "~/.dms",
"column.favourites": "~/.florps",
"column.follow_requests": "~/.follow-requests",
"column.home": "/timelines/home",
"column.lists": "Lists",
"column.mutes": "~/.muted",
"column.notifications": "~/.notifications",
"column.pins": "~/.pinned",
"column.public": "/timelines/federated",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
"compose.attach": "Attach...",
"compose.attach.doodle": "Draw something",
"compose.attach.upload": "Upload a file",
"compose_form.hashtag_warning": "This ping won't be listed under any hashtag as it is unlisted. Only public pings can be searched by hashtag.",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "What is in your databanks?",
"compose_form.publish": "Ping",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"doodle_button.label": "Add a drawing",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use query to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.heading": "Getting started",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advanced",
"home.column_settings.basic": "Basic",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_reblogs": "Show relays",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.boost": "to boost",
"keyboard_shortcuts.column": "to focus a status in one of the columns",
"keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.down": "to move down in the list",
"keyboard_shortcuts.enter": "to open status",
"keyboard_shortcuts.favourite": "to favourite",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.toot": "to start a brand new ping",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "~/.blocks",
"navigation_bar.community_timeline": "/timelines/local",
"navigation_bar.direct": "~/.dms",
"navigation_bar.edit_profile": "edit ~/.profile",
"navigation_bar.favourites": "~/.florps",
"navigation_bar.follow_requests": "~/.follow-requests",
"navigation_bar.info": "/about/more",
"navigation_bar.keyboard_shortcuts": "~/.kbd/shortcuts.conf",
"navigation_bar.lists": "~/.lists",
"navigation_bar.logout": "Jack out",
"navigation_bar.mutes": "~/.muted",
"navigation_bar.pins": "~/.pinned",
"navigation_bar.preferences": "edit ~/.config",
"navigation_bar.public_timeline": "/timelines/federated",
"notification.favourite": "{name} florped your ping",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
"notification.reblog": "{name} relayed your ping",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
"onboarding.done": "Done",
"onboarding.next": "Next",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.full_handle": "Your full handle",
"onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Hang ten on the cybrewaves!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not post to public timelines",
"privacy.unlisted.short": "Unlisted",
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancel",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting {target}",
"search.placeholder": "Query...",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Pings",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "Peer into the data grid...",
"status.block": "Block @{name}",
"status.cannot_reblog": "This ping cannot be relayed",
"status.delete": "Delete",
"status.embed": "Embed",
"status.favourite": "Florp",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned ping",
"status.reblog": "Relay",
"status.reblogged_by": "{name} relayed",
"status.reply": "Reply",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.federated_timeline": "/timelines/federated",
"tabs_bar.home": "/timelines/home",
"tabs_bar.local_timeline": "/timelines/local",
"tabs_bar.notifications": "~/.notifications",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media",
"upload_form.description": "Describe for the visually impaired",
"upload_form.focus": "Crop",
"upload_form.undo": "Undo",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",
"video_player.video_error": "Video could not be played"
}

View File

@ -0,0 +1,2 @@
[
]

View File

@ -28,6 +28,7 @@ import {
COMPOSE_UPLOAD_CHANGE_REQUEST,
COMPOSE_UPLOAD_CHANGE_SUCCESS,
COMPOSE_UPLOAD_CHANGE_FAIL,
COMPOSE_DOODLE_SET,
COMPOSE_RESET,
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
@ -61,6 +62,17 @@ const initialState = ImmutableMap({
resetFileKey: Math.floor((Math.random() * 0x10000)),
idempotencyKey: null,
tagHistory: ImmutableList(),
doodle: ImmutableMap({
fg: 'rgb( 0, 0, 0)',
bg: 'rgb(255, 255, 255)',
swapped: false,
mode: 'draw',
size: 'normal',
weight: 2,
opacity: 1,
adaptiveStroke: true,
smoothing: false,
}),
});
function statusToTextMentions(state, status) {
@ -326,6 +338,8 @@ export default function compose(state = initialState, action) {
map.set('spoiler_text', '');
}
});
case COMPOSE_DOODLE_SET:
return state.mergeIn(['doodle'], action.options);
default:
return state;
}

View File

@ -56,7 +56,13 @@ const expandNormalizedConversations = (state, conversations, next) => {
list = list.concat(items);
return list.sortBy(x => x.get('last_status'), (a, b) => compareId(a, b) * -1);
return list.sortBy(x => x.get('last_status'), (a, b) => {
if(a === null || b === null) {
return -1;
}
return compareId(a, b) * -1;
});
});
}

View File

@ -1,6 +1,10 @@
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_FOLLOW_REQUEST,
ACCOUNT_FOLLOW_FAIL,
ACCOUNT_UNFOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_REQUEST,
ACCOUNT_UNFOLLOW_FAIL,
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_UNBLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
@ -37,6 +41,14 @@ const initialState = ImmutableMap();
export default function relationships(state = initialState, action) {
switch(action.type) {
case ACCOUNT_FOLLOW_REQUEST:
return state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
case ACCOUNT_FOLLOW_FAIL:
return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
case ACCOUNT_UNFOLLOW_REQUEST:
return state.setIn([action.id, 'following'], false);
case ACCOUNT_UNFOLLOW_FAIL:
return state.setIn([action.id, 'following'], true);
case ACCOUNT_FOLLOW_SUCCESS:
case ACCOUNT_UNFOLLOW_SUCCESS:
case ACCOUNT_BLOCK_SUCCESS:

View File

@ -38,11 +38,11 @@ export default function statuses(state = initialState, action) {
case FAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], true);
case FAVOURITE_FAIL:
return state.setIn([action.status.get('id'), 'favourited'], false);
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case REBLOG_REQUEST:
return state.setIn([action.status.get('id'), 'reblogged'], true);
case REBLOG_FAIL:
return state.setIn([action.status.get('id'), 'reblogged'], false);
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
case STATUS_MUTE_SUCCESS:
return state.setIn([action.id, 'muted'], true);
case STATUS_UNMUTE_SUCCESS:

View File

@ -8,3 +8,5 @@ loadPolyfills().then(() => {
}).catch(e => {
console.error(e);
});
require('what-input');

View File

@ -1,4 +1,4 @@
import loadPolyfills from '../mastodon/load_polyfills';
import loadPolyfills from '../mastodon/load_polyfills';
import ready from '../mastodon/ready';
import { start } from '../mastodon/common';
@ -17,6 +17,12 @@ window.addEventListener('message', e => {
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(sizeBioText);
} else {
sizeBioText();
}
});
});
@ -93,6 +99,17 @@ function main() {
detailedStatuses[0].scrollIntoView();
history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
}
[].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
const props = JSON.parse(content.getAttribute('data-props'));
ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
});
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(sizeBioText);
} else {
sizeBioText();
}
});
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@ -188,6 +205,22 @@ function main() {
console.error(err);
}
});
delegate(document, '#account_note', 'input', sizeBioText);
function sizeBioText() {
const noteCounter = document.querySelector('.note-counter');
const bioTextArea = document.querySelector('#account_note');
if (noteCounter) {
noteCounter.textContent = 413 - length(bioTextArea.value);
}
if (bioTextArea) {
bioTextArea.style.height = 'auto';
bioTextArea.style.height = (bioTextArea.scrollHeight+3) + 'px';
}
}
}
loadPolyfills().then(main).catch(error => {

View File

@ -0,0 +1,72 @@
@import 'application';
/* Allow columns to grow wider as the screen gets
* wider, but don't ever let them get more than
* 400px (some people have a bunch of columns!) */
@media screen and (min-width: 1300px) {
.column {
flex-grow: 1 !important;
max-width: 400px;
}
.drawer {
width: 17%; /* Not part of the flex fun */
max-width: 400px;
min-width: 330px;
}
}
/* Cap the column height at 100vh (fixed an old
* bug someone encountered in safari, but which
* I've seen resurface from time to time) */
.column {
max-height:100vh;
}
/* Don't show outline around statuses if we're in
* mouse or touch mode (rather than keyboard) */
[data-whatinput="mouse"], [data-whatinput="touch"] {
.status__content:focus, .status:focus,
.status__wrapper:focus, .status__content__text:focus {
outline: none;
}
}
/* Show a little arrowey thing after the time in a
* status to signal that you can click it to see
* a detailed view */
.status time:after,
.detailed-status__datetime span:after {
font: normal normal normal 14px/1 FontAwesome;
content: "\00a0\00a0\f08e";
}
/* Don't display the elephant mascot (we have our
* own, thanks) */
.drawer__inner__mastodon {
display: none;
}
/* Let the compose area/drawer be short, but
* expand if necessary */
.drawer .drawer__inner {
overflow: visible;
height:inherit;
background-image: none;
}
.drawer__pager {
overflow-y:auto;
}
/* Use display: none instead of visibility:hidden
* to hide the suggested follows list on non-mobile */
@media screen and (min-width: 630px) {
.search-results .trends {
display:none;
}
}
@import 'fullwidth-media';

View File

@ -0,0 +1,932 @@
$success-green: #B64579; // Padua
$ui-base-color: #f7e8ed; // "darkest"
$ui-base-alt: #f9f2f5;
$ui-base-lighter-color: darken($ui-base-color, 40%); // Lighter darkest
$ui-secondary-color: #ead0d6; // "lightest"
$ui-primary-color: #bf5677; // "lighter"
$ui-highlight-color: #bf5677; // "vibrant"
$primary-text-color: #382b32;
$secondary-text-color: #382b32;
$header-color: $ui-primary-color;
$header-text-color: #fff;
$icon-button-inactive-color: lighten(desaturate($ui-base-lighter-color, 20%), 20%);
$action-button-color: $icon-button-inactive-color;
$about-page-text: $primary-text-color;
@import 'cybre-base';
$gold-star: #dd9d08;
/* cybre-specific additions */
.column .static-content.getting-started {
background-image: url('../images/logo-cybre-light.png');
background-size:auto 50%;
background-position: 50% 75%;
background-repeat:no-repeat;
}
.ui, body {
background: $ui-base-color url('../images/background-cybre-light.png');
}
.drawer__inner__mastodon {
display: none;
}
.landing-page .header-wrapper {
background-image:url('../images/header-cybre-alt.jpg');
background-size:cover;
background-position:50% 50%;
}
.landing-page.alternative .header {
background-image:url('../images/header-cybre-colour.jpg');
background-repeat: repeat-x;
background-size:contain;
height:45vh;
width: 100%;
position:absolute;
z-index: 1;
text-align:center;
display: unset!important;
}
.landing-page.alternative .header img {
margin: auto;
max-height:45vh;
}
.landing-page.alternative .grid {
position: relative;
z-index:2;
margin-top:15vh;
}
.landing-page.alternative .landing-page__hero img {
visibility: hidden;
max-height:170px;
}
.landing-page.alternative .landing-page__forms {
height:auto;
}
.landing-page.alternative .column-1 {
display:flex;
align-items:flex-end;
}
.landing-page.alternative .column {
max-height:initial;
}
.landing-page.alternative .row__mascot {
.floats {
position:absolute;
img {
width:100%;
height:100%;
}
transition: all 0.1s linear;
animation-name: floating;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}
.float-1 {
width:50px;
height:50px;
bottom:60px;
left:110px;
animation-duration: 3s;
}
.float-2 {
width:130px;
height:130px;
left:85px;
bottom: -60px;
animation-duration: 3.5s;
animation-delay: 0.2s;
}
.float-3 {
width:100px;
height:100px;
right: 50;
top: -10px;
animation-duration: 4s;
animation-delay: 0.5s;
}
}
/* about.scss */
.landing-page {
h1 {
color: $about-page-text;
small {
color: lighten($about-page-text, 10%);
}
}
p, li {
color: $about-page-text;
}
.header-wrapper {
padding-top:0px;
background-size:cover;
background-position:50% 55%;
}
.header-wrapper {
.mascot {
width:500px;
bottom:-52px;
left:-120px;
}
}
.container.links {
background-color: $ui-base-color;
border-top: 5px solid $ui-primary-color;
width:100%;
max-width:100%;
padding:0px calc(50% - 400px);
a {
&:hover {
color: lighten($ui-primary-color, 10%);
}
}
}
.container.hero {
.floats {
display:none;
}
.closed-registrations-message, form {
border-top: 50px solid #5f4770;
-webkit-box-shadow: 0 0 6px rgba(0,0,0,.1);
box-shadow: 0 0 6px rgba(0,0,0,.1);
&:before {
font-size: 16px;
font-family:inherit;
line-height:inherit;
font-weight:normal;
color:white;
position:absolute;
top:-35px;
}
}
.closed-registrations-message:before {
content: "Registrations closed";
}
form:before {
content: "Register now";
}
}
#mastodon-timeline {
.column-header {
color:white;
}
}
}
.features-list__row {
.text {
color: $about-page-text;
}
}
.information-board {
.panel {
.panel-header {
color: $primary-text-color;
border-bottom: 1px solid lighten($ui-secondary-color, 4%);
a,
span {
font-weight: 400;
color: lighten($ui-primary-color, 4%);
}
}
}
}
/* components.scss */
.onboarding-modal__page {
p {
color: $primary-text-color;
}
}
.column-header {
background: $header-color;
color: $header-text-color;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
box-shadow: 0px 0px 3px rgba(0,0,0,0.3);
}
.column-header__button {
background: $header-color;
color: $header-text-color;
border-top-right-radius: 3px;
&:hover {
color: darken($ui-base-color, 10%);
}
&.active {
color: $primary-text-color;
background: darken($ui-base-color, 5%);
&:hover {
background: darken($ui-base-color, 5%);
}
}
}
.status-card, .status-card.compact {
border-color: $ui-highlight-color;
}
// selectivity -- needs to override .column-header > button
.column-header .column-header__back-button {
background: $header-color;
color:$header-text-color;
}
.column-back-button {
background: $header-color;
color:$header-text-color;
}
.column-header__collapsible-inner {
background: darken($ui-base-alt, 2%);
}
.empty-column-indicator,
.error-column {
color: darken($ui-base-lighter-color, 15%);
}
.compose-form {
.autosuggest-textarea__textarea,
.spoiler-input__input {
color: $primary-text-color;
border: 1px solid $ui-primary-color;
}
.autosuggest-textarea__textarea {
border-bottom-width:0px;
}
.compose-form__modifiers {
border: 1px solid $ui-primary-color;
border-top-width:0px;
}
.compose-form__buttons button.active:last-child {
border-radius:3px;
background: $ui-base-color;
color: $ui-primary-color;
}
.compose-form__buttons-wrapper {
background-color:$ui-primary-color;
}
.icon-button.inverted {
color:white;
&:hover {
color:$ui-secondary-color;
}
}
}
button.icon-button {
&.disabled {
}
}
.icon-button {
&.inverted {
color: darken($ui-base-lighter-color, 10%);
}
&.overlayed {
background: rgba($base-overlay-background, 0.2);
color: rgba($white, 0.7);
&:hover {
background: rgba($base-overlay-background, 0.4);
}
}
&.disabled {
color: desaturate($icon-button-inactive-color, 5%);
&:hover,
&:active,
&:focus {
color: desaturate($icon-button-inactive-color, 5%);
}
}
color: $icon-button-inactive-color;
&:hover,
&:active,
&:focus {
color: darken($icon-button-inactive-color, 5%);
}
}
.icon-button.star-icon,
.icon-button.star-icon:active {
background:transparent;
border:none;
}
.icon-button.star-icon.active {
color: $gold-star;
&:active, &:hover, &:focus {
color: $gold-star;
}
}
.text-icon-button {
color: $white;
&.active {
background: $ui-base-color;
color: $ui-primary-color;
}
&:focus, &:hover {
color: darken($ui-base-color, 3%);
}
}
.status.status-direct {
background: darken($ui-base-alt, 5%);
.icon-button.disabled {
color: lighten($ui-base-lighter-color, 10%);
}
}
.account__header, .account-card {
& > div {
background: rgba(lighten($ui-base-color, 4%), 0.6);
}
.account__header__content {
color: $primary-text-color;
}
.detailed-status__display-name .display-name strong {
color: $ui-highlight-color;
}
.icon-button {
&, &:hover {
color:desaturate($ui-base-lighter-color, 20%);
}
&.active {
&, &:hover {
color:$ui-base-lighter-color;
}
}
}
}
.account__section-headline a {
&.active {
color: $primary-text-color;
&::after {
border-bottom-color: $ui-base-alt;
}
&::after {
border-bottom-color: $ui-base-alt;
}
}
}
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
color: $ui-primary-color;
}
.privacy-dropdown__option {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
&:hover,
&.active {
color: $white;
.privacy-dropdown__option__content {
color: $white;
strong {
color: $white;
}
}
}
}
.emoji-picker-dropdown__menu {
.emoji-search-wrapper {
border-color: darken($ui-base-color, 10%);
}
.emoji-search {
background: darken($ui-base-color, 5%);
border-color: darken($ui-base-color, 10%);
}
.emoji-mart {
color: $ui-primary-color;
}
}
.search-popout {
background: $ui-base-color;
color: $ui-primary-color;
h4 {
color: $ui-primary-color;
}
em {
color: $ui-highlight-color;
}
}
.search__icon .fa.active {
opacity:1.0;
}
.search-results__hashtag {
color: darken($ui-primary-color, 10%);
&:hover {
color: lighten($ui-primary-color, 5%);
}
}
.static-content {
/*color: $primary-text-color;*/
}
#Getting-started {
background: $ui-primary-color;
border-bottom:0px;
color:white;
}
.getting-started {
p {
color: $primary-text-color;
}
a {
color: darken($ui-base-lighter-color, 10%);
}
}
.getting-started__wrapper {
flex: 0 0.5 auto;
}
.getting-started {
.column-link {
background: lighten($ui-primary-color, 5%);
color:$white;
&:hover {
background: lighten($ui-primary-color, 10%);
}
}
}
.column-link__badge {
background: saturate(darken($ui-primary-color, 5%), 5%);
}
.column-subheading {
background: darken($ui-primary-color, 5%);
color:$white;
}
.media-spoiler,
.video-player__spoiler.active {
color: $white;
&:hover {
color: darken($white, 5%);
}
}
.status {
border-bottom: 1px solid $ui-secondary-color;
}
.status__relative-time, .status__display-name {
color: darken($ui-base-color, 40%);
}
.status__content {
.status__content__spoiler-link {
background: $ui-base-lighter-color;
&:hover {
background: lighten($ui-base-lighter-color, 5%);
}
}
}
.muted .status__content p {
color: $icon-button-inactive-color;
}
.dropdown-menu__item {
& > a {
color: $primary-text-color;
&:hover, &:active, &:focus {
color: $white;
}
}
}
.dropdown--active .dropdown__content {
& > ul {
background: $ui-base-color;
box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
& > li > a {
background: $ui-base-color;
color: $primary-text-color;
&:hover {
background: $ui-highlight-color;
color: $ui-base-color;
}
}
}
}
.boost-modal,
.confirmation-modal,
.report-modal,
.actions-modal,
.mute-modal
{
color: $primary-text-color;
}
.boost-modal__action-bar,
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.report-modal__action-bar,
.mute-modal__action-bar {
& > div {
color: $ui-primary-color;
}
}
.actions-modal
{
ul {
li:not(:empty) {
a {
color: $primary-text-color;
button {
}
&.active, &:hover, &:active, &:focus {
color: $white;
button {
color: $white;
}
}
}
}
}
}
.react-toggle-track {
background-color: $icon-button-inactive-color;
}
.report-modal__comment .setting-text {
color: $primary-text-color;
border-bottom-color: lighten($ui-primary-color, 10%);
&:focus, &:active {
color: $primary-text-color;
}
}
.status.light {
.status__content {
color: $primary-text-color;
}
.display-name strong {
color: $primary-text-color;
}
}
.reply-indicator__content a {
color: lighten($ui-highlight-color, 30%);
}
.status__content
{
a {
color: $ui-highlight-color;
&:hover {
.fa {
color: darken($ui-base-color, 40%);
}
}
}
}
.detailed-status__display-name {
color: $ui-base-lighter-color;
}
.drawer .drawer__inner {
overflow: visible;
height:inherit;
background:$ui-base-alt;
}
.search__icon .fa {
color: $ui-highlight-color;
}
.drawer__pager {
overflow-y:auto;
}
.drawer .drawer__header {
background: $ui-base-color;
border-radius:3px;
}
.onboarding-modal__page h1 {
background-color: darken($ui-primary-color, 5%);
}
/* forms.scss */
.block-button, .button, button {
background-color: $ui-primary-color;
color: $white;
&.button-alternative {
color: $ui-base-color;
}
&.logo-button {
color: $white;
svg path:first-child {
fill: $white;
}
}
}
.simple_form {
p.hint {
color: $primary-text-color;
}
.block-button, .button, button {
background-color: $ui-primary-color;
color: $white;
&:hover {
background-color: lighten($ui-primary-color, 5%);
}
&:active,
&:focus {
background-color: darken($ui-primary-color, 5%);
}
}
}
/* admin.scss */
.table > thead > tr > th {
border-bottom-color: $ui-secondary-color;
}
.simple_form h4 {
border-bottom: 1px solid $ui-highlight-color;
}
.admin-wrapper {
.content {
h2, p.hint, h4, h6 {
color: $primary-text-color;
}
.muted-hint {
color: $primary-text-color;
}
}
.sidebar {
.logo {
-webkit-filter: invert(100%);
filter: invert(100%);
}
ul {
ul {
a {
&.selected {
background-color: $ui-primary-color;
color: $white;
&:hover {
background-color: lighten($ui-primary-color, 10%);
}
}
}
}
}
}
}
.pagination .current {
color: $ui-primary-color;
}
.report-accounts__item > strong {
color: $primary-text-color;
}
.admin-wrapper .content {
& > p {
color: $primary-text-color;
}
hr {
border-color: $ui-highlight-color;
}
}
/* accounts.scss */
.card {
.name {
color: $white;
}
.counter {
.counter-number {
color: $white;
}
}
}
/* stream_entries.scss */
.activity-stream {
.entry {
}
.status.light {
.display-name {
strong {
color: $primary-text-color;
}
}
.status__content {
color: $primary-text-color;
}
}
.detailed-status.light {
.detailed-status__display-name {
.display-name {
strong {
color: $primary-text-color;
}
}
}
.status__content {
color: $primary-text-color;
}
.status-card,
.status-card__title,
.status-card__description {
color: $primary-text-color;
}
}
}
/* accounts.scss */
.card {
.name {
color: darken($ui-primary-color, 15%);
}
.counter {
.counter-number {
color: darken($ui-primary-color, 15%);
}
border-color: $ui-primary-color;
}
}
.activity-stream-tabs {
a {
color: lighten($ui-primary-color, 10%);
&.active {
color: darken($ui-primary-color, 10%);
}
}
}
/* uncategorized */
@media screen and (min-width: 1300px) {
.column {
flex-grow: 1 !important;
max-width: 400px;
}
.drawer {
width: 17%;
max-width: 400px;
min-width: 330px;
}
}
.status time:after,
.detailed-status__datetime span:after {
font: normal normal normal 14px/1 FontAwesome;
content: "\00a0\00a0\f08e";
}
.column {
max-height:100vh;
& > .scrollable {
background-color: $ui-base-alt;
}
}
.empty-column-indicator, .error-column {
background-color: $ui-base-alt;
}
.actions .button.button-alternative {
background: $ui-highlight-color;
color: $white;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-highlight-color, 4%);
}
}
.public-layout .header {
background: $ui-highlight-color;
color: $white;
}
.public-layout .public-account-header__tabs__name h1 {
color: $white;
small {
color: $white;
}
}
.public-layout .header .brand:hover,
.public-layout .header .brand:focus,
.public-layout .header .brand:active {
background: lighten($ui-highlight-color, 5%);
}
.public-layout .container:last-child {
background:$ui-highlight-color;
padding-left: 100px;
padding-right: 100px;
border-radius: 4px;
h4 {
color: white;
}
}
.modal-layout, .modal-layout__mastodon > * {
background: none;
}
.dashboard__widgets a:not(.name-tag) {
color: $primary-text-color;
}

View File

@ -0,0 +1,272 @@
$ui-base-color: #181818; // darkest
$ui-highlight-color: #1ea21e; // vibrant
$ui-secondary-color: #E4F2E4; // lightest
$ui-primary-color: #E4F2E4; // lighter
$ui-primary-color-alt: #a0b49c; // darker, for external pages
$about-page-text: lighten($ui-base-color, 50%);
@import 'cybre-base';
@keyframes floating {
from {
transform: translate(0, 0);
}
65% {
transform: translate(0, 4px);
}
to {
transform: translate(0, -0);
}
}
body, body.about-body {
background: $ui-base-color url('../images/background-cybre.png');
}
body.about-body {
// basics.scss &.about-body
background: darken($ui-base-color, 8%) url('../images/background-cybre.png');
background-position-y: 200px;
background-position-x: center;
}
.about-body .mascot {
display: none;
}
.muted {
.status__content p, .status__content a {
color: lighten($ui-base-color, 35%);
}
.status__display-name strong {
color: lighten($ui-base-color, 35%);
}
}
.compose-form__buttons button.active:last-child {
color:$ui-secondary-color;
background-color: $ui-highlight-color;
border-radius:3px;
}
.screenshot-with-signup {
min-height:300px;
}
.container.hero .closed-registrations-message .clock {
font-size: 150%;
margin: 1em auto;
}
.column .static-content.getting-started {
background-image: url('../images/logo-cybre.png'), url('../images/background-cybre.png');
background-size:auto 50%, cover;
background-position: 50% 75%, center center;
background-repeat:no-repeat, no-repeat;
}
.columns-area {
background: $ui-base-color url('../images/background-cybre.png');
}
.actions .button.button-alternative {
background: $ui-highlight-color;
color: $ui-primary-color;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-highlight-color, 4%);
}
}
@media screen and (max-width: 1280px) {
.landing-page .container.links {
top: -15px;
}
}
.landing-page.alternative .header {
background-image:url('../images/header-cybre-colour.jpg');
background-repeat: repeat-x;
background-size:contain;
height:45vh;
width: 100%;
position:absolute;
z-index: 1;
text-align:center;
display: unset!important;
}
.landing-page.alternative .header img {
margin: auto;
max-height:45vh;
}
.landing-page.alternative .grid {
position: relative;
z-index:2;
margin-top:15vh;
}
.landing-page.alternative .landing-page__hero img {
visibility: hidden;
max-height:170px;
}
.landing-page.alternative .landing-page__forms {
height:auto;
}
.landing-page.alternative .column-1 {
display:flex;
align-items:flex-end;
}
.landing-page.alternative .column {
max-height:initial;
}
.landing-page.alternative .row__mascot {
.floats {
position:absolute;
img {
width:100%;
height:100%;
}
transition: all 0.1s linear;
animation-name: floating;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}
.float-1 {
width:50px;
height:50px;
bottom:60px;
left:110px;
animation-duration: 3s;
}
.float-2 {
width:130px;
height:130px;
left:85px;
bottom: -60px;
animation-duration: 3.5s;
animation-delay: 0.2s;
}
.float-3 {
width:100px;
height:100px;
right: 50;
top: -10px;
animation-duration: 4s;
animation-delay: 0.5s;
}
}
.activity-stream {
.status.light {
.status__header .status__meta .status__relative-time {
color: $ui-primary-color-alt;
}
.display-name span {
color: $ui-primary-color-alt;
}
.status__content {
a.status__content__spoiler-link {
background: $ui-primary-color-alt;
&:hover {
background: lighten($ui-primary-color-alt, 8%);
}
}
}
}
.detailed-status.light {
.detailed-status__display-name .display-name span {
color: $ui-primary-color-alt;
}
.status__content a.status__content__spoiler-link {
background: $ui-primary-color-alt;
&:hover {
background: lighten($ui-primary-color-alt, 8%);
}
}
.detailed-status__meta {
color: $ui-primary-color-alt;
}
}
.media-spoiler {
background: $ui-primary-color-alt;
&:hover {
background: darken($ui-primary-color-alt, 5%);
}
}
.pre-header {
color: $ui-primary-color-alt;
.status__display-name.muted strong {
color: $ui-primary-color-alt;
}
}
}
.embed .activity-stream .entry .detailed-status.light .button.button-secondary.logo-button {
color: $ui-primary-color-alt;
svg {
path:first-child {
fill: $ui-primary-color-alt;
}
}
&:active,
&:focus,
&:hover {
svg path:first-child {
fill: lighten($ui-primary-color-alt, 4%);
}
}
}
.emoji-mart-search {
background: $simple-background-color;
input {
color: $ui-primary-color-alt;
border: 1px solid $ui-primary-color-alt;
}
}
.emoji-mart-anchor {
color: $ui-primary-color-alt;
&:hover {
color: darken($ui-primary-color-alt, 8%);
}
}
.search-popout {
background: $ui-base-color;
color: $ui-primary-color;
h4 {
color: $ui-primary-color;
}
em {
color: $ui-highlight-color;
}
}

View File

@ -0,0 +1,96 @@
$doodleBg: #d9e1e8;
.doodle-modal {
@extend .boost-modal;
width: unset;
}
.doodle-modal__container {
background: $doodleBg;
text-align: center;
line-height: 0; // remove weird gap under canvas
canvas {
border: 5px solid $doodleBg;
}
}
.doodle-modal__action-bar {
@extend .boost-modal__action-bar;
.filler {
flex-grow: 1;
margin: 0;
padding: 0;
}
.doodle-toolbar {
line-height: 1;
display: flex;
flex-direction: column;
flex-grow: 0;
justify-content: space-around;
&.with-inputs {
label {
display: inline-block;
width: 70px;
text-align: right;
margin-right: 2px;
}
input[type="number"],input[type="text"] {
width: 40px;
}
span.val {
display: inline-block;
text-align: left;
width: 50px;
}
}
}
.doodle-palette {
padding-right: 0 !important;
border: 1px solid black;
line-height: .2rem;
flex-grow: 0;
background: white;
button {
appearance: none;
width: 1rem;
height: 1rem;
margin: 0; padding: 0;
text-align: center;
color: black;
text-shadow: 0 0 1px white;
cursor: pointer;
box-shadow: inset 0 0 1px rgba(white, .5);
border: 1px solid black;
outline-offset:-1px;
&.foreground {
outline: 1px dashed white;
}
&.background {
outline: 1px dashed red;
}
&.foreground.background {
outline: 1px dashed red;
border-color: white;
}
}
}
}
.compose-form__buttons-separator {
border-left: 1px solid #c3c3c3;
margin: 0 3px;
}
.compose-form__upload-button-icon {
line-height: 27px;
}

View File

@ -0,0 +1,48 @@
.detailed-status > .media-spoiler,
.status > .media-spoiler,
.status .video-player,
.media-gallery,
.status .status-card.interactive {
margin-top: 20px;
margin-left: -68px;
width: calc(100% + 80px);
}
.detailed-status > .media-spoiler,
.status > .media-spoiler,
.video-player {
max-width: none;
}
/* If there's no status text, add an extra margin on top */
.status .status__info + .media-gallery,
.status .status__info + .media-spoiler,
.status .status__info + .video-player,
.status .status__info + .status-card {
margin-top: 40px;
}
.status__video-player-video {
transform: unset;
top: unset;
}
.detailed-status .media-gallery {
margin-left: -10px;
width: calc(100% + 22px);
}
.public-layout .status {
.status__content {
min-height: 15px;
}
& > .media-spoiler,
.video-player,
.media-gallery,
.status-card {
margin-top: 20px;
width: calc(100% + 94px);
margin-left: -78px;
}
}

View File

@ -383,7 +383,6 @@
padding: 10px;
cursor: pointer;
border-radius: 4px;
&:hover,
&:focus,
&:active,
@ -1847,7 +1846,7 @@ a.account__display-name {
}
.column {
width: 330px;
width: 350px;
position: relative;
box-sizing: border-box;
display: flex;
@ -2579,10 +2578,6 @@ a.status-card {
width: 100%;
}
.status-card__image-image {
border-radius: 4px 4px 0 0;
}
.status-card__title {
white-space: inherit;
}
@ -2614,7 +2609,6 @@ a.status-card.compact:hover {
}
.status-card__image-image {
border-radius: 4px 0 0 4px;
display: block;
margin: 0;
width: 100%;
@ -3468,6 +3462,78 @@ a.status-card.compact:hover {
}
}
.advanced-options-dropdown {
position: relative;
}
.advanced-options-dropdown__dropdown {
display: none;
position: absolute;
left: 0;
top: 27px;
width: 210px;
background: $simple-background-color;
border-radius: 0 4px 4px;
z-index: 2;
overflow: hidden;
}
.advanced-options-dropdown__option {
color: $ui-base-color;
padding: 10px;
cursor: pointer;
display: flex;
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
.advanced-options-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
}
.advanced-options-dropdown__option__toggle {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.advanced-options-dropdown__option__content {
flex: 1 1 auto;
color: darken($ui-primary-color, 24%);
strong {
font-weight: 500;
display: block;
color: $ui-base-color;
}
}
.advanced-options-dropdown.open {
.advanced-options-dropdown__value {
background: $simple-background-color;
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
}
.advanced-options-dropdown__dropdown {
display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
}
}
.search {
position: relative;
}
@ -5538,3 +5604,4 @@ noscript {
}
}
}
@import 'doodle';

View File

@ -330,9 +330,12 @@ code {
}
input[type=text],
input[type=number],
input[type=email],
input[type=password] {
border-bottom-color: $valid-value-color;
input[type=password],
textarea,
select {
border-color: lighten($error-red, 12%);
}
.error {

File diff suppressed because it is too large Load Diff

View File

@ -129,4 +129,10 @@ class ActivityPub::Activity
::FetchRemoteStatusService.new.call(@object['url'])
end
end
def lock_or_return(key, expire_after = 7.days.seconds)
yield if redis.set(key, true, nx: true, ex: expire_after)
ensure
redis.del(key)
end
end

View File

@ -177,7 +177,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
updated = tag['updated']
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
emoji.image_remote_url = image_url

View File

@ -12,8 +12,10 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
private
def delete_person
SuspendAccountService.new.call(@account)
@account.destroy!
lock_or_return("delete_in_progress:#{@account.id}") do
SuspendAccountService.new.call(@account)
@account.destroy!
end
end
def delete_note

View File

@ -21,7 +21,7 @@ class EntityCache
end
unless uncached_ids.empty?
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).map { |item| [item.shortcode, item] }.to_h
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).each_with_object({}) { |item, h| h[item.shortcode] = item }
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
end

View File

@ -40,7 +40,11 @@ class FeedManager
end
def push_to_list(list, status)
return false if status.reply? && status.in_reply_to_account_id != status.account_id
if status.reply? && status.in_reply_to_account_id != status.account_id
should_filter = status.in_reply_to_account_id != list.account_id
should_filter &&= !ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?
return false if should_filter
end
return false unless add_to_feed(:list, list.id, status)
trim(:list, list.id)
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")

View File

@ -128,9 +128,9 @@ class Formatter
return html if emojis.empty?
emoji_map = if animate
emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url) }
else
emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url(:static)) }
end
i = -1
@ -212,8 +212,9 @@ class Formatter
def link_to_mention(entity, linkable_accounts)
acct = entity[:screen_name]
username, domain = acct.split('@')
return link_to_account(acct) unless linkable_accounts
return link_to_account(acct) unless linkable_accounts and domain != "twitter.com"
account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) }
account ? mention_html(account) : "@#{encode(acct)}"
@ -222,6 +223,10 @@ class Formatter
def link_to_account(acct)
username, domain = acct.split('@')
if domain == "twitter.com"
return mention_twitter_html(username)
end
domain = nil if TagManager.instance.local_domain?(domain)
account = EntityCache.instance.mention(username, domain)
@ -249,4 +254,8 @@ class Formatter
def mention_html(account)
"<span class=\"h-card\"><a href=\"#{encode(TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
end
def mention_twitter_html(username)
"<span class=\"h-card\"><a href=\"https://twitter.com/#{username}\" class=\"u-url mention\">@<span>#{username}@twitter.com</span></a></span>"
end
end

View File

@ -2,6 +2,17 @@
require 'ipaddr'
require 'socket'
require 'resolv'
# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
# around the Socket#open method, since we use our own timeout blocks inside
# that method
class HTTP::Timeout::PerOperation
def connect(socket_class, host, port, nodelay = false)
@socket = socket_class.open(host, port)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
end
end
class Request
REQUEST_TARGET = '(request-target)'
@ -45,7 +56,7 @@ class Request
end
begin
yield response.extend(ClientLimit)
yield response.extend(ClientLimit) if block_given?
ensure
http_client.close
end
@ -94,7 +105,11 @@ class Request
end
def timeout
{ write: 10, connect: 10, read: 10 }
# We enforce a 1s timeout on DNS resolving, 10s timeout on socket opening
# and 5s timeout on the TLS handshake, meaning the worst case should take
# about 16s in total
{ connect: 5, read: 10, write: 10 }
end
def http_client
@ -139,17 +154,34 @@ class Request
class Socket < TCPSocket
class << self
def open(host, *args)
return super host, *args if thru_hidden_service? host
return super(host, *args) if thru_hidden_service?(host)
outer_e = nil
Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
begin
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address.ip_address)
return super address.ip_address, *args
rescue => e
outer_e = e
Resolv::DNS.open do |dns|
dns.timeouts = 1
addresses = dns.getaddresses(host).take(2)
time_slot = 10.0 / addresses.size
addresses.each do |address|
begin
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
::Timeout.timeout(time_slot, HTTP::TimeoutError) do
return super(address.to_s, *args)
end
rescue => e
outer_e = e
end
end
end
raise outer_e if outer_e
if outer_e
raise outer_e
else
raise SocketError, "No address for #{host}"
end
end
alias new open

View File

@ -31,7 +31,7 @@ module Settings
def all_as_records
vars = thing_scoped
records = vars.map { |r| [r.var, r] }.to_h
records = vars.each_with_object({}) { |r, h| h[r.var] = r }
Setting.default_settings.each do |key, default_value|
next if records.key?(key) || default_value.is_a?(Hash)
@ -65,7 +65,7 @@ module Settings
class << self
def default_settings
defaulting = DEFAULTING_TO_UNSCOPED.map { |k| [k, Setting[k]] }.to_h
defaulting = DEFAULTING_TO_UNSCOPED.each_with_object({}) { |k, h| h[k] = Setting[k] }
Setting.default_settings.merge!(defaulting)
end
end

View File

@ -49,7 +49,7 @@
#
class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.]+[a-z0-9_]+)?/i
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
include AccountAvatar
@ -75,7 +75,8 @@ class Account < ApplicationRecord
validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
validates :note, length: { maximum: 413 }, if: -> { local? && will_save_change_to_note? }
validate :note_has_eight_newlines?, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
# Timelines
@ -279,10 +280,8 @@ class Account < ApplicationRecord
def save_with_optional_media!
save!
rescue ActiveRecord::RecordInvalid
self.avatar = nil
self.header = nil
self[:avatar_remote_url] = ''
self[:header_remote_url] = ''
self.avatar = nil if errors[:avatar].present?
self.header = nil if errors[:header].present?
save!
end
@ -306,6 +305,10 @@ class Account < ApplicationRecord
shared_inbox_url.presence || inbox_url
end
def note_has_eight_newlines?
errors.add(:note, 'Bio can\'t have more then 8 newlines') unless note.count("\n") <= 8
end
class Field < ActiveModelSerializers::Model
attributes :name, :value, :verified_at, :account, :errors

View File

@ -45,9 +45,9 @@ module AccountInteractions
end
def domain_blocking_map(target_account_ids, account_id)
accounts_map = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
end
def domain_blocking_map_by_domain(target_domains, account_id)

View File

@ -59,6 +59,7 @@ class MediaAttachment < ApplicationRecord
format: 'mp4',
convert_options: {
output: {
'loglevel' => 'fatal',
'movflags' => 'faststart',
'pix_fmt' => 'yuv420p',
'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',

View File

@ -75,7 +75,7 @@ class Notification < ApplicationRecord
return if account_ids.empty?
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
cached_items.each do |item|
item.from_account = accounts[item.from_account_id]

View File

@ -40,7 +40,7 @@ class Setting < RailsSettings::Base
def all_as_records
vars = thing_scoped
records = vars.map { |r| [r.var, r] }.to_h
records = vars.each_with_object({}) { |r, h| h[r.var] = r }
default_settings.each do |key, default_value|
next if records.key?(key) || default_value.is_a?(Hash)

View File

@ -310,19 +310,19 @@ class Status < ApplicationRecord
end
def favourites_map(status_ids, account_id)
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
end
def reblogs_map(status_ids, account_id)
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
end
def mutes_map(conversation_ids, account_id)
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
end
def pins_map(status_ids, account_id)
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
end
def reload_stale_associations!(cached_items)
@ -337,7 +337,7 @@ class Status < ApplicationRecord
return if account_ids.empty?
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
cached_items.each do |item|
item.account = accounts[item.account_id]

View File

@ -18,7 +18,7 @@ class TrendingTags
def get(limit)
key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}"
tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i)
tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h
tags = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag }
tag_ids.map { |tag_id| tags[tag_id] }.compact
end

View File

@ -18,7 +18,7 @@ class InstancePresenter
end
def user_count
Rails.cache.fetch('user_count') { User.confirmed.count }
Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
end
def status_count

View File

@ -3,4 +3,8 @@
class REST::FilterSerializer < ActiveModel::Serializer
attributes :id, :phrase, :context, :whole_word, :expires_at,
:irreversible
def id
object.id.to_s
end
end

View File

@ -232,7 +232,7 @@ class ActivityPub::ProcessAccountService < BaseService
updated = tag['updated']
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
emoji.image_remote_url = image_url

View File

@ -12,12 +12,12 @@ class BatchedRemoveStatusService < BaseService
def call(statuses)
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
@mentions = statuses.map { |s| [s.id, s.active_mentions.includes(:account).to_a] }.to_h
@tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h
@mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a }
@tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) }
@stream_entry_batches = []
@salmon_batches = []
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
@json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
@activity_xml = {}
# Ensure that rendered XML reflects destroyed state

View File

@ -18,6 +18,6 @@ module AuthorExtractor
acct = "#{username}@#{domain}"
end
ResolveAccountService.new.call(acct, update_profile)
ResolveAccountService.new.call(acct, update_profile: update_profile)
end
end

View File

@ -29,7 +29,7 @@ class FetchAtomService < BaseService
def perform_request(&block)
accept = 'text/html'
accept = 'application/activity+json, application/ld+json, application/atom+xml, ' + accept unless @unsupported_activity
accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity
Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
end
@ -39,7 +39,7 @@ class FetchAtomService < BaseService
if response.mime_type == 'application/atom+xml'
[@url, { prefetched_body: response.body_with_limit }, :ostatus]
elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(response.mime_type)
elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
body = response.body_with_limit
json = body_to_json(body)
if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?

View File

@ -136,14 +136,15 @@ class FetchLinkCardService < BaseService
detector = CharlockHolmes::EncodingDetector.new
detector.strip_tags = true
guess = detector.detect(@html, @html_charset)
page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
guess = detector.detect(@html, @html_charset)
page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
player_url = meta_property(page, 'twitter:player')
if meta_property(page, 'twitter:player')
if player_url && !bad_url?(Addressable::URI.parse(player_url))
@card.type = :video
@card.width = meta_property(page, 'twitter:player:width') || 0
@card.height = meta_property(page, 'twitter:player:height') || 0
@card.html = content_tag(:iframe, nil, src: meta_property(page, 'twitter:player'),
@card.html = content_tag(:iframe, nil, src: player_url,
width: @card.width,
height: @card.height,
allowtransparency: 'true',

View File

@ -7,9 +7,9 @@ class FollowService < BaseService
# @param [Account] source_account From which to follow
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
def call(source_account, uri, reblogs: nil)
def call(source_account, target_account, reblogs: nil)
reblogs = true if reblogs.nil?
target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri)
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
@ -42,7 +42,7 @@ class FollowService < BaseService
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
if target_account.local?
NotifyService.new.call(target_account, follow_request)
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
elsif target_account.ostatus?
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
@ -57,7 +57,7 @@ class FollowService < BaseService
follow = source_account.follow!(target_account, reblogs: reblogs)
if target_account.local?
NotifyService.new.call(target_account, follow)
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
else
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)

Some files were not shown because too many files have changed in this diff Show More