Browse Source

Merge branches 'dependency_whatinput', 'feature_512_char_toots', 'feature_cybrespace_locale', 'feature_doodlebox', 'feature_hotlink_twitter_mentions', 'feature_longer_bios', 'feature_machine_registration_task', 'branding_cybre' and 'theme_base' into cybrespace

64 changed files with 2682 additions and 74 deletions
  1. 6
    1
      app/controllers/concerns/localized.rb
  2. 1
    0
      app/helpers/settings_helper.rb
  3. 64
    0
      app/javascript/images/floppy-1.svg
  4. 64
    0
      app/javascript/images/floppy-2.svg
  5. 64
    0
      app/javascript/images/floppy-3.svg
  6. BIN
      app/javascript/images/header-cybre-alt.jpg
  7. BIN
      app/javascript/images/logo-cybre.png
  8. 9
    0
      app/javascript/mastodon/actions/compose.js
  9. 9
    3
      app/javascript/mastodon/components/icon_button.js
  10. 2
    0
      app/javascript/mastodon/components/relative_timestamp.js
  11. 1
    1
      app/javascript/mastodon/components/status_action_bar.js
  12. 133
    0
      app/javascript/mastodon/features/compose/components/attach_options.js
  13. 76
    0
      app/javascript/mastodon/features/compose/components/compose_dropdown.js
  14. 7
    6
      app/javascript/mastodon/features/compose/components/compose_form.js
  15. 2
    2
      app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
  16. 3
    3
      app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
  17. 14
    18
      app/javascript/mastodon/features/getting_started/index.js
  18. 1
    1
      app/javascript/mastodon/features/notifications/components/notification.js
  19. 1
    1
      app/javascript/mastodon/features/status/components/action_bar.js
  20. 1
    1
      app/javascript/mastodon/features/status/components/detailed_status.js
  21. 614
    0
      app/javascript/mastodon/features/ui/components/doodle_modal.js
  22. 13
    1
      app/javascript/mastodon/features/ui/components/modal_root.js
  23. 293
    0
      app/javascript/mastodon/locales/en-CY.json
  24. 2
    0
      app/javascript/mastodon/locales/whitelist_en-CY.json
  25. 14
    0
      app/javascript/mastodon/reducers/compose.js
  26. 32
    7
      app/javascript/packs/public.js
  27. 63
    0
      app/javascript/styles/cybre-base.scss
  28. 96
    0
      app/javascript/styles/doodle.scss
  29. 48
    0
      app/javascript/styles/fullwidth-media.scss
  30. 73
    1
      app/javascript/styles/mastodon/components.scss
  31. 10
    1
      app/lib/formatter.rb
  32. 8
    5
      app/models/account.rb
  33. 1
    1
      app/validators/status_length_validator.rb
  34. 12
    4
      app/views/about/show.html.haml
  35. 1
    1
      app/views/layouts/admin.html.haml
  36. 2
    1
      app/views/layouts/auth.html.haml
  37. 1
    1
      app/views/settings/profiles/show.html.haml
  38. 1
    1
      app/views/stream_entries/_detailed_status.html.haml
  39. 1
    1
      app/views/stream_entries/_simple_status.html.haml
  40. 6
    3
      config/application.rb
  41. 1
    0
      config/i18n-tasks.yml
  42. 61
    0
      config/locales/devise.en-CY.yml
  43. 119
    0
      config/locales/doorkeeper.en-CY.yml
  44. 563
    0
      config/locales/en-CY.yml
  45. 66
    0
      config/locales/simple_form.en-CY.yml
  46. 1
    1
      config/settings.yml
  47. 17
    0
      db/migrate/20171026200500_en_to_en_cy.rb
  48. 2
    2
      lib/mastodon/version.rb
  49. 22
    0
      lib/tasks/mastodon.rake
  50. 1
    0
      package.json
  51. BIN
      public/android-chrome-192x192.png
  52. BIN
      public/apple-touch-icon.png
  53. BIN
      public/background-cybre.png
  54. 1
    1
      public/browserconfig.xml
  55. 54
    0
      public/clock.js
  56. 17
    1
      public/emoji/1f418.svg
  57. BIN
      public/favicon.ico
  58. BIN
      public/header.jpeg
  59. BIN
      public/logo-cybre-glitch.gif
  60. BIN
      public/logo-cybre.png
  61. BIN
      public/mstile-150x150.png
  62. BIN
      public/riot-glitch.png
  63. 4
    4
      spec/models/account_spec.rb
  64. 4
    0
      yarn.lock

+ 6
- 1
app/controllers/concerns/localized.rb View File

@@ -20,7 +20,12 @@ module Localized
20 20
     if ENV['DEFAULT_LOCALE'].present?
21 21
       I18n.default_locale
22 22
     else
23
-      request_locale || I18n.default_locale
23
+      case request_locale
24
+      when /en\b/, nil
25
+        I18n.default_locale
26
+      else
27
+        request_locale
28
+      end
24 29
     end
25 30
   end
26 31
 

+ 1
- 0
app/helpers/settings_helper.rb View File

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

+ 64
- 0
app/javascript/images/floppy-1.svg View File

@@ -0,0 +1,64 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
2
+  <g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
3
+    <g id="strokes">
4
+      <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" />
5
+      <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" />
6
+      <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" />
7
+      <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" />
8
+      <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" />
9
+      <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" />
10
+      <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" />
11
+      <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" />
12
+      <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" />
13
+      <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" />
14
+      <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" />
15
+      <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" />
16
+      <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" />
17
+      <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" />
18
+      <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" />
19
+      <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" />
20
+      <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" />
21
+      <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" />
22
+      <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" />
23
+      <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" />
24
+      <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" />
25
+      <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" />
26
+      <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" />
27
+      <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" />
28
+      <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" />
29
+      <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" />
30
+      <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" />
31
+      <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" />
32
+      <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" />
33
+      <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" />
34
+      <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" />
35
+      <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" />
36
+      <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" />
37
+      <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" />
38
+      <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" />
39
+      <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" />
40
+      <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" />
41
+      <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" />
42
+      <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" />
43
+      <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" />
44
+      <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" />
45
+      <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" />
46
+      <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" />
47
+      <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" />
48
+      <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" />
49
+      <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" />
50
+      <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" />
51
+      <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" />
52
+      <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" />
53
+      <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" />
54
+      <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" />
55
+      <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" />
56
+      <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" />
57
+      <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" />
58
+      <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" />
59
+      <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" />
60
+      <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" />
61
+      <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" />
62
+    </g>
63
+  </g>
64
+</svg>

+ 64
- 0
app/javascript/images/floppy-2.svg View File

@@ -0,0 +1,64 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
2
+  <g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
3
+    <g id="strokes">
4
+      <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" />
5
+      <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" />
6
+      <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" />
7
+      <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" />
8
+      <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" />
9
+      <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" />
10
+      <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" />
11
+      <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" />
12
+      <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" />
13
+      <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" />
14
+      <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" />
15
+      <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" />
16
+      <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" />
17
+      <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" />
18
+      <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" />
19
+      <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" />
20
+      <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" />
21
+      <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" />
22
+      <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" />
23
+      <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" />
24
+      <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" />
25
+      <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" />
26
+      <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" />
27
+      <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" />
28
+      <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" />
29
+      <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" />
30
+      <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" />
31
+      <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" />
32
+      <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" />
33
+      <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" />
34
+      <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" />
35
+      <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" />
36
+      <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" />
37
+      <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" />
38
+      <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" />
39
+      <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" />
40
+      <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" />
41
+      <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" />
42
+      <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" />
43
+      <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" />
44
+      <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" />
45
+      <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" />
46
+      <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" />
47
+      <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" />
48
+      <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" />
49
+      <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" />
50
+      <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" />
51
+      <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" />
52
+      <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" />
53
+      <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" />
54
+      <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" />
55
+      <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" />
56
+      <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" />
57
+      <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" />
58
+      <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" />
59
+      <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" />
60
+      <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" />
61
+      <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" />
62
+    </g>
63
+  </g>
64
+</svg>

+ 64
- 0
app/javascript/images/floppy-3.svg View File

@@ -0,0 +1,64 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="384" version="1.1" height="384">
2
+  <g transform="translate(-282.71845,-76)" id="RenderLayer_LineSet">
3
+    <g id="strokes">
4
+      <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" />
5
+      <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" />
6
+      <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" />
7
+      <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" />
8
+      <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" />
9
+      <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" />
10
+      <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" />
11
+      <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" />
12
+      <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" />
13
+      <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" />
14
+      <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" />
15
+      <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" />
16
+      <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" />
17
+      <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" />
18
+      <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" />
19
+      <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" />
20
+      <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" />
21
+      <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" />
22
+      <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" />
23
+      <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" />
24
+      <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" />
25
+      <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" />
26
+      <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" />
27
+      <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" />
28
+      <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" />
29
+      <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" />
30
+      <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" />
31
+      <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" />
32
+      <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" />
33
+      <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" />
34
+      <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" />
35
+      <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" />
36
+      <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" />
37
+      <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" />
38
+      <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" />
39
+      <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" />
40
+      <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" />
41
+      <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" />
42
+      <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" />
43
+      <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" />
44
+      <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" />
45
+      <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" />
46
+      <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" />
47
+      <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" />
48
+      <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" />
49
+      <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" />
50
+      <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" />
51
+      <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" />
52
+      <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" />
53
+      <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" />
54
+      <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" />
55
+      <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" />
56
+      <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" />
57
+      <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" />
58
+      <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" />
59
+      <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" />
60
+      <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" />
61
+      <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" />
62
+    </g>
63
+  </g>
64
+</svg>

BIN
app/javascript/images/header-cybre-alt.jpg View File


BIN
app/javascript/images/logo-cybre.png View File


+ 9
- 0
app/javascript/mastodon/actions/compose.js View File

@@ -49,6 +49,8 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST     = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
49 49
 export const COMPOSE_UPLOAD_CHANGE_SUCCESS     = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
50 50
 export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
51 51
 
52
+export const COMPOSE_DOODLE_SET        = 'COMPOSE_DOODLE_SET';
53
+
52 54
 export function changeCompose(text) {
53 55
   return {
54 56
     type: COMPOSE_CHANGE,
@@ -175,6 +177,13 @@ export function submitComposeFail(error) {
175 177
   };
176 178
 };
177 179
 
180
+export function doodleSet(options) {
181
+  return {
182
+    type: COMPOSE_DOODLE_SET,
183
+    options: options,
184
+  };
185
+};
186
+
178 187
 export function uploadCompose(files) {
179 188
   return function (dispatch, getState) {
180 189
     if (getState().getIn(['compose', 'media_attachments']).size > 3) {

+ 9
- 3
app/javascript/mastodon/components/icon_button.js View File

@@ -22,6 +22,7 @@ export default class IconButton extends React.PureComponent {
22 22
     animate: PropTypes.bool,
23 23
     overlay: PropTypes.bool,
24 24
     tabIndex: PropTypes.string,
25
+    label: PropTypes.string,
25 26
   };
26 27
 
27 28
   static defaultProps = {
@@ -42,14 +43,18 @@ export default class IconButton extends React.PureComponent {
42 43
   }
43 44
 
44 45
   render () {
45
-    const style = {
46
+    let style = {
46 47
       fontSize: `${this.props.size}px`,
47
-      width: `${this.props.size * 1.28571429}px`,
48 48
       height: `${this.props.size * 1.28571429}px`,
49 49
       lineHeight: `${this.props.size}px`,
50 50
       ...this.props.style,
51 51
       ...(this.props.active ? this.props.activeStyle : {}),
52 52
     };
53
+    if (!this.props.label) {
54
+      style.width = `${this.props.size * 1.28571429}px`;
55
+    } else {
56
+      style.textAlign = 'left';
57
+    }
53 58
 
54 59
     const {
55 60
       active,
@@ -104,7 +109,8 @@ export default class IconButton extends React.PureComponent {
104 109
             style={style}
105 110
             tabIndex={tabIndex}
106 111
           >
107
-            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
112
+            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
113
+            {this.props.label}
108 114
           </button>
109 115
         )}
110 116
       </Motion>

+ 2
- 0
app/javascript/mastodon/components/relative_timestamp.js View File

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

+ 1
- 1
app/javascript/mastodon/components/status_action_bar.js View File

@@ -206,7 +206,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
206 206
       <div className='status__action-bar'>
207 207
         <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>
208 208
         <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} />
209
-        <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} />
209
+        <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} />
210 210
         {shareButton}
211 211
 
212 212
         <div className='status__action-bar-dropdown'>

+ 133
- 0
app/javascript/mastodon/features/compose/components/attach_options.js View File

@@ -0,0 +1,133 @@
1
+//  Package imports  // 
2
+import React from 'react'; 
3
+import PropTypes from 'prop-types'; 
4
+import { connect } from 'react-redux'; 
5
+import { injectIntl, defineMessages } from 'react-intl'; 
6
+ 
7
+//  Our imports  // 
8
+import ComposeDropdown from './compose_dropdown'; 
9
+import { uploadCompose } from '../../../actions/compose'; 
10
+import ImmutablePropTypes from 'react-immutable-proptypes'; 
11
+import ImmutablePureComponent from 'react-immutable-pure-component'; 
12
+import { openModal } from '../../../actions/modal'; 
13
+ 
14
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
15
+ 
16
+const messages = defineMessages({ 
17
+  upload : 
18
+    { id: 'compose.attach.upload', defaultMessage: 'Upload a file' }, 
19
+  doodle : 
20
+    { id: 'compose.attach.doodle', defaultMessage: 'Draw something' }, 
21
+  attach : 
22
+    { id: 'compose.attach', defaultMessage: 'Attach...' }, 
23
+}); 
24
+ 
25
+const mapStateToProps = state => ({ 
26
+  // This horrible expression is copied from vanilla upload_button_container 
27
+  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), 
28
+  resetFileKey: state.getIn(['compose', 'resetFileKey']), 
29
+  acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), 
30
+}); 
31
+ 
32
+const mapDispatchToProps = dispatch => ({ 
33
+  onSelectFile (files) { 
34
+    dispatch(uploadCompose(files)); 
35
+  }, 
36
+  onOpenDoodle () { 
37
+    dispatch(openModal('DOODLE', { noEsc: true })); 
38
+  }, 
39
+}); 
40
+ 
41
+@injectIntl 
42
+@connect(mapStateToProps, mapDispatchToProps) 
43
+export default class ComposeAttachOptions extends ImmutablePureComponent { 
44
+ 
45
+  static propTypes = { 
46
+    intl     : PropTypes.object.isRequired, 
47
+    resetFileKey: PropTypes.number, 
48
+    acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, 
49
+    disabled: PropTypes.bool, 
50
+    onSelectFile: PropTypes.func.isRequired, 
51
+    onOpenDoodle: PropTypes.func.isRequired, 
52
+  }; 
53
+ 
54
+  handleItemClick = bt => { 
55
+    if (bt === 'upload') { 
56
+      this.fileElement.click(); 
57
+    } 
58
+ 
59
+    if (bt === 'doodle') { 
60
+      this.props.onOpenDoodle(); 
61
+    } 
62
+ 
63
+    this.dropdown.setState({ open: false }); 
64
+  }; 
65
+ 
66
+  handleFileChange = (e) => { 
67
+    if (e.target.files.length > 0) { 
68
+      this.props.onSelectFile(e.target.files); 
69
+    } 
70
+  } 
71
+ 
72
+  setFileRef = (c) => { 
73
+    this.fileElement = c; 
74
+  } 
75
+ 
76
+  setDropdownRef = (c) => { 
77
+    this.dropdown = c; 
78
+  } 
79
+ 
80
+  render () { 
81
+    const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; 
82
+ 
83
+    const options = [ 
84
+      { icon: 'cloud-upload', text: messages.upload, name: 'upload' }, 
85
+      { icon: 'paint-brush', text: messages.doodle, name: 'doodle' }, 
86
+    ]; 
87
+ 
88
+    const optionElems = options.map((item) => { 
89
+      const hdl = () => this.handleItemClick(item.name); 
90
+      return ( 
91
+        <div 
92
+          role='button' 
93
+          tabIndex='0' 
94
+          key={item.name} 
95
+          onClick={hdl} 
96
+          className='privacy-dropdown__option' 
97
+        > 
98
+          <div className='privacy-dropdown__option__icon'> 
99
+            <i className={`fa fa-fw fa-${item.icon}`} /> 
100
+          </div> 
101
+ 
102
+          <div className='privacy-dropdown__option__content'> 
103
+            <strong>{intl.formatMessage(item.text)}</strong> 
104
+          </div> 
105
+        </div> 
106
+      ); 
107
+    }); 
108
+ 
109
+    return ( 
110
+      <div> 
111
+        <ComposeDropdown 
112
+          title={intl.formatMessage(messages.attach)} 
113
+          icon='paperclip' 
114
+          disabled={disabled} 
115
+          ref={this.setDropdownRef} 
116
+        > 
117
+          {optionElems} 
118
+        </ComposeDropdown> 
119
+        <input 
120
+          key={resetFileKey} 
121
+          ref={this.setFileRef} 
122
+          type='file' 
123
+          multiple={false} 
124
+          accept={acceptContentTypes.toArray().join(',')} 
125
+          onChange={this.handleFileChange} 
126
+          disabled={disabled} 
127
+          style={{ display: 'none' }} 
128
+        /> 
129
+      </div> 
130
+    ); 
131
+  } 
132
+ 
133
+} 

+ 76
- 0
app/javascript/mastodon/features/compose/components/compose_dropdown.js View File

@@ -0,0 +1,76 @@
1
+//  Package imports  // 
2
+import React from 'react'; 
3
+import PropTypes from 'prop-types'; 
4
+ 
5
+//  Mastodon imports  // 
6
+import IconButton from '../../../components/icon_button'; 
7
+ 
8
+const iconStyle = { 
9
+  height     : null, 
10
+  lineHeight : '27px', 
11
+}; 
12
+ 
13
+export default class ComposeDropdown extends React.PureComponent {
14
+ 
15
+  static propTypes = { 
16
+    title: PropTypes.string.isRequired, 
17
+    icon: PropTypes.string, 
18
+    highlight: PropTypes.bool, 
19
+    disabled: PropTypes.bool, 
20
+    children: PropTypes.arrayOf(PropTypes.node).isRequired, 
21
+  }; 
22
+ 
23
+  state = { 
24
+    open: false, 
25
+  }; 
26
+ 
27
+  onGlobalClick = (e) => { 
28
+    if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { 
29
+      this.setState({ open: false }); 
30
+    } 
31
+  }; 
32
+ 
33
+  componentDidMount () { 
34
+    window.addEventListener('click', this.onGlobalClick); 
35
+    window.addEventListener('touchstart', this.onGlobalClick); 
36
+  } 
37
+  componentWillUnmount () { 
38
+    window.removeEventListener('click', this.onGlobalClick); 
39
+    window.removeEventListener('touchstart', this.onGlobalClick); 
40
+  } 
41
+ 
42
+  onToggleDropdown = () => { 
43
+    if (this.props.disabled) return; 
44
+    this.setState({ open: !this.state.open }); 
45
+  }; 
46
+ 
47
+  setRef = (c) => { 
48
+    this.node = c; 
49
+  }; 
50
+ 
51
+  render () { 
52
+    const { open } = this.state; 
53
+    let { highlight, title, icon, disabled } = this.props; 
54
+ 
55
+    if (!icon) icon = 'ellipsis-h'; 
56
+ 
57
+    return ( 
58
+      <div ref={this.setRef} className={`advanced-options-dropdown ${open ?  'open' : ''} ${highlight ? 'active' : ''} `}> 
59
+        <div className='advanced-options-dropdown__value'> 
60
+          <IconButton 
61
+            className={'inverted'} 
62
+            title={title} 
63
+            icon={icon} active={open || highlight} 
64
+            size={18} 
65
+            style={iconStyle} 
66
+            disabled={disabled} 
67
+            onClick={this.onToggleDropdown} 
68
+          /> 
69
+        </div> 
70
+        <div className='advanced-options-dropdown__dropdown'> 
71
+          {this.props.children} 
72
+        </div> 
73
+      </div> 
74
+    ); 
75
+  }
76
+}

+ 7
- 6
app/javascript/mastodon/features/compose/components/compose_form.js View File

@@ -5,7 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
5 5
 import PropTypes from 'prop-types';
6 6
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
7 7
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
8
-import UploadButtonContainer from '../containers/upload_button_container';
9 8
 import { defineMessages, injectIntl } from 'react-intl';
10 9
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
11 10
 import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
@@ -17,6 +16,7 @@ import { isMobile } from '../../../is_mobile';
17 16
 import ImmutablePureComponent from 'react-immutable-pure-component';
18 17
 import { length } from 'stringz';
19 18
 import { countableText } from '../util/counter';
19
+import ComposeAttachOptions from './attach_options';
20 20
 
21 21
 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';
22 22
 
@@ -80,7 +80,7 @@ export default class ComposeForm extends ImmutablePureComponent {
80 80
     const { is_submitting, is_uploading, anyMedia } = this.props;
81 81
     const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
82 82
 
83
-    if (is_submitting || is_uploading || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
83
+    if (is_submitting || is_uploading || length(fulltext) > 512 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
84 84
       return;
85 85
     }
86 86
 
@@ -156,7 +156,7 @@ export default class ComposeForm extends ImmutablePureComponent {
156 156
     const { intl, onPaste, showSearch, anyMedia } = this.props;
157 157
     const disabled = this.props.is_submitting;
158 158
     const text     = [this.props.spoiler_text, countableText(this.props.text)].join('');
159
-    const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
159
+    const disabledButton = disabled || this.props.is_uploading || length(text) > 512 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
160 160
     let publishText = '';
161 161
 
162 162
     if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@@ -203,12 +203,13 @@ export default class ComposeForm extends ImmutablePureComponent {
203 203
 
204 204
         <div className='compose-form__buttons-wrapper'>
205 205
           <div className='compose-form__buttons'>
206
-            <UploadButtonContainer />
207
-            <PrivacyDropdownContainer />
206
+            <ComposeAttachOptions />
208 207
             <SensitiveButtonContainer />
208
+            <div className='compose-form__buttons-separator' /> 
209
+            <PrivacyDropdownContainer />
209 210
             <SpoilerButtonContainer />
210 211
           </div>
211
-          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
212
+          <div className='character-counter__wrapper'><CharacterCounter max={512} text={text} /></div>
212 213
         </div>
213 214
 
214 215
         <div className='compose-form__publish'>

+ 2
- 2
app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js View File

@@ -356,8 +356,8 @@ export default class EmojiPickerDropdown extends React.PureComponent {
356 356
         <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}>
357 357
           <img
358 358
             className={classNames('emojione', { 'pulse-loading': active && loading })}
359
-            alt='🙂'
360
-            src={`${assetHost}/emoji/1f602.svg`}
359
+            alt='🤔'
360
+            src={`${assetHost}/emoji/1f914.svg`}
361 361
           />
362 362
         </div>
363 363
 

+ 3
- 3
app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js View File

@@ -12,13 +12,13 @@ const DEFAULTS = [
12 12
   '+1',
13 13
   'grinning',
14 14
   'kissing_heart',
15
-  'heart_eyes',
15
+  'vhs',
16 16
   'laughing',
17
-  'stuck_out_tongue_winking_eye',
17
+  'floppy_disk',
18 18
   'sweat_smile',
19 19
   'joy',
20 20
   'yum',
21
-  'disappointed',
21
+  'computer',
22 22
   'thinking_face',
23 23
   'weary',
24 24
   'sob',

+ 14
- 18
app/javascript/mastodon/features/getting_started/index.js View File

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

+ 1
- 1
app/javascript/mastodon/features/notifications/components/notification.js View File

@@ -116,7 +116,7 @@ export default class Notification extends ImmutablePureComponent {
116 116
         <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'))}>
117 117
           <div className='notification__message'>
118 118
             <div className='notification__favourite-icon-wrapper'>
119
-              <i className='fa fa-fw fa-star star-icon' />
119
+              <i className='fa fa-fw fa-floppy-o star-icon' />
120 120
             </div>
121 121
             <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
122 122
           </div>

+ 1
- 1
app/javascript/mastodon/features/status/components/action_bar.js View File

@@ -161,7 +161,7 @@ export default class ActionBar extends React.PureComponent {
161 161
       <div className='detailed-status__action-bar'>
162 162
         <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>
163 163
         <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>
164
-        <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>
164
+        <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>
165 165
         {shareButton}
166 166
 
167 167
         <div className='detailed-status__action-bar-dropdown'>

+ 1
- 1
app/javascript/mastodon/features/status/components/detailed_status.js View File

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

+ 614
- 0
app/javascript/mastodon/features/ui/components/doodle_modal.js View File

@@ -0,0 +1,614 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import Button from '../../../components/button';
4
+import ImmutablePureComponent from 'react-immutable-pure-component';
5
+import Atrament from 'atrament'; // the doodling library
6
+import { connect } from 'react-redux';
7
+import ImmutablePropTypes from 'react-immutable-proptypes';
8
+import { doodleSet, uploadCompose } from '../../../actions/compose';
9
+import IconButton from '../../../components/icon_button';
10
+import { debounce, mapValues } from 'lodash';
11
+import classNames from 'classnames';
12
+
13
+// palette nicked from MyPaint, CC0
14
+const palette = [
15
+  ['rgb(  0,    0,    0)', 'Black'],
16
+  ['rgb( 38,   38,   38)', 'Gray 15'],
17
+  ['rgb( 77,   77,   77)', 'Grey 30'],
18
+  ['rgb(128,  128,  128)', 'Grey 50'],
19
+  ['rgb(171,  171,  171)', 'Grey 67'],
20
+  ['rgb(217,  217,  217)', 'Grey 85'],
21
+  ['rgb(255,  255,  255)', 'White'],
22
+  ['rgb(128,    0,    0)', 'Maroon'],
23
+  ['rgb(209,    0,    0)', 'English-red'],
24
+  ['rgb(255,   54,   34)', 'Tomato'],
25
+  ['rgb(252,   60,    3)', 'Orange-red'],
26
+  ['rgb(255,  140,  105)', 'Salmon'],
27
+  ['rgb(252,  232,   32)', 'Cadium-yellow'],
28
+  ['rgb(243,  253,   37)', 'Lemon yellow'],
29
+  ['rgb(121,    5,   35)', 'Dark crimson'],
30
+  ['rgb(169,   32,   62)', 'Deep carmine'],
31
+  ['rgb(255,  140,    0)', 'Orange'],
32
+  ['rgb(255,  168,   18)', 'Dark tangerine'],
33
+  ['rgb(217,  144,   88)', 'Persian orange'],
34
+  ['rgb(194,  178,  128)', 'Sand'],
35
+  ['rgb(255,  229,  180)', 'Peach'],
36
+  ['rgb(100,   54,   46)', 'Bole'],
37
+  ['rgb(108,   41,   52)', 'Dark cordovan'],
38
+  ['rgb(163,   65,   44)', 'Chestnut'],
39
+  ['rgb(228,  136,  100)', 'Dark salmon'],
40
+  ['rgb(255,  195,  143)', 'Apricot'],
41
+  ['rgb(255,  219,  188)', 'Unbleached silk'],
42
+  ['rgb(242,  227,  198)', 'Straw'],
43
+  ['rgb( 53,   19,   13)', 'Bistre'],
44
+  ['rgb( 84,   42,   14)', 'Dark chocolate'],
45
+  ['rgb(102,   51,   43)', 'Burnt sienna'],
46
+  ['rgb(184,   66,    0)', 'Sienna'],
47
+  ['rgb(216,  153,   12)', 'Yellow ochre'],
48
+  ['rgb(210,  180,  140)', 'Tan'],
49
+  ['rgb(232,  204,  144)', 'Dark wheat'],
50
+  ['rgb(  0,   49,   83)', 'Prussian blue'],
51
+  ['rgb( 48,   69,  119)', 'Dark grey blue'],
52
+  ['rgb(  0,   71,  171)', 'Cobalt blue'],
53
+  ['rgb( 31,  117,  254)', 'Blue'],
54
+  ['rgb(120,  180,  255)', 'Bright french blue'],
55
+  ['rgb(171,  200,  255)', 'Bright steel blue'],
56
+  ['rgb(208,  231,  255)', 'Ice blue'],
57
+  ['rgb( 30,   51,   58)', 'Medium jungle green'],
58
+  ['rgb( 47,   79,   79)', 'Dark slate grey'],
59
+  ['rgb( 74,  104,   93)', 'Dark grullo green'],
60
+  ['rgb(  0,  128,  128)', 'Teal'],
61
+  ['rgb( 67,  170,  176)', 'Turquoise'],
62
+  ['rgb(109,  174,  199)', 'Cerulean frost'],
63
+  ['rgb(173,  217,  186)', 'Tiffany green'],
64
+  ['rgb( 22,   34,   29)', 'Gray-asparagus'],
65
+  ['rgb( 36,   48,   45)', 'Medium dark teal'],
66
+  ['rgb( 74,  104,   93)', 'Xanadu'],
67
+  ['rgb(119,  198,  121)', 'Mint'],
68
+  ['rgb(175,  205,  182)', 'Timberwolf'],
69
+  ['rgb(185,  245,  246)', 'Celeste'],
70
+  ['rgb(193,  255,  234)', 'Aquamarine'],
71
+  ['rgb( 29,   52,   35)', 'Cal Poly Pomona'],
72
+  ['rgb(  1,   68,   33)', 'Forest green'],
73
+  ['rgb( 42,  128,    0)', 'Napier green'],
74
+  ['rgb(128,  128,    0)', 'Olive'],
75
+  ['rgb( 65,  156,  105)', 'Sea green'],
76
+  ['rgb(189,  246,   29)', 'Green-yellow'],
77
+  ['rgb(231,  244,  134)', 'Bright chartreuse'],
78
+  ['rgb(138,   23,  137)', 'Purple'],
79
+  ['rgb( 78,   39,  138)', 'Violet'],
80
+  ['rgb(193,   75,  110)', 'Dark thulian pink'],
81
+  ['rgb(222,   49,   99)', 'Cerise'],
82
+  ['rgb(255,   20,  147)', 'Deep pink'],
83
+  ['rgb(255,  102,  204)', 'Rose pink'],
84
+  ['rgb(255,  203,  219)', 'Pink'],
85
+  ['rgb(255,  255,  255)', 'White'],
86
+  ['rgb(229,   17,    1)', 'RGB Red'],
87
+  ['rgb(  0,  255,    0)', 'RGB Green'],
88
+  ['rgb(  0,    0,  255)', 'RGB Blue'],
89
+  ['rgb(  0,  255,  255)', 'CMYK Cyan'],
90
+  ['rgb(255,    0,  255)', 'CMYK Magenta'],
91
+  ['rgb(255,  255,    0)', 'CMYK Yellow'],
92
+];
93
+
94
+// re-arrange to the right order for display
95
+let palReordered = [];
96
+for (let row = 0; row < 7; row++) {
97
+  for (let col = 0; col < 11; col++) {
98
+    palReordered.push(palette[col * 7 + row]);
99
+  }
100
+  palReordered.push(null); // null indicates a <br />
101
+}
102
+
103
+// Utility for converting base64 image to binary for upload
104
+// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f
105
+function dataURLtoFile(dataurl, filename) {
106
+  let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
107
+    bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
108
+  while(n--){
109
+    u8arr[n] = bstr.charCodeAt(n);
110
+  }
111
+  return new File([u8arr], filename, { type: mime });
112
+}
113
+
114
+const DOODLE_SIZES = {
115
+  normal: [500, 500, 'Square 500'],
116
+  tootbanner: [702, 330, 'Tootbanner'],
117
+  s640x480: [640, 480, '640×480 - 480p'],
118
+  s800x600: [800, 600, '800×600 - SVGA'],
119
+  s720x480: [720, 405, '720x405 - 16:9'],
120
+};
121
+
122
+
123
+const mapStateToProps = state => ({
124
+  options: state.getIn(['compose', 'doodle']),
125
+});
126
+
127
+const mapDispatchToProps = dispatch => ({
128
+  /** Set options in the redux store */
129
+  setOpt: (opts) => dispatch(doodleSet(opts)),
130
+  /** Submit doodle for upload */
131
+  submit: (file) => dispatch(uploadCompose([file])),
132
+});
133
+
134
+/**
135
+ * Doodling dialog with drawing canvas
136
+ *
137
+ * Keyboard shortcuts:
138
+ * - Delete: Clear screen, fill with background color
139
+ * - Backspace, Ctrl+Z: Undo one step
140
+ * - Ctrl held while drawing: Use background color
141
+ * - Shift held while clicking screen: Use fill tool
142
+ *
143
+ * Palette:
144
+ * - Left mouse button: pick foreground
145
+ * - Ctrl + left mouse button: pick background
146
+ * - Right mouse button: pick background
147
+ */
148
+@connect(mapStateToProps, mapDispatchToProps)
149
+export default class DoodleModal extends ImmutablePureComponent {
150
+
151
+  static propTypes = {
152
+    options: ImmutablePropTypes.map,
153
+    onClose: PropTypes.func.isRequired,
154
+    setOpt: PropTypes.func.isRequired,
155
+    submit: PropTypes.func.isRequired,
156
+  };
157
+
158
+  //region Option getters/setters
159
+
160
+  /** Foreground color */
161
+  get fg () {
162
+    return this.props.options.get('fg');
163
+  }
164
+  set fg (value) {
165
+    this.props.setOpt({ fg: value });
166
+  }
167
+
168
+  /** Background color */
169
+  get bg () {
170
+    return this.props.options.get('bg');
171
+  }
172
+  set bg (value) {
173
+    this.props.setOpt({ bg: value });
174
+  }
175
+
176
+  /** Swap Fg and Bg for drawing */
177
+  get swapped () {
178
+    return this.props.options.get('swapped');
179
+  }
180
+  set swapped (value) {
181
+    this.props.setOpt({ swapped: value });
182
+  }
183
+
184
+  /** Mode - 'draw' or 'fill' */
185
+  get mode () {
186
+    return this.props.options.get('mode');
187
+  }
188
+  set mode (value) {
189
+    this.props.setOpt({ mode: value });
190
+  }
191
+
192
+  /** Base line weight */
193
+  get weight () {
194
+    return this.props.options.get('weight');
195
+  }
196
+  set weight (value) {
197
+    this.props.setOpt({ weight: value });
198
+  }
199
+
200
+  /** Drawing opacity */
201
+  get opacity () {
202
+    return this.props.options.get('opacity');
203
+  }
204
+  set opacity (value) {
205
+    this.props.setOpt({ opacity: value });
206
+  }
207
+
208
+  /** Adaptive stroke - change width with speed */
209
+  get adaptiveStroke () {
210
+    return this.props.options.get('adaptiveStroke');
211
+  }
212
+  set adaptiveStroke (value) {
213
+    this.props.setOpt({ adaptiveStroke: value });
214
+  }
215
+
216
+  /** Smoothing (for mouse drawing) */
217
+  get smoothing () {
218
+    return this.props.options.get('smoothing');
219
+  }
220
+  set smoothing (value) {
221
+    this.props.setOpt({ smoothing: value });
222
+  }
223
+
224
+  /** Size preset */
225
+  get size () {
226
+    return this.props.options.get('size');
227
+  }
228
+  set size (value) {
229
+    this.props.setOpt({ size: value });
230
+  }
231
+
232
+  //endregion
233
+
234
+  /** Key up handler */
235
+  handleKeyUp = (e) => {
236
+    if (e.target.nodeName === 'INPUT') return;
237
+
238
+    if (e.key === 'Delete') {
239
+      e.preventDefault();
240
+      this.handleClearBtn();
241
+      return;
242
+    }
243
+
244
+    if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) {
245
+      e.preventDefault();
246
+      this.undo();
247
+    }
248
+
249
+    if (e.key === 'Control' || e.key === 'Meta') {
250
+      this.controlHeld = false;
251
+      this.swapped = false;
252
+    }
253
+
254
+    if (e.key === 'Shift') {
255
+      this.shiftHeld = false;
256
+      this.mode = 'draw';
257
+    }
258
+  };
259
+
260
+  /** Key down handler */
261
+  handleKeyDown = (e) => {
262
+    if (e.key === 'Control' || e.key === 'Meta') {
263
+      this.controlHeld = true;
264
+      this.swapped = true;
265
+    }
266
+
267
+    if (e.key === 'Shift') {
268
+      this.shiftHeld = true;
269
+      this.mode = 'fill';
270
+    }
271
+  };
272
+
273
+  /**
274
+   * Component installed in the DOM, do some initial set-up
275
+   */
276
+  componentDidMount () {
277
+    this.controlHeld = false;
278
+    this.shiftHeld = false;
279
+    this.swapped = false;
280
+    window.addEventListener('keyup', this.handleKeyUp, false);
281
+    window.addEventListener('keydown', this.handleKeyDown, false);
282
+  };
283
+
284
+  /**
285
+   * Tear component down
286
+   */
287
+  componentWillUnmount () {
288
+    window.removeEventListener('keyup', this.handleKeyUp, false);
289
+    window.removeEventListener('keydown', this.handleKeyDown, false);
290
+    if (this.sketcher) this.sketcher.destroy();
291
+  }
292
+
293
+  /**
294
+   * Set reference to the canvas element.
295
+   * This is called during component init
296
+   *
297
+   * @param elem - canvas element
298
+   */
299
+  setCanvasRef = (elem) => {
300
+    this.canvas = elem;
301
+    if (elem) {
302
+      elem.addEventListener('dirty', () => {
303
+        this.saveUndo();
304
+        this.sketcher._dirty = false;
305
+      });
306
+
307
+      elem.addEventListener('click', () => {
308
+        // sketcher bug - does not fire dirty on fill
309
+        if (this.mode === 'fill') {
310
+          this.saveUndo();
311
+        }
312
+      });
313
+
314
+      // prevent context menu
315
+      elem.addEventListener('contextmenu', (e) => {
316
+        e.preventDefault();
317
+      });
318
+
319
+      elem.addEventListener('mousedown', (e) => {
320
+        if (e.button === 2) {
321
+          this.swapped = true;
322
+        }
323
+      });
324
+
325
+      elem.addEventListener('mouseup', (e) => {
326
+        if (e.button === 2) {
327
+          this.swapped = this.controlHeld;
328
+        }
329
+      });
330
+
331
+      this.initSketcher(elem);
332
+      this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
333
+    }
334
+  };
335
+
336
+  /**
337
+   * Set up the sketcher instance
338
+   *
339
+   * @param canvas - canvas element. Null if we're just resizing
340
+   */
341
+  initSketcher (canvas = null) {
342
+    const sizepreset = DOODLE_SIZES[this.size];
343
+
344
+    if (this.sketcher) this.sketcher.destroy();
345
+    this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]);
346
+
347
+    if (canvas) {
348
+      this.ctx = this.sketcher.context;
349
+      this.updateSketcherSettings();
350
+    }
351
+
352
+    this.clearScreen();
353
+  }
354
+
355
+  /**
356
+   * Done button handler
357
+   */
358
+  onDoneButton = () => {
359
+    const dataUrl = this.sketcher.toImage();
360
+    const file = dataURLtoFile(dataUrl, 'doodle.png');
361
+    this.props.submit(file);
362
+    this.props.onClose(); // close dialog
363
+  };
364
+
365
+  /**
366
+   * Cancel button handler
367
+   */
368
+  onCancelButton = () => {
369
+    if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) {
370
+      return;
371
+    }
372
+
373
+    this.props.onClose(); // close dialog
374
+  };
375
+
376
+  /**
377
+   * Update sketcher options based on state
378
+   */
379
+  updateSketcherSettings () {
380
+    if (!this.sketcher) return;
381
+
382
+    if (this.oldSize !== this.size) this.initSketcher();
383
+
384
+    this.sketcher.color = (this.swapped ? this.bg : this.fg);
385
+    this.sketcher.opacity = this.opacity;
386
+    this.sketcher.weight = this.weight;
387
+    this.sketcher.mode = this.mode;
388
+    this.sketcher.smoothing = this.smoothing;
389
+    this.sketcher.adaptiveStroke = this.adaptiveStroke;
390
+
391
+    this.oldSize = this.size;
392
+  }
393
+
394
+  /**
395
+   * Fill screen with background color
396
+   */
397
+  clearScreen = () => {
398
+    this.ctx.fillStyle = this.bg;
399
+    this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2);
400
+    this.undos = [];
401
+
402
+    this.doSaveUndo();
403
+  };
404
+
405
+  /**
406
+   * Undo one step
407
+   */
408
+  undo = () => {
409
+    if (this.undos.length > 1) {
410
+      this.undos.pop();
411
+      const buf = this.undos.pop();
412
+
413
+      this.sketcher.clear();
414
+      this.ctx.putImageData(buf, 0, 0);
415
+      this.doSaveUndo();
416
+    }
417
+  };
418
+
419
+  /**
420
+   * Save canvas content into the undo buffer immediately
421
+   */
422
+  doSaveUndo = () => {
423
+    this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height));
424
+  };
425
+
426
+  /**
427
+   * Called on each canvas change.
428
+   * Saves canvas content to the undo buffer after some period of inactivity.
429
+   */
430
+  saveUndo = debounce(() => {
431
+    this.doSaveUndo();
432
+  }, 100);
433
+
434
+  /**
435
+   * Palette left click.
436
+   * Selects Fg color (or Bg, if Control/Meta is held)
437
+   *
438
+   * @param e - event
439
+   */
440
+  onPaletteClick = (e) => {
441
+    const c = e.target.dataset.color;
442
+
443
+    if (this.controlHeld) {
444
+      this.bg = c;
445
+    } else {
446
+      this.fg = c;
447
+    }
448
+
449
+    e.target.blur();
450
+    e.preventDefault();
451
+  };
452
+
453
+  /**
454
+   * Palette right click.
455
+   * Selects Bg color
456
+   *
457
+   * @param e - event
458
+   */
459
+  onPaletteRClick = (e) => {
460
+    this.bg = e.target.dataset.color;
461
+    e.target.blur();
462
+    e.preventDefault();
463
+  };
464
+
465
+  /**
466
+   * Handle click on the Draw mode button
467
+   *
468
+   * @param e - event
469
+   */
470
+  setModeDraw = (e) => {
471
+    this.mode = 'draw';
472
+    e.target.blur();
473
+  };
474
+
475
+  /**
476
+   * Handle click on the Fill mode button
477
+   *
478
+   * @param e - event
479
+   */
480
+  setModeFill = (e) => {
481
+    this.mode = 'fill';
482
+    e.target.blur();
483
+  };
484
+
485
+  /**
486
+   * Handle click on Smooth checkbox
487
+   *
488
+   * @param e - event
489
+   */
490
+  tglSmooth = (e) => {
491
+    this.smoothing = !this.smoothing;
492
+    e.target.blur();
493
+  };
494
+
495
+  /**
496
+   * Handle click on Adaptive checkbox
497
+   *
498
+   * @param e - event
499
+   */
500
+  tglAdaptive = (e) => {
501
+    this.adaptiveStroke = !this.adaptiveStroke;
502
+    e.target.blur();
503
+  };
504
+
505
+  /**
506
+   * Handle change of the Weight input field
507
+   *
508
+   * @param e - event
509
+   */
510
+  setWeight = (e) => {
511
+    this.weight = +e.target.value || 1;
512
+  };
513
+
514
+  /**
515
+   * Set size - clalback from the select box
516
+   *
517
+   * @param e - event
518
+   */
519
+  changeSize = (e) => {
520
+    let newSize = e.target.value;
521
+    if (newSize === this.oldSize) return;
522
+
523
+    if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) {
524
+      return;
525
+    }
526
+
527
+    this.size = newSize;
528
+  };
529
+
530
+  handleClearBtn = () => {
531
+    if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) {
532
+      return;
533
+    }
534
+
535
+    this.clearScreen();
536
+  };
537
+
538
+  /**
539
+   * Render the component
540
+   */
541
+  render () {
542
+    this.updateSketcherSettings();
543
+
544
+    return (
545
+      <div className='modal-root__modal doodle-modal'>
546
+        <div className='doodle-modal__container'>
547
+          <canvas ref={this.setCanvasRef} />
548
+        </div>
549
+
550
+        <div className='doodle-modal__action-bar'>
551
+          <div className='doodle-toolbar'>
552
+            <Button text='Done' onClick={this.onDoneButton} />
553
+            <Button text='Cancel' onClick={this.onCancelButton} />
554
+          </div>
555
+          <div className='filler' />
556
+          <div className='doodle-toolbar with-inputs'>
557
+            <div>
558
+              <label htmlFor='dd_smoothing'>Smoothing</label>
559
+              <span className='val'>
560
+                <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} />
561
+              </span>
562
+            </div>
563
+            <div>
564
+              <label htmlFor='dd_adaptive'>Adaptive</label>
565
+              <span className='val'>
566
+                <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} />
567
+              </span>
568
+            </div>
569
+            <div>
570
+              <label htmlFor='dd_weight'>Weight</label>
571
+              <span className='val'>
572
+                <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
573
+              </span>
574
+            </div>
575
+            <div>
576
+              <select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}>
577
+                { Object.values(mapValues(DOODLE_SIZES, (val, k) =>
578
+                  <option key={k} value={k}>{val[2]}</option>
579
+                )) }
580
+              </select>
581
+            </div>
582
+          </div>
583
+          <div className='doodle-toolbar'>
584
+            <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
585
+            <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
586
+            <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
587
+            <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
588
+          </div>
589
+          <div className='doodle-palette'>
590
+            {
591
+              palReordered.map((c, i) =>
592
+                c === null ?
593
+                  <br key={i} /> :
594
+                  <button
595
+                    key={i}
596
+                    style={{ backgroundColor: c[0] }}
597
+                    onClick={this.onPaletteClick}
598
+                    onContextMenu={this.onPaletteRClick}
599
+                    data-color={c[0]}
600
+                    title={c[1]}
601
+                    className={classNames({
602
+                      'foreground': this.fg === c[0],
603
+                      'background': this.bg === c[0],
604
+                    })}
605
+                  />
606
+              )
607
+            }
608
+          </div>
609
+        </div>
610
+      </div>
611
+    );
612
+  }
613
+
614
+}

+ 13
- 1
app/javascript/mastodon/features/ui/components/modal_root.js View File

@@ -8,6 +8,7 @@ import ActionsModal from './actions_modal';
8 8
 import MediaModal from './media_modal';
9 9
 import VideoModal from './video_modal';
10 10
 import BoostModal from './boost_modal';
11
+import DoodleModal from './doodle_modal';
11 12
 import ConfirmationModal from './confirmation_modal';
12 13
 import FocalPointModal from './focal_point_modal';
13 14
 import {
@@ -23,6 +24,7 @@ const MODAL_COMPONENTS = {
23 24
   'ONBOARDING': OnboardingModal,
24 25
   'VIDEO': () => Promise.resolve({ default: VideoModal }),
25 26
   'BOOST': () => Promise.resolve({ default: BoostModal }),
27
+  'DOODLE': () => Promise.resolve({ default: DoodleModal }),
26 28
   'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
27 29
   'MUTE': MuteModal,
28 30
   'REPORT': ReportModal,
@@ -43,6 +45,16 @@ export default class ModalRoot extends React.PureComponent {
43 45
   getSnapshotBeforeUpdate () {
44 46
     return { visible: !!this.props.type };
45 47
   }
48
+  state = {
49
+    revealed: false,
50
+  };
51
+
52
+  handleKeyUp = (e) => {
53
+    if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
54
+         && !!this.props.type && !this.props.props.noEsc) {
55
+      this.props.onClose();
56
+    }
57
+  }
46 58
 
47 59
   componentDidUpdate (prevProps, prevState, { visible }) {
48 60
     if (visible) {
@@ -53,7 +65,7 @@ export default class ModalRoot extends React.PureComponent {
53 65
   }
54 66
 
55 67
   renderLoading = modalId => () => {
56
-    return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
68
+    return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
57 69
   }
58 70
 
59 71
   renderError = (props) => {

+ 293
- 0
app/javascript/mastodon/locales/en-CY.json View File

@@ -0,0 +1,293 @@
1
+{
2
+  "account.block": "Block @{name}",
3
+  "account.block_domain": "Hide everything from {domain}",
4
+  "account.blocked": "Blocked",
5
+  "account.disclaimer_full": "THESE NUMBERS ARE THE STUFF WHAT YOUR SERVER KNOWS ABOUT AND THERE MIGHT BE MORE THAT IT DONT KNOW ABOUT.",
6
+  "account.domain_blocked": "Domain hidden",
7
+  "account.edit_profile": "edit ~/.profile",
8
+  "account.follow": "Follow",
9
+  "account.followers": "Followers",
10
+  "account.follows": "Follows",
11
+  "account.follows_you": "Follows you",
12
+  "account.hide_reblogs": "Hide boosts from @{name}",
13
+  "account.media": "Media",
14
+  "account.mention": "Mention @{name}",
15
+  "account.moved_to": "{name} has moved to:",
16
+  "account.mute": "Mute @{name}",
17
+  "account.mute_notifications": "Mute notifications from @{name}",
18
+  "account.muted": "Muted",
19
+  "account.posts": "Pings",
20
+  "account.posts_with_replies": "Pings with replies",
21
+  "account.report": "Report @{name}",
22
+  "account.requested": "Awaiting approval. Click to cancel follow request",
23
+  "account.share": "Share @{name}'s profile",
24
+  "account.show_reblogs": "Show boosts from @{name}",
25
+  "account.unblock": "Unblock @{name}",
26
+  "account.unblock_domain": "Unhide {domain}",
27
+  "account.unfollow": "Unfollow",
28
+  "account.unmute": "Unmute @{name}",
29
+  "account.unmute_notifications": "Unmute notifications from @{name}",
30
+  "account.view_full_profile": "View full profile",
31
+  "boost_modal.combo": "You can press {combo} to skip this next time",
32
+  "bundle_column_error.body": "Something went wrong while loading this component.",
33
+  "bundle_column_error.retry": "Try again",
34
+  "bundle_column_error.title": "Network error",
35
+  "bundle_modal_error.close": "Close",
36
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
37
+  "bundle_modal_error.retry": "Try again",
38
+  "column.blocks": "~/.blocked",
39
+  "column.community": "/timelines/local",
40
+  "column.direct": "~/.dms",
41
+  "column.favourites": "~/.florps",
42
+  "column.follow_requests": "~/.follow-requests",
43
+  "column.home": "/timelines/home",
44
+  "column.lists": "Lists",
45
+  "column.mutes": "~/.muted",
46
+  "column.notifications": "~/.notifications",
47
+  "column.pins": "~/.pinned",
48
+  "column.public": "/timelines/federated",
49
+  "column_back_button.label": "Back",
50
+  "column_header.hide_settings": "Hide settings",
51
+  "column_header.moveLeft_settings": "Move column to the left",
52
+  "column_header.moveRight_settings": "Move column to the right",
53
+  "column_header.pin": "Pin",
54
+  "column_header.show_settings": "Show settings",
55
+  "column_header.unpin": "Unpin",
56
+  "column_subheading.navigation": "Navigation",
57
+  "column_subheading.settings": "Settings",
58
+  "compose.attach": "Attach...",
59
+  "compose.attach.doodle": "Draw something",
60
+  "compose.attach.upload": "Upload a file",
61
+  "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.",
62
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
63
+  "compose_form.lock_disclaimer.lock": "locked",
64
+  "compose_form.placeholder": "What is in your databanks?",
65
+  "compose_form.publish": "Ping",
66
+  "compose_form.publish_loud": "{publish}!",
67
+  "compose_form.sensitive.marked": "Media is marked as sensitive",
68
+  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
69
+  "compose_form.spoiler.marked": "Text is hidden behind warning",
70
+  "compose_form.spoiler.unmarked": "Text is not hidden",
71
+  "compose_form.spoiler_placeholder": "Write your warning here",
72
+  "confirmation_modal.cancel": "Cancel",
73
+  "confirmations.block.confirm": "Block",
74
+  "confirmations.block.message": "Are you sure you want to block {name}?",
75
+  "confirmations.delete.confirm": "Delete",
76
+  "confirmations.delete.message": "Are you sure you want to delete this status?",
77
+  "confirmations.delete_list.confirm": "Delete",
78
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
79
+  "confirmations.domain_block.confirm": "Hide entire domain",
80
+  "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.",
81
+  "confirmations.mute.confirm": "Mute",
82
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
83
+  "confirmations.unfollow.confirm": "Unfollow",
84
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
85
+  "doodle_button.label": "Add a drawing",
86
+  "embed.instructions": "Embed this status on your website by copying the code below.",
87
+  "embed.preview": "Here is what it will look like:",
88
+  "emoji_button.activity": "Activity",
89
+  "emoji_button.custom": "Custom",
90
+  "emoji_button.flags": "Flags",
91
+  "emoji_button.food": "Food & Drink",
92
+  "emoji_button.label": "Insert emoji",
93
+  "emoji_button.nature": "Nature",
94
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
95
+  "emoji_button.objects": "Objects",
96
+  "emoji_button.people": "People",
97
+  "emoji_button.recent": "Frequently used",
98
+  "emoji_button.search": "Search...",
99
+  "emoji_button.search_results": "Search results",
100
+  "emoji_button.symbols": "Symbols",
101
+  "emoji_button.travel": "Travel & Places",
102
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
103
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
104
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
105
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use query to get started and meet other users.",
106
+  "empty_column.home.public_timeline": "the public timeline",
107
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
108
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
109
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
110
+  "follow_request.authorize": "Authorize",
111
+  "follow_request.reject": "Reject",
112
+  "getting_started.appsshort": "Apps",
113
+  "getting_started.faq": "FAQ",
114
+  "getting_started.heading": "Getting started",
115
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
116
+  "getting_started.userguide": "User Guide",
117
+  "home.column_settings.advanced": "Advanced",
118
+  "home.column_settings.basic": "Basic",
119
+  "home.column_settings.filter_regex": "Filter out by regular expressions",
120
+  "home.column_settings.show_reblogs": "Show relays",
121
+  "home.column_settings.show_replies": "Show replies",
122
+  "home.settings": "Column settings",
123
+  "keyboard_shortcuts.back": "to navigate back",
124
+  "keyboard_shortcuts.boost": "to boost",
125
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
126
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
127
+  "keyboard_shortcuts.description": "Description",
128
+  "keyboard_shortcuts.down": "to move down in the list",
129
+  "keyboard_shortcuts.enter": "to open status",
130
+  "keyboard_shortcuts.favourite": "to favourite",
131
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
132
+  "keyboard_shortcuts.hotkey": "Hotkey",
133
+  "keyboard_shortcuts.legend": "to display this legend",
134
+  "keyboard_shortcuts.mention": "to mention author",
135
+  "keyboard_shortcuts.reply": "to reply",
136
+  "keyboard_shortcuts.search": "to focus search",
137
+  "keyboard_shortcuts.toot": "to start a brand new ping",
138
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
139
+  "keyboard_shortcuts.up": "to move up in the list",
140
+  "lightbox.close": "Close",
141
+  "lightbox.next": "Next",
142
+  "lightbox.previous": "Previous",
143
+  "lists.account.add": "Add to list",
144
+  "lists.account.remove": "Remove from list",
145
+  "lists.delete": "Delete list",
146
+  "lists.edit": "Edit list",
147
+  "lists.new.create": "Add list",
148
+  "lists.new.title_placeholder": "New list title",
149
+  "lists.search": "Search among people you follow",
150
+  "lists.subheading": "Your lists",
151
+  "loading_indicator.label": "Loading...",
152
+  "media_gallery.toggle_visible": "Toggle visibility",
153
+  "missing_indicator.label": "Not found",
154
+  "missing_indicator.sublabel": "This resource could not be found",
155
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
156
+  "navigation_bar.blocks": "~/.blocks",
157
+  "navigation_bar.community_timeline": "/timelines/local",
158
+  "navigation_bar.direct": "~/.dms",
159
+  "navigation_bar.edit_profile": "edit ~/.profile",
160
+  "navigation_bar.favourites": "~/.florps",
161
+  "navigation_bar.follow_requests": "~/.follow-requests",
162
+  "navigation_bar.info": "/about/more",
163
+  "navigation_bar.keyboard_shortcuts": "~/.kbd/shortcuts.conf",
164
+  "navigation_bar.lists": "~/.lists",
165
+  "navigation_bar.logout": "Jack out",
166
+  "navigation_bar.mutes": "~/.muted",
167
+  "navigation_bar.pins": "~/.pinned",
168
+  "navigation_bar.preferences": "edit ~/.config",
169
+  "navigation_bar.public_timeline": "/timelines/federated",
170
+  "notification.favourite": "{name} florped your ping",
171
+  "notification.follow": "{name} followed you",
172
+  "notification.mention": "{name} mentioned you",
173
+  "notification.reblog": "{name} relayed your ping",
174
+  "notifications.clear": "Clear notifications",
175
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
176
+  "notifications.column_settings.alert": "Desktop notifications",
177
+  "notifications.column_settings.favourite": "Favourites:",
178
+  "notifications.column_settings.follow": "New followers:",
179
+  "notifications.column_settings.mention": "Mentions:",
180
+  "notifications.column_settings.push": "Push notifications",
181
+  "notifications.column_settings.push_meta": "This device",
182
+  "notifications.column_settings.reblog": "Boosts:",
183
+  "notifications.column_settings.show": "Show in column",
184
+  "notifications.column_settings.sound": "Play sound",
185
+  "onboarding.done": "Done",
186
+  "onboarding.next": "Next",
187
+  "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.",
188
+  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
189
+  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
190
+  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
191
+  "onboarding.page_one.full_handle": "Your full handle",
192
+  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
193
+  "onboarding.page_one.welcome": "Welcome to Mastodon!",
194
+  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
195
+  "onboarding.page_six.almost_done": "Almost done...",
196
+  "onboarding.page_six.appetoot": "Hang ten on the cybrewaves!",
197
+  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
198
+  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
199
+  "onboarding.page_six.guidelines": "community guidelines",
200
+  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
201
+  "onboarding.page_six.various_app": "mobile apps",
202
+  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
203
+  "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.",
204
+  "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.",
205
+  "onboarding.skip": "Skip",
206
+  "privacy.change": "Adjust status privacy",
207
+  "privacy.direct.long": "Post to mentioned users only",
208
+  "privacy.direct.short": "Direct",
209
+  "privacy.private.long": "Post to followers only",
210
+  "privacy.private.short": "Followers-only",
211
+  "privacy.public.long": "Post to public timelines",
212
+  "privacy.public.short": "Public",
213
+  "privacy.unlisted.long": "Do not post to public timelines",
214
+  "privacy.unlisted.short": "Unlisted",
215
+  "regeneration_indicator.label": "Loading…",
216
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
217
+  "relative_time.days": "{number}d",
218
+  "relative_time.hours": "{number}h",
219
+  "relative_time.just_now": "now",
220
+  "relative_time.minutes": "{number}m",
221
+  "relative_time.seconds": "{number}s",
222
+  "reply_indicator.cancel": "Cancel",
223
+  "report.forward": "Forward to {target}",
224
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
225
+  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
226
+  "report.placeholder": "Additional comments",
227
+  "report.submit": "Submit",
228
+  "report.target": "Reporting {target}",
229
+  "search.placeholder": "Query...",
230
+  "search_popout.search_format": "Advanced search format",
231
+  "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.",
232
+  "search_popout.tips.hashtag": "hashtag",
233
+  "search_popout.tips.status": "status",
234
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
235
+  "search_popout.tips.user": "user",
236
+  "search_results.accounts": "People",
237
+  "search_results.hashtags": "Hashtags",
238
+  "search_results.statuses": "Pings",
239
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
240
+  "standalone.public_title": "Peer into the data grid...",
241
+  "status.block": "Block @{name}",
242
+  "status.cannot_reblog": "This ping cannot be relayed",
243
+  "status.delete": "Delete",
244
+  "status.embed": "Embed",
245
+  "status.favourite": "Florp",
246
+  "status.load_more": "Load more",
247
+  "status.media_hidden": "Media hidden",
248
+  "status.mention": "Mention @{name}",
249
+  "status.more": "More",
250
+  "status.mute": "Mute @{name}",
251
+  "status.mute_conversation": "Mute conversation",
252
+  "status.open": "Expand this status",
253
+  "status.pin": "Pin on profile",
254
+  "status.pinned": "Pinned ping",
255
+  "status.reblog": "Relay",
256
+  "status.reblogged_by": "{name} relayed",
257
+  "status.reply": "Reply",
258
+  "status.replyAll": "Reply to thread",
259
+  "status.report": "Report @{name}",
260
+  "status.sensitive_toggle": "Click to view",
261
+  "status.sensitive_warning": "Sensitive content",
262
+  "status.share": "Share",
263
+  "status.show_less": "Show less",
264
+  "status.show_less_all": "Show less for all",
265
+  "status.show_more": "Show more",
266
+  "status.show_more_all": "Show more for all",
267
+  "status.unmute_conversation": "Unmute conversation",
268
+  "status.unpin": "Unpin from profile",
269
+  "tabs_bar.federated_timeline": "/timelines/federated",
270
+  "tabs_bar.home": "/timelines/home",
271
+  "tabs_bar.local_timeline": "/timelines/local",
272
+  "tabs_bar.notifications": "~/.notifications",
273
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
274
+  "upload_area.title": "Drag & drop to upload",
275
+  "upload_button.label": "Add media",
276
+  "upload_form.description": "Describe for the visually impaired",
277
+  "upload_form.focus": "Crop",
278
+  "upload_form.undo": "Undo",
279
+  "upload_progress.label": "Uploading...",
280
+  "video.close": "Close video",
281
+  "video.exit_fullscreen": "Exit full screen",
282
+  "video.expand": "Expand video",
283
+  "video.fullscreen": "Full screen",
284
+  "video.hide": "Hide video",
285
+  "video.mute": "Mute sound",
286
+  "video.pause": "Pause",
287
+  "video.play": "Play",
288
+  "video.unmute": "Unmute sound",
289
+  "video_player.expand": "Expand video",
290
+  "video_player.toggle_sound": "Toggle sound",
291
+  "video_player.toggle_visible": "Toggle visibility",
292
+  "video_player.video_error": "Video could not be played"
293
+}

+ 2
- 0
app/javascript/mastodon/locales/whitelist_en-CY.json View File

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

+ 14
- 0
app/javascript/mastodon/reducers/compose.js View File

@@ -28,6 +28,7 @@ import {
28 28
   COMPOSE_UPLOAD_CHANGE_REQUEST,
29 29
   COMPOSE_UPLOAD_CHANGE_SUCCESS,
30 30
   COMPOSE_UPLOAD_CHANGE_FAIL,
31
+  COMPOSE_DOODLE_SET,
31 32
   COMPOSE_RESET,
32 33
 } from '../actions/compose';
33 34
 import { TIMELINE_DELETE } from '../actions/timelines';
@@ -61,6 +62,17 @@ const initialState = ImmutableMap({
61 62
   resetFileKey: Math.floor((Math.random() * 0x10000)),
62 63
   idempotencyKey: null,
63 64
   tagHistory: ImmutableList(),
65
+  doodle: ImmutableMap({
66
+    fg: 'rgb(  0,    0,    0)',
67
+    bg: 'rgb(255,  255,  255)',
68
+    swapped: false,
69
+    mode: 'draw',
70
+    size: 'normal',
71
+    weight: 2,
72
+    opacity: 1,
73
+    adaptiveStroke: true,
74
+    smoothing: false,
75
+  }),
64 76
 });
65 77
 
66 78
 function statusToTextMentions(state, status) {
@@ -326,6 +338,8 @@ export default function compose(state = initialState, action) {
326 338
         map.set('spoiler_text', '');
327 339
       }
328 340
     });
341
+  case COMPOSE_DOODLE_SET:
342
+    return state.mergeIn(['doodle'], action.options);
329 343
   default:
330 344
     return state;
331 345
   }

+ 32
- 7
app/javascript/packs/public.js View File

@@ -17,6 +17,12 @@ window.addEventListener('message', e => {
17 17
       id: data.id,
18 18
       height: document.getElementsByTagName('html')[0].scrollHeight,
19 19
     }, '*');
20
+
21
+    if (document.fonts && document.fonts.ready) {
22
+      document.fonts.ready.then(sizeBioText);
23
+    } else {
24
+      sizeBioText();
25
+    }
20 26
   });
21 27
 });
22 28
 
@@ -91,6 +97,17 @@ function main() {
91 97
       detailedStatuses[0].scrollIntoView();
92 98
       history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
93 99
     }
100
+
101
+    [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
102
+      const props = JSON.parse(content.getAttribute('data-props'));
103
+      ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
104
+    });
105
+
106
+    if (document.fonts && document.fonts.ready) {
107
+      document.fonts.ready.then(sizeBioText);
108
+    } else {
109
+      sizeBioText();
110
+    }
94 111
   });
95 112
 
96 113
   delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@@ -142,13 +159,7 @@ function main() {
142 159
     }
143 160
   });
144 161
 
145
-  delegate(document, '#account_note', 'input', ({ target }) => {
146
-    const noteCounter = document.querySelector('.note-counter');
147
-
148
-    if (noteCounter) {
149
-      noteCounter.textContent = 160 - length(target.value);
150
-    }
151
-  });
162
+  delegate(document, '#account_note', 'input', sizeBioText);
152 163
 
153 164
   delegate(document, '#account_avatar', 'change', ({ target }) => {
154 165
     const avatar = document.querySelector('.card .avatar img');
@@ -175,6 +186,20 @@ function main() {
175 186
       lock.style.display = 'none';
176 187
     }
177 188
   });
189
+
190
+  function sizeBioText() {
191
+    const noteCounter = document.querySelector('.note-counter');
192
+    const bioTextArea = document.querySelector('#account_note');
193
+
194
+    if (noteCounter) {
195
+      noteCounter.textContent = 413 - length(bioTextArea.value);
196
+    }
197
+
198
+    if (bioTextArea) {
199
+      bioTextArea.style.height = 'auto';
200
+      bioTextArea.style.height = (bioTextArea.scrollHeight+3) + 'px';
201
+    }
202
+  }
178 203
 }
179 204
 
180 205
 loadPolyfills().then(main).catch(error => {

+ 63
- 0
app/javascript/styles/cybre-base.scss View File

@@ -0,0 +1,63 @@
1
+
2
+@import 'application';
3
+
4
+/* Allow columns to grow wider as the screen gets 
5
+ * wider, but don't ever let them get more than
6
+ * 400px (some people have a bunch of columns!) */
7
+@media screen and (min-width: 1300px) {
8
+  .column {
9
+    flex-grow: 1 !important;
10
+    max-width: 400px;
11
+  }
12
+
13
+  .drawer {
14
+    width: 17%; /* Not part of the flex fun */
15
+    max-width: 400px;
16
+    min-width: 330px;
17
+  }
18
+}
19
+
20
+/* Cap the column height at 100vh (fixed an old
21
+ * bug someone encountered in safari, but which
22
+ * I've seen resurface from time to time) */
23
+.column {
24
+  max-height:100vh;
25
+}
26
+
27
+/* Don't show outline around statuses if we're in 
28
+ * mouse or touch mode (rather than keyboard) */
29
+[data-whatinput="mouse"], [data-whatinput="touch"] {
30
+  .status__content:focus, .status:focus,
31
+  .status__wrapper:focus, .status__content__text:focus {
32
+    outline: none;
33
+  }
34
+}
35
+
36
+/* Show a little arrowey thing after the time in a
37
+ * status to signal that you can click it to see
38
+ * a detailed view */
39
+.status time:after,
40
+.detailed-status__datetime span:after {
41
+  font: normal normal normal 14px/1 FontAwesome;
42
+  content: "\00a0\00a0\f08e";
43
+}
44
+
45
+/* Don't display the elephant mascot (we have our
46
+ * own, thanks) */
47
+.drawer__inner__mastodon {
48
+  display: none;
49
+}
50
+
51
+/* Let the compose area/drawer be short, but
52
+ * expand if necessary */
53
+.drawer .drawer__inner {
54
+  overflow: visible;
55
+  height:inherit;
56
+  background-image: none;
57
+}
58
+.drawer__pager {
59
+  overflow-y:auto;
60
+}
61
+
62
+@import 'fullwidth-media';
63
+

+ 96
- 0
app/javascript/styles/doodle.scss View File

@@ -0,0 +1,96 @@
1
+$doodleBg: #d9e1e8;
2
+.doodle-modal {
3
+  @extend .boost-modal;
4
+  width: unset;
5
+}
6
+
7
+.doodle-modal__container {
8
+  background: $doodleBg;
9
+  text-align: center;
10
+  line-height: 0; // remove weird gap under canvas
11
+  canvas {
12
+    border: 5px solid $doodleBg;
13
+  }
14
+}
15
+
16
+.doodle-modal__action-bar {
17
+  @extend .boost-modal__action-bar;
18
+
19
+  .filler {
20
+    flex-grow: 1;
21
+    margin: 0;
22
+    padding: 0;
23
+  }
24
+
25
+  .doodle-toolbar {
26
+    line-height: 1;
27
+
28
+    display: flex;
29
+    flex-direction: column;
30
+    flex-grow: 0;
31
+    justify-content: space-around;
32
+
33
+    &.with-inputs {
34
+      label {
35
+        display: inline-block;
36
+        width: 70px;
37
+        text-align: right;
38
+        margin-right: 2px;
39
+      }
40
+
41
+      input[type="number"],input[type="text"] {
42
+        width: 40px;
43
+      }
44
+      span.val {
45
+        display: inline-block;
46
+        text-align: left;
47
+        width: 50px;
48
+      }
49
+    }
50
+  }
51
+
52
+  .doodle-palette {
53
+    padding-right: 0 !important;
54
+    border: 1px solid black;
55
+    line-height: .2rem;
56
+    flex-grow: 0;
57
+    background: white;
58
+
59
+    button {
60
+      appearance: none;
61
+      width: 1rem;
62
+      height: 1rem;
63
+      margin: 0; padding: 0;
64
+      text-align: center;
65
+      color: black;
66
+      text-shadow: 0 0 1px white;
67
+      cursor: pointer;
68
+      box-shadow: inset 0 0 1px rgba(white, .5);
69
+      border: 1px solid black;
70
+      outline-offset:-1px;
71
+
72
+      &.foreground {
73
+        outline: 1px dashed white;
74
+      }
75
+
76
+      &.background {
77
+        outline: 1px dashed red;
78
+      }
79
+
80
+      &.foreground.background {
81
+        outline: 1px dashed red;
82
+        border-color: white;
83
+      }
84
+    }
85
+  }
86
+}
87
+
88
+.compose-form__buttons-separator { 
89
+  border-left: 1px solid #c3c3c3; 
90
+  margin: 0 3px; 
91
+} 
92
+
93
+.compose-form__upload-button-icon {
94
+  line-height: 27px;
95
+}
96
+

+ 48
- 0
app/javascript/styles/fullwidth-media.scss View File

@@ -0,0 +1,48 @@
1
+
2
+/* So we can position things absolute to it*/
3
+.status__content {
4
+  position:relative;
5
+}
6
+
7
+/* Use 30% of the viewport height always*/
8
+.detailed-status > .media-spoiler,
9
+.status > .media-spoiler,
10
+.video-player,
11
+.media-gallery {
12
+  max-height:30vh;
13
+  height:30vh !important;
14
+  position:relative;
15
+  margin-top:20px;
16
+  margin-left:-68px;
17
+  width: calc(100% + 80px);
18
+}
19
+
20
+/* Unset max-width for spoilers and the video
21
+ * player */
22
+.detailed-status > .media-spoiler,
23
+.status > .media-spoiler,
24
+.video-player {
25
+  max-width: none;
26
+}
27
+
28
+
29
+.detailed-status .media-spoiler,
30
+.status .media-spoiler {
31
+  height:30vh !important;
32
+  vertical-align:middle;
33
+}
34
+
35
+
36
+.status__video-player-video {
37
+  transform:unset;
38
+  top:unset;
39
+}
40
+
41
+
42
+/* Adjust offset and width for detail view */
43
+.detailed-status .media-gallery {
44
+  margin-left:-10px;
45
+  width: calc(100% + 22px);
46
+}
47
+
48
+

+ 73
- 1
app/javascript/styles/mastodon/components.scss View File

@@ -383,7 +383,6 @@
383 383
     padding: 10px;
384 384
     cursor: pointer;
385 385
     border-radius: 4px;
386
-
387 386
     &:hover,
388 387
     &:focus,
389 388
     &:active,
@@ -3404,6 +3403,78 @@ a.status-card {
3404 3403
   }
3405 3404
 }
3406 3405
 
3406
+.advanced-options-dropdown {
3407
+  position: relative;
3408
+}
3409
+
3410
+.advanced-options-dropdown__dropdown {
3411
+  display: none;
3412
+  position: absolute;
3413
+  left: 0;
3414
+  top: 27px;
3415
+  width: 210px;
3416
+  background: $simple-background-color;
3417
+  border-radius: 0 4px 4px;
3418
+  z-index: 2;
3419
+  overflow: hidden;
3420
+}
3421
+
3422
+.advanced-options-dropdown__option {
3423
+  color: $ui-base-color;
3424
+  padding: 10px;
3425
+  cursor: pointer;
3426
+  display: flex;
3427
+
3428
+  &:hover,
3429
+  &.active {
3430
+    background: $ui-highlight-color;
3431
+    color: $primary-text-color;
3432
+
3433
+    .advanced-options-dropdown__option__content {
3434
+      color: $primary-text-color;
3435
+
3436
+      strong {
3437
+        color: $primary-text-color;
3438
+      }
3439
+    }
3440
+  }
3441
+
3442
+  &.active:hover {
3443
+    background: lighten($ui-highlight-color, 4%);
3444
+  }
3445
+}
3446
+
3447
+.advanced-options-dropdown__option__toggle {
3448
+  display: flex;
3449
+  align-items: center;
3450
+  justify-content: center;
3451
+  margin-right: 10px;
3452
+}
3453
+
3454
+.advanced-options-dropdown__option__content {
3455
+  flex: 1 1 auto;
3456
+  color: darken($ui-primary-color, 24%);
3457
+
3458
+  strong {
3459
+    font-weight: 500;
3460
+    display: block;
3461
+    color: $ui-base-color;
3462
+  }
3463
+}
3464
+
3465
+.advanced-options-dropdown.open {
3466
+  .advanced-options-dropdown__value {
3467
+    background: $simple-background-color;
3468
+    border-radius: 4px 4px 0 0;
3469
+    box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
3470
+  }
3471
+
3472
+  .advanced-options-dropdown__dropdown {
3473
+    display: block;
3474
+    box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
3475
+  }
3476
+}
3477
+
3407 3478
 .search {
3408 3479
   position: relative;
3409 3480
 }
@@ -5467,3 +5538,4 @@ noscript {
5467 5538
     }
5468 5539
   }
5469 5540
 }
5541
+@import 'doodle';

+ 10
- 1
app/lib/formatter.rb View File

@@ -208,8 +208,9 @@ class Formatter
208 208
 
209 209
   def link_to_mention(entity, linkable_accounts)
210 210
     acct = entity[:screen_name]
211
+    username, domain = acct.split('@')
211 212
 
212
-    return link_to_account(acct) unless linkable_accounts
213
+    return link_to_account(acct) unless linkable_accounts and domain != "twitter.com"
213 214
 
214 215
     account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) }
215 216
     account ? mention_html(account) : "@#{acct}"
@@ -218,6 +219,10 @@ class Formatter
218 219
   def link_to_account(acct)
219 220
     username, domain = acct.split('@')
220 221
 
222
+    if domain == "twitter.com"
223
+      return mention_twitter_html(username)
224
+    end
225
+
221 226
     domain  = nil if TagManager.instance.local_domain?(domain)
222 227
     account = EntityCache.instance.mention(username, domain)
223 228
 
@@ -245,4 +250,8 @@ class Formatter
245 250
   def mention_html(account)
246 251
     "<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@<span>#{account.username}</span></a></span>"
247 252
   end
253
+
254
+  def mention_twitter_html(username)
255
+      "<span class=\"h-card\"><a href=\"https://twitter.com/#{username}\" class=\"u-url mention\">@<span>#{username}@twitter.com</span></a></span>"
256
+  end
248 257
 end

+ 8
- 5
app/models/account.rb View File

@@ -75,7 +75,8 @@ class Account < ApplicationRecord
75 75
   validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
76 76
   validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
77 77
   validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
78
-  validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
78
+  validates :note, length: { maximum: 413 }, if: -> { local? && will_save_change_to_note? }
79
+  validate :note_has_eight_newlines?, if: -> { local? && will_save_change_to_note? }
79 80
   validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
80 81
 
81 82
   # Timelines
@@ -266,10 +267,8 @@ class Account < ApplicationRecord
266 267
   def save_with_optional_media!
267 268
     save!
268 269
   rescue ActiveRecord::RecordInvalid
269
-    self.avatar              = nil
270
-    self.header              = nil
271
-    self[:avatar_remote_url] = ''
272
-    self[:header_remote_url] = ''
270
+    self.avatar = nil if errors[:avatar].present?
271
+    self.header = nil if errors[:header].present?
273 272
     save!
274 273
   end
275 274
 
@@ -293,6 +292,10 @@ class Account < ApplicationRecord
293 292
     shared_inbox_url.presence || inbox_url
294 293
   end
295 294
 
295
+  def note_has_eight_newlines?
296
+    errors.add(:note, 'Bio can\'t have more then 8 newlines') unless note.count("\n") <= 8
297
+  end
298
+
296 299
   class Field < ActiveModelSerializers::Model
297 300
     attributes :name, :value, :account, :errors
298 301
 

+ 1
- 1
app/validators/status_length_validator.rb View File

@@ -1,7 +1,7 @@
1 1
 # frozen_string_literal: true
2 2
 
3 3
 class StatusLengthValidator < ActiveModel::Validator
4
-  MAX_CHARS = 500
4
+  MAX_CHARS = 512
5 5
 
6 6
   def validate(status)
7 7
     return unless status.local? && !status.reblog?

+ 12
- 4
app/views/about/show.html.haml View File

@@ -8,12 +8,15 @@
8 8
   = render partial: 'shared/og'
9 9
 
10 10
 .landing-page.alternative
11
+  .header{ style: 'display: none' }
12
+    = image_tag asset_pack_path('header-cybre-alt.jpg'), alt: @instance_presenter.site_title
11 13
   .container
12 14
     .grid
13 15
       .column-0
14 16
         .brand
15 17
           = link_to root_url do
16
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
18
+            = image_tag asset_pack_path('logo-cybre.png')
19
+            = Setting.site_title
17 20
 
18 21
       - if Setting.timeline_preview
19 22
         .column-1
@@ -36,13 +39,13 @@
36 39
       - if Setting.timeline_preview
37 40
         .column-2
38 41
           .landing-page__hero
39
-            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
42
+            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('background-cybre.png'), alt: @instance_presenter.site_title
40 43
 
41 44
           .landing-page__information
42 45
             .landing-page__short-description
43 46
               .row
44 47
                 .landing-page__logo
45
-                  = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
48
+                  = image_tag asset_pack_path('logo-cybre.png'), alt: 'Cybrespace'
46 49
 
47 50
                 %h1
48 51
                   = @instance_presenter.site_title
@@ -63,7 +66,12 @@
63 66
                   %span= t 'about.status_count_after', count: @instance_presenter.status_count
64 67
               .row__mascot
65 68
                 .landing-page__mascot
66
-                  = image_tag asset_pack_path('elephant_ui_plane.svg')
69
+                  .floats.float-1
70
+                    = image_tag asset_pack_path('floppy-3.svg')
71
+                  .floats.float-2
72
+                    = image_tag asset_pack_path('floppy-1.svg')
73
+                  .floats.float-3
74
+                    = image_tag asset_pack_path('floppy-2.svg')
67 75
 
68 76
       - else
69 77
         .column-2.non-preview

+ 1
- 1
app/views/layouts/admin.html.haml View File

@@ -6,7 +6,7 @@
6 6
     .sidebar-wrapper
7 7
       .sidebar
8 8
         = link_to root_path do
9
-          = image_tag asset_pack_path('logo.svg'), class: 'logo', alt: 'Mastodon'
9
+          = image_tag asset_pack_path('logo-cybre.png'), class: 'logo', alt: 'Cybrespace'
10 10
 
11 11
         = render_navigation
12 12
     .content-wrapper

+ 2
- 1
app/views/layouts/auth.html.haml View File

@@ -6,7 +6,8 @@
6 6
     .logo-container
7 7
       %h1
8 8
         = link_to root_path do
9
-          = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
9
+          = image_tag asset_pack_path('logo-cybre.png')
10
+          = Setting.site_title
10 11
 
11 12
     .form-container
12 13
       = render 'flashes'

+ 1
- 1
app/views/settings/profiles/show.html.haml View File

@@ -6,7 +6,7 @@
6 6
 
7 7
   .fields-group
8 8
     = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
9
-    = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 160 - @account.note.size).html_safe
9
+    = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 413 - @account.note.size).html_safe
10 10
 
11 11
   = render 'application/card', account: @account
12 12
 

+ 1
- 1
app/views/stream_entries/_detailed_status.html.haml View File

@@ -60,7 +60,7 @@
60 60
         = " "
61 61
     ·
62 62
     = link_to remote_interaction_path(status), class: 'modal-button detailed-status__link' do
63
-      = fa_icon('star')
63
+      = fa_icon('floppy-o')
64 64
       %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
65 65
       = " "
66 66
 

+ 1
- 1
app/views/stream_entries/_simple_status.html.haml View File

@@ -44,4 +44,4 @@
44 44
       - else
45 45
         = fa_icon 'envelope fw'
46 46
     = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
47
-      = fa_icon 'star fw'
47
+      = fa_icon 'floppy-o fw'

+ 6
- 3
config/application.rb View File

@@ -37,6 +37,7 @@ module Mastodon
37 37
     # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
38 38
     config.i18n.available_locales = [
39 39
       :en,
40
+      :'en-CY',
40 41
       :ar,
41 42
       :ast,
42 43
       :bg,
@@ -87,9 +88,11 @@ module Mastodon
87 88
     ]
88 89
 
89 90
     config.i18n.default_locale = ENV['DEFAULT_LOCALE']&.to_sym
90
-
91
-    unless config.i18n.available_locales.include?(config.i18n.default_locale)
92
-      config.i18n.default_locale = :en
91
+    if config.i18n.available_locales.include?(config.i18n.default_locale)
92
+      config.i18n.fallbacks = [:en]
93
+    else
94
+      config.i18n.default_locale = :'en-CY'
95
+      config.i18n.fallbacks = [:en]
93 96
     end
94 97
 
95 98
     # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')

+ 1
- 0
config/i18n-tasks.yml View File

@@ -30,6 +30,7 @@ search:
30 30
     - app/assets/images
31 31
     - app/assets/fonts
32 32
     - app/assets/videos
33
+    - app/javascript/images
33 34
 
34 35
 ignore_missing:
35 36
   - 'activemodel.errors.*'

+ 61
- 0
config/locales/devise.en-CY.yml View File

@@ -0,0 +1,61 @@
1
+---
2
+en-CY:
3
+  devise:
4
+    confirmations:
5
+      confirmed: Your email address has been successfully confirmed.
6
+      send_instructions: You will receive an email with instructions for how to confirm your email address in a few minutes. Please check your spam folder if you didn't receive this email.
7
+      send_paranoid_instructions: If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes. Please check your spam folder if you didn't receive this email.
8
+    failure:
9
+      already_authenticated: You are already signed in.
10
+      inactive: Your account is not activated yet.
11
+      invalid: Invalid %{authentication_keys} or password.
12
+      last_attempt: You have one more attempt before your account is locked.
13
+      locked: Your account is locked.
14
+      not_found_in_database: Invalid %{authentication_keys} or password.
15
+      timeout: Your session expired. Please sign in again to continue.
16
+      unauthenticated: You need to sign in or sign up before continuing.
17
+      unconfirmed: You have to confirm your email address before continuing.
18
+    mailer:
19
+      confirmation_instructions:
20
+        subject: 'Mastodon: Confirmation instructions for %{instance}'
21
+      password_change:
22
+        subject: 'Mastodon: Password changed'
23
+      reset_password_instructions:
24
+        subject: 'Mastodon: Reset password instructions'
25
+      unlock_instructions:
26
+        subject: 'Mastodon: Unlock instructions'
27
+    omniauth_callbacks:
28
+      failure: Could not authenticate you from %{kind} because "%{reason}".
29
+      success: Successfully authenticated from %{kind} account.
30
+    passwords:
31
+      no_token: You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided.
32
+      send_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. Please check your spam folder if you didn't receive this email.
33
+      send_paranoid_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. Please check your spam folder if you didn't receive this email.
34
+      updated: Your password has been changed successfully. You are now signed in.
35
+      updated_not_active: Your password has been changed successfully.
36
+    registrations:
37
+      destroyed: Bye! Your account has been successfully cancelled. We hope to see you again soon.
38
+      signed_up: Welcome! You have signed up successfully.
39
+      signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated.
40
+      signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked.
41
+      signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account. Please check your spam folder if you didn't receive this email.
42
+      update_needs_confirmation: You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address. Please check your spam folder if you didn't receive this email.
43
+      updated: Your account has been updated successfully.
44
+    sessions:
45
+      already_signed_out: Signed out successfully.
46
+      signed_in: Signed in successfully.
47
+      signed_out: Signed out successfully.
48
+    unlocks:
49
+      send_instructions: You will receive an email with instructions for how to unlock your account in a few minutes. Please check your spam folder if you didn't receive this email.
50
+      send_paranoid_instructions: If your account exists, you will receive an email with instructions for how to unlock it in a few minutes. Please check your spam folder if you didn't receive this email.
51
+      unlocked: Your account has been unlocked successfully. Please sign in to continue.
52
+  errors:
53
+    messages:
54
+      already_confirmed: was already confirmed, please try signing in
55
+      confirmation_period_expired: needs to be confirmed within %{period}, please request a new one
56
+      expired: has expired, please request a new one
57
+      not_found: not found
58
+      not_locked: was not locked
59
+      not_saved:
60
+        one: '1 error prohibited this %{resource} from being saved:'
61
+        other: "%{count} errors prohibited this %{resource} from being saved:"

+ 119
- 0
config/locales/doorkeeper.en-CY.yml View File

@@ -0,0 +1,119 @@
1
+---
2
+en-CY:
3
+  activerecord:
4
+    attributes:
5
+      doorkeeper/application:
6
+        name: Application name
7
+        redirect_uri: Redirect URI
8
+        scopes: Scopes
9
+        website: Application website
10
+    errors:
11
+      models:
12
+        doorkeeper/application:
13
+          attributes:
14
+            redirect_uri:
15
+              fragment_present: cannot contain a fragment.
16
+              invalid_uri: must be a valid URI.
17
+              relative_uri: must be an absolute URI.
18
+              secured_uri: must be an HTTPS/SSL URI.
19
+  doorkeeper:
20
+    applications:
21
+      buttons:
22
+        authorize: Authorize
23
+        cancel: Cancel
24
+        destroy: Destroy
25
+        edit: Edit
26
+        submit: Submit
27
+      confirmations:
28
+        destroy: Are you sure?
29
+      edit:
30
+        title: Edit application
31
+      form:
32
+        error: Whoops! Check your form for possible errors
33
+      help:
34
+        native_redirect_uri: Use %{native_redirect_uri} for local tests
35
+        redirect_uri: Use one line per URI
36
+        scopes: Separate scopes with spaces. Leave blank to use the default scopes.
37
+      index:
38
+        application: Application
39
+        callback_url: Callback URL
40
+        delete: Delete
41
+        name: Name
42
+        new: New application
43
+        scopes: Scopes
44
+        show: Show
45
+        title: Your applications
46
+      new:
47
+        title: New application
48
+      show:
49
+        actions: Actions
50
+        application_id: Client key
51
+        callback_urls: Callback URLs
52
+        scopes: Scopes
53
+        secret: Client secret
54
+        title: 'Application: %{name}'
55
+    authorizations:
56
+      buttons:
57
+        authorize: Authorize
58
+        deny: Deny
59
+      error:
60
+        title: An error has occurred
61
+      new:
62
+        able_to: It will be able to
63
+        prompt: Application %{client_name} requests access to your account
64
+        title: Authorization required
65
+      show:
66
+        title: Authorization code
67
+    authorized_applications:
68
+      buttons:
69
+        revoke: Revoke
70
+      confirmations:
71
+        revoke: Are you sure?
72
+      index:
73
+        application: Application
74
+        created_at: Authorized
75
+        date_format: "%Y-%m-%d %H:%M:%S"
76
+        scopes: Scopes
77
+        title: Your authorized applications
78
+    errors:
79
+      messages:
80
+        access_denied: The resource owner or authorization server denied the request.
81
+        credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
82
+        invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
83
+        invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
84