Last active
March 26, 2021 23:24
-
-
Save andrei0x309/496c328a34e2a2c4bada00c439ef629b to your computer and use it in GitHub Desktop.
Files for custom AMP WordPress Commenting System
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// this file is loaded in worker-dom by amp-script and handles all comment functions | |
(async function ampComments() { | |
const addSiSpinner = function (element, prepend = false) { | |
const spinner = document.createElement('div'); | |
spinner.classList.add('loadingspinner'); | |
if (prepend) { | |
element.insertBefore(element, element.firstElementChild); | |
} else { | |
element.appendChild(spinner); | |
} | |
return spinner; | |
}; | |
const delSiSpinner = (spinner) => { | |
if (spinner) | |
spinner.parentNode.removeChild(spinner); | |
}; | |
const delAlertBox = ( ) => { | |
const oldAlertBox = document.getElementById('a309-alert-box'); | |
if (oldAlertBox) { | |
oldAlertBox.parentNode.removeChild(oldAlertBox); | |
} | |
}; | |
const alertBox = (alertClass = 'error', alertMsg = '', delAlertBox = '') => { | |
if (delAlertBox) | |
delAlertBox(); | |
switch (alertClass) { | |
case 'error': | |
alertClass = 'alert-error'; | |
break; | |
case 'info': | |
alertClass = 'alert-info'; | |
break; | |
case 'success': | |
alertClass = 'alert-success'; | |
break; | |
default: | |
alertClass = 'alert-error'; | |
break; | |
} | |
const alertBox = document.createElement('div'); | |
alertBox.id = 'a309-alert-box'; | |
alertBox.classList.add('alert'); | |
alertBox.classList.add(alertClass); | |
alertBox.classList.add('fade-in'); | |
alertBox.innerHTML = alertMsg; | |
return alertBox; | |
}; | |
const commentsEl = document.getElementById('comments'); | |
const commentFromEl = document.getElementById('commentform'); | |
commentFromEl.addEventListener('submit', (e) => { | |
e.target.preventDefault(); | |
}); | |
const actionUrl = document.getElementById('commentform').getAttribute('action-xhr'); | |
const postId = document.getElementById('comments').getAttribute('data-post-id'); | |
let storeRespEl = null; | |
let replyEl = null; | |
let hiddenRepLink = null; | |
let commentsList = null; | |
let commentsShowMoreBtn = null; | |
const showCommentsBtn = document.getElementById('comments-show-btn'); | |
const postCommentBtn = document.getElementById('submit-amp'); | |
let page = null; | |
const HTMLtoEL = (html) => { | |
const t = document.createElement('div'); | |
t.innerHTML = html; | |
return t.children; | |
}; | |
const DelShowMoreCBtn = () => { | |
if (commentsShowMoreBtn) { | |
commentsShowMoreBtn.parentNode.removeChild(commentsShowMoreBtn); | |
commentsShowMoreBtn = null; | |
} | |
}; | |
const AddShowMoreCBtn = () => { | |
const showMoreBtn = document.createElement('button'); | |
showMoreBtn.id = 'comments-show-more-btn'; | |
showMoreBtn.innerHTML = 'Show More Comments'; | |
showMoreBtn.addEventListener('click', showMoreCommentsFn); | |
commentsEl.appendChild(showMoreBtn); | |
return showMoreBtn; | |
}; | |
const fetchCommentsNo = async () => { | |
const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments-no/post/${postId}`; | |
const response = await fetch(fetchUrl, { | |
headers: { | |
'Content-Type': 'application/json' | |
} | |
}); | |
const data = await response.json(); | |
page = Math.ceil(data.commentNumber / 5); | |
}; | |
const fetchComments = async () => { | |
// fetch Comments | |
const fetchUrl = `${window.location.origin}/wp-json/a309/v1/get-comments/post/${postId}/page/${page}`; | |
const response = await fetch(fetchUrl, { | |
headers: { | |
'Content-Type': 'application/json', | |
} | |
}); | |
const data = await response.json(); // parses JSON response into native JavaScript objects | |
page -= 1; | |
return data; | |
}; | |
const repImgAMP = (...args) => { | |
const src = args[1].replace(/#038;/gms, ''); | |
return `<amp-img | |
alt | |
src="${src}" | |
width="60" | |
height="60" | |
layout="fixed" | |
> | |
</amp-img>`; | |
}; | |
const AMPifyComments = (comments) => { | |
return HTMLtoEL(comments | |
.replace(/<img.*?src=['"]{1}(.*?)['"]{1}.*?>/gms, repImgAMP) | |
.trim() | |
.replace(/<!--.*?-->/gms, '') | |
.replace(/\t/gm, '') | |
.replace(/\n/gm, '')); | |
}; | |
const AddCommentsToList = (ListEl, comments) => { | |
for (const comment of comments) { | |
ListEl.appendChild(comment); | |
} | |
}; | |
const showCommentsFn = async () => { | |
showCommentsBtn.innerHTML = ` | |
Loading | |
<div class="loadingspinner"></div> | |
`; | |
showCommentsBtn.disabled = true; | |
await fetchCommentsNo(); | |
const data = await fetchComments(); | |
commentsList = document.createElement('ol'); | |
commentsList.id = 'comment-list'; | |
commentsList.classList.add('comment-list'); | |
const comments = AMPifyComments(data.comments); | |
addReplyEvent(comments); | |
AddCommentsToList(commentsList, comments); | |
commentsEl.appendChild(commentsList); | |
commentsEl.removeChild(showCommentsBtn); | |
if (page) { | |
await showMoreCommentsFn(); | |
} | |
}; | |
const showMoreCommentsFn = async () => { | |
DelShowMoreCBtn(); | |
const spinner = addSiSpinner(commentsEl); | |
const data = await fetchComments(); | |
const commentsList = document.getElementById('comment-list'); | |
const comments = AMPifyComments(data.comments); | |
addReplyEvent(comments); | |
AddCommentsToList(commentsList, comments); | |
if (page) { | |
commentsShowMoreBtn = AddShowMoreCBtn(); | |
} | |
delSiSpinner(spinner); | |
}; | |
const sumbitComment = async (e) => { | |
delAlertBox(); | |
const respondEl = document.getElementById('amp-respond'); | |
const spinner = addSiSpinner(respondEl); | |
const submitBtn = document.getElementById('submit-amp'); | |
submitBtn.disabled = true; | |
//serialize and store form data in a variable | |
console.log(actionUrl); | |
const bodyData = {comment: document.getElementById('comment') ? document.getElementById('comment').value : '', | |
author: document.getElementById('author') ? document.getElementById('author').value : '', | |
email: document.getElementById('email') ? document.getElementById('email').value : '', | |
comment_post_ID: document.getElementById('comment_post_ID') ? document.getElementById('comment_post_ID').value : '', | |
comment_parent: document.getElementById('comment_parent') ? document.getElementById('comment_parent').value : '', | |
'wp-comment-cookies-consent': document.getElementById('wp-comment-cookies-consent') ? document.getElementById('wp-comment-cookies-consent').value : '', | |
akismet_comment_nonce: document.getElementById('akismet_comment_nonce') ? document.getElementById('akismet_comment_nonce').value : '', | |
'ak_js': document.getElementById('ak_js') ? document.getElementById('ak_js').value : '', | |
'ak_hp_textarea': document.getElementById('ak_hp_textarea') ? document.getElementById('ak_hp_textarea').value : '', | |
'redirect_to': document.getElementById('redirect_to') ? document.getElementById('redirect_to').value : '' | |
}; | |
const searchParams = Object.keys(bodyData).map((key) => { | |
return `${encodeURIComponent(key)}${ bodyData[key] ? '=' + encodeURIComponent(bodyData[key]) : ''}`; | |
}).join('&'); | |
const response = await fetch(actionUrl, { | |
method: 'POST', // *GET, POST, PUT, DELETE, etc. | |
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached | |
credentials: 'same-origin', // include, *same-origin, omit | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
body: searchParams // body data type must match "Content-Type" header | |
}); | |
let alert = null; | |
if (response.ok) { | |
const data = await response.json(); | |
if (data.error) { | |
alert = alertBox('error', `⚠ ${data.msg}`, delAlertBox); | |
respondEl.appendChild(alert); | |
} else { | |
const approved = parseInt(data.comment.comment_approved) === 1; | |
if (approved) { | |
const noComments = document.getElementById('comments').getAttribute('data-no-comments'); | |
if (noComments) { | |
const nCom = parseInt(noComments) + 1; | |
document.getElementById('comments').setAttribute('data-no-comments', nCom); | |
const comText = commentsEl.querySelector('.comments-title'); | |
if (comText) { | |
comText.textContent = [nCom, comText.textContent.split(' ')[1]].join(' '); | |
} | |
} | |
} | |
console.log(approved); | |
const notApprovedText = 'Comment was posted but was not approved it will be live after approval.'; | |
const commentList = document.getElementById('comment-list'); | |
if (commentList) { | |
const com = addCommentToDOM(data.comment); | |
if (!approved) { | |
alert = alertBox('info', notApprovedText, delAlertBox); | |
com.appendChild(alert); | |
} | |
} else { | |
if (approved) { | |
alert = alertBox('success', 'Comment was posted', delAlertBox); | |
} else { | |
alert = alertBox('info', notApprovedText, delAlertBox); | |
} | |
respondEl.appendChild(alert); | |
} | |
} | |
} else { | |
alert = alertBox('error', '⚠ HTTP fetch error, API down!', delAlertBox); | |
respondEl.appendChild(alert); | |
} | |
submitBtn.disabled = false; | |
delSiSpinner(spinner); | |
}; | |
const addCommentToDOM = (comment) => { | |
const slug = commentsEl.getAttribute('data-post-slug'); | |
const date = new Date(comment.comment_date); | |
const options = {year: 'numeric', month: 'long', day: 'numeric'}; | |
const newCom = `<li id="comment-${comment.comment_ID}" class="comment byuser comment-author-${comment.comment_author} even thread-even comment-added fade-in"> | |
<article id="div-comment-${comment.comment_ID}" class="comment-body"> | |
<footer class="comment-meta"> | |
<div class="comment-author vcard"> | |
${comment.comment_avatar} | |
<b class="fn"><a href="${comment.comment_author_url}" rel="external nofollow ugc" class="url">${comment.comment_author}</a></b> | |
<span class="says">says:</span> </div><!-- .comment-author --> | |
<div class="comment-metadata"> | |
<a href="${window.location.origin}/${slug}/#comment-${comment.comment_ID}"> | |
<time datetime="${comment.comment_date}">${date.toLocaleDateString('en-US', options)} at ${date.toLocaleTimeString('en-US')} | |
</time></a></div><!-- .comment-metadata --> | |
</footer><!-- .comment-meta --> | |
<div class="comment-content"> | |
<p>${comment.comment_content}</p> | |
</div><!-- .comment-content --> | |
</article><!-- .comment-body --> | |
</li>`; | |
const ampComment = AMPifyComments(newCom)[0]; | |
const commentList = document.getElementById('comment-list'); | |
if (Number(comment.comment_parent) === 0) { | |
commentList.insertBefore(ampComment, commentList.firstChild); | |
} else { | |
const cancelLink = document.getElementById('cancel-comment-reply-link'); | |
cancelLink.click(); | |
let commentParent = document.getElementById(`comment-${comment.comment_parent}`); | |
let children = commentParent.querySelector('.children'); | |
if (!children) { | |
children = document.createElement('ol'); | |
children.classList.add('children'); | |
commentParent.appendChild(children); | |
} | |
children.insertBefore(ampComment, children.firstChild); | |
return ampComment; | |
} | |
}; | |
const replyMoveForm = (e) => { | |
e.preventDefault(); | |
if (hiddenRepLink) | |
hiddenRepLink.removeAttribute('hidden'); | |
const commentId = e.target.getAttribute('data-commentid'); | |
const divComment = document.getElementById(`div-comment-${commentId}`); | |
hiddenRepLink = divComment.querySelector('.comment-reply-link'); | |
hiddenRepLink.setAttribute('hidden'); | |
if (storeRespEl === null) { | |
const oldCancelLink = document.getElementById('cancel-comment-reply-link'); | |
if (oldCancelLink) | |
oldCancelLink.parentNode.removeChild(oldCancelLink); | |
const respondEl = document.getElementById('amp-respond'); | |
const form = respondEl.querySelector('form'); | |
if (form) { | |
form.removeAttribute('amp-novalidate'); | |
form.removeAttribute('class'); | |
} | |
const noScript = respondEl.getElementsByTagName('noscript'); | |
if (noScript[0]) | |
noScript[0].parentNode.removeChild(noScript[0]); | |
replyEl = respondEl.cloneNode(true); | |
storeRespEl = respondEl; | |
const resp = replyEl.querySelector('#respond'); | |
const fSubmit = resp.querySelector('#form-submit'); | |
const cancelLink = document.createElement('a'); | |
cancelLink.id = 'cancel-comment-reply-link'; | |
cancelLink.setAttribute('rel', 'nofollow'); | |
cancelLink.setAttribute('href', '#comments'); | |
cancelLink.textContent = `Cancel Reply`; | |
resp.insertBefore(cancelLink, fSubmit); | |
cancelLink.addEventListener('click', replyFormCancel); | |
replyEl.querySelector('#submit-amp').addEventListener('click', sumbitComment); | |
} | |
document.getElementById('amp-respond').parentNode.removeChild(storeRespEl); | |
let replyName = divComment.querySelector('.fn'); | |
replyName = replyName ? replyName.textContent : ''; | |
const replyTitle = replyEl.querySelector('#reply-title'); | |
replyTitle.textContent = `Reply to ${replyName}`; | |
const resp = replyEl.querySelector('#respond'); | |
const inParent = resp.querySelector('#comment_parent'); | |
inParent.setAttribute('value', commentId); | |
document.getElementById(`comment-${commentId}`).insertBefore(replyEl, divComment.nextSibling); | |
}; | |
const replyFormCancel = () => { | |
delAlertBox(); | |
const respondEl = document.getElementById('amp-respond'); | |
respondEl.parentNode.removeChild(respondEl); | |
if (hiddenRepLink) { | |
hiddenRepLink.removeAttribute('hidden'); | |
hiddenRepLink = null; | |
} | |
commentsEl.insertBefore(storeRespEl, commentsEl.firstChild); | |
}; | |
const addReplyEvent = (comments) => { | |
for (const comment of comments) { | |
const links = comment.querySelectorAll('.comment-reply-link'); | |
for (const link of links) { | |
//link.removeAttribute('href'); | |
link.addEventListener('click', replyMoveForm); | |
} | |
} | |
}; | |
postCommentBtn.addEventListener('click', sumbitComment); | |
showCommentsBtn.addEventListener('click', showCommentsFn); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This file is the comments.php form the template | |
<?php | |
if (post_password_required()) { | |
return; | |
} | |
$a309_no_comments = get_comments_number(); | |
//global $current_user; | |
//if($current_user) echo $current_user->user_login; | |
?> | |
<?php if (a309_is_amp()): ?> | |
<amp-script id="comments-script" layout="container" src="<?php echo get_stylesheet_directory_uri() ?>/js/AMP/amp_comments.js" sandbox="allow-forms"> | |
<?php endif; ?> | |
<div id="comments" data-post-id="<?php echo $post->ID; ?>" data-no-comments="<?php echo $a309_no_comments; ?>" data-post-slug="<?php echo $post->post_name; ?>" class="comments-area default-max-width <?php echo get_option('show_avatars') ? 'show-avatars' : ''; ?>"> | |
<?php if (a309_is_amp()): ?> | |
<div id="amp-respond"> <?php | |
endif; | |
$aria_req = ($req) ? " aria-required='true'" : ''; | |
$comments_args = array( | |
'comment_field' => '<p class="comment-form-comment"><textarea id="comment" name="comment" placeholder="Your Comment* " aria-required="true"></textarea></p>', | |
'comment_notes_before' => '', | |
'fields' => | |
[ | |
'author' => | |
'<div class="comment-name-email-block"><p class="comment-form-author">' . | |
'<i class="icon-user-solid-square"></i><input id="author" class="blog-form-input" placeholder="Name* " name="author" type="text" value="' . esc_attr($commenter['comment_author']) . | |
'" size="30"' . $aria_req . ' /></p>', | |
'email' => | |
'<p class="comment-form-email">' . | |
'<i class="icon-alternate_email"></i><input | |
id="email" class="blog-form-input" placeholder="Email Address* " name="email" type="text" value="' . esc_attr($commenter['comment_author_email']) . | |
'" size="30"' . $aria_req . ' /></p></div>', | |
'url' => '', | |
], | |
'logged_in_as' => null, | |
'title_reply' => esc_html__('Leave a comment', 'a309'), | |
'title_reply_before' => '<h2 id="reply-title" class="comment-reply-title">', | |
'title_reply_after' => '</h2>', | |
); | |
if (a309_is_amp()) { | |
$comments_args['cancel_reply_before'] = ''; | |
$comments_args['cancel_reply_after'] = ''; | |
$comments_args['cancel_reply_link'] = ''; | |
$comments_args['submit_button'] = '<noscript data-ampdevmode><input name="%1$s" type="submit" id="%2$s" class="%3$s" value="%4$s" /></noscript>'; | |
} | |
comment_form($comments_args); | |
if (a309_is_amp()): | |
?> | |
<p class="form-submit"> | |
<button id="submit-amp" class="submit fade-in">Post Comment</button> | |
</p> | |
</div> | |
<?php | |
endif; | |
if (have_comments()) : | |
?> | |
<h2 class="comments-title"><?php | |
if ('1' === $a309_no_comments) : | |
esc_html_e('1 comment', 'a309'); | |
else : | |
printf( | |
/* translators: %s: comment count number. */ | |
esc_html(_nx('%s comment', '%s comments', $a309_no_comments, 'Comments title', 'a309')), | |
esc_html(number_format_i18n($a309_no_comments)) | |
); | |
endif; | |
?></h2><!-- .comments-title --> | |
<button id="comments-show-btn" > | |
Show Comments | |
</button> | |
<!-- TODO: NO-JS page with comments later | |
<noscript data-ampdevmode><a id="comments-page" > | |
Go to comments page | |
</a> | |
</noscript> | |
--> | |
<?php if (!comments_open()) : ?> | |
<p class="no-comments"><?php esc_html_e('Comments are closed.', 'a309'); ?></p> | |
<?php endif; ?> | |
<?php endif; ?> | |
</div><!-- #comments --> | |
<?php if (a309_is_amp()): ?> | |
</amp-script> | |
<?php | |
endif; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//... | |
// The custom REST API for comments alos adding custom wp-comments-post.php file path | |
add_action('rest_api_init', 'change_rest_post' ); | |
function change_rest_post(){ | |
register_rest_route( 'a309/v1', '/get-comments-no/post/(?P<post_id>\d+)', array( | |
'methods' => 'GET', | |
'callback' => 'get_top_level_comments_number', | |
'permission_callback' => '__return_true', | |
) ); | |
register_rest_route( 'a309/v1', '/get-comments/post/(?P<post_id>\d+)/page/(?P<page_no>\d+)', array( | |
'methods' => 'GET', | |
'callback' => 'get_comments_post', | |
'permission_callback' => '__return_true', | |
) ); | |
} | |
function get_top_level_comments_number( $data ) { | |
global $wpdb; | |
$data['post_id'] = esc_sql($data['post_id']); | |
$noComments = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_parent = 0 AND comment_post_ID = '".$data['post_id']."'" ); | |
wp_send_json([ 'commentNumber' => $noComments ]); | |
} | |
function get_comments_post($data){ | |
// setup a fake POST to trick wp_list_comments | |
global $post; | |
$post = new stdClass(); | |
$post->ID = $data['post_id']; | |
setup_postdata( $post ); | |
$comment_args = array( | |
'avatar_size' => 60, | |
'reverse_top_level' => true, | |
'reverse_children' => true, | |
'style' => 'ol', | |
'short_ping' => true, | |
'max_depth' => 5, | |
'per_page' => 5, | |
'page' => $data['page_no'], | |
'echo' => false, | |
); | |
$template = wp_list_comments( $comment_args ); | |
wp_reset_postdata(); | |
wp_send_json([ 'comments' => $template, 'post_id' => $data['post_id'], 'page_no' => $data['page_no'] ]); | |
} | |
// Ajax Comment | |
function change_comment_action_url( $defaults ) { | |
$defaults['action'] = get_template_directory_uri().'/inc/comment-post.php'; | |
return $defaults; | |
} | |
add_filter( 'comment_form_defaults', 'change_comment_action_url'); | |
//... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Mostly is the wp-comments-post.php but returns JSON instead of redirect | |
/** | |
* Handles Comment Post to WordPress and prevents duplicate comment posting. | |
* | |
* @package WordPress | |
*/ | |
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { | |
$protocol = $_SERVER['SERVER_PROTOCOL']; | |
if ( ! in_array( $protocol, array( 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0' ), true ) ) { | |
$protocol = 'HTTP/1.0'; | |
} | |
header( 'Allow: POST' ); | |
header( "$protocol 405 Method Not Allowed" ); | |
header( 'Content-Type: text/plain' ); | |
exit; | |
} | |
/** Sets up the WordPress Environment. */ | |
require dirname(__DIR__, 4) . '/wp-load.php'; | |
nocache_headers(); | |
$comment = wp_handle_comment_submission( wp_unslash( $_POST ) ); | |
if ( is_wp_error( $comment ) ) { | |
$data = (int) $comment->get_error_data(); | |
if ( ! empty( $data ) ) { | |
/*wp_die( | |
'<p>' . $comment->get_error_message() . '</p>', | |
__( 'Comment Submission Failure' ), | |
array( | |
'response' => $data, | |
'back_link' => true, | |
) | |
);*/ | |
wp_send_json([ 'error' => true, 'msg' => $comment->get_error_message() ]); | |
} else { | |
wp_send_json([ 'error' => true ]); | |
} | |
} | |
$user = wp_get_current_user(); | |
$cookies_consent = ( isset( $_POST['wp-comment-cookies-consent'] ) ); | |
/** | |
* Perform other actions when comment cookies are set. | |
* | |
* @since 3.4.0 | |
* @since 4.9.6 The `$cookies_consent` parameter was added. | |
* | |
* @param WP_Comment $comment Comment object. | |
* @param WP_User $user Comment author's user object. The user may not exist. | |
* @param bool $cookies_consent Comment author's consent to store cookies. | |
*/ | |
do_action( 'set_comment_cookies', $comment, $user, $cookies_consent ); | |
$location = empty( $_POST['redirect_to'] ) ? get_comment_link( $comment ) : $_POST['redirect_to'] . '#comment-' . $comment->comment_ID; | |
// If user didn't consent to cookies, add specific query arguments to display the awaiting moderation message. | |
if ( ! $cookies_consent && 'unapproved' === wp_get_comment_status( $comment ) && ! empty( $comment->comment_author_email ) ) { | |
$location = add_query_arg( | |
array( | |
'unapproved' => $comment->comment_ID, | |
'moderation-hash' => wp_hash( $comment->comment_date_gmt ), | |
), | |
$location | |
); | |
} | |
/** | |
* Filters the location URI to send the commenter after posting. | |
* | |
* @since 2.0.5 | |
* | |
* @param string $location The 'redirect_to' URI sent via $_POST. | |
* @param WP_Comment $comment Comment object. | |
*/ | |
/* | |
$location = apply_filters( 'comment_post_redirect', $location, $comment ); | |
wp_safe_redirect( $location ); | |
exit; | |
*/ | |
$comment->comment_avatar = get_avatar( $comment->comment_author_email, 64); | |
wp_send_json([ 'error' => false, 'comment' => $comment ]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment