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.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
- data/app/cells/decidim/data_consent/category.erb +5 -5
- data/app/cells/decidim/upload_modal_cell.rb +5 -0
- data/app/controllers/decidim/download_your_data_controller.rb +1 -1
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
- data/app/controllers/decidim/private_downloads_controller.rb +29 -0
- data/app/helpers/decidim/mailer_helper.rb +36 -0
- data/app/helpers/decidim/menu_helper.rb +2 -1
- data/app/helpers/decidim/newsletters_helper.rb +4 -22
- data/app/mailers/decidim/application_mailer.rb +4 -0
- data/app/models/decidim/attachment.rb +20 -2
- data/app/models/decidim/authorization.rb +7 -0
- data/app/models/decidim/private_download.rb +61 -0
- data/app/models/decidim/private_export.rb +6 -0
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
- data/app/packs/src/decidim/datepicker/generate_timepicker.js +9 -1
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
- data/app/packs/src/decidim/sw/push-permissions.js +47 -12
- data/app/presenters/decidim/menu_item_presenter.rb +7 -1
- data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
- data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
- data/app/services/decidim/send_push_notification.rb +5 -1
- data/app/views/decidim/devise/registrations/new.html.erb +1 -0
- data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
- data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
- data/app/views/decidim/notifications_settings/show.html.erb +5 -5
- data/config/locales/ca-IT.yml +1 -0
- data/config/locales/ca.yml +1 -0
- data/config/locales/cs.yml +5 -0
- data/config/locales/de.yml +13 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/es-MX.yml +1 -0
- data/config/locales/es-PY.yml +1 -0
- data/config/locales/es.yml +1 -0
- data/config/locales/eu.yml +4 -0
- data/config/locales/fi-plain.yml +5 -0
- data/config/locales/fi.yml +7 -2
- data/config/locales/fr-CA.yml +1 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/it.yml +10 -0
- data/config/locales/pt-BR.yml +1 -1
- data/config/locales/sk.yml +1417 -0
- data/config/locales/sv.yml +1 -0
- data/config/routes.rb +1 -0
- data/lib/decidim/content_parsers/blob_parser.rb +2 -2
- data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
- data/lib/decidim/core/version.rb +1 -1
- metadata +11 -6
data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js
ADDED
|
@@ -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("
|
|
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("
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
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
|
-
|
|
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">
|
data/config/locales/ca-IT.yml
CHANGED
|
@@ -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
|
data/config/locales/ca.yml
CHANGED
|
@@ -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
|
data/config/locales/cs.yml
CHANGED
|
@@ -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}"
|
data/config/locales/de.yml
CHANGED
|
@@ -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
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/config/locales/es-MX.yml
CHANGED
|
@@ -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
|
data/config/locales/es-PY.yml
CHANGED
|
@@ -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
|