// site-app.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict' ;
const DTP _COMPONENT _NAME = 'SiteApp' ;
const dtp = window . dtp = window . dtp || { } ;
import DtpApp from 'dtp/dtp-app.js' ;
import UIkit from 'uikit' ;
import QRCode from 'qrcode' ;
import Cropper from 'cropperjs' ;
import { EmojiButton } from '@joeattardi/emoji-button' ;
const GRID _COLOR = 'rgb(64, 64, 64)' ;
const GRID _TICK _COLOR = 'rgb(192,192,192)' ;
const AXIS _TICK _COLOR = 'rgb(192, 192, 192)' ;
const CHART _LINE _USER = 'rgb(0, 192, 0)' ;
const CHART _FILL _USER = 'rgb(0, 128, 0)' ;
export default class DtpSiteApp extends DtpApp {
constructor ( user ) {
super ( DTP _COMPONENT _NAME , user ) ;
this . log . debug ( 'constructor' , 'app instance created' ) ;
this . chat = {
form : document . querySelector ( '#chat-input-form' ) ,
messageList : document . querySelector ( '#chat-message-list' ) ,
messages : [ ] ,
messageMenu : document . querySelector ( '.chat-message-menu' ) ,
input : document . querySelector ( '#chat-input-text' ) ,
isAtBottom : true ,
} ;
this . emojiPicker = new EmojiButton ( { theme : 'dark' } ) ;
this . emojiPicker . on ( 'emoji' , this . onEmojiSelected . bind ( this ) ) ;
if ( this . chat . messageList ) {
this . chat . messageList . addEventListener ( 'scroll' , this . onChatMessageListScroll . bind ( this ) ) ;
}
if ( this . chat . input ) {
this . chat . input . addEventListener ( 'keydown' , this . onChatInputKeyDown . bind ( this ) ) ;
}
this . charts = { /* will hold rendered charts */ } ;
this . scrollToHash ( ) ;
}
async scrollToHash ( ) {
const { hash } = window . location ;
if ( hash === '' ) {
return ;
}
const target = document . getElementById ( hash . slice ( 1 ) ) ;
if ( target && target . scrollIntoView ) {
target . scrollIntoView ( { behavior : 'smooth' } ) ;
}
}
async connect ( ) {
// can't use "super" because Webpack
this . log . info ( 'connect' , 'connecting WebSocket layer' ) ;
await DtpApp . prototype . connect . call ( this , { withRetry : true , withError : false } ) ;
if ( this . user ) {
const { socket } = this . socket ;
socket . on ( 'user-chat' , this . onUserChat . bind ( this ) ) ;
}
}
async onChatInputKeyDown ( event ) {
this . log . info ( 'onChatInputKeyDown' , 'chat input received' , { event } ) ;
if ( event . key === 'Enter' && ! event . shiftKey ) {
return this . sendUserChat ( event ) ;
}
}
async sendUserChat ( event ) {
event . preventDefault ( ) ;
if ( ! dtp . channel || ! dtp . channel . _id ) {
UIkit . modal . alert ( 'There is a problem with Chat. Please refresh the page.' ) ;
return ;
}
const channelId = dtp . channel . _id ;
this . log . info ( 'chat form' , channelId ) ;
const content = this . chat . input . value ;
this . chat . input . value = '' ;
if ( content . length === 0 ) {
return true ;
}
this . log . info ( 'sendUserChat' , 'sending chat message' , { channel : this . user . _id , content } ) ;
this . socket . sendUserChat ( channelId , content ) ;
// set focus back to chat input
this . chat . input . focus ( ) ;
return true ;
}
async onUserChat ( message ) {
this . log . info ( 'onUserChat' , 'message received' , { user : message . user , content : message . content } ) ;
const chatMessage = document . createElement ( 'div' ) ;
chatMessage . classList . add ( 'uk-margin-small' ) ;
chatMessage . classList . add ( 'chat-message' ) ;
const chatUser = document . createElement ( 'div' ) ;
chatUser . classList . add ( 'uk-text-small' ) ;
chatUser . classList . add ( 'chat-username' ) ;
chatUser . textContent = message . user . username ;
chatMessage . appendChild ( chatUser ) ;
const chatContent = document . createElement ( 'div' ) ;
chatContent . classList . add ( 'chat-content' ) ;
chatContent . innerHTML = message . content ;
chatMessage . appendChild ( chatContent ) ;
if ( Array . isArray ( message . stickers ) && message . stickers . length ) {
message . stickers . forEach ( ( sticker ) => {
const chatContent = document . createElement ( 'div' ) ;
chatContent . classList . add ( 'chat-sticker' ) ;
chatContent . innerHTML = ` <video playsinline autoplay muted loop><source src="/sticker/ ${ sticker } .mp4"></source></video> ` ;
chatMessage . appendChild ( chatContent ) ;
} ) ;
}
this . chat . messageList . appendChild ( chatMessage ) ;
this . chat . messages . push ( chatMessage ) ;
while ( this . chat . messages . length > 50 ) {
const message = this . chat . messages . shift ( ) ;
this . chat . messageList . removeChild ( message ) ;
}
if ( this . chat . isAtBottom ) {
this . chat . messageList . scrollTo ( 0 , this . chat . messageList . scrollHeight ) ;
}
}
async onChatMessageListScroll ( /* event */ ) {
const prevBottom = this . chat . isAtBottom ;
const scrollPos = this . chat . messageList . scrollTop + this . chat . messageList . clientHeight ;
this . chat . isAtBottom = scrollPos >= this . chat . messageList . scrollHeight ;
if ( this . chat . isAtBottom !== prevBottom ) {
this . log . info ( 'onChatMessageListScroll' , 'at-bottom status change' , { atBottom : this . chat . isAtBottom } ) ;
if ( this . chat . isAtBottom ) {
this . chat . messageMenu . classList . remove ( 'chat-menu-visible' ) ;
} else {
this . chat . messageMenu . classList . add ( 'chat-menu-visible' ) ;
}
}
}
async resumeChatScroll ( ) {
this . chat . messageList . scrollTop = this . chat . messageList . scrollHeight ;
}
async goBack ( ) {
if ( document . referrer && ( document . referrer . indexOf ( ` :// ${ window . dtp . domain } ` ) >= 0 ) ) {
window . history . back ( ) ;
} else {
window . location . href = '/' ;
}
return false ;
}
async submitForm ( event , userAction ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
try {
const formElement = event . currentTarget || event . target ;
const form = new FormData ( formElement ) ;
this . log . info ( 'submitForm' , userAction , { event , action : formElement . action } ) ;
const response = await fetch ( formElement . action , {
method : formElement . method ,
body : form ,
} ) ;
if ( ! response . ok ) {
let json ;
try {
json = await response . json ( ) ;
} catch ( error ) {
throw new Error ( 'Server error' ) ;
}
throw new Error ( json . message || 'Server error' ) ;
}
await this . processResponse ( response ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to ${ userAction } : ${ error . message } ` ) ;
}
return ;
}
async submitImageForm ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const formElement = event . currentTarget || event . target ;
const form = new FormData ( formElement ) ;
this . cropper . getCroppedCanvas ( ) . toBlob ( async ( imageData ) => {
try {
form . append ( 'imageFile' , imageData , 'profile.png' ) ;
this . log . info ( 'submitImageForm' , 'updating user settings' , { event , action : formElement . action } ) ;
const response = await fetch ( formElement . action , {
method : formElement . method ,
body : form ,
} ) ;
if ( ! response . ok ) {
let json ;
try {
json = await response . json ( ) ;
} catch ( error ) {
throw new Error ( 'Server error' ) ;
}
throw new Error ( json . message || 'Server error' ) ;
}
await this . processResponse ( response ) ;
window . location . reload ( ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to update profile photo: ${ error . message } ` ) ;
}
} ) ;
return ;
}
async closeCurrentDialog ( ) {
if ( ! this . currentDialog ) {
return ;
}
this . currentDialog . hide ( ) ;
delete this . currentDialog ;
}
async copyHtmlToText ( event , textContentId ) {
const content = this . editor . getContent ( { format : 'text' } ) ;
const text = document . getElementById ( textContentId ) ;
text . value = content ;
}
async selectImageFile ( event ) {
event . preventDefault ( ) ;
const imageId = event . target . getAttribute ( 'data-image-id' ) ;
//z read the cropper options from the element on the page
let cropperOptions = event . target . getAttribute ( 'data-cropper-options' ) ;
if ( cropperOptions ) {
cropperOptions = JSON . parse ( cropperOptions ) ;
}
this . log . debug ( 'selectImageFile' , 'cropper options' , { cropperOptions } ) ; //z remove when done
const fileSelectContainerId = event . target . getAttribute ( 'data-file-select-container' ) ;
if ( ! fileSelectContainerId ) {
UIkit . modal . alert ( 'Missing file select container element ID information' ) ;
return ;
}
const fileSelectContainer = document . getElementById ( fileSelectContainerId ) ;
if ( ! fileSelectContainer ) {
UIkit . modal . alert ( 'Missing file select element' ) ;
return ;
}
const fileSelect = fileSelectContainer . querySelector ( 'input[type="file"]' ) ;
if ( ! fileSelect . files || ( fileSelect . files . length === 0 ) ) {
return ;
}
const selectedFile = fileSelect . files [ 0 ] ;
if ( ! selectedFile ) {
return ;
}
this . log . debug ( 'selectImageFile' , 'thumbnail file select' , { event , selectedFile } ) ;
const filter = /^(image\/jpg|image\/jpeg|image\/png)$/i ;
if ( ! filter . test ( selectedFile . type ) ) {
UIkit . modal . alert ( ` Unsupported image file type selected: ${ selectedFile . type } ` ) ;
return ;
}
const fileSizeId = event . target . getAttribute ( 'data-file-size-element' ) ;
const FILE _MAX _SIZE = parseInt ( fileSelect . getAttribute ( 'data-file-max-size' ) , 10 ) ;
const fileSize = document . getElementById ( fileSizeId ) ;
fileSize . textContent = numeral ( selectedFile . size ) . format ( '0,0.0b' ) ;
if ( selectedFile . size > ( FILE _MAX _SIZE ) ) {
UIkit . modal . alert ( ` File is too large: ${ fileSize . textContent } . Custom thumbnail images may be up to ${ numeral ( FILE _MAX _SIZE ) . format ( '0,0.00b' ) } in size. ` ) ;
return ;
}
const reader = new FileReader ( ) ;
reader . onload = ( e ) => {
const img = document . getElementById ( imageId ) ;
img . onload = ( e ) => {
console . log ( 'image loaded' , e , img . naturalWidth , img . naturalHeight ) ;
fileSelectContainer . querySelector ( '#file-name' ) . textContent = selectedFile . name ;
fileSelectContainer . querySelector ( '#file-modified' ) . textContent = moment ( selectedFile . lastModifiedDate ) . fromNow ( ) ;
fileSelectContainer . querySelector ( '#image-resolution-w' ) . textContent = img . naturalWidth . toString ( ) ;
fileSelectContainer . querySelector ( '#image-resolution-h' ) . textContent = img . naturalHeight . toString ( ) ;
fileSelectContainer . querySelector ( '#file-select' ) . setAttribute ( 'hidden' , true ) ;
fileSelectContainer . querySelector ( '#file-info' ) . removeAttribute ( 'hidden' ) ;
fileSelectContainer . querySelector ( '#file-save-btn' ) . removeAttribute ( 'hidden' ) ;
} ;
// set the image as the "src" of the <img> in the DOM.
img . src = e . target . result ;
//z create cropper and set options here
this . createImageCropper ( img , cropperOptions ) ;
} ;
// read in the file, which will trigger everything else in the event handler above.
reader . readAsDataURL ( selectedFile ) ;
}
async createImageCropper ( img , options ) {
options = Object . assign ( {
aspectRatio : 1 ,
dragMode : 'move' ,
autoCropArea : 0.85 ,
restore : false ,
guides : false ,
center : false ,
highlight : false ,
cropBoxMovable : true ,
cropBoxResizable : true ,
toggleDragModeOnDblclick : false ,
modal : true ,
} , options ) ;
this . log . info ( "createImageCropper" , "Creating image cropper" , { img } ) ;
this . cropper = new Cropper ( img , options ) ;
}
async attachTinyMCE ( editor ) {
editor . on ( 'KeyDown' , async ( e ) => {
if ( dtp . autosaveTimeout ) {
window . clearTimeout ( dtp . autosaveTimeout ) ;
delete dtp . autosaveTimeout ;
}
dtp . autosaveTimeout = window . setTimeout ( async ( ) => {
console . log ( 'document autosave' ) ;
} , 1000 ) ;
if ( ( e . keyCode === 8 || e . keyCode === 46 ) && editor . selection ) {
var selectedNode = editor . selection . getNode ( ) ;
if ( selectedNode && selectedNode . nodeName === 'IMG' ) {
console . log ( 'removing image' , selectedNode ) ;
await dtp . app . deleteImage ( selectedNode . src . slice ( - 24 ) ) ;
}
}
} ) ;
}
async deleteImage ( imageId ) {
try {
throw new Error ( ` would want to delete /image/ ${ imageId } ` ) ;
} catch ( error ) {
UIkit . modal . alert ( error . message ) ;
}
}
async openPaymentModal ( ) {
await UIkit . modal ( '#donate-modal' ) . show ( ) ;
await UIkit . slider ( '#payment-slider' ) . show ( 0 ) ;
}
async generateQRCode ( event ) {
const selectorQR = event . target . getAttribute ( 'data-selector-qr' ) ;
const selectorPrompt = event . target . getAttribute ( 'data-selector-prompt' ) ;
const targetAmount = event . target . getAttribute ( 'data-amount' ) ;
const amountLabel = numeral ( targetAmount ) . format ( '$0,0.00' ) ;
const currencyAmountLabel = numeral ( targetAmount * this . exchangeRates [ this . paymentCurrency ] . conversionRateUSD ) . format ( '0,0.0000000000000000' ) ;
const prompt = ` Donate ${ amountLabel } using ${ this . paymentCurrencyLabel } . ` ;
event . preventDefault ( ) ;
let targetUrl ;
switch ( this . paymentCurrency ) {
case 'BTC' :
targetUrl = ` bitcoin: ${ dtp . channel . wallet . btc } ?amount= ${ targetAmount } &message=Donation to ${ dtp . channel . name } ` ;
break ;
case 'ETH' :
targetUrl = ` ethereum: ${ dtp . channel . wallet . eth } ?amount= ${ targetAmount } ` ;
break ;
case 'LTC' :
targetUrl = ` litecoin: ${ dtp . channel . wallet . ltc } ?amount= ${ targetAmount } &message=Donation to ${ dtp . channel . name } ` ;
break ;
}
try {
let elements ;
const imageUrl = await QRCode . toDataURL ( targetUrl ) ;
elements = document . querySelectorAll ( selectorQR ) ;
elements . forEach ( ( element ) => element . setAttribute ( 'src' , imageUrl ) ) ;
elements = document . querySelectorAll ( selectorPrompt ) ;
elements . forEach ( ( e ) => e . textContent = prompt ) ;
elements = document . querySelectorAll ( 'span.prompt-donate-amount' ) ;
elements . forEach ( ( element ) => {
element . textContent = amountLabel ;
} ) ;
elements = document . querySelectorAll ( 'span.prompt-donate-amount-crypto' ) ;
elements . forEach ( ( element ) => {
element . textContent = currencyAmountLabel ;
} ) ;
elements = document . querySelectorAll ( 'a.payment-target-link' ) ;
elements . forEach ( ( element ) => {
element . setAttribute ( 'href' , targetUrl ) ;
} ) ;
const e = document . getElementById ( 'payment-link' ) ;
e . setAttribute ( 'href' , targetUrl ) ;
UIkit . slider ( '#payment-slider' ) . show ( 2 ) ;
} catch ( error ) {
this . log . error ( 'failed to generate QR code to image' , { error } ) ;
UIkit . modal . alert ( ` Failed to generate QR code: ${ error . message } ` ) ;
}
return true ;
}
async setPaymentCurrency ( currency ) {
this . paymentCurrency = currency ;
this . log . info ( 'setPaymentCurrency' , 'payment currency' , { currency : this . paymentCurrency } ) ;
await this . updateExchangeRates ( ) ;
switch ( this . paymentCurrency ) {
case 'BTC' :
this . paymentCurrencyLabel = 'Bitcoin (BTC)' ;
break ;
case 'ETH' :
this . paymentCurrencyLabel = 'Ethereum (ETH)' ;
break ;
case 'LTC' :
this . paymentCurrencyLabel = 'Litecoin (LTC)' ;
break ;
}
let elements = document . querySelectorAll ( 'span.prompt-donate-currency' ) ;
elements . forEach ( ( element ) => {
element . textContent = this . paymentCurrencyLabel ;
} ) ;
UIkit . slider ( '#payment-slider' ) . show ( 1 ) ;
}
async updateExchangeRates ( ) {
const NOW = Date . now ( ) ;
try {
let exchangeRates ;
if ( ! window . localStorage . exchangeRates ) {
exchangeRates = await this . loadExchangeRates ( ) ;
this . log . info ( 'updateExchangeRates' , 'current exchange rates received and cached' , { exchangeRates } ) ;
window . localStorage . exchangesRates = JSON . stringify ( exchangeRates ) ;
} else {
exchangeRates = JSON . parse ( window . localStorage . exchangeRates ) ;
if ( exchangeRates . timestamp < ( NOW - 60000 ) ) {
exchangeRates = await this . loadExchangeRates ( ) ;
this . log . info ( 'updateExchangeRates' , 'current exchange rates received and cached' , { exchangeRates } ) ;
window . localStorage . exchangesRates = JSON . stringify ( exchangeRates ) ;
}
}
this . exchangeRates = exchangeRates . symbols ;
} catch ( error ) {
this . log . error ( 'updateExchangeRates' , 'failed to fetch currency exchange rates' , { error } ) ;
UIkit . modal . alert ( ` Failed to fetch current exchange rates: ${ error . message } ` ) ;
}
}
async loadExchangeRates ( ) {
this . log . info ( 'loadExchangeRates' , 'fetching current exchange rates' ) ;
const response = await fetch ( '/crypto-exchange/current-rates' ) ;
if ( ! response . ok ) {
throw new Error ( 'Server error' ) ;
}
let exchangeRates = await response . json ( ) ;
if ( ! exchangeRates . success ) {
throw new Error ( exchangeRates . message ) ;
}
exchangeRates . timestamp = Date . now ( ) ;
return exchangeRates ;
}
async generateOtpQR ( canvas , keyURI ) {
QRCode . toCanvas ( canvas , keyURI ) ;
}
async removeImageFile ( event ) {
const imageType = ( event . target || event . currentTarget ) . getAttribute ( 'data-image-type' ) ;
try {
this . log . info ( 'removeImageFile' , 'request to remove image' , event ) ;
let response ;
switch ( imageType ) {
case 'profile-picture-file' :
response = await fetch ( ` /user/ ${ this . user . _id } /profile-photo ` , { method : 'DELETE' } ) ;
break ;
default :
throw new Error ( 'Invalid image type' ) ;
}
if ( ! response . ok ) {
throw new Error ( 'Server error' ) ;
}
await this . processResponse ( response ) ;
window . location . reload ( ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to remove image: ${ error . message } ` ) ;
}
}
async onCommentInput ( event ) {
const label = document . getElementById ( 'comment-character-count' ) ;
label . textContent = numeral ( event . target . value . length ) . format ( '0,0' ) ;
}
async showEmojiPicker ( event ) {
const targetElementName = ( event . currentTarget || event . target ) . getAttribute ( 'data-target-element' ) ;
this . emojiTargetElement = document . getElementById ( targetElementName ) ;
this . emojiPicker . togglePicker ( this . emojiTargetElement ) ;
}
async onEmojiSelected ( selection ) {
this . emojiTargetElement . value += selection . emoji ;
}
async showReportCommentForm ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const resourceType = event . currentTarget . getAttribute ( 'data-resource-type' ) ;
const resourceId = event . currentTarget . getAttribute ( 'data-resource-id' ) ;
const commentId = event . currentTarget . getAttribute ( 'data-comment-id' ) ;
this . closeCommentDropdownMenu ( commentId ) ;
try {
const response = await fetch ( '/content-report/comment/form' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( {
resourceType , resourceId , commentId
} ) ,
} ) ;
if ( ! response . ok ) {
throw new Error ( 'failed to load report form' ) ;
}
const html = await response . text ( ) ;
this . currentDialog = UIkit . modal . dialog ( html ) ;
} catch ( error ) {
this . log . error ( 'reportComment' , 'failed to process comment request' , { resourceType , resourceId , commentId , error } ) ;
UIkit . modal . alert ( ` Failed to report comment: ${ error . message } ` ) ;
}
return true ;
}
async deleteComment ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const commentId = ( event . currentTarget || event . target ) . getAttribute ( 'data-comment-id' ) ;
try {
const response = fetch ( ` /comment/ ${ commentId } ` , { method : 'DELETE' } ) ;
if ( ! response . ok ) {
throw new Error ( 'Server error' ) ;
}
this . processResponse ( response ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to delete comment: ${ error . message } ` ) ;
}
}
async submitDialogForm ( event , userAction ) {
await this . submitForm ( event , userAction ) ;
await this . closeCurrentDialog ( ) ;
}
async blockCommentAuthor ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const resourceType = event . currentTarget . getAttribute ( 'data-resource-type' ) ;
const resourceId = event . currentTarget . getAttribute ( 'data-resource-id' ) ;
const commentId = event . currentTarget . getAttribute ( 'data-comment-id' ) ;
const actionUrl = this . getCommentActionUrl ( resourceType , resourceId , commentId , 'block-author' ) ;
this . closeCommentDropdownMenu ( commentId ) ;
try {
this . log . info ( 'blockCommentAuthor' , 'blocking comment author' , { resourceType , resourceId , commentId } ) ;
const response = await fetch ( actionUrl , { method : 'POST' } ) ;
await this . processResponse ( response ) ;
} catch ( error ) {
this . log . error ( 'reportComment' , 'failed to process comment request' , { resourceType , resourceId , commentId , error } ) ;
UIkit . modal . alert ( ` Failed to block comment author: ${ error . message } ` ) ;
}
return true ;
}
closeCommentDropdownMenu ( commentId ) {
const dropdown = document . querySelector ( ` [data-comment-id=" ${ commentId } "][uk-dropdown] ` ) ;
UIkit . dropdown ( dropdown ) . hide ( false ) ;
}
getCommentActionUrl ( resourceType , resourceId , commentId , action ) {
switch ( resourceType ) {
case 'Newsletter' :
return ` /newsletter/ ${ resourceId } /comment/ ${ commentId } / ${ action } ` ;
case 'Page' :
return ` /page/ ${ resourceId } /comment/ ${ commentId } / ${ action } ` ;
case 'Post' :
return ` /post/ ${ resourceId } /comment/ ${ commentId } / ${ action } ` ;
default :
break ;
}
throw new Error ( 'Invalid resource type for comment operation' ) ;
}
async submitCommentVote ( event ) {
const target = ( event . currentTarget || event . target ) ;
const commentId = target . getAttribute ( 'data-comment-id' ) ;
const vote = target . getAttribute ( 'data-vote' ) ;
try {
const response = await fetch ( ` /comment/ ${ commentId } /vote ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { vote } ) ,
} ) ;
await this . processResponse ( response ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to submit vote: ${ error . message } ` ) ;
}
}
async renderStatsGraph ( selector , title , data ) {
try {
const canvas = document . querySelector ( selector ) ;
const ctx = canvas . getContext ( '2d' ) ;
this . charts . profileStats = new Chart ( ctx , {
type : 'bar' ,
data : {
labels : data . map ( ( item ) => new Date ( item . date ) ) ,
datasets : [
{
label : title ,
data : data . map ( ( item ) => item . count ) ,
borderColor : CHART _LINE _USER ,
borderWidth : 1 ,
backgroundColor : CHART _FILL _USER ,
tension : 0 ,
} ,
] ,
} ,
options : {
scales : {
yAxis : {
display : true ,
ticks : {
color : AXIS _TICK _COLOR ,
callback : ( value ) => {
return numeral ( value ) . format ( value > 1000 ? '0,0.0a' : '0,0' ) ;
} ,
} ,
grid : {
color : GRID _COLOR ,
tickColor : GRID _TICK _COLOR ,
} ,
} ,
x : {
type : 'time' ,
} ,
xAxis : {
display : false ,
grid : {
color : GRID _COLOR ,
tickColor : GRID _TICK _COLOR ,
} ,
} ,
} ,
plugins : {
title : { display : false } ,
subtitle : { display : false } ,
legend : { display : false } ,
} ,
maintainAspectRatio : true ,
aspectRatio : 16.0 / 9.0 ,
onResize : ( chart , event ) => {
if ( event . width >= 960 ) {
chart . config . options . aspectRatio = 16.0 / 5.0 ;
}
else if ( event . width >= 640 ) {
chart . config . options . aspectRatio = 16.0 / 9.0 ;
} else if ( event . width >= 480 ) {
chart . config . options . aspectRatio = 16.0 / 12.0 ;
} else {
chart . config . options . aspectRatio = 16.0 / 16.0 ;
}
} ,
} ,
} ) ;
} catch ( error ) {
this . log . error ( 'renderStatsGraph' , 'failed to render stats graph' , { title , error } ) ;
UIkit . modal . alert ( ` Failed to render chart: ${ error . message } ` ) ;
}
}
async openReplies ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const target = event . currentTarget || event . target ;
const commentId = target . getAttribute ( 'data-comment-id' ) ;
const container = document . querySelector ( ` .dtp-reply-list-container[data-comment-id=" ${ commentId } "] ` ) ;
const replyList = document . querySelector ( ` ul.dtp-reply-list[data-comment-id=" ${ commentId } "] ` ) ;
const isOpen = ! container . hasAttribute ( 'hidden' ) ;
if ( isOpen ) {
container . setAttribute ( 'hidden' , '' ) ;
while ( replyList . firstChild ) {
replyList . removeChild ( replyList . firstChild ) ;
}
return ;
}
try {
const response = await fetch ( ` /comment/ ${ commentId } /replies ` ) ;
this . processResponse ( response ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to load replies: ${ error . message } ` ) ;
}
return true ;
}
async openReplyComposer ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const target = event . currentTarget || event . target ;
const commentId = target . getAttribute ( 'data-comment-id' ) ;
const composer = document . querySelector ( ` .dtp-reply-composer[data-comment-id=" ${ commentId } "] ` ) ;
composer . toggleAttribute ( 'hidden' ) ;
return true ;
}
async loadMoreComments ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const target = event . currentTarget || event . target ;
const buttonId = target . getAttribute ( 'data-button-id' ) ;
const rootUrl = target . getAttribute ( 'data-root-url' ) ;
const nextPage = target . getAttribute ( 'data-next-page' ) ;
try {
const response = await fetch ( ` ${ rootUrl } ?p= ${ nextPage } &buttonId= ${ buttonId } ` ) ;
await this . processResponse ( response ) ;
} catch ( error ) {
UIkit . modal . alert ( ` Failed to load more comments: ${ error . message } ` ) ;
}
}
}
dtp . DtpSiteApp = DtpSiteApp ;