decidim-core 0.31.4 → 0.31.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/amendable/amend_button_card/show.erb +2 -2
  3. data/app/cells/decidim/author_cell.rb +0 -4
  4. data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
  5. data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +2 -2
  6. data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
  7. data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
  8. data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
  9. data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
  10. data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
  11. data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
  12. data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
  13. data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
  14. data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
  15. data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
  16. data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
  17. data/app/cells/decidim/data_consent/category.erb +5 -5
  18. data/app/cells/decidim/resource_types_filter/show.erb +2 -2
  19. data/app/cells/decidim/upload_modal_cell.rb +5 -0
  20. data/app/commands/decidim/destroy_account.rb +12 -1
  21. data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
  22. data/app/controllers/concerns/decidim/devise_controllers.rb +10 -0
  23. data/app/controllers/decidim/download_your_data_controller.rb +1 -1
  24. data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
  25. data/app/controllers/decidim/private_downloads_controller.rb +29 -0
  26. data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
  27. data/app/mailers/decidim/delete_user_mailer.rb +14 -0
  28. data/app/mailers/decidim/participants_account_mailer.rb +0 -16
  29. data/app/models/decidim/attachment.rb +20 -2
  30. data/app/models/decidim/authorization.rb +7 -0
  31. data/app/models/decidim/moderation.rb +1 -1
  32. data/app/models/decidim/participatory_space_private_user.rb +1 -1
  33. data/app/models/decidim/private_download.rb +61 -0
  34. data/app/models/decidim/private_export.rb +6 -0
  35. data/app/models/decidim/user_base_entity.rb +17 -2
  36. data/app/models/decidim/user_moderation.rb +1 -1
  37. data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
  38. data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
  39. data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
  40. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
  41. data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
  42. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
  43. data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
  44. data/app/packs/src/decidim/direct_uploads/upload_modal.js +11 -7
  45. data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
  46. data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
  47. data/app/packs/src/decidim/index.js +4 -3
  48. data/app/packs/src/decidim/sw/push-permissions.js +48 -13
  49. data/app/packs/src/decidim/sw/sw.js +1 -1
  50. data/app/packs/src/decidim/utilities/text.js +6 -6
  51. data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
  52. data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
  53. data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
  54. data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
  55. data/app/packs/stylesheets/decidim/application.scss +2 -0
  56. data/app/packs/stylesheets/decidim/editor.scss +2 -33
  57. data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
  58. data/app/presenters/decidim/menu_item_presenter.rb +9 -3
  59. data/app/presenters/decidim/stats_presenter.rb +1 -1
  60. data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
  61. data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
  62. data/app/services/decidim/send_push_notification.rb +5 -1
  63. data/app/uploaders/decidim/image_uploader.rb +1 -1
  64. data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
  65. data/app/views/decidim/gamification/badges/index.html.erb +1 -1
  66. data/app/views/decidim/homepage/show.html.erb +1 -1
  67. data/app/views/decidim/last_activities/index.html.erb +1 -1
  68. data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
  69. data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
  70. data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
  71. data/app/views/decidim/notifications_settings/show.html.erb +5 -5
  72. data/app/views/decidim/offline/show.html.erb +1 -1
  73. data/app/views/decidim/pages/index.html.erb +1 -1
  74. data/app/views/decidim/profiles/show.html.erb +1 -1
  75. data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
  76. data/app/views/decidim/user_activities/index.html.erb +1 -1
  77. data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
  78. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  79. data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
  80. data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
  81. data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
  82. data/config/locales/ar.yml +0 -5
  83. data/config/locales/bg.yml +0 -6
  84. data/config/locales/ca-IT.yml +10 -10
  85. data/config/locales/ca.yml +10 -10
  86. data/config/locales/cs.yml +4 -13
  87. data/config/locales/de.yml +5 -17
  88. data/config/locales/el.yml +0 -4
  89. data/config/locales/en.yml +9 -9
  90. data/config/locales/es-MX.yml +9 -9
  91. data/config/locales/es-PY.yml +9 -9
  92. data/config/locales/es.yml +9 -9
  93. data/config/locales/eu.yml +49 -49
  94. data/config/locales/fi-plain.yml +9 -9
  95. data/config/locales/fi.yml +11 -11
  96. data/config/locales/fr-CA.yml +10 -9
  97. data/config/locales/fr.yml +10 -9
  98. data/config/locales/gl.yml +0 -3
  99. data/config/locales/hu.yml +0 -5
  100. data/config/locales/id-ID.yml +0 -3
  101. data/config/locales/it.yml +11 -3
  102. data/config/locales/ja.yml +24 -21
  103. data/config/locales/lb.yml +0 -5
  104. data/config/locales/lt.yml +0 -5
  105. data/config/locales/lv.yml +0 -3
  106. data/config/locales/nl.yml +0 -5
  107. data/config/locales/no.yml +0 -5
  108. data/config/locales/pl.yml +2 -8
  109. data/config/locales/pt-BR.yml +3 -16
  110. data/config/locales/pt.yml +0 -5
  111. data/config/locales/ro-RO.yml +1 -14
  112. data/config/locales/sk.yml +1408 -4
  113. data/config/locales/sv.yml +3 -11
  114. data/config/locales/tr-TR.yml +0 -6
  115. data/config/locales/zh-CN.yml +0 -5
  116. data/config/locales/zh-TW.yml +0 -5
  117. data/config/routes.rb +1 -0
  118. data/lib/decidim/api/functions/user_entity_list.rb +2 -0
  119. data/lib/decidim/attachment_attributes.rb +58 -9
  120. data/lib/decidim/command.rb +1 -1
  121. data/lib/decidim/content_renderers/base_renderer.rb +112 -0
  122. data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
  123. data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
  124. data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
  125. data/lib/decidim/content_renderers/user_renderer.rb +11 -9
  126. data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
  127. data/lib/decidim/core/engine.rb +8 -0
  128. data/lib/decidim/core/test/factories.rb +3 -0
  129. data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
  130. data/lib/decidim/core/test/shared_examples/comments_examples.rb +6 -6
  131. data/lib/decidim/core/version.rb +1 -1
  132. data/lib/decidim/map/autocomplete.rb +4 -3
  133. data/lib/decidim/searchable.rb +5 -0
  134. data/lib/decidim/view_model.rb +1 -1
  135. data/lib/tasks/decidim_mailers_tasks.rake +31 -9
  136. metadata +14 -10
  137. data/app/commands/decidim/gallery_methods.rb +0 -107
  138. data/app/packs/src/decidim/vendor/tribute.js +0 -1890
  139. data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
  140. data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
@@ -1,16 +1,38 @@
1
- window.addEventListener("turbo:load", async () => {
1
+ document.addEventListener("turbo:load", async () => {
2
2
  const GRANTED_PERMISSION = "granted"
3
3
 
4
4
  const hideReminder = function() {
5
- const reminder = document.querySelector("#push-notifications-reminder")
5
+ const reminder = document.querySelector("[data-push-notifications-reminder]")
6
+ if (!reminder) {
7
+ return;
8
+ }
9
+
6
10
  reminder.classList.add("hide")
7
11
  }
8
12
 
13
+ const showError = (message) => {
14
+ const container = document.querySelector("[data-push-notifications-container]")
15
+ if (!container) {
16
+ return;
17
+ }
18
+
19
+ const existingError = container.querySelector("[data-push-notifications-error]")
20
+ if (existingError) {
21
+ existingError.remove()
22
+ }
23
+
24
+ const errorElement = document.createElement("div")
25
+ errorElement.dataset.pushNotificationsError = "true"
26
+ errorElement.classList.add("flash", "alert", "push-notifications__error")
27
+ errorElement.innerText = message
28
+ container.prepend(errorElement)
29
+ }
30
+
9
31
  const subscribeToNotifications = async (registration) => {
10
32
  const permission = await window.Notification.requestPermission();
11
33
 
12
34
  if (registration && permission === GRANTED_PERMISSION) {
13
- const vapidElement = document.querySelector("#vapidPublicKey")
35
+ const vapidElement = document.querySelector("[data-push-vapid-public-key]")
14
36
  // element could not exist in DOM
15
37
  if (vapidElement) {
16
38
  const vapidPublicKeyElement = JSON.parse(vapidElement.value)
@@ -20,7 +42,7 @@ window.addEventListener("turbo:load", async () => {
20
42
  });
21
43
 
22
44
  if (subscription) {
23
- await fetch("/notifications_subscriptions", {
45
+ const response = await fetch("/notifications_subscriptions", {
24
46
  headers: {
25
47
  "Content-Type": "application/json",
26
48
  "X-CSRF-Token": document.querySelector("meta[name=csrf-token]")?.content
@@ -28,6 +50,11 @@ window.addEventListener("turbo:load", async () => {
28
50
  method: "POST",
29
51
  body: JSON.stringify(subscription)
30
52
  });
53
+
54
+ if (!response.ok) {
55
+ const body = await response.json()
56
+ throw new Error(body.error)
57
+ }
31
58
  }
32
59
  }
33
60
  hideReminder()
@@ -57,10 +84,13 @@ window.addEventListener("turbo:load", async () => {
57
84
  hideReminder()
58
85
  if (currentSubscription) {
59
86
  const auth = currentSubscription.toJSON().keys.auth
60
- const subKeys = JSON.parse(document.querySelector("#subKeys").value)
61
- // Subscribed && browser notifications enabled
62
- if (subKeys.includes(auth)) {
63
- toggleChecked = true
87
+ const subKeysElement = document.querySelector("[data-push-sub-keys]")
88
+ if (subKeysElement) {
89
+ const subKeys = JSON.parse(subKeysElement.value)
90
+ // Subscribed && browser notifications enabled
91
+ if (subKeys.includes(auth)) {
92
+ toggleChecked = true
93
+ }
64
94
  }
65
95
  }
66
96
  }
@@ -68,7 +98,7 @@ window.addEventListener("turbo:load", async () => {
68
98
  }
69
99
 
70
100
  if ("serviceWorker" in navigator) {
71
- const toggle = document.getElementById("allow_push_notifications")
101
+ const toggle = document.querySelector("[data-push-notifications-toggle]")
72
102
 
73
103
  if (toggle) {
74
104
  const registration = await navigator.serviceWorker.ready
@@ -76,10 +106,15 @@ window.addEventListener("turbo:load", async () => {
76
106
  setToggleState(registration, toggle)
77
107
 
78
108
  toggle.addEventListener("change", async ({ target }) => {
79
- if (target.checked) {
80
- await subscribeToNotifications(registration);
81
- } else {
82
- await unsubscribeFromNotifications(registration)
109
+ try {
110
+ if (target.checked) {
111
+ await subscribeToNotifications(registration)
112
+ } else {
113
+ await unsubscribeFromNotifications(registration)
114
+ }
115
+ } catch (error) {
116
+ target.checked = false
117
+ showError(error.message)
83
118
  }
84
119
  })
85
120
  }
@@ -53,7 +53,7 @@ self.addEventListener("notificationclick", (event) => {
53
53
  // avoid caching admin or users paths
54
54
  registerRoute(
55
55
  ({ url }) =>
56
- ["/admin/", "/users/"].some((path) => url.pathname.startsWith(path)),
56
+ ["admin", "users"].some((pathSegment) => url.pathname.split("/").includes(pathSegment)),
57
57
  new NetworkOnly()
58
58
  );
59
59
 
@@ -1,17 +1,17 @@
1
- export const escapeHtml = (text) => {
1
+ const escapeQuotes = (text) => {
2
2
  if (!text) {
3
3
  return "";
4
4
  }
5
5
 
6
- const el = document.createElement("div");
7
- el.appendChild(document.createTextNode(text));
8
- return el.innerHTML;
6
+ return text.replace(/"/g, """);
9
7
  }
10
8
 
11
- export const escapeQuotes = (text) => {
9
+ export const escapeHtml = (text) => {
12
10
  if (!text) {
13
11
  return "";
14
12
  }
15
13
 
16
- return text.replace(/"/g, """);
14
+ const el = document.createElement("div");
15
+ el.appendChild(document.createTextNode(text));
16
+ return escapeQuotes(el.innerHTML);
17
17
  }
@@ -88,6 +88,20 @@
88
88
  }
89
89
 
90
90
  &__modal {
91
+ &-error {
92
+ [data-dialog-closable] {
93
+ @apply top-6 right-6;
94
+ }
95
+
96
+ [data-dialog-container] {
97
+ @apply inline grid-cols-none;
98
+ }
99
+
100
+ [data-dialog-title] {
101
+ @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
102
+ }
103
+ }
104
+
91
105
  &-results {
92
106
  @apply mb-20 flex flex-wrap gap-x-8 gap-y-4;
93
107
  }
@@ -0,0 +1,49 @@
1
+ .editor-suggestions-props {
2
+ --editor-suggestions-border-color: #000;
3
+ --editor-suggestions-background-color: #eee;
4
+ --editor-suggestions-selected-highlight-color: #ccc;
5
+ }
6
+
7
+ .editor-suggestions {
8
+ @apply editor-suggestions-props border-0 bg-[var(--editor-suggestions-background-color)] max-w-sm drop-shadow-md rounded-md;
9
+
10
+ &:hover {
11
+ .editor-suggestions-item[data-selected]:not(:hover) {
12
+ @apply bg-transparent;
13
+ }
14
+ }
15
+
16
+ .editor-suggestions-item {
17
+ @apply flex items-center gap-2 w-full text-left py-[0.375rem] px-1.5 border-0 rounded-none text-sm;
18
+
19
+ &:first-child {
20
+ @apply rounded-t-md;
21
+ }
22
+
23
+ &:last-child {
24
+ @apply rounded-b-md;
25
+ }
26
+
27
+ &:hover,
28
+ &[data-selected] {
29
+ @apply bg-[var(--editor-suggestions-selected-highlight-color)];
30
+ }
31
+
32
+ .editor-suggestions-item-avatar {
33
+ @apply rounded-full w-6 h-6 object-cover flex-none;
34
+ }
35
+
36
+ .editor-suggestions-item-label {
37
+ @apply truncate;
38
+ }
39
+ }
40
+
41
+ .editor-suggestions-item-disabled {
42
+ @apply cursor-default;
43
+
44
+ &:hover,
45
+ &[data-selected] {
46
+ @apply bg-transparent;
47
+ }
48
+ }
49
+ }
@@ -1,5 +1,5 @@
1
1
  [data-floating-help] {
2
- @apply absolute z-10 top-5 md:top-10 right-0 [&_button]:bg-background [&_button]:px-2 [&_button]:rounded-r-none [&_svg]:text-gray [&_svg]:fill-current;
2
+ @apply absolute z-10 top-20 md:top-10 right-0 [&_button]:bg-background [&_button]:px-2 [&_button]:rounded-r-none [&_svg]:text-gray [&_svg]:fill-current;
3
3
 
4
4
  &:hover {
5
5
  @apply [&_svg]:text-white [&_button]:text-white [&_button]:bg-secondary [&_button]:no-underline;
@@ -0,0 +1,23 @@
1
+ @use "tom-select/dist/scss/tom-select";
2
+
3
+ .ts {
4
+ &-control {
5
+ @apply border-gray text-md min-h-[40px];
6
+
7
+ input {
8
+ @apply font-normal text-black text-md;
9
+ }
10
+ }
11
+
12
+ &-dropdown {
13
+ @apply text-md text-gray-2 font-normal;
14
+
15
+ .option img {
16
+ @apply inline;
17
+ }
18
+
19
+ .active {
20
+ @apply bg-gray-3;
21
+ }
22
+ }
23
+ }
@@ -6,6 +6,8 @@
6
6
  // Datepicker
7
7
  @use "stylesheets/decidim/vendor/datepicker_light";
8
8
  @use "stylesheets/decidim/datepicker";
9
+ // TomSelect
10
+ @use "stylesheets/decidim/tom_select";
9
11
  // On the other hand, the following styles match with specific routes
10
12
  @use "stylesheets/decidim/header";
11
13
  @use "stylesheets/decidim/footer";
@@ -1,3 +1,5 @@
1
+ @use "stylesheets/decidim/editor_suggestions";
2
+
1
3
  .editor-props {
2
4
  --editor-active-color: #487bda;
3
5
  --editor-disabled-color: #eee;
@@ -5,12 +7,6 @@
5
7
  --editor-bubble-shadow-color: #333;
6
8
  }
7
9
 
8
- .editor-suggestions-props {
9
- --editor-suggestions-border-color: #000;
10
- --editor-suggestions-background-color: #eee;
11
- --editor-suggestions-selected-highlight-color: #ccc;
12
- }
13
-
14
10
  .editor-active {
15
11
  @apply outline outline-4 outline-[var(--editor-active-color)];
16
12
  }
@@ -186,30 +182,3 @@
186
182
  }
187
183
  }
188
184
  }
189
-
190
- .editor-suggestions {
191
- @apply editor-suggestions-props border-0 bg-[var(--editor-suggestions-background-color)] max-w-sm drop-shadow-md rounded-md;
192
-
193
- &:hover {
194
- .editor-suggestions-item[data-selected]:not(:hover) {
195
- @apply bg-transparent;
196
- }
197
- }
198
-
199
- .editor-suggestions-item {
200
- @apply block w-full text-left py-[0.375rem] px-1.5 border-0 rounded-none truncate text-sm;
201
-
202
- &:first-child {
203
- @apply rounded-t-md;
204
- }
205
-
206
- &:last-child {
207
- @apply rounded-b-md;
208
- }
209
-
210
- &:hover,
211
- &[data-selected] {
212
- @apply bg-[var(--editor-suggestions-selected-highlight-color)];
213
- }
214
- }
215
- }
@@ -1,5 +1,13 @@
1
- .geocoding-container {
2
- .user-device-location button {
1
+ .geocoding {
2
+ &__locate .geocoding__button {
3
3
  @apply mt-4;
4
+
5
+ &--locating {
6
+ @apply opacity-70 cursor-wait;
7
+ }
8
+
9
+ .geocoding__spinner {
10
+ @apply inline-block w-4 h-4 rounded-full animate-spin border-2 border-l-transparent border-y-transparent border-r-secondary align-middle;
11
+ }
4
12
  }
5
13
  }
@@ -29,9 +29,9 @@ module Decidim
29
29
  delegate :content_tag, :safe_join, :link_to, :active_link_to_class, :is_active_link?, :icon, to: :@view
30
30
 
31
31
  def render
32
- content_tag :li, role: menuitem_role, class: link_wrapper_classes do
32
+ content_tag :li, role: wrapper_role, class: link_wrapper_classes do
33
33
  output = if url == "#"
34
- [content_tag(:span, composed_label, class: "sidebar-menu__item-disabled")]
34
+ [content_tag(:span, composed_label, class: "sidebar-menu__item-disabled", role: menuitem_role)]
35
35
  else
36
36
  [link_to(composed_label, url, link_options)]
37
37
  end
@@ -52,7 +52,7 @@ module Decidim
52
52
  { aria: { current: "page" } }
53
53
  else
54
54
  {}
55
- end.merge({ class: link_classes })
55
+ end.merge({ class: link_classes, role: menuitem_role })
56
56
  end
57
57
 
58
58
  def composed_label
@@ -75,6 +75,12 @@ module Decidim
75
75
  @options.role || :menuitem
76
76
  end
77
77
 
78
+ def wrapper_role
79
+ return if menuitem_role.blank?
80
+
81
+ :presentation
82
+ end
83
+
78
84
  def active_class
79
85
  active_link_to_class(
80
86
  url,
@@ -15,7 +15,7 @@ module Decidim
15
15
  hash[name][:data][idx] += value
16
16
  end
17
17
  else
18
- hash[name] = stat
18
+ hash[name] = stat.merge(data: stat[:data].dup)
19
19
  end
20
20
  end.values
21
21
  end
@@ -4,6 +4,10 @@ module Decidim
4
4
  # This class manages the creation and deletion of user notifications
5
5
 
6
6
  class NotificationsSubscriptionsPersistor
7
+ include PushSubscriptionEndpointValidator
8
+
9
+ class UnsupportedPushSubscriptionEndpointError < StandardError; end
10
+
7
11
  attr_reader :user
8
12
 
9
13
  def initialize(user)
@@ -11,6 +15,8 @@ module Decidim
11
15
  end
12
16
 
13
17
  def add_subscription(params)
18
+ raise UnsupportedPushSubscriptionEndpointError unless supported_push_subscription_endpoint?(params[:endpoint])
19
+
14
20
  subscriptions = user.notification_settings["subscriptions"] || {}
15
21
  filtered_params = filter_params(params)
16
22
  new_subscription = { filtered_params[:auth] => filtered_params }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # Shared validation for browser push subscription endpoints.
5
+ module PushSubscriptionEndpointValidator
6
+ private
7
+
8
+ def supported_push_subscription_endpoint?(endpoint)
9
+ return false if endpoint.blank?
10
+
11
+ uri = URI.parse(endpoint)
12
+ return false unless uri.is_a?(URI::HTTPS)
13
+
14
+ host = uri.host&.downcase
15
+ return false if host.blank?
16
+
17
+ allowed_push_subscription_endpoint_patterns.any? { |pattern| pattern.match?(host) }
18
+ rescue URI::InvalidURIError
19
+ false
20
+ end
21
+
22
+ # Override this method to customize the browser push endpoint allowlist.
23
+ def allowed_push_subscription_endpoint_patterns
24
+ [
25
+ /\A(?:.*\.)?push\.services\.mozilla\.com\z/,
26
+ /\A(?:.*\.)?fcm\.googleapis\.com\z/,
27
+ /\A(?:.*\.)?android\.googleapis\.com\z/,
28
+ /\A(?:.*\.)?push\.apple\.com\z/,
29
+ /\A(?:.*\.)?opera\.com\z/,
30
+ /\A(?:.*\.)?notify\.windows\.com\z/
31
+ ]
32
+ end
33
+ end
34
+ end
@@ -10,6 +10,7 @@ module Decidim
10
10
 
11
11
  class SendPushNotification
12
12
  include ActionView::Helpers::UrlHelper
13
+ include PushSubscriptionEndpointValidator
13
14
 
14
15
  # Send the push notification. Returns `nil` if the user did not allowed push notifications
15
16
  # or if the subscription to push notifications does not exist
@@ -24,9 +25,12 @@ module Decidim
24
25
  raise ArgumentError, "Need to provide a title if the notification is a PushNotificationMessage" if notification.is_a?(Decidim::PushNotificationMessage) && title.nil?
25
26
 
26
27
  user = notification.user
28
+ subscriptions = user.notifications_subscriptions.values.select do |subscription|
29
+ supported_push_subscription_endpoint?(subscription["endpoint"])
30
+ end
27
31
 
28
32
  I18n.with_locale(user.locale || user.organization.default_locale) do
29
- user.notifications_subscriptions.values.map do |subscription|
33
+ subscriptions.map do |subscription|
30
34
  payload = build_payload(message_params(notification, title), subscription)
31
35
  # Capture webpush exceptions in order to avoid this call to be repeated by the background job runner
32
36
  # Webpush::Error class is the parent class of all defined errors
@@ -8,7 +8,7 @@ module Decidim
8
8
  end
9
9
 
10
10
  def content_type_allowlist
11
- extension_allowlist.map { |ext| MiniMime.lookup_by_extension(ext).content_type }.uniq
11
+ extension_allowlist.filter_map { |ext| MiniMime.lookup_by_extension(ext)&.content_type }.uniq
12
12
  end
13
13
 
14
14
  # Fetches info about different variants, their processors and dimensions
@@ -0,0 +1,6 @@
1
+ <p class="email-greeting"><%= t(".hello", username: @user_name) %></p>
2
+
3
+ <p class="email-instructions"><%= t ".body_1" %></p>
4
+ <p class="email-instructions"><%= t ".body_2" %></p>
5
+
6
+ <p class="email-closing"><%= t(".greetings_html", organization_name: organization_name(@organization), organization_url: decidim.root_url(host: @organization.host)) %></p>
@@ -3,7 +3,7 @@
3
3
  description: t(".page_description")
4
4
  ) %>
5
5
 
6
- <main class="layout-1col cols-10">
6
+ <main class="layout-1col cols-10" role="main">
7
7
 
8
8
  <header class="text-center py-10">
9
9
  <h1 class="title-decorator inline-block text-left mb-12">
@@ -7,7 +7,7 @@
7
7
  )
8
8
  %>
9
9
 
10
- <main class="home">
10
+ <main class="home" role="main">
11
11
  <% Decidim::ContentBlock.published.for_scope(:homepage, organization: current_organization).each do |content_block| %>
12
12
  <% next unless content_block.manifest %>
13
13
 
@@ -1,7 +1,7 @@
1
1
  <% add_decidim_page_title(t("last_activity", scope: "decidim.last_activities.index")) %>
2
2
 
3
3
  <%# NOTE: this page does not use a regular layout %>
4
- <main class="container">
4
+ <main class="container" role="main">
5
5
  <h1 class="title-decorator my-12"><%= t("last_activity", scope: "decidim.last_activities.index") %></h1>
6
6
 
7
7
  <div class="profile__activity pb-16">
@@ -1,21 +1,13 @@
1
- <div id="messageErrorModal" data-reveal>
2
- <div>
3
- <h2><%= error %></h2>
4
- <button class="close-button" data-close aria-label="<%= t(".close") %>"
5
- type="button">
6
- <span aria-hidden="true">&times;</span>
7
- </button>
8
- </div>
9
- <div class="reveal__body">
10
- <p><%= t(".intro") %></p>
11
- <p>
12
- <%= messages.join("<br>") %>
13
- </p>
14
- <p><%= t(".correct_errors") %></p>
15
- </div>
16
- <div>
17
- <div>
18
- <a class="button" role="button" href="#" data-close aria-label="<%= t(".ok") %>"><%= t(".ok") %></a>
1
+ <%= decidim_modal id: "messageErrorModal", class: "conversation__modal-error" do %>
2
+ <div data-dialog-container>
3
+ <h2 id="dialog-title-messageErrorModal" tabindex="-1" data-dialog-title><%= error %></h2>
4
+ <div id="dialog-desc-messageErrorModal" class="mt-4">
5
+ <p><%= t(".intro") %></p>
6
+ <p><%= safe_join(messages, tag.br) %></p>
7
+ <p><%= t(".correct_errors") %></p>
8
+ </div>
9
+ <div data-dialog-actions>
10
+ <button type="button" class="button button__sm lg:button__lg button__secondary" data-dialog-close="messageErrorModal" aria-label="<%= t(".ok") %>"><%= t(".ok") %></button>
19
11
  </div>
20
12
  </div>
21
- </div>
13
+ <% end %>
@@ -1,10 +1,15 @@
1
- var $messageError = $("#messageErrorModal");
2
- if ($messageError.length > 0) {
3
- $messageError.foundation("_destroy").remove();
1
+ var messageError = document.getElementById("messageErrorModal")
2
+
3
+ if (messageError) {
4
+ messageError.remove();
4
5
  }
6
+ document.body.insertAdjacentHTML("beforeend", "<%= j(render "error_modal", error:, messages:).html_safe %>");
5
7
 
6
- $("#messages").append("<%= j(render "error_modal", error:, messages:).html_safe %>");
7
- $messageError = $("#messageErrorModal");
8
+ messageError = document.getElementById("messageErrorModal")
9
+ if (messageError) {
10
+ window.createDialog(messageError);
8
11
 
9
- var messageErrorReveal = new Foundation.Reveal($messageError);
10
- messageErrorReveal.open();
12
+ if (Decidim.currentDialogs.messageErrorModal) {
13
+ window.Decidim.currentDialogs.messageErrorModal.open();
14
+ }
15
+ }
@@ -1,4 +1,4 @@
1
- <main class="layout-1col cols-6">
1
+ <main class="layout-1col cols-6" role="main">
2
2
  <div class="text-center py-12">
3
3
  <h1 class="h1 decorator inline-block text-left"><%= t ".unsubscribe" %></h1>
4
4
  </div>
@@ -167,20 +167,20 @@
167
167
  <% end %>
168
168
 
169
169
  <% if @notifications_settings.meet_push_notifications_requirements? %>
170
- <div class="push-notifications js-sw-mandatory">
170
+ <div class="push-notifications js-sw-mandatory" data-push-notifications-container>
171
171
  <label>
172
172
  <%= t("push_notifications", scope: "decidim.notifications_settings.show") %>
173
173
  </label>
174
- <p id="push-notifications-reminder" class="push-notifications__reminder block my-4">
174
+ <p id="push-notifications-reminder" class="push-notifications__reminder block my-4" data-push-notifications-reminder>
175
175
  <%= t("push_notifications_reminder", scope: "decidim.notifications_settings.show") %>
176
176
  </p>
177
177
  <div class="toggle__switch-trigger">
178
178
  <label class="toggle__switch-toggle" for="allow_push_notifications">
179
179
  <span>
180
180
  <input
181
- <%== %(checked="checked") if @notifications_settings.meet_push_notifications_requirements? %>
182
181
  id="allow_push_notifications"
183
182
  type="checkbox"
183
+ data-push-notifications-toggle
184
184
  name="allow_push_notifications">
185
185
  <span class="toggle__switch-toggle-content">
186
186
  </span>
@@ -194,8 +194,8 @@
194
194
  </div>
195
195
  </div>
196
196
 
197
- <input id="vapidPublicKey" name="vapid_public_key" type="hidden" value="<%= Base64.urlsafe_decode64(Decidim.vapid_public_key.to_s).bytes %>">
198
- <input id="subKeys" name="sub_key" type="hidden" value="<%= current_user.notifications_subscriptions.keys %>">
197
+ <input id="vapidPublicKey" name="vapid_public_key" data-push-vapid-public-key type="hidden" value="<%= Base64.urlsafe_decode64(Decidim.vapid_public_key.to_s).bytes.to_json %>">
198
+ <input id="subKeys" name="sub_key" data-push-sub-keys type="hidden" value="<%= current_user.notifications_subscriptions.keys.to_json %>">
199
199
  <% end %>
200
200
 
201
201
  <div class="form__wrapper-block">
@@ -1,6 +1,6 @@
1
1
  <% add_decidim_page_title(t("decidim.offline.name")) %>
2
2
 
3
- <main id="offline-fallback-html" class="text-center my-8">
3
+ <main id="offline-fallback-html" class="text-center my-8" role="main">
4
4
  <div class="flex justify-center">
5
5
  <svg xmlns="http://www.w3.org/2000/svg" width="5rem" height="r5em" viewBox="0 0 25 25">
6
6
  <path
@@ -42,7 +42,7 @@ edit_link(
42
42
  <div id="accordion-panel-<%= ix %>" class="page__accordion-panel" aria-hidden="true">
43
43
  <ul role="menu">
44
44
  <% topic.pages.each do |page| %>
45
- <li role="menuitem"><%= link_to translated_attribute(page.title), page_path(page), class: "text-secondary" %></li>
45
+ <li role="presentation"><%= link_to translated_attribute(page.title), page_path(page), class: "text-secondary", role: "menuitem" %></li>
46
46
  <% end %>
47
47
  </ul>
48
48
  </div>
@@ -6,6 +6,6 @@
6
6
  <% end %>
7
7
 
8
8
  <%# NOTE: this page does not use a regular layout %>
9
- <main>
9
+ <main role="main">
10
10
  <%= cell "decidim/profile", profile_holder, context: { content_cell: @content_cell } %>
11
11
  </main>
@@ -12,12 +12,12 @@
12
12
  <ul class="dropdown dropdown__bottom divide-y divide-gray-3" role="menu">
13
13
  <%= yield %>
14
14
  <% unless defined?(skip_report) && skip_report %>
15
- <li role="menuitem" class="dropdown__item">
16
- <%= cell "decidim/report_button", resource, button_classes: "dropdown__button" %>
15
+ <li role="presentation" class="dropdown__item">
16
+ <%= cell "decidim/report_button", resource, button_classes: "dropdown__button", html_options: { role: "menuitem" } %>
17
17
  </li>
18
18
  <% end %>
19
- <li role="menuitem" class="dropdown__item">
20
- <%= cell "decidim/follow_button", resource, large: false, button_classes: "dropdown__button" %>
19
+ <li role="presentation" class="dropdown__item">
20
+ <%= cell "decidim/follow_button", resource, large: false, button_classes: "dropdown__button", html_options: { role: "menuitem" } %>
21
21
  </li>
22
22
  </ul>
23
23
  </div>
@@ -6,7 +6,7 @@
6
6
  <% end %>
7
7
 
8
8
  <%# NOTE: this page does not use a regular layout %>
9
- <main>
9
+ <main role="main">
10
10
  <%= cell "decidim/profile", user, context: {
11
11
  content_cell: "decidim/user_activity",
12
12
  activities:,