feat: react to mobile conversation comments (Closes #1380)
This commit is contained in:
1 parent
fc7a83fbd7
commit
b4f3c6d419
6 files changed
+708
-2
No files matched your search
@@ -1,4 +1,8 @@
|
||||
function createCommentActions({ fetchJson, getLogin, confirmDelete = () => false }) {
|
||||
const reactionLabels = {
|
||||
'+1': 'Thumbs up', '-1': 'Thumbs down', laugh: 'Laugh', hooray: 'Hooray',
|
||||
confused: 'Confused', heart: 'Heart', rocket: 'Rocket', eyes: 'Eyes',
|
||||
};
|
||||
const encodedRepository = repository => String(repository || '').split('/')
|
||||
.map(encodeURIComponent).join('/');
|
||||
|
||||
@@ -14,6 +18,12 @@ function createCommentActions({ fetchJson, getLogin, confirmDelete = () => false
|
||||
'/comments/' + encodeURIComponent(commentId);
|
||||
}
|
||||
|
||||
function reactionPath(context, commentId, content = '') {
|
||||
return pathFor(context, commentId) + '/reactions' +
|
||||
(content ? '/' + encodeURIComponent(content) : '');
|
||||
}
|
||||
|
||||
const pendingReactions = new Set();
|
||||
const controller = {
|
||||
isOwned(comment) {
|
||||
const login = String(getLogin() || '').trim();
|
||||
@@ -41,15 +51,98 @@ function createCommentActions({ fetchJson, getLogin, confirmDelete = () => false
|
||||
pager.remove(commentId);
|
||||
return pager.snapshot();
|
||||
},
|
||||
async loadReactions(context, commentId) {
|
||||
return fetchJson(reactionPath(context, commentId), {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
},
|
||||
async setReaction(context, commentId, content, active) {
|
||||
return fetchJson(reactionPath(context, commentId, content), {
|
||||
method: 'PUT',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ active: Boolean(active) }),
|
||||
});
|
||||
},
|
||||
reactionHtml(state) {
|
||||
const current = new Map((state?.reactions || []).map(item => [item.content, item]));
|
||||
return Object.entries(reactionLabels).map(([content, label]) => {
|
||||
const item = current.get(content) || {};
|
||||
const count = Number.isInteger(item.count) && item.count > 0 ? item.count : 0;
|
||||
const selected = item.selected === true;
|
||||
return '<button type="button" data-comment-reaction="' + content + '" ' +
|
||||
'aria-pressed="' + selected + '" aria-label="' + label + ' ' + count + '">' +
|
||||
label + ' ' + count + '</button>';
|
||||
}).join('') + '<button type="button" data-comment-reactions-close>Cancel</button>';
|
||||
},
|
||||
actionHtml(comment) {
|
||||
return controller.isOwned(comment) ?
|
||||
const owned = controller.isOwned(comment) ?
|
||||
'<div class="comment-owned-actions" aria-label="Your comment actions">' +
|
||||
'<button type="button" data-comment-action="edit">Edit</button>' +
|
||||
'<button type="button" data-comment-action="delete">Delete</button></div>' : '';
|
||||
return owned + '<div class="comment-reactions" aria-label="Comment reactions">' +
|
||||
'<button type="button" data-comment-reactions-open aria-expanded="false" ' +
|
||||
'aria-label="React to this comment">React</button>' +
|
||||
'<div class="comment-reaction-menu" data-comment-reaction-menu hidden></div></div>';
|
||||
},
|
||||
wire({ root, getSurface, isOffline, escapeHtml }) {
|
||||
root.addEventListener('click', async event => {
|
||||
const button = event.target.closest('[data-comment-action]');
|
||||
const actionButton = event.target.closest('[data-comment-action]');
|
||||
const reactionClose = actionButton ? null : event.target.closest('[data-comment-reactions-close]');
|
||||
const reactionButton = actionButton ? null : event.target.closest('[data-comment-reaction]');
|
||||
const reactionTrigger = actionButton ? null : event.target.closest('[data-comment-reactions-open]');
|
||||
if (reactionClose) {
|
||||
const card = reactionClose.closest('.issue-comment');
|
||||
const menu = card?.querySelector('[data-comment-reaction-menu]');
|
||||
const trigger = card?.querySelector('[data-comment-reactions-open]');
|
||||
if (menu && trigger) {
|
||||
menu.hidden = true;
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
trigger.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (reactionButton || reactionTrigger) {
|
||||
const control = reactionButton || reactionTrigger;
|
||||
const card = control.closest('.issue-comment');
|
||||
const commentId = Number(card?.dataset.commentId);
|
||||
const surface = getSurface();
|
||||
const menu = card?.querySelector('[data-comment-reaction-menu]');
|
||||
const trigger = card?.querySelector('[data-comment-reactions-open]');
|
||||
if (!commentId || !menu || !trigger) return;
|
||||
if (isOffline()) {
|
||||
surface.status.textContent = 'Reconnect to react.';
|
||||
return;
|
||||
}
|
||||
if (pendingReactions.has(commentId)) return;
|
||||
pendingReactions.add(commentId);
|
||||
control.disabled = true;
|
||||
try {
|
||||
if (reactionButton) {
|
||||
const content = reactionButton.dataset.commentReaction;
|
||||
const active = reactionButton.getAttribute('aria-pressed') !== 'true';
|
||||
const state = await controller.setReaction(surface.context, commentId, content, active);
|
||||
menu.innerHTML = controller.reactionHtml(state);
|
||||
menu.hidden = true;
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
trigger.focus();
|
||||
surface.status.textContent = 'Reaction updated.';
|
||||
} else {
|
||||
surface.status.textContent = 'Loading reactions…';
|
||||
const state = await controller.loadReactions(surface.context, commentId);
|
||||
menu.innerHTML = controller.reactionHtml(state);
|
||||
menu.hidden = false;
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
surface.status.textContent = '';
|
||||
}
|
||||
} catch (error) {
|
||||
surface.status.textContent = error.message || 'Reactions are unavailable. Please retry.';
|
||||
} finally {
|
||||
pendingReactions.delete(commentId);
|
||||
control.disabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const button = actionButton;
|
||||
if (!button) return;
|
||||
const card = button.closest('.issue-comment');
|
||||
const commentId = Number(card?.dataset.commentId);
|
||||
|
||||
@@ -146,6 +146,12 @@ button:hover { filter: brightness(1.15); }
|
||||
.issue-comment { min-width:0; overflow-wrap:anywhere; }
|
||||
.comment-owned-actions { display:flex; gap:8px; flex-wrap:wrap; margin:8px 0; }
|
||||
.comment-owned-actions button { min-height:44px; min-width:72px; }
|
||||
.comment-reactions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin:8px 0; min-width:0; }
|
||||
.comment-reactions > button { min-height:44px; min-width:72px; }
|
||||
.comment-reaction-menu { display:flex; gap:8px; flex-wrap:wrap; width:100%; min-width:0; }
|
||||
.comment-reaction-menu[hidden] { display:none; }
|
||||
.comment-reaction-menu button { min-height:44px; min-width:88px; flex:1 1 104px; overflow-wrap:anywhere; }
|
||||
.comment-reaction-menu button[aria-pressed="true"] { border-color:#4ade80; background:#123c2b; color:#dcfce7; }
|
||||
.comment-edit-textarea { box-sizing:border-box; display:block; width:100%; max-width:100%; min-height:132px; resize:vertical; overflow-wrap:anywhere; }
|
||||
.panel { border: 1px solid #1b2d45; border-radius: 14px; padding: 12px; background: rgba(11,21,38,.92); }
|
||||
.panel > summary { cursor: pointer; list-style-position: inside; }
|
||||
|
||||
Reference in new issue
Block a user