decidim-core 0.30.7 → 0.30.9

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
  3. data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
  4. data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
  5. data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
  6. data/app/cells/decidim/data_consent/category.erb +5 -5
  7. data/app/cells/decidim/upload_modal_cell.rb +5 -0
  8. data/app/controllers/decidim/download_your_data_controller.rb +1 -1
  9. data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
  10. data/app/controllers/decidim/private_downloads_controller.rb +29 -0
  11. data/app/helpers/decidim/mailer_helper.rb +36 -0
  12. data/app/helpers/decidim/menu_helper.rb +2 -1
  13. data/app/helpers/decidim/newsletters_helper.rb +4 -22
  14. data/app/mailers/decidim/application_mailer.rb +4 -0
  15. data/app/models/decidim/attachment.rb +20 -2
  16. data/app/models/decidim/authorization.rb +7 -0
  17. data/app/models/decidim/private_download.rb +61 -0
  18. data/app/models/decidim/private_export.rb +6 -0
  19. data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
  20. data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
  21. data/app/packs/src/decidim/datepicker/generate_timepicker.js +9 -1
  22. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
  23. data/app/packs/src/decidim/sw/push-permissions.js +47 -12
  24. data/app/presenters/decidim/menu_item_presenter.rb +7 -1
  25. data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
  26. data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
  27. data/app/services/decidim/send_push_notification.rb +5 -1
  28. data/app/views/decidim/devise/registrations/new.html.erb +1 -0
  29. data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
  30. data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
  31. data/app/views/decidim/notifications_settings/show.html.erb +5 -5
  32. data/config/locales/ca-IT.yml +1 -0
  33. data/config/locales/ca.yml +1 -0
  34. data/config/locales/cs.yml +5 -0
  35. data/config/locales/de.yml +13 -0
  36. data/config/locales/en.yml +1 -0
  37. data/config/locales/es-MX.yml +1 -0
  38. data/config/locales/es-PY.yml +1 -0
  39. data/config/locales/es.yml +1 -0
  40. data/config/locales/eu.yml +4 -0
  41. data/config/locales/fi-plain.yml +5 -0
  42. data/config/locales/fi.yml +7 -2
  43. data/config/locales/fr-CA.yml +1 -0
  44. data/config/locales/fr.yml +1 -0
  45. data/config/locales/it.yml +10 -0
  46. data/config/locales/pt-BR.yml +1 -1
  47. data/config/locales/sk.yml +1417 -0
  48. data/config/locales/sv.yml +1 -0
  49. data/config/routes.rb +1 -0
  50. data/lib/decidim/content_parsers/blob_parser.rb +2 -2
  51. data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
  52. data/lib/decidim/core/version.rb +1 -1
  53. metadata +11 -6
@@ -0,0 +1,234 @@
1
+ /* global jest */
2
+
3
+ import { adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions";
4
+
5
+ describe("adjustDatePickerPosition", () => {
6
+ let input = null;
7
+ let parent = null;
8
+ let datePickerContainer = null;
9
+
10
+ let originalInnerHeight = window.innerHeight;
11
+
12
+ beforeEach(() => {
13
+ // Setup DOM structure
14
+ parent = document.createElement("div");
15
+ parent.className = "datepicker__date-column";
16
+ document.body.appendChild(parent);
17
+
18
+ input = document.createElement("input");
19
+ Reflect.defineProperty(input, "offsetHeight", {
20
+ configurable: true,
21
+ value: 40
22
+ });
23
+ parent.appendChild(input);
24
+
25
+ datePickerContainer = document.createElement("div");
26
+ datePickerContainer.className = "datepicker__container";
27
+ parent.appendChild(datePickerContainer);
28
+
29
+ // Mock offsetHeight for calendar
30
+ Reflect.defineProperty(datePickerContainer, "offsetHeight", {
31
+ configurable: true,
32
+ value: 300
33
+ });
34
+
35
+ // store original viewport height
36
+ originalInnerHeight = window.innerHeight;
37
+ });
38
+
39
+ afterEach(() => {
40
+ document.body.removeChild(parent);
41
+
42
+ Reflect.defineProperty(window, "innerHeight", {
43
+ writable: true,
44
+ configurable: true,
45
+ value: originalInnerHeight
46
+ });
47
+
48
+ jest.restoreAllMocks();
49
+ });
50
+
51
+ it("sets parent position to relative when static", () => {
52
+ parent.style.position = "static";
53
+
54
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
55
+
56
+ expect(parent.style.position).toBe("relative");
57
+ });
58
+
59
+ it("does not change parent position when already positioned", () => {
60
+ parent.style.position = "absolute";
61
+
62
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
63
+
64
+ expect(parent.style.position).toBe("absolute");
65
+ });
66
+
67
+ it("opens below when sufficient space below", () => {
68
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
69
+ top: 100,
70
+ bottom: 140
71
+ });
72
+
73
+ Reflect.defineProperty(window, "innerHeight", {
74
+ writable: true,
75
+ configurable: true,
76
+ value: 800
77
+ });
78
+
79
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
80
+
81
+ expect(datePickerContainer.style.top).toBe("40px");
82
+ expect(datePickerContainer.style.bottom).toBe("");
83
+ });
84
+
85
+ it("opens above when insufficient space below", () => {
86
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
87
+ top: 400,
88
+ bottom: 440
89
+ });
90
+
91
+ Reflect.defineProperty(window, "innerHeight", {
92
+ writable: true,
93
+ configurable: true,
94
+ value: 500
95
+ });
96
+
97
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
98
+
99
+ expect(datePickerContainer.style.top).toBe("");
100
+ expect(datePickerContainer.style.bottom).toBe("40px");
101
+ });
102
+
103
+ it("prefers opening below when space is equal above and below", () => {
104
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
105
+ top: 250,
106
+ bottom: 290
107
+ });
108
+
109
+ Reflect.defineProperty(window, "innerHeight", {
110
+ writable: true,
111
+ configurable: true,
112
+ value: 540
113
+ });
114
+
115
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
116
+
117
+ expect(datePickerContainer.style.top).toBe("40px");
118
+ expect(datePickerContainer.style.bottom).toBe("");
119
+ });
120
+
121
+ it("always sets right position to 0px", () => {
122
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
123
+ top: 100,
124
+ bottom: 140
125
+ });
126
+
127
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
128
+
129
+ expect(datePickerContainer.style.right).toBe("0px");
130
+ });
131
+ });
132
+
133
+
134
+ describe("adjustTimePickerPosition", () => {
135
+ let input = null;
136
+ let parent = null;
137
+ let timePicker = null;
138
+
139
+ let originalInnerHeight = window.innerHeight;
140
+
141
+ beforeEach(() => {
142
+ parent = document.createElement("div");
143
+ parent.className = "datepicker__time-column";
144
+ document.body.appendChild(parent);
145
+
146
+ input = document.createElement("input");
147
+ Reflect.defineProperty(input, "offsetHeight", {
148
+ configurable: true,
149
+ value: 30
150
+ });
151
+ parent.appendChild(input);
152
+
153
+ timePicker = document.createElement("div");
154
+ timePicker.className = "timepicker__container";
155
+ parent.appendChild(timePicker);
156
+
157
+ Reflect.defineProperty(timePicker, "offsetHeight", {
158
+ configurable: true,
159
+ value: 200
160
+ });
161
+
162
+ // store original value before any test mutates it
163
+ originalInnerHeight = window.innerHeight;
164
+ });
165
+
166
+ afterEach(() => {
167
+ // restore DOM
168
+ document.body.removeChild(parent);
169
+
170
+ // restore window.innerHeight (fix for CodeRabbit warning)
171
+ Reflect.defineProperty(window, "innerHeight", {
172
+ writable: true,
173
+ configurable: true,
174
+ value: originalInnerHeight
175
+ });
176
+
177
+ jest.restoreAllMocks();
178
+ });
179
+
180
+ it("sets parent position to relative when static", () => {
181
+ parent.style.position = "static";
182
+
183
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
184
+
185
+ expect(parent.style.position).toBe("relative");
186
+ });
187
+
188
+ it("opens below when there is enough space", () => {
189
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
190
+ top: 100,
191
+ bottom: 130
192
+ });
193
+
194
+ Reflect.defineProperty(window, "innerHeight", {
195
+ writable: true,
196
+ configurable: true,
197
+ value: 700
198
+ });
199
+
200
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
201
+
202
+ expect(timePicker.style.top).toBe("30px");
203
+ expect(timePicker.style.bottom).toBe("");
204
+ });
205
+
206
+ it("opens above when there is not enough space below", () => {
207
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
208
+ top: 400,
209
+ bottom: 430
210
+ });
211
+
212
+ Reflect.defineProperty(window, "innerHeight", {
213
+ writable: true,
214
+ configurable: true,
215
+ value: 500
216
+ });
217
+
218
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
219
+
220
+ expect(timePicker.style.top).toBe("");
221
+ expect(timePicker.style.bottom).toBe("30px");
222
+ });
223
+
224
+ it("always aligns to the right", () => {
225
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
226
+ top: 100,
227
+ bottom: 130
228
+ });
229
+
230
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
231
+
232
+ expect(timePicker.style.right).toBe("0px");
233
+ });
234
+ });
@@ -2,15 +2,37 @@ window.addEventListener("DOMContentLoaded", 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("DOMContentLoaded", 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("DOMContentLoaded", 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("DOMContentLoaded", 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("DOMContentLoaded", 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("DOMContentLoaded", 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
  }
@@ -26,7 +26,7 @@ module Decidim
26
26
  delegate :content_tag, :safe_join, :link_to, :active_link_to_class, :is_active_link?, :icon, to: :@view
27
27
 
28
28
  def render
29
- content_tag :li, role: :menuitem, class: link_wrapper_classes do
29
+ content_tag :li, role: menuitem_role, class: link_wrapper_classes do
30
30
  output = if url == "#"
31
31
  [content_tag(:span, composed_label, class: "sidebar-menu__item-disabled")]
32
32
  else
@@ -62,6 +62,12 @@ module Decidim
62
62
  [@options.element_class, active_class].compact.join(" ")
63
63
  end
64
64
 
65
+ def menuitem_role
66
+ return if @options.role == false
67
+
68
+ @options.role || :menuitem
69
+ end
70
+
65
71
  def active_class
66
72
  active_link_to_class(
67
73
  url,
@@ -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
@@ -23,9 +24,12 @@ module Decidim
23
24
  raise ArgumentError, "Need to provide a title if the notification is a PushNotificationMessage" if notification.is_a?(Decidim::PushNotificationMessage) && title.nil?
24
25
 
25
26
  user = notification.user
27
+ subscriptions = user.notifications_subscriptions.values.select do |subscription|
28
+ supported_push_subscription_endpoint?(subscription["endpoint"])
29
+ end
26
30
 
27
31
  I18n.with_locale(user.locale || user.organization.default_locale) do
28
- user.notifications_subscriptions.values.map do |subscription|
32
+ subscriptions.map do |subscription|
29
33
  payload = build_payload(message_params(notification, title), subscription)
30
34
  # Capture webpush exceptions in order to avoid this call to be repeated by the background job runner
31
35
  # Webpush::Error class is the parent class of all defined errors
@@ -34,6 +34,7 @@
34
34
  <%= f.text_field :name, help_text: t("decidim.devise.registrations.new.username_help"), autocomplete: "name", placeholder: "John Doe" %>
35
35
 
36
36
  <%= f.email_field :email, autocomplete: "email", placeholder: t("placeholder_email", scope: "decidim.devise.shared") %>
37
+ <span class="sr-only"><%= t("placeholder_email", scope: "decidim.devise.shared") %></span>
37
38
 
38
39
  <%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %>
39
40
  </div>
@@ -1,13 +1,13 @@
1
1
  <div id="card__tos" class="form__wrapper-block border-y-2">
2
2
  <h2 class="h4"><%= t("decidim.devise.registrations.new.tos_title") %></h2>
3
+ <span class="sr-only"><%= t("forms.required") %></span>
3
4
 
4
- <div>
5
+ <div id="terms_of_service_summary">
5
6
  <% terms_of_service_summary_content_blocks.each do |content_block| %>
6
7
  <%= cell content_block.manifest.cell, content_block %>
7
8
  <% end %>
8
9
  </div>
9
-
10
- <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service"))), label_options: { class: "form__wrapper-checkbox-label" } %>
10
+ <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service"))), label_options: { class: "form__wrapper-checkbox-label" }, "aria-describedby": "terms_of_service_summary" %>
11
11
  </div>
12
12
 
13
13
  <div id="card__newsletter" class="form__wrapper-block">
@@ -15,7 +15,7 @@
15
15
 
16
16
  <blockquote>
17
17
  <p>
18
- <%= @event_instance.safe_resource_text %>
18
+ <%= decidim_transform_image_urls(@event_instance.safe_resource_text, @organization.host).html_safe %>
19
19
  </p>
20
20
  </blockquote>
21
21
  <% end %>
@@ -28,7 +28,7 @@
28
28
  <p style="font-weight: bold"><%= t(".translated_text") %></p>
29
29
  <blockquote>
30
30
  <p>
31
- <%= @event_instance.safe_resource_translated_text %>
31
+ <%= decidim_transform_image_urls(@event_instance.safe_resource_translated_text, @organization.host).html_safe %>
32
32
  </p>
33
33
  </blockquote>
34
34
  <% end %>
@@ -40,7 +40,7 @@
40
40
  <table>
41
41
  <tr>
42
42
  <td>
43
- <%= link_to @event_instance.button_text, @event_instance.button_url, target: :blank %>
43
+ <%= link_to decidim_sanitize(@event_instance.button_text, strip_tags: true), @event_instance.button_url, target: :blank %>
44
44
  </td>
45
45
  </tr>
46
46
  </table>
@@ -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(Rails.application.secrets.vapid[:public_key]).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(Rails.application.secrets.vapid[:public_key]).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">
@@ -1525,6 +1525,7 @@ ca-IT:
1525
1525
  own_activity: La meva pròpia activitat, com quan algú fa comentaris a la meva proposta o em menciona
1526
1526
  push_notifications: Notificacions emergents
1527
1527
  push_notifications_reminder: Per rebre notificacions de la plataforma, primer les has de permetre a la configuració del teu navegador.
1528
+ push_notifications_unsupported_browser: El navegador no és compatible.
1528
1529
  receive_notifications_about: Vull rebre notificacions
1529
1530
  update_notifications_settings: Guardar canvis
1530
1531
  valuators: Avaluadores
@@ -1525,6 +1525,7 @@ ca:
1525
1525
  own_activity: La meva pròpia activitat, com quan algú fa comentaris a la meva proposta o em menciona
1526
1526
  push_notifications: Notificacions emergents
1527
1527
  push_notifications_reminder: Per rebre notificacions de la plataforma, primer les has de permetre a la configuració del teu navegador.
1528
+ push_notifications_unsupported_browser: El navegador no és compatible.
1528
1529
  receive_notifications_about: Vull rebre notificacions
1529
1530
  update_notifications_settings: Guardar canvis
1530
1531
  valuators: Avaluadores
@@ -844,6 +844,7 @@ cs:
844
844
  delete_reason: Důvod pro odstranění tohoto uživatele
845
845
  deleted_at: Datum a čas, kdy byl tento uživatel odstraněn
846
846
  email: E-mailová adresa tohoto uživatele
847
+ followers_count: Počet účastníků, kteří sledují tohoto uživatele
847
848
  following_count: Počet účastníků, které tento uživatel sleduje
848
849
  id: Jedinečný identifikátor tohoto uživatele
849
850
  invitation_accepted_at: Datum a čas, kdy byla pozvánka přijata
@@ -1214,6 +1215,9 @@ cs:
1214
1215
  actions:
1215
1216
  confirm_modal:
1216
1217
  ok_add: Přidat administrátora
1218
+ ok_remove: Odstranit správce
1219
+ title_add: Potvrdit nového správce
1220
+ title_remove: Odstranit správce
1217
1221
  demote_admin: Odstranit admin
1218
1222
  demote:
1219
1223
  error: Při odebrání tohoto účastníka ze seznamu administrátorů došlo k chybě.
@@ -1328,6 +1332,7 @@ cs:
1328
1332
  create_with_space: "%{user_name} vytvořil %{resource_name} v %{space_name}"
1329
1333
  delete: "%{user_name} odstraněno %{resource_name}"
1330
1334
  delete_with_space: "%{user_name} smazán %{resource_name} v %{space_name}"
1335
+ publish: "%{user_name} publikoval %{resource_name}"
1331
1336
  publish_with_space: "%{user_name} publikoval %{resource_name} v %{space_name}"
1332
1337
  unknown_action: "%{user_name} provedla nějakou akci na %{resource_name}"
1333
1338
  unknown_action_with_space: "%{user_name} provedlo nějakou akci na %{resource_name} v %{space_name}"
@@ -1118,6 +1118,8 @@ de:
1118
1118
  explanation: 'Anleitung für Bild:'
1119
1119
  message_1: Vorzugsweise ein Bild im Querformat, das keinen Text enthält.
1120
1120
  message_2: Der Dienst schneidet die Datei zu.
1121
+ import_file:
1122
+ message_1: Muss ein JSON-Dokument sein, das über die Exportfunktion heruntergeladen wurde.
1121
1123
  file_validation:
1122
1124
  allowed_file_extensions: 'Erlaubte Dateiformate: %{extensions}'
1123
1125
  max_file_dimension: 'Maximale Dateigröße: %{resolution} Pixel'
@@ -1478,9 +1480,13 @@ de:
1478
1480
  same_language: Der Inhalt wurde in Ihrer bevorzugten Sprache (%{language}) veröffentlicht, daher wird in dieser E-Mail keine automatisierte Übersetzung angezeigt.
1479
1481
  translated_text: 'Automatisch übersetzter Text:'
1480
1482
  notifications:
1483
+ action_error: Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten.
1481
1484
  no_notifications: Noch keine Benachrichtigungen
1482
1485
  show:
1486
+ deleted: Inhalt wurde vom Autor gelöscht.
1483
1487
  missing_event: Hoppla, diese Benachrichtigung gehört zu einem Artikel, der nicht mehr verfügbar ist. Du kannst sie verwerfen.
1488
+ moderated: Inhalt wurde durch Moderation versteckt.
1489
+ not_available: Hoppla, diese Benachrichtigung gehört zu einem Artikel, der nicht mehr verfügbar ist. Es ist keine weitere Aktion erforderlich.
1484
1490
  notifications_digest_mailer:
1485
1491
  header:
1486
1492
  daily: Tägliche Zusammenfassung
@@ -1489,6 +1495,7 @@ de:
1489
1495
  hello: Hallo %{name}
1490
1496
  intro:
1491
1497
  daily: 'Dies sind die Benachrichtigungen vom letzten Tag basierend auf den Aktivitäten, denen Sie folgen:'
1498
+ real_time: 'Es gibt eine Benachrichtigung über die Aktivität, die Sie folgen:'
1492
1499
  weekly: 'Dies sind die Benachrichtigungen der letzten Woche, basierend auf den Aktivitäten, die Sie folgen:'
1493
1500
  outro: Sie haben diese Benachrichtigung erhalten, weil Sie diesen Inhalt oder seine Verfassenden folgen. Sie können dem Inhalt direkt auf seiner Seite entfolgen.
1494
1501
  see_more: Weitere Benachrichtigungen ansehen
@@ -1642,7 +1649,9 @@ de:
1642
1649
  title: Wie man diese Dateien öffnet und mit ihnen arbeitet
1643
1650
  license:
1644
1651
  body_1_html: Diese Datenbank von %{organization_name} wird unter %{link_database} zur Verfügung gestellt. Alle Rechte an einzelnen Inhalten der Datenbank sind unter %{link_contents} lizenziert.
1652
+ license_contents_link: https://opendatacommons.org/licenses/dbcl/1.0/
1645
1653
  license_contents_name: Lizenz für Datenbankinhalte
1654
+ license_database_link: https://opendatacommons.org/licenses/odbl/1.0/
1646
1655
  license_database_name: Offene Datenbanklizenz
1647
1656
  title: Lizenz
1648
1657
  title: Offene Daten
@@ -1962,6 +1971,7 @@ de:
1962
1971
  send_paranoid_instructions: Wenn Ihre E-Mail-Adresse in unserer Datenbank vorhanden ist, erhalten Sie innerhalb weniger Minuten eine E-Mail mit Anweisungen zur Bestätigung Ihrer E-Mail-Adresse.
1963
1972
  failure:
1964
1973
  already_authenticated: Sie sind bereits angemeldet.
1974
+ csrf_token: Ihre Anfrage konnte nicht verifiziert werden. Bitte versuchen Sie es erneut.
1965
1975
  inactive: Dein Benutzerkonto ist noch nicht aktiviert.
1966
1976
  invalid: Ungültige %{authentication_keys} oder Passwort
1967
1977
  invited: Sie haben eine ausstehende Einladung, akzeptieren Sie sie, um die Erstellung Ihres Kontos abzuschließen.
@@ -1976,6 +1986,7 @@ de:
1976
1986
  nickname_help: Ihr Pseudonym auf %{organization}. Kann nur Buchstaben, Zahlen, '-' und '_' enthalten.
1977
1987
  submit_button: Speichern
1978
1988
  subtitle: Wenn Sie die Einladung annehmen, geben Sie bitte Ihren Kontonamen und Ihr Passwort ein.
1989
+ subtitle_no_password: Wenn Sie die Einladung annehmen, geben Sie bitte Ihren Kontonamen ein.
1979
1990
  invitation_removed: Ihre Einladung wurde entfernt.
1980
1991
  invitation_token_invalid: Das angegebene Einladungstoken ist nicht gültig!
1981
1992
  new:
@@ -1983,6 +1994,8 @@ de:
1983
1994
  submit_button: Eine Einladung schicken
1984
1995
  no_invitations_remaining: Keine Einladungen übrig
1985
1996
  send_instructions: Eine Einladungs-E-Mail wurde an %{email}gesendet.
1997
+ updated: Einladung erfolgreich angenommen. Sie sind jetzt angemeldet.
1998
+ updated_not_active: Einladung erfolgreich angenommen.
1986
1999
  mailer:
1987
2000
  confirmation_instructions:
1988
2001
  action: Konto bestätigen
@@ -1532,6 +1532,7 @@ en:
1532
1532
  own_activity: My own activity, like when someone comments in my proposal or mentions me
1533
1533
  push_notifications: Push notifications
1534
1534
  push_notifications_reminder: To get notifications from the platform, you will need to allow them in your browser settings first.
1535
+ push_notifications_unsupported_browser: Your browser is not supported.
1535
1536
  receive_notifications_about: I want to get notifications about
1536
1537
  update_notifications_settings: Save changes
1537
1538
  valuators: Evaluators
@@ -1528,6 +1528,7 @@ es-MX:
1528
1528
  own_activity: Mi propia actividad, como cuando alguien comenta en mi propuesta o me menciona.
1529
1529
  push_notifications: Notificaciones emergentes
1530
1530
  push_notifications_reminder: Para obtener notificaciones de la plataforma, primero tienes que permitirlas en la configuración de tu navegador.
1531
+ push_notifications_unsupported_browser: Tu navegador no es compatible.
1531
1532
  receive_notifications_about: Quiero recibir notificaciones sobre
1532
1533
  update_notifications_settings: Guardar cambios
1533
1534
  valuators: Evaluadoras
@@ -1528,6 +1528,7 @@ es-PY:
1528
1528
  own_activity: Mi propia actividad, como cuando alguien comenta en mi propuesta o me menciona.
1529
1529
  push_notifications: Notificaciones emergentes
1530
1530
  push_notifications_reminder: Para obtener notificaciones de la plataforma, primero tienes que permitirlas en la configuración de tu navegador.
1531
+ push_notifications_unsupported_browser: Tu navegador no es compatible.
1531
1532
  receive_notifications_about: Quiero recibir notificaciones sobre
1532
1533
  update_notifications_settings: Guardar cambios
1533
1534
  valuators: Evaluadoras