Skip to content

Commit f492bd1

Browse files
authored
Merge branch 'develop' into 0valt/1459/draft-discard
2 parents 373d943 + ee9e02a commit f492bd1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+635
-177
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ gem 'counter_culture', '~> 3.2'
77
gem 'fastimage', '~> 2.2'
88
gem 'image_processing', '~> 1.12'
99
gem 'jquery-rails', '~> 4.5.0'
10+
gem 'mime-types', '~> 3.7'
1011
gem 'mysql2', '~> 0.5.4'
1112
gem 'puma', '~> 5.6'
1213
gem 'rails', '~> 7.2'

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ DEPENDENCIES
493493
listen (~> 3.7)
494494
maintenance_tasks (~> 2.2)
495495
memory_profiler (~> 1.0)
496+
mime-types (~> 3.7)
496497
minitest (~> 5.16.0)
497498
minitest-ci (~> 3.4.0)
498499
msgpack (~> 1.8)

app/assets/javascripts/flags.js

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
$(() => {
2-
$(document).on('click', '.flag-link', (ev) => {
2+
$(document).on('click', '.flag-link', async (ev) => {
33
ev.preventDefault();
44
const self = $(ev.target);
55
const isCommentFlag = self.hasClass('js-comment-flag');
@@ -21,6 +21,10 @@ $(() => {
2121
}
2222

2323
const postId = self.data('post-id');
24+
25+
/**
26+
* @type {QPixelFlagData}
27+
*/
2428
const data = {
2529
'flag_type': (reason !== -1) ? reason : null,
2630
'post_id': postId,
@@ -29,47 +33,26 @@ $(() => {
2933
};
3034

3135
if (requiresDetails && data['reason'].length < 1) {
32-
QPixel.createNotification('danger',
33-
'Details are required for this flag type - please enter a message.');
36+
QPixel.createNotification('danger', 'Details are required for this flag type - please enter a message.');
3437
return;
3538
}
3639

37-
const responseType = isCommentFlag ? null : activeRadio.data('response-type');
40+
const closeFlagModal = () => {
41+
self.parents('.js-flag-box').removeClass('is-active');
42+
$(`#flag-comment-${postId}`).removeClass('is-active');
43+
};
3844

39-
$.ajax({
40-
'type': 'POST',
41-
'url': '/flags/new',
42-
'data': data,
43-
// TODO: review
44-
// 'target': self
45-
})
46-
.done((response) => {
47-
if(response.status !== 'success') {
48-
QPixel.createNotification('danger', '<strong>Failed:</strong> ' + response.message);
49-
}
50-
else {
51-
const messages = {
52-
comment: `<strong>Thanks!</strong> Your flag has been added as a comment for the author to review.`
53-
};
54-
const defaultMessage = `<strong>Thanks!</strong> We will review your flag.`;
55-
QPixel.createNotification('success', messages[responseType] || defaultMessage);
56-
$(`#flag-post-${postId}`).val('');
57-
}
58-
self.parents('.js-flag-box').removeClass('is-active');
59-
$(`#flag-comment-${postId}`).removeClass('is-active');
45+
try {
46+
const response = await QPixel.flag(data);
6047

61-
})
62-
.fail((jqXHR, _textStatus, _errorThrown) => {
63-
let message = jqXHR.status;
64-
try {
65-
message = JSON.parse(jqXHR.responseText)['message'];
66-
}
67-
finally {
68-
QPixel.createNotification('danger', '<strong>Failed:</strong> ' + message);
69-
}
70-
self.parents('.js-flag-box').removeClass('is-active');
71-
$(`#flag-comment-${postId}`).removeClass('is-active');
72-
});
48+
QPixel.handleJSONResponse(response, (data) => {
49+
QPixel.createNotification('success', data.message);
50+
$(`#flag-post-${postId}`).val('');
51+
}, closeFlagModal);
52+
} catch(e) {
53+
console.warn(`[flags/new] API error:`, e);
54+
QPixel.createNotification('danger', 'Failed to flag.');
55+
}
7356
});
7457

7558
$('.js-start-escalate').on('click', (ev) => {

app/assets/javascripts/modals.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
document.addEventListener('DOMContentLoaded', () => {
2-
document.addEventListener('keypress', (ev) => {
3-
if (ev.code === 'Escape') {
4-
document.querySelectorAll('.modal').forEach((el) => el.classList.remove('is-active'));
2+
document.addEventListener('keyup', (ev) => {
3+
if (ev.code === 'Escape' && !ev.metaKey && !ev.ctrlKey) {
4+
document.querySelectorAll('.modal').forEach((el) => {
5+
el.classList.remove('is-active');
6+
});
57
}
68
});
79
});

app/assets/javascripts/posts.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ $(() => {
4848
$uploadForm.trigger('submit')
4949
});
5050

51+
new MutationObserver((records) => {
52+
for (const record of records) {
53+
if (record.target instanceof HTMLElement &&
54+
record.target.id === 'markdown-image-upload' &&
55+
record.target.classList.contains('is-active')) {
56+
const fileInput = record.target.querySelector('input[type="file"]');
57+
58+
if (fileInput instanceof HTMLInputElement) {
59+
fileInput.focus();
60+
}
61+
}
62+
}
63+
}).observe(document, {
64+
attributeFilter: ['class'],
65+
subtree: true,
66+
});
67+
5168
$uploadForm.on('submit', async (evt) => {
5269
evt.preventDefault();
5370

app/assets/javascripts/qpixel_api.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,10 @@ window.QPixel = {
217217
}
218218

219219
let prefs = await QPixel._getPreferences();
220-
let value = community ? prefs.community[name] : prefs.global[name];
220+
let value = community ? prefs?.community[name] : prefs?.global[name];
221221

222222
// Note that null is a valid value for a preference, but undefined means we haven't fetched it.
223-
if (typeof (value) !== 'undefined') {
223+
if (typeof value !== 'undefined') {
224224
return value;
225225
}
226226
// If we haven't fetched a preference, that probably means it's new - run a full re-fetch.
@@ -443,13 +443,25 @@ window.QPixel = {
443443
return content;
444444
},
445445

446-
handleJSONResponse: (data, onSuccess) => {
446+
handleJSONResponse: (data, onSuccess, onFinally) => {
447447
if (data.status === 'success') {
448-
onSuccess(data)
448+
onSuccess(data);
449449
}
450450
else {
451451
QPixel.createNotification('danger', data.message);
452452
}
453+
454+
onFinally?.(data);
455+
},
456+
457+
flag: async (flag) => {
458+
const resp = await QPixel.fetchJSON(`/flags/new`, { ...flag }, {
459+
headers: { 'Accept': 'application/json' }
460+
});
461+
462+
const data = await resp.json();
463+
464+
return data;
453465
},
454466

455467
deleteComment: async (id) => {

app/assets/javascripts/site_settings.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
$(() => {
2+
/**
3+
* @type {Record<string, JQuery<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>>}
4+
*/
25
const settingEditFields = {
6+
'array': $(`<select class="form-element js-setting-edit" multiple></select>`),
37
'string': $(`<input type="text" class="form-element js-setting-edit" />`),
48
'integer': $('<input type="number" class="form-element js-setting-edit" />'),
59
'float': $('<input type="number" step="0.0001" class="form-element js-setting-edit" />'),
6-
'boolean': $(`<select class="form-element js-setting-edit"><option value></option><option value="true">true</option><option value="false">false</option></select>`),
10+
'boolean': $(`<select class="form-element js-setting-edit">
11+
<option value></option>
12+
<option value="true">true</option>
13+
<option value="false">false</option>
14+
</select>`),
715
'json': $(`<textarea rows="5" cols="100" class="form-element js-setting-edit"></textarea>`),
816
'text': $(`<textarea rows="5" cols="100" class="form-element js-setting-edit"></textarea>`)
917
};
@@ -26,9 +34,34 @@ $(() => {
2634
const data = await resp.json();
2735
const value = data.typed;
2836

29-
const form = settingEditFields[valueType].clone().val(!!value ? value.toString() : '')
30-
.attr('data-name', name).attr('data-community-id', communityId);
31-
$tgt.addClass('editing').html(form).append(`<button class="button is-primary is-filled js-setting-submit">Update</button>`);
37+
const field = settingEditFields[valueType].clone()
38+
.attr('data-name', name)
39+
.attr('data-community-id', communityId)
40+
.get(0);
41+
42+
if (valueType === 'array' && field instanceof HTMLSelectElement) {
43+
for (const opt of data.options ?? []) {
44+
const option = document.createElement('option');
45+
option.textContent = opt;
46+
option.value = opt;
47+
option.selected = value.includes(opt);
48+
field.add(option);
49+
}
50+
}
51+
else if (valueType === 'boolean') {
52+
field.value = value.toString();
53+
}
54+
else {
55+
field.value = !!value ? value.toString() : '';
56+
}
57+
58+
$tgt.addClass('editing')
59+
.html(field)
60+
.append(`<button class="button is-primary is-filled js-setting-submit has-display-block">Update</button>`);
61+
62+
if (valueType === 'array') {
63+
$(field).select2();
64+
}
3265
});
3366

3467
$(document).on('click', '.js-setting-submit', async (evt) => {
@@ -39,9 +72,14 @@ $(() => {
3972
const communityId = $input.data('community-id');
4073
const value = $input.val();
4174

42-
let body = {site_setting: {value}};
75+
const body = {
76+
site_setting: {
77+
value: Array.isArray(value) ? value.join(' ') : value
78+
}
79+
};
80+
4381
if (!!communityId) {
44-
body = Object.assign(body, {community_id: communityId});
82+
body.community_id = communityId;
4583
}
4684

4785
const resp = await QPixel.fetchJSON(`/admin/settings/${name}`, body);

app/controllers/advertisement_controller.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ def specific_question
3535

3636
def specific_category
3737
@category = Category.unscoped.find(params[:id])
38-
@post = Rails.cache.fetch "ca_random_category_post/#{params[:id]}",
39-
expires_in: 5.minutes do
40-
select_random_post(@category, days: params[:days]&.to_i, score: params[:score]&.to_f)
41-
end
38+
@post = Rails.cache.fetch_collection "ca_random_category_post/#{params[:id]}",
39+
expires_in: 5.minutes do
40+
random_post_collection(@category, days: params[:days]&.to_i, score: params[:score]&.to_f)
41+
end.sample
4242

4343
if @post.nil?
4444
not_found!
@@ -53,9 +53,9 @@ def specific_category
5353
end
5454

5555
def random_question
56-
@post = Rails.cache.fetch 'ca_random_hot_post', expires_in: 5.minutes do
57-
select_random_post
58-
end
56+
@post = Rails.cache.fetch_collection 'ca_random_hot_post', expires_in: 5.minutes do
57+
random_post_collection
58+
end.sample
5959
if @post.nil?
6060
return community
6161
end
@@ -95,7 +95,7 @@ def promoted_post
9595
# @param score [Float] the minimum post score to consider
9696
# @param count [Integer] a maximum number of posts to query for; the final post will be randomly selected from this
9797
# @return [Post]
98-
def select_random_post(category = nil, days: nil, score: nil, count: nil)
98+
def random_post_collection(category = nil, days: nil, score: nil, count: nil)
9999
if category.nil?
100100
category = Category.where(use_for_advertisement: true)
101101
end
@@ -106,7 +106,7 @@ def select_random_post(category = nil, days: nil, score: nil, count: nil)
106106
.where(posts: { last_activity: days.days.ago..DateTime.now })
107107
.where(posts: { category: category })
108108
.where('posts.score > ?', score.nil? ? SiteSetting['HotPostsScoreThreshold'] : score)
109-
.order('posts.score DESC').limit(count.nil? ? SiteSetting['HotQuestionsCount'] : count).all.sample
109+
.order('posts.score DESC').limit(count.nil? ? SiteSetting['HotQuestionsCount'] : count).all
110110
end
111111

112112
def send_resp(data)

app/controllers/application_controller.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,10 @@ def setup_request_context
205205
RequestContext.clear!
206206

207207
host_name = request.raw_host_with_port # include port to support multiple localhost instances
208-
RequestContext.community = @community = Rails.cache.fetch("#{host_name}/community", expires_in: 1.hour) do
209-
Community.unscoped.find_by(host: host_name)
210-
end
208+
@community = Rails.cache.fetch_collection("#{host_name}/community", expires_in: 1.hour) do
209+
Community.unscoped.where(host: host_name)
210+
end.first
211+
RequestContext.community = @community
211212

212213
Rails.logger.info " Host #{host_name}, community ##{RequestContext.community_id} " \
213214
"(#{RequestContext.community&.name})"
@@ -230,7 +231,7 @@ def setup_user
230231
end
231232

232233
def pull_pinned_links_and_hot_questions
233-
@pinned_links = Rails.cache.fetch('pinned_links', expires_in: 2.hours) do
234+
@pinned_links = Rails.cache.fetch_collection('pinned_links', expires_in: 2.hours) do
234235
Rack::MiniProfiler.step 'pinned_links: cache miss' do
235236
PinnedLink.where(active: true).where('shown_before IS NULL OR shown_before > NOW()').all
236237
end
@@ -247,7 +248,7 @@ def pull_pinned_links_and_hot_questions
247248
# I.e., if pinned_post_ids contains null, the selection will never return records
248249
pinned_post_ids = @pinned_links.map(&:post_id).compact
249250

250-
@hot_questions = Rails.cache.fetch('hot_questions', expires_in: 4.hours) do
251+
@hot_questions = Rails.cache.fetch_collection('hot_questions', expires_in: 4.hours) do
251252
Rack::MiniProfiler.step 'hot_questions: cache miss' do
252253
Post.undeleted.not_locked.where(closed: false)
253254
.where(last_activity: (Rails.env.development? ? 365 : 7).days.ago..DateTime.now)
@@ -261,7 +262,7 @@ def pull_pinned_links_and_hot_questions
261262
end
262263

263264
def pull_categories
264-
@header_categories = Rails.cache.fetch('header_categories') do
265+
@header_categories = Rails.cache.fetch_collection('header_categories') do
265266
Category.all.order(sequence: :asc, id: :asc)
266267
end
267268
end

app/controllers/flags_controller.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ def new
1414
max_flags_per_day = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_Flags' : 'RL_NewUserFlags']
1515

1616
if recent_flags >= max_flags_per_day
17-
flag_limit_msg = 'Thank you. Flags from people like you help us keep this site clean. ' \
18-
"However, you have reached your daily flag limit of #{max_flags_per_day} " \
19-
'flags. Please come back tomorrow to continue flagging.'
17+
flag_limit_msg = I18n.t('flags.errors.rate_limited', count: max_flags_per_day)
2018

2119
AuditLog.rate_limit_log(event_type: 'flag', related: Post.find(params[:post_id]), user: current_user,
2220
comment: "limit: #{max_flags_per_day}\n\ntype:#{type}\ncomment:\n#{params[:reason].to_i}")
@@ -27,16 +25,19 @@ def new
2725

2826
if type&.name == "needs author's attention"
2927
create_as_feedback_comment Post.find(params[:post_id]), current_user, params[:reason]
30-
render json: { status: 'success' }, status: :created
28+
render json: { status: 'success', message: I18n.t('flags.success.create_author_attention') },
29+
status: :created
3130
return
3231
end
3332

3433
@flag = Flag.new(post_flag_type: type, reason: params[:reason], post_id: params[:post_id],
3534
post_type: params[:post_type], user: current_user)
3635
if @flag.save
37-
render json: { status: 'success' }, status: :created
36+
render json: { status: 'success', message: I18n.t('flags.success.create_generic') },
37+
status: :created
3838
else
39-
render json: { status: 'failed', message: 'Flag failed to save.' }, status: :internal_server_error
39+
render json: { status: 'failed', message: I18n.t('flags.errors.create_generic') },
40+
status: :bad_request
4041
end
4142
end
4243

0 commit comments

Comments
 (0)