Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f7784c
added Post#deleted_by_owner? predicate
Oaphi Sep 5, 2025
c7fc046
fixed server error when rendering posts/_expanded if deleter user is …
Oaphi Sep 5, 2025
a078946
fixed server error when rendering reactions from hard-deleted users
Oaphi Sep 5, 2025
037f560
fixed unexpected error when opening a comment thread lock modal
Oaphi Sep 5, 2025
595d9d1
added missing locked_at column to comment threads
Oaphi Sep 5, 2025
0dd8fa1
added Ability#post_score_percent_for(user) nil-safe method
Oaphi Sep 6, 2025
94076a2
added Ability#edit_score_percent_for(user) nil-safe method
Oaphi Sep 6, 2025
d32ae6b
added Ability#flag_score_percent_for(user) nil-safe method
Oaphi Sep 6, 2025
f89cf87
consider abilities with threshold set to 0 to be automatically reached
Oaphi Sep 6, 2025
4f8465b
added failure path tests for score_percent_for methods of Ability model
Oaphi Sep 6, 2025
5de8faa
added auto success path tests for score_percent_for methods of Abilit…
Oaphi Sep 6, 2025
d12c773
added test for edit_score_percent_for actual calculation
Oaphi Sep 6, 2025
aed68e6
added test for flag_score_percent_for actual calculation
Oaphi Sep 6, 2025
85797f1
added missing 'everyone' user ability to new user fixtures
Oaphi Sep 6, 2025
f67cb47
added missing 'unrestricted' user ability to new user fixtures
Oaphi Sep 6, 2025
4ad6032
made User#ability_on? tests more robust
Oaphi Sep 6, 2025
bbd8420
added test for post_score_percent_for actual calculation
Oaphi Sep 6, 2025
9968126
made ability score user fixtures clearer in intent
Oaphi Sep 6, 2025
ce1dcbb
moved comment thread locking into a proper lock_thread action with er…
Oaphi Sep 7, 2025
92014eb
don't forget to remove testing overrides
Oaphi Sep 7, 2025
5149019
split thread restrict actions into individual actions
Oaphi Sep 7, 2025
a191df6
split thread unrestrict actions into individual actions
Oaphi Sep 7, 2025
b22c736
disambiguated post_scorer_question fixture's meaning of 'positively s…
Oaphi Sep 8, 2025
bc6bacb
finished splitting up thread_restrict action
Oaphi Sep 9, 2025
3b974bf
restored follow comment thread client-side functionality
Oaphi Sep 9, 2025
027d186
fixed retrict actions on expanded thread view loading inline version …
Oaphi Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 111 additions & 45 deletions app/assets/javascripts/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@ $(() => {
return $tgt.closest('.js-comment-thread-wrapper')[0] ?? null;
};

$(document).on('click', '.post--comments-thread.is-inline a', async (evt) => {
if (evt.ctrlKey) { return; }

evt.preventDefault();

const $tgt = $(evt.target);
const $threadId = $tgt.data('thread');
const wrapper = getCommentThreadWrapper($tgt);

openThread(wrapper, $threadId);
});
/**
* @param {HTMLElement} wrapper
* @returns {boolean}
*/
const isInlineCommentThread = (wrapper) => {
return !!wrapper.querySelector('[data-inline=true]');
};

/**
* @param {HTMLElement} wrapper
Expand All @@ -45,8 +41,24 @@ $(() => {
window.hljs && hljs.highlightAll();
}

$(document).on('click', '.post--comments-thread.is-inline a', async (evt) => {
if (evt.ctrlKey) {
return; // TODO: do we need this early exit?
}

evt.preventDefault();

const $tgt = $(evt.target);
const $threadId = $tgt.data('thread');
const wrapper = getCommentThreadWrapper($tgt);

openThread(wrapper, $threadId);
});

$(document).on('click', '.js-show-deleted-comments', (ev) => {
if (ev.ctrlKey) { return; } // do we really need it?
if (ev.ctrlKey) {
return;
} // do we really need it?

ev.preventDefault();

Expand Down Expand Up @@ -80,7 +92,9 @@ $(() => {
}

if (isDeleted) {
$container.append(`<i class="fas fa-trash h-c-red-600 fa-fw" title="Deleted thread" aria-label="Deleted thread"></i>`);
$container.append(
`<i class="fas fa-trash h-c-red-600 fa-fw" title="Deleted thread" aria-label="Deleted thread"></i>`
);
$container.addClass('is-deleted');
}

Expand Down Expand Up @@ -171,8 +185,7 @@ $(() => {
if (isDelete) {
$comment.addClass('deleted-content');
$tgt.removeClass('js-comment-delete').addClass('js-comment-undelete').val('undelete');
}
else {
} else {
$comment.removeClass('deleted-content');
$tgt.removeClass('js-comment-undelete').addClass('js-comment-delete').val('delete');
}
Expand All @@ -189,36 +202,90 @@ $(() => {
const $modal = $($tgt.data('modal'));

const resp = await QPixel.fetch(`/comments/thread/${threadId}/followers`, {
headers: { 'Accept': 'text/html' }
headers: { Accept: 'text/html' }
});

const data = await resp.text();

$modal.find('.js-follower-display').html(data);
});

$(document).on('click', '[class*=js--lock-thread] form', async (evt) => {
evt.preventDefault();
$(document).on('click', '.js-archive-thread', async (ev) => {
ev.preventDefault();

const $tgt = $(evt.target);
const threadID = $tgt.data("thread");
const $tgt = $(ev.target);
const threadID = $tgt.data('thread');

const data = await QPixel.lockThread(threadID);
const data = await QPixel.archiveThread(threadID);

QPixel.handleJSONResponse(data, () => {
window.location.reload();
const wrapper = getCommentThreadWrapper($tgt);
const inline = isInlineCommentThread(wrapper);
openThread(wrapper, threadID, { inline });
});
});

$(document).on('click', '.js--restrict-thread, .js--unrestrict-thread', async (evt) => {
$(document).on('click', '.js-delete-thread', async (ev) => {
ev.preventDefault();

const $tgt = $(ev.target);
const threadID = $tgt.data('thread');

const data = await QPixel.deleteThread(threadID);

QPixel.handleJSONResponse(data, () => {
const wrapper = getCommentThreadWrapper($tgt);
const inline = isInlineCommentThread(wrapper);
openThread(wrapper, threadID, { inline });
});
});

$(document).on('click', '.js-follow-thread', async (ev) => {
ev.preventDefault();

const $tgt = $(ev.target);
const threadID = $tgt.data('thread');

const data = await QPixel.followThread(threadID);

QPixel.handleJSONResponse(data, () => {
const wrapper = getCommentThreadWrapper($tgt);
const inline = isInlineCommentThread(wrapper);
openThread(wrapper, threadID, { inline });
});
});

$(document).on('click', '.js-lock-thread', async (ev) => {
ev.preventDefault();

const $tgt = $(ev.target);
const threadID = $tgt.data('thread');
const form = $tgt.closest(`form[data-thread=${threadID}]`).get(0);

if (form instanceof HTMLFormElement) {
const { value: duration } = form.elements['duration'] ?? {};

const data = await QPixel.lockThread(threadID, duration ? Math.round(+duration) : void 0);

QPixel.handleJSONResponse(data, () => {
const wrapper = getCommentThreadWrapper($tgt);
const inline = isInlineCommentThread(wrapper);
openThread(wrapper, threadID, { inline });
});
} else {
QPixel.createNotification('danger', 'Failed to find thread to lock');
}
});

// TODO: split into individual handlers once unrestrict_thread is split
$(document).on('click', '.js--unrestrict-thread', async (evt) => {
evt.preventDefault();

const $tgt = $(evt.target);
const threadID = $tgt.data("thread");
const action = $tgt.data("action");
const route = $tgt.hasClass("js--restrict-thread") ? 'restrict' : 'unrestrict';
const threadID = $tgt.data('thread');
const action = $tgt.data('action');

const resp = await QPixel.fetchJSON(`/comments/thread/${threadID}/${route}`, { type: action });
const resp = await QPixel.fetchJSON(`/comments/thread/${threadID}/unrestrict`, { type: action });

const data = await resp.json();

Expand Down Expand Up @@ -261,7 +328,7 @@ $(() => {
const $item = $(ev.target).hasClass('item') ? $(ev.target) : $(ev.target).parents('.item');
const id = $item.data('user-id');
$tgt[0].selectionStart = caretPos - posInWord;
$tgt[0].selectionEnd = (caretPos - posInWord) + currentWord.length;
$tgt[0].selectionEnd = caretPos - posInWord + currentWord.length;
QPixel.replaceSelection($tgt, `@#${id}`);
popup.destroy();
$tgt.focus();
Expand All @@ -282,17 +349,20 @@ $(() => {
pingable[`${threadId}-${postId}`] = await resp.json();
}

const items = Object.entries(pingable[`${threadId}-${postId}`]).filter((e) => {
return e[0].toLowerCase().startsWith(currentWord.substr(1).toLowerCase());
}).map((e) => {
const username = e[0].replace(/</g, '&#x3C;').replace(/>/g, '&#x3E;');
const id = e[1];
return itemTemplate.clone().html(`${username} <span class="has-color-tertiary-600">#${id}</span>`)
.attr('data-user-id', id);
});
const items = Object.entries(pingable[`${threadId}-${postId}`])
.filter((e) => {
return e[0].toLowerCase().startsWith(currentWord.substr(1).toLowerCase());
})
.map((e) => {
const username = e[0].replace(/</g, '&#x3C;').replace(/>/g, '&#x3E;');
const id = e[1];
return itemTemplate
.clone()
.html(`${username} <span class="has-color-tertiary-600">#${id}</span>`)
.attr('data-user-id', id);
});
QPixel.Popup.getPopup(items, $tgt[0], callback);
}
else {
} else {
QPixel.Popup.destroyAll();
}
}
Expand All @@ -306,8 +376,7 @@ $(() => {
if ($thread.is(':hidden')) {
$thread.show();
$thread.find('.js-comment-field').trigger('focus');
}
else {
} else {
$thread.hide();
}
});
Expand All @@ -322,8 +391,7 @@ $(() => {
if ($reply.is(':hidden')) {
$reply.show();
$reply.find('.js-comment-field').trigger('focus');
}
else {
} else {
$reply.hide();
}
});
Expand Down Expand Up @@ -358,9 +426,7 @@ $(() => {

const shouldFollow = action === 'follow';

const data = shouldFollow ?
await QPixel.followComments(postId) :
await QPixel.unfollowComments(postId);
const data = shouldFollow ? await QPixel.followComments(postId) : await QPixel.unfollowComments(postId);

QPixel.handleJSONResponse(data, () => {
target.dataset.action = shouldFollow ? 'unfollow' : 'follow';
Expand Down
40 changes: 32 additions & 8 deletions app/assets/javascripts/qpixel_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,38 @@ window.QPixel = {
return QPixel.parseJSONResponse(resp, 'Failed to vote');
},

archiveThread: async (id) => {
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/archive`, {}, {
headers: { 'Accept': 'application/json' },
});

return QPixel.parseJSONResponse(resp, 'Failed to archive thread');
},

deleteThread: async (id) => {
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/delete`, {}, {
headers: { 'Accept': 'application/json' },
});

return QPixel.parseJSONResponse(resp, 'Failed to delete thread');
},

followThread: async (id) => {
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/follow`, {}, {
headers: { 'Accept': 'application/json' },
});

return QPixel.parseJSONResponse(resp, 'Failed to follow thread');
},

lockThread: async (id, duration) => {
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/lock`, {
duration,
});

return QPixel.parseJSONResponse(resp, 'Failed to lock thread');
},

deleteComment: async (id) => {
const resp = await QPixel.fetchJSON(`/comments/${id}/delete`, {}, {
headers: { 'Accept': 'application/json' },
Expand Down Expand Up @@ -495,14 +527,6 @@ window.QPixel = {
return QPixel.parseJSONResponse(resp, 'Failed to unfollow post comments');
},

lockThread: async (id) => {
const resp = await QPixel.fetchJSON(`/comments/thread/${id}/restrict`, {
type: 'lock'
});

return QPixel.parseJSONResponse(resp, 'Failed to lock thread');
},

renameTag: async (categoryId, tagId, name) => {
const resp = await QPixel.fetchJSON(`/categories/${categoryId}/tags/${tagId}/rename`, { name }, {
headers: { 'Accept': 'application/json' }
Expand Down
Loading