Skip to content

Commit 7d284a7

Browse files
authored
Merge branch 'develop' into art/mod-spam-tools
2 parents 05e8eba + dc5aef4 commit 7d284a7

16 files changed

Lines changed: 332 additions & 157 deletions

.prettierrc.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @type {import('prettier').Config}
3+
*/
4+
const config = {
5+
braceStyle: "stroustrup",
6+
plugins: ["prettier-plugin-brace-style"],
7+
printWidth: 120,
8+
singleQuote: true,
9+
tabWidth: 2,
10+
};
11+
12+
export default config;

app/assets/javascripts/filters.js

Lines changed: 143 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,169 @@
1-
$(() => {
2-
$('.js-filter-select').toArray().forEach(async (el) => {
3-
const $select = $(el);
4-
const $form = $select.closest('form');
5-
const $formFilters = $form.find('.form--filter');
6-
const $saveButton = $form.find('.filter-save');
7-
const $isDefaultCheckbox = $form.find('.filter-is-default');
8-
const categoryId = $isDefaultCheckbox.val()?.toString();
9-
let defaultFilter = await QPixel.defaultFilter(categoryId);
10-
const $deleteButton = $form.find('.filter-delete');
11-
12-
// Enables/Disables Save & Delete buttons programatically
13-
async function computeEnables() {
14-
const filters = await QPixel.filters();
15-
const filterName = $select.val()?.toString();
16-
17-
// Nothing set
18-
if (!filterName) {
19-
$saveButton.prop('disabled', true);
20-
$deleteButton.prop('disabled', true);
21-
return;
22-
}
1+
document.addEventListener('DOMContentLoaded', () => {
2+
$('.js-filter-select')
3+
.toArray()
4+
.forEach(async (el) => {
5+
const $select = $(el);
6+
const $form = $select.closest('form');
7+
const $formFilters = $form.find('.form--filter');
8+
const $saveButton = $form.find('.filter-save');
9+
const $isDefaultCheckbox = $form.find('.filter-is-default');
10+
const categoryId = $isDefaultCheckbox.val()?.toString();
11+
let defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
12+
const $deleteButton = $form.find('.filter-delete');
13+
14+
// Enables/Disables Save & Delete buttons programatically
15+
async function computeEnables() {
16+
const filters = await QPixel.filters();
17+
const filterName = $select.val()?.toString();
18+
19+
// Nothing set
20+
if (!filterName) {
21+
$saveButton.prop('disabled', true);
22+
$deleteButton.prop('disabled', true);
23+
return;
24+
}
2325

24-
const filter = filters[filterName]
26+
const filter = filters[filterName];
2527

26-
// New filter
27-
if (!filter) {
28-
$saveButton.prop('disabled', false);
29-
$deleteButton.prop('disabled', true);
30-
return;
31-
}
28+
// New filter
29+
if (!filter) {
30+
$saveButton.prop('disabled', false);
31+
$deleteButton.prop('disabled', true);
32+
return;
33+
}
3234

33-
// Not a new filter
34-
$deleteButton.prop('disabled', filter.system);
35+
// Not a new filter
36+
$deleteButton.prop('disabled', filter.system);
3537

36-
const hasChanges = [...$formFilters].some((el) => {
37-
const filterValue = filter[el.dataset.name];
38-
let elValue = /** @type {string | undefined[]} */ ($(el).val());
39-
if (filterValue?.constructor == Array) {
40-
elValue = elValue ?? [];
41-
return filterValue.length != elValue.length || filterValue.some((v, i) => v[1] != elValue[i]);
42-
}
43-
else {
44-
return filterValue ? filterValue != elValue : elValue;
45-
}
46-
});
47-
const defaultStatusChanged = $isDefaultCheckbox.prop('checked') != (defaultFilter === $select.val());
48-
$saveButton.prop('disabled', !defaultStatusChanged && (filter.system || !hasChanges));
49-
}
50-
51-
async function initializeSelect() {
52-
defaultFilter = await QPixel.defaultFilter(categoryId);
53-
$isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
54-
const filters = await QPixel.filters();
55-
56-
function template(option) {
57-
if (option.id == '') { return 'Default'; }
58-
59-
const filter = filters[option.id];
60-
const name = `<span>${option.text}</span>`;
61-
const systemIndicator = filter?.system
62-
? ' <span has-font-size-caption">(System)</span>'
63-
: '';
64-
const newIndicator = !filter
65-
? ' <span has-font-size-caption">(New)</span>'
66-
: '';
67-
return $(name + systemIndicator + newIndicator);
38+
const hasChanges = [...$formFilters].some((el) => {
39+
const filterValue = filter[el.dataset.name];
40+
let elValue = /** @type {string | undefined[]} */ ($(el).val());
41+
if (filterValue?.constructor == Array) {
42+
elValue = elValue ?? [];
43+
return filterValue.length != elValue.length || filterValue.some((v, i) => v[1] != elValue[i]);
44+
}
45+
else {
46+
return filterValue ? filterValue != elValue : elValue;
47+
}
48+
});
49+
const defaultStatusChanged = $isDefaultCheckbox.prop('checked') != (defaultFilter === $select.val());
50+
$saveButton.prop('disabled', !defaultStatusChanged && (filter.system || !hasChanges));
6851
}
6952

70-
// Clear out any old options
71-
$select.children().filter((_, /** @type{HTMLOptionElement} */ option) => {
72-
return option.value && !filters[option.value];
73-
}).detach();
53+
async function initializeSelect() {
54+
defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
55+
$isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
56+
const filters = await QPixel.filters();
7457

75-
$select.select2({
76-
data: Object.keys(filters).map((filterName) => {
77-
return {
78-
id: filterName,
79-
text: filterName
58+
function template(option) {
59+
if (option.id == '') {
60+
return 'Default';
8061
}
81-
}),
82-
tags: true,
83-
templateResult: template,
84-
templateSelection: template
85-
});
8662

87-
$select.on('select2:select', /** @type {(event: Select2.Event) => void} */ (async (evt) => {
88-
const filterName = evt.params.data.id;
89-
const preset = filters[filterName];
63+
const filter = filters[option.id];
64+
const name = `<span>${option.text}</span>`;
65+
const systemIndicator = filter?.system ? ' <span has-font-size-caption">(System)</span>' : '';
66+
const newIndicator = !filter ? ' <span has-font-size-caption">(New)</span>' : '';
67+
return $(name + systemIndicator + newIndicator);
68+
}
9069

91-
$isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
70+
// Clear out any old options
71+
$select
72+
.children()
73+
.filter((_, /** @type{HTMLOptionElement} */ option) => {
74+
return option.value && !filters[option.value];
75+
})
76+
.detach();
77+
78+
$select.select2({
79+
data: Object.keys(filters).map((filterName) => {
80+
return {
81+
id: filterName,
82+
text: filterName,
83+
};
84+
}),
85+
tags: true,
86+
templateResult: template,
87+
templateSelection: template,
88+
});
89+
90+
$select.on(
91+
'select2:select',
92+
/** @type {(event: Select2.Event) => void} */ (
93+
async (evt) => {
94+
const filterName = evt.params.data.id;
95+
const preset = filters[filterName];
96+
97+
$isDefaultCheckbox.prop('checked', defaultFilter === $select.val());
98+
computeEnables();
99+
100+
// Name is not one of the presets, i.e user is creating a new preset
101+
if (!preset) {
102+
return;
103+
}
104+
105+
for (const [name, value] of Object.entries(preset)) {
106+
const $el = $form.find(`.form--filter[data-name=${name}]`);
107+
if (value?.constructor == Array) {
108+
$el.val(null);
109+
for (const val of value) {
110+
$el.append(new Option(val[0], val[1].toString(), false, true));
111+
}
112+
$el.trigger('change');
113+
}
114+
else {
115+
$el.val(/** @type {string} */ (value)).trigger('change');
116+
}
117+
}
118+
}
119+
),
120+
);
92121
computeEnables();
122+
}
93123

94-
// Name is not one of the presets, i.e user is creating a new preset
95-
if (!preset) {
96-
return;
97-
}
124+
initializeSelect();
98125

99-
for (const [name, value] of Object.entries(preset)) {
100-
const $el = $form.find(`.form--filter[data-name=${name}]`);
101-
if (value?.constructor == Array) {
102-
$el.val(null);
103-
for (const val of value) {
104-
$el.append(new Option(val[0], val[1].toString(), false, true));
105-
}
106-
$el.trigger('change');
107-
}
108-
else {
109-
$el.val(/** @type {string} */ (value)).trigger('change');
110-
}
126+
// Enable saving when the filter is changed
127+
$formFilters.on('change', computeEnables);
128+
$isDefaultCheckbox.on('change', computeEnables);
129+
130+
async function saveFilter() {
131+
if (!$form[0].reportValidity()) {
132+
return;
111133
}
112-
}));
113-
computeEnables();
114-
}
115134

116-
initializeSelect();
135+
const filter = /** @type {QPixelFilter} */ ({});
117136

118-
// Enable saving when the filter is changed
119-
$formFilters.on('change', computeEnables);
120-
$isDefaultCheckbox.on('change', computeEnables);
137+
for (const el of $formFilters) {
138+
filter[el.dataset.name] = $(el).val();
139+
}
121140

122-
async function saveFilter() {
123-
if (!$form[0].reportValidity()) { return; }
141+
await QPixel.setFilter($select.val()?.toString(), filter, categoryId, $isDefaultCheckbox.prop('checked'));
124142

125-
const filter = /** @type {QPixelFilter} */({});
143+
defaultFilter = categoryId ? await QPixel.defaultFilter(categoryId) : null;
126144

127-
for (const el of $formFilters) {
128-
filter[el.dataset.name] = $(el).val();
145+
// Reinitialize to get new options
146+
await initializeSelect();
129147
}
130148

131-
await QPixel.setFilter($select.val()?.toString(), filter, categoryId, $isDefaultCheckbox.prop('checked'));
132-
defaultFilter = await QPixel.defaultFilter(categoryId);
149+
$saveButton.on('click', saveFilter);
133150

134-
// Reinitialize to get new options
135-
await initializeSelect();
136-
}
137-
138-
$saveButton.on('click', saveFilter);
151+
function clear() {
152+
$select.val(null).trigger('change');
153+
$form.find('.form--filter').val(null).trigger('change');
154+
$isDefaultCheckbox.prop('checked', false);
155+
computeEnables();
156+
}
139157

140-
function clear() {
141-
$select.val(null).trigger('change');
142-
$form.find('.form--filter').val(null).trigger('change');
143-
$isDefaultCheckbox.prop('checked', false);
144-
computeEnables();
145-
}
158+
$deleteButton?.on('click', async (_evt) => {
159+
if (confirm(`Are you sure you want to delete ${$select.val()}?`)) {
160+
await QPixel.deleteFilter($select.val()?.toString());
161+
// Reinitialize to get new options
162+
await initializeSelect();
163+
clear();
164+
}
165+
});
146166

147-
$deleteButton?.on('click', async (_evt) => {
148-
if (confirm(`Are you sure you want to delete ${$select.val()}?`)) {
149-
await QPixel.deleteFilter($select.val()?.toString());
150-
// Reinitialize to get new options
151-
await initializeSelect();
152-
clear();
153-
}
167+
$form.find('.filter-clear').on('click', clear);
154168
});
155-
156-
$form.find('.filter-clear').on('click', clear);
157-
});
158169
});

app/assets/javascripts/notifications.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ $(() => {
5252
ev.preventDefault();
5353
const $inbox = $('.inbox');
5454
if($inbox.hasClass("is-active")) {
55-
const resp = await QPixel.getJSON(`/users/me/notifications`);
55+
const data = await QPixel.getNotifications();
5656

57-
const data = await resp.json();
5857
const $inboxContainer = $inbox.find(".inbox--container");
5958
$inboxContainer.html('');
6059

app/assets/javascripts/qpixel_api.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,16 @@ window.QPixel = {
374374
return data;
375375
},
376376

377+
getNotifications: async () => {
378+
const resp = await QPixel.getJSON(`/users/me/notifications`, {
379+
headers: { 'Cache-Control': 'no-cache' }
380+
});
381+
382+
const data = await resp.json();
383+
384+
return data;
385+
},
386+
377387
getThreadContent: async (id, options) => {
378388
const inline = options?.inline ?? true;
379389
const showDeleted = options?.showDeleted ?? false;

app/controllers/comments_controller.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,12 @@ def thread
194194
end
195195

196196
def thread_content
197-
fresh_when last_modified: @comment_thread.last_activity_at.utc, etag: @comment_thread
198-
199-
render partial: 'comment_threads/expanded', locals: { inline: params[:inline] == 'true',
200-
show_deleted: params[:show_deleted_comments] == '1',
201-
thread: @comment_thread }
197+
if stale?(last_modified: @comment_thread.last_activity_at.utc)
198+
render partial: 'comment_threads/expanded',
199+
locals: { inline: params[:inline] == 'true',
200+
show_deleted: params[:show_deleted_comments] == '1',
201+
thread: @comment_thread }
202+
end
202203
end
203204

204205
def thread_followers

app/controllers/notifications_controller.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ class NotificationsController < ApplicationController
77
def index
88
@notifications = Notification.unscoped.where(user: current_user).paginate(page: params[:page], per_page: 100)
99
.order(Arel.sql('is_read ASC, created_at DESC'))
10-
respond_to do |format|
11-
format.html { render :index, layout: 'without_sidebar' }
12-
format.json do
13-
render json: (@notifications.to_a.map do |notif|
14-
notif.as_json.merge(content: helpers.render_pings_text(notif.content),
15-
community_name: notif.community_name)
16-
end)
10+
11+
if stale?(@notifications)
12+
respond_to do |format|
13+
format.html { render :index, layout: 'without_sidebar' }
14+
format.json do
15+
render json: (@notifications.to_a.map do |notif|
16+
notif.as_json.merge(content: helpers.render_pings_text(notif.content),
17+
community_name: notif.community_name)
18+
end)
19+
end
1720
end
1821
end
1922
end

0 commit comments

Comments
 (0)