2024-02-12 21:30:33 +08:00
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define ( [
'jquery' ,
'/api/config' ,
'/customize/application_config.js' ,
'/common/toolbar.js' ,
'/components/nthen/index.js' ,
'/common/sframe-common.js' ,
'/common/hyperscript.js' ,
'/customize/messages.js' ,
'/common/common-interface.js' ,
'/common/common-ui-elements.js' ,
'/common/common-util.js' ,
'/common/common-hash.js' ,
2024-02-22 00:31:07 +08:00
'/common/inner/sidebar-layout.js' ,
2024-02-12 21:30:33 +08:00
'/support/ui.js' ,
2024-03-01 01:36:17 +08:00
'/components/file-saver/FileSaver.min.js' ,
2024-02-12 21:30:33 +08:00
'css!/components/components-font-awesome/css/font-awesome.min.css' ,
'less!/moderation/app-moderation.less' ,
] , function (
$ ,
ApiConfig ,
AppConfig ,
Toolbar ,
nThen ,
SFCommon ,
h ,
Messages ,
UI ,
UIElements ,
Util ,
Hash ,
2024-02-22 00:31:07 +08:00
Sidebar ,
2024-02-28 00:17:45 +08:00
Support
2024-02-12 21:30:33 +08:00
)
{
2024-02-22 00:31:07 +08:00
var APP = { } ;
2024-03-01 01:36:17 +08:00
var saveAs = window . saveAs ;
2024-02-12 21:30:33 +08:00
var common ;
2024-02-22 00:31:07 +08:00
var sframeChan ;
2024-02-20 21:00:09 +08:00
var events = {
2024-03-01 23:34:27 +08:00
NEW _TICKET : Util . mkEvent ( ) ,
UPDATE _TICKET : Util . mkEvent ( ) ,
UPDATE _RIGHTS : Util . mkEvent ( ) ,
2024-03-05 01:14:59 +08:00
RECORDED _CHANGE : Util . mkEvent ( ) ,
REFRESH _FILTER : Util . mkEvent ( ) ,
REFRESH _TAGS : Util . mkEvent ( )
2024-02-20 21:00:09 +08:00
} ;
2024-02-12 21:30:33 +08:00
2024-02-22 00:31:07 +08:00
// XXX
2024-03-01 18:15:29 +08:00
Messages . support _pending = "Pending tickets:" ;
2024-03-06 22:29:28 +08:00
Messages . support _pending _tag = "Pending" ;
Messages . support _active _tag = "Active" ;
Messages . support _closed _tag = "Closed" ;
2024-03-01 18:15:29 +08:00
/ *
2024-02-22 00:31:07 +08:00
Messages . support _activeListTitle = "Active tickets" ;
Messages . support _pendingListTitle = "Pending tickets" ;
2024-03-01 18:15:29 +08:00
Messages . support _activeListHint = "List of tickets that are in an active state" ;
2024-02-22 00:31:07 +08:00
Messages . support _pendingListHint = "List of tickets that may not be updated for a while but should not be closed" ;
2024-03-01 18:15:29 +08:00
* /
2024-02-21 00:50:41 +08:00
2024-02-22 00:31:07 +08:00
Messages . support _privacyTitle = "Answer anonymously" ;
2024-02-22 01:52:12 +08:00
Messages . support _privacyHint = "Check this option to reply as 'The Support Team' instead of your own username" ;
Messages . support _notificationsTitle = "Disable notifications" ;
Messages . support _notificationsHint = "Check this option to disable notifications on new or updated ticket" ;
2024-02-16 01:37:08 +08:00
2024-02-29 00:25:32 +08:00
Messages . support _openTicketTitle = "Open a ticket for a user" ;
Messages . support _openTicketHint = "Create a ticket for a user. They will receive a CryptPad notification to warn them. You can copy their user data from an existing support ticket, using the Copy button in the user data." ;
Messages . support _userChannel = "User's notifications channel ID" ;
Messages . support _userKey = "User's curvePublic key" ;
Messages . support _invalChan = "Invalid notifications channel" ;
Messages . support _pasteUserData = "Paste user data here" ;
2024-03-05 01:14:59 +08:00
Messages . support _recordedTitle = "Prerecorded messages" ;
Messages . support _recordedHint = "You can store prerecorded message in order to insert them with one click in a support ticket." ;
Messages . support _recordedList = "List of recorded messages" ;
Messages . support _recordedEmpty = "No recorded messages" ;
Messages . support _recordedId = "Unique Identifier" ;
Messages . support _recordedContent = "Content" ;
Messages . support _legacyTitle = "View old support data" ;
Messages . support _legacyHint = "View tickets from the legacy system. You'll be able to recreate thse tickets on the new support system." ;
Messages . support _legacyButton = "Get active" ;
Messages . support _legacyDump = "Export all" ;
Messages . support _legacyClear = "Delete from my account" ;
2024-03-06 22:29:28 +08:00
Messages . support _searchLabel = "Search (title or ticketId)" ;
2024-02-22 00:31:07 +08:00
var andThen = function ( common , $container , linkedTicket ) {
const sidebar = Sidebar . create ( common , 'support' , $container ) ;
2024-02-29 00:25:32 +08:00
const blocks = sidebar . blocks ;
2024-03-01 23:34:27 +08:00
APP . recorded = { } ;
2024-03-05 01:14:59 +08:00
APP . allTags = [ ] ;
2024-03-01 23:34:27 +08:00
APP . openTicketCategory = Util . mkEvent ( ) ;
2024-02-16 01:37:08 +08:00
2024-03-06 22:29:28 +08:00
var sortTicket = tickets => ( c1 , c2 ) => {
return tickets [ c2 ] . time - tickets [ c1 ] . time ;
} ;
const onShowTicket = function ( ticket , channel , data , done ) {
APP . module . execCommand ( 'LOAD_TICKET_ADMIN' , {
channel : channel ,
curvePublic : data . authorKey ,
supportKey : data . supportKey
} , function ( obj ) {
if ( ! Array . isArray ( obj ) ) {
console . error ( obj && obj . error ) ;
done ( false ) ;
return void UI . warn ( Messages . error ) ;
}
var $ticket = $ ( ticket ) ;
obj . forEach ( function ( msg ) {
// Only add notifications channel if this is coming from the other user
if ( ! data . notifications && msg . sender . drive ) {
data . notifications = Util . find ( msg , [ 'sender' , 'notifications' ] ) ;
}
if ( msg . close ) {
$ticket . addClass ( 'cp-support-list-closed' ) ;
return $ticket . append ( APP . support . makeCloseMessage ( msg ) ) ;
}
if ( msg . legacy && msg . messages ) {
msg . messages . forEach ( c => {
$ticket . append ( APP . support . makeMessage ( c ) ) ;
} ) ;
return ;
}
$ticket . append ( APP . support . makeMessage ( msg ) ) ;
} ) ;
done ( true ) ;
} ) ;
} ;
2024-02-22 00:31:07 +08:00
// Support panel functions
2024-02-20 21:00:09 +08:00
let open = [ ] ;
2024-02-28 00:17:45 +08:00
let refreshAll = function ( ) { } ;
2024-03-01 19:21:30 +08:00
let refresh = ( $container , type , _cb ) => {
let cb = Util . mkAsync ( _cb || function ( ) { } ) ;
2024-02-21 00:50:41 +08:00
APP . module . execCommand ( 'LIST_TICKETS_ADMIN' , {
type : type
} , ( tickets ) => {
2024-02-27 02:07:39 +08:00
if ( tickets . error ) {
2024-03-01 19:21:30 +08:00
cb ( ) ;
2024-02-27 02:07:39 +08:00
if ( tickets . error === 'EFORBIDDEN' ) {
return void UI . errorLoadingScreen ( Messages . admin _authError || '403 Forbidden' ) ;
}
return void UI . errorLoadingScreen ( tickets . error ) ;
}
2024-03-01 23:34:27 +08:00
open = open . filter ( chan => {
// Remove deleted tickets from memory
return tickets [ chan ] ;
} ) ;
2024-02-27 02:07:39 +08:00
UI . removeLoadingScreen ( ) ;
2024-02-20 21:00:09 +08:00
let activeForms = { } ;
$container . find ( '.cp-support-form-container' ) . each ( ( i , el ) => {
let id = $ ( el ) . attr ( 'data-id' ) ;
if ( ! id ) { return ; }
activeForms [ id ] = el ;
} ) ;
2024-02-16 01:37:08 +08:00
$container . empty ( ) ;
var col1 = h ( 'div.cp-support-column' , h ( 'h1' , [
h ( 'span' , Messages . admin _support _premium ) ,
h ( 'span.cp-support-count' ) ,
] ) ) ;
var col2 = h ( 'div.cp-support-column' , h ( 'h1' , [
h ( 'span' , Messages . admin _support _normal ) ,
h ( 'span.cp-support-count' ) ,
] ) ) ;
var col3 = h ( 'div.cp-support-column' , h ( 'h1' , [
h ( 'span' , Messages . admin _support _answered ) ,
h ( 'span.cp-support-count' ) ,
] ) ) ;
2024-02-21 00:50:41 +08:00
var col4 = h ( 'div.cp-support-column' , h ( 'h1' , [
h ( 'span' , Messages . admin _support _closed ) ,
h ( 'span.cp-support-count' ) ,
] ) ) ;
2024-03-01 18:15:29 +08:00
var col5 = h ( 'div.cp-support-column' , h ( 'h1' , [
h ( 'span' , Messages . support _pending ) ,
h ( 'span.cp-support-count' ) ,
] ) ) ;
2024-02-21 00:50:41 +08:00
if ( type === 'closed' ) {
// Only one column
col1 = col2 = col3 = col4 ;
}
2024-03-01 18:15:29 +08:00
if ( type === 'pending' ) {
// Only one column
col1 = col2 = col3 = col5 ;
}
2024-02-16 01:37:08 +08:00
$container . append ( [ col1 , col2 , col3 ] ) ;
2024-02-12 21:30:33 +08:00
2024-02-20 21:00:09 +08:00
const onShow = function ( ticket , channel , data , done ) {
2024-03-06 22:29:28 +08:00
onShowTicket ( ticket , channel , data , ( success ) => {
if ( success ) {
if ( ! open . includes ( channel ) ) { open . push ( channel ) ; }
2024-02-16 01:37:08 +08:00
}
2024-02-20 21:00:09 +08:00
done ( ) ;
} ) ;
} ;
const onHide = function ( ticket , channel , data , done ) {
$ ( ticket ) . find ( '.cp-support-list-message' ) . remove ( ) ;
open = open . filter ( ( chan ) => {
return chan !== channel ;
2024-02-16 01:37:08 +08:00
} ) ;
2024-02-20 21:00:09 +08:00
done ( ) ;
2024-02-16 01:37:08 +08:00
} ;
2024-02-28 00:17:45 +08:00
const onReply = function ( ticket , channel , data , form ) {
2024-02-16 01:37:08 +08:00
var formData = APP . support . getFormData ( form ) ;
APP . module . execCommand ( 'REPLY_TICKET_ADMIN' , {
channel : channel ,
curvePublic : data . authorKey ,
2024-02-19 22:20:50 +08:00
notifChannel : data . notifications ,
2024-02-27 23:52:30 +08:00
supportKey : data . supportKey ,
2024-02-16 01:37:08 +08:00
ticket : formData
} , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj && obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
$ ( ticket ) . find ( '.cp-support-list-message' ) . remove ( ) ;
2024-02-20 21:00:09 +08:00
$ ( ticket ) . find ( '.cp-support-form-container' ) . remove ( ) ;
2024-02-21 00:50:41 +08:00
refresh ( $container , type ) ;
} ) ;
} ;
const onClose = function ( ticket , channel , data ) {
APP . module . execCommand ( 'CLOSE_TICKET_ADMIN' , {
channel : channel ,
curvePublic : data . authorKey ,
notifChannel : data . notifications ,
2024-02-27 23:52:30 +08:00
supportKey : data . supportKey ,
2024-02-21 00:50:41 +08:00
ticket : APP . support . getDebuggingData ( {
close : true
} )
} , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj && obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
refreshAll ( ) ;
2024-02-16 01:37:08 +08:00
} ) ;
} ;
2024-02-29 00:25:32 +08:00
const onMove = function ( ticket , channel ) {
2024-02-28 19:37:00 +08:00
APP . module . execCommand ( 'MOVE_TICKET_ADMIN' , {
channel : channel ,
from : type ,
to : onMove . isTicketActive ? 'pending' : 'active'
} , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj && obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
refreshAll ( ) ;
} ) ;
} ;
2024-03-06 22:39:22 +08:00
onMove . disableMove = type === 'closed' ;
2024-02-28 19:37:00 +08:00
onMove . isTicketActive = type === 'active' ;
2024-02-12 21:30:33 +08:00
2024-03-04 20:48:56 +08:00
const onTag = ( channel , tags ) => {
APP . module . execCommand ( 'SET_TAGS_ADMIN' , {
channel , tags
} , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj && obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
2024-03-05 01:14:59 +08:00
// XXX check deleted tags
2024-03-04 20:48:56 +08:00
( tags || [ ] ) . forEach ( tag => {
2024-03-05 01:14:59 +08:00
if ( ! APP . allTags . includes ( tag ) ) { APP . allTags . push ( tag ) ; }
2024-03-04 20:48:56 +08:00
} ) ;
2024-03-05 01:14:59 +08:00
events . REFRESH _TAGS . fire ( ) ;
2024-03-04 20:48:56 +08:00
//UI.log(Messags.saved);
//refreshAll();
} ) ;
} ;
onTag . getAllTags = ( ) => {
2024-03-05 01:14:59 +08:00
return APP . allTags || [ ] ;
2024-03-04 20:48:56 +08:00
} ;
2024-03-01 19:21:30 +08:00
// Show tickets, reload the previously open ones and cal back
// once everything is loaded
let n = nThen ;
2024-03-06 22:29:28 +08:00
Object . keys ( tickets ) . sort ( sortTicket ( tickets ) ) . forEach ( function ( channel ) {
2024-03-04 20:48:56 +08:00
// Update allTags
2024-02-16 01:37:08 +08:00
var d = tickets [ channel ] ;
2024-03-04 20:48:56 +08:00
( d . tags || [ ] ) . forEach ( tag => {
2024-03-05 01:14:59 +08:00
if ( ! APP . allTags . includes ( tag ) ) { APP . allTags . push ( tag ) ; }
2024-03-04 20:48:56 +08:00
} ) ;
// Make ticket
2024-02-20 21:00:09 +08:00
var ticket = APP . support . makeTicket ( {
id : channel ,
content : d ,
form : activeForms [ channel ] ,
2024-03-01 23:34:27 +08:00
recorded : APP . recorded ,
2024-03-04 20:48:56 +08:00
onShow , onHide , onClose , onReply , onMove , onTag
2024-02-20 21:00:09 +08:00
} ) ;
2024-02-16 01:37:08 +08:00
var container ;
if ( d . lastAdmin ) { container = col3 ; }
else if ( d . premium ) { container = col1 ; }
else { container = col2 ; }
$ ( container ) . append ( ticket ) ;
2024-02-20 21:00:09 +08:00
2024-03-01 19:21:30 +08:00
if ( open . includes ( channel ) ) {
n = n ( waitFor => {
ticket . open ( true , waitFor ( ) ) ;
} ) . nThen ;
2024-02-20 21:00:09 +08:00
}
2024-02-16 01:37:08 +08:00
} ) ;
2024-03-01 19:21:30 +08:00
// Wait for all open tickets to be loaded before calling back
// otherwise we may have a wrong scroll position
n ( ( ) => {
cb ( ) ;
} ) ;
2024-02-16 01:37:08 +08:00
} ) ;
} ;
2024-03-05 01:14:59 +08:00
let onFilter = ( ) => {
let tags = APP . filterTags || [ ] ;
APP . module . execCommand ( 'FILTER_TAGS_ADMIN' , { tags } , function ( obj ) {
if ( ! obj || obj . error ) { return ; }
$container . find ( '.cp-support-list-ticket' ) . toggleClass ( 'cp-filtered' , false ) ;
if ( obj . all || ! obj . tickets || ! obj . tickets . length ) { return ; }
obj . tickets . forEach ( id => {
$container . find ( ` .cp-support-list-ticket[data-id=" ${ id } "] ` )
. toggleClass ( 'cp-filtered' , true ) ;
} ) ;
} ) ;
} ;
2024-02-22 00:31:07 +08:00
let activeContainer , pendingContainer , closedContainer ;
2024-02-28 00:17:45 +08:00
refreshAll = function ( ) {
2024-03-01 19:21:30 +08:00
let $rightside = sidebar . $rightside ;
let s = $rightside . scrollTop ( ) ;
nThen ( waitFor => {
2024-03-01 23:34:27 +08:00
APP . module . execCommand ( 'GET_RECORDED' , { } , waitFor ( function ( obj ) {
if ( obj && obj . error ) {
APP . recorded = { } ;
return ;
}
APP . recorded = {
all : obj . messages ,
onClick : id => {
APP . module . execCommand ( 'USE_RECORDED' , { id } , ( ) => { } ) ;
}
} ;
} ) ) ;
} ) . nThen ( waitFor => {
2024-03-05 01:14:59 +08:00
APP . allTags = [ ] ;
2024-03-01 19:21:30 +08:00
refresh ( $ ( activeContainer ) , 'active' , waitFor ( ) ) ;
refresh ( $ ( pendingContainer ) , 'pending' , waitFor ( ) ) ;
refresh ( $ ( closedContainer ) , 'closed' , waitFor ( ) ) ;
2024-03-05 01:14:59 +08:00
} ) . nThen ( ( ) => {
onFilter ( ) ;
events . REFRESH _TAGS . fire ( ) ;
2024-03-01 19:21:30 +08:00
} ) . nThen ( waitFor => {
if ( ! linkedTicket ) { return ; }
let $ticket = $container . find ( ` [data-link-id=" ${ linkedTicket } "] ` ) ;
linkedTicket = undefined ;
if ( $ticket . length ) {
let ticket = $ticket [ 0 ] ;
if ( typeof ( ticket . open ) === "function" ) {
waitFor . abort ( ) ;
ticket . open ( true , ( ) => {
ticket . scrollIntoView ( ) ;
} ) ;
}
}
} ) . nThen ( ( ) => {
$rightside . scrollTop ( s ) ;
} ) ;
2024-02-21 00:50:41 +08:00
} ;
2024-02-22 01:52:12 +08:00
let _refresh = Util . throttle ( refreshAll , 500 ) ;
events . NEW _TICKET . reg ( _refresh ) ;
events . UPDATE _TICKET . reg ( _refresh ) ;
2024-03-01 23:34:27 +08:00
events . RECORDED _CHANGE . reg ( _refresh ) ;
2024-02-27 02:07:39 +08:00
events . UPDATE _RIGHTS . reg ( _refresh ) ;
2024-03-05 01:14:59 +08:00
events . REFRESH _FILTER . reg ( onFilter ) ;
2024-02-21 00:50:41 +08:00
2024-02-22 00:31:07 +08:00
// Make sidebar layout
const categories = {
'open' : {
icon : undefined ,
content : [
'privacy' ,
2024-03-05 01:14:59 +08:00
'filter' ,
2024-02-22 00:31:07 +08:00
'active-list' ,
'pending-list' ,
]
} ,
'closed' : {
icon : undefined ,
content : [
2024-03-05 01:14:59 +08:00
'filter' ,
2024-02-22 00:31:07 +08:00
'closed-list'
]
} ,
2024-03-06 22:29:28 +08:00
'search' : {
icon : undefined ,
content : [
'filter' ,
'search'
] ,
onOpen : ( ) => {
APP . searchAutoRefresh = true ;
setTimeout ( ( ) => {
$ ( '.cp-support-search-input' ) . focus ( ) ;
} ) ;
}
} ,
2024-02-22 01:52:12 +08:00
'settings' : {
icon : undefined ,
content : [
2024-03-01 23:34:27 +08:00
'notifications' ,
'recorded'
2024-03-06 22:29:28 +08:00
] ,
onOpen : ( ) => {
setTimeout ( ( ) => {
$ ( '.cp-support-recorded-id' ) . focus ( ) ;
} ) ;
}
2024-02-22 01:52:12 +08:00
} ,
2024-02-29 00:25:32 +08:00
'ticket' : {
icon : undefined ,
content : [
'open-ticket'
2024-03-01 23:34:27 +08:00
] ,
onOpen : ( ) => {
APP . openTicketCategory . fire ( ) ;
2024-03-06 22:29:28 +08:00
setTimeout ( ( ) => {
$ ( '.cp-support-newticket-paste' ) . focus ( ) ;
} ) ;
2024-03-01 23:34:27 +08:00
}
2024-02-29 00:25:32 +08:00
} ,
2024-02-29 01:37:15 +08:00
'legacy' : {
icon : undefined ,
content : [
'legacy'
]
} ,
2024-02-22 00:31:07 +08:00
'refresh' : {
icon : undefined ,
2024-03-01 23:34:27 +08:00
onClick : ( ) => { refreshAll ( ) ; }
2024-02-22 00:31:07 +08:00
}
} ;
2024-03-01 23:34:27 +08:00
if ( ! APP . privateKey ) { delete categories . legacy ; }
2024-02-22 00:31:07 +08:00
sidebar . addCheckboxItem ( {
key : 'privacy' ,
getState : ( ) => false ,
query : ( val , setState ) => {
APP . support . setAnonymous ( val ) ;
setState ( val ) ;
}
} ) ;
sidebar . addItem ( 'active-list' , cb => {
2024-02-28 00:17:45 +08:00
activeContainer = h ( 'div.cp-support-container' ) ; // XXX block
cb ( activeContainer ) ;
2024-03-01 18:15:29 +08:00
} , { noTitle : true , noHint : true } ) ;
2024-02-22 00:31:07 +08:00
sidebar . addItem ( 'pending-list' , cb => {
2024-02-28 00:17:45 +08:00
pendingContainer = h ( 'div.cp-support-container' ) ;
cb ( pendingContainer ) ;
2024-03-01 18:15:29 +08:00
} , { noTitle : true , noHint : true } ) ;
2024-02-22 00:31:07 +08:00
sidebar . addItem ( 'closed-list' , cb => {
2024-02-28 00:17:45 +08:00
closedContainer = h ( 'div.cp-support-container' ) ;
cb ( closedContainer ) ;
2024-02-22 00:31:07 +08:00
} , { noTitle : true , noHint : true } ) ;
2024-02-21 00:50:41 +08:00
refreshAll ( ) ;
2024-02-22 00:31:07 +08:00
2024-02-22 01:52:12 +08:00
sidebar . addCheckboxItem ( {
key : 'notifications' ,
getState : ( ) => APP . disableSupportNotif ,
query : ( val , setState ) => {
common . setAttribute ( [ 'general' , 'disableSupportNotif' ] , val , function ( err ) {
if ( err ) { val = APP . disableSupportNotif ; }
APP . disableSupportNotif = val ;
setState ( val ) ;
} ) ;
}
} ) ;
2024-03-06 22:29:28 +08:00
sidebar . addItem ( 'search' , cb => {
let inputSearch = blocks . input ( { type : 'text' , class : 'cp-support-search-input' } ) ;
let button = blocks . button ( 'primary' , 'fa-search' ) ;
let inputBlock = blocks . inputButton ( inputSearch , button , { onEnterDelegate : true } ) ;
let searchBlock = blocks . labelledInput ( Messages . support _searchLabel ,
inputSearch , inputBlock ) ;
2024-03-15 23:51:02 +08:00
let list = blocks . block ( [ ] , 'cp-support-container' ) ;
let container = blocks . block ( [ searchBlock , list ] , 'cp-support-search-container' ) ;
2024-03-06 22:29:28 +08:00
let $list = $ ( list ) ;
let searchText = '' ;
APP . searchAutoRefresh = false ;
let redraw = ( _cb ) => {
let cb = _cb || function ( ) { } ;
$list . empty ( ) ;
let tags = APP . filterTags || [ ] ;
let text = searchText ;
if ( ! text . length && ! tags . length ) { return void cb ( ) ; }
APP . module . execCommand ( 'SEARCH_ADMIN' , { text , tags } , function ( obj ) {
cb ( ) ;
if ( obj && obj . error ) {
console . error ( obj && obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
$list . empty ( ) ;
let tickets = obj . tickets || { } ;
const onShow = onShowTicket ;
const onHide = function ( ticket , channel , data , done ) {
$ ( ticket ) . find ( '.cp-support-list-message' ) . remove ( ) ;
done ( ) ;
} ;
const onTag = ( ) => { } ;
onTag . readOnly = true ;
onTag . getAllTags = ( ) => [ ] ;
Object . keys ( tickets ) . sort ( sortTicket ( tickets ) ) . forEach ( id => {
let content = tickets [ id ] ;
content . tags = content . tags || [ ] ;
let catTag = Messages [ ` support_ ${ content . category } _tag ` ] ;
if ( catTag ) {
// Msg.support_active_tag.support_pending_tag.support_closed_tag
content . tags . unshift ( catTag . toUpperCase ( ) ) ;
}
var ticket = APP . support . makeTicket ( {
id ,
content ,
onTag , onShow , onHide
} ) ;
$list . append ( ticket ) ;
} ) ;
} ) ;
} ;
let $input = $ ( inputSearch ) ;
let $button = $ ( button ) ;
Util . onClickEnter ( $button , function ( ) {
$button . prop ( 'disabled' , 'disabled' ) ;
searchText = $input . val ( ) . trim ( ) ;
redraw ( ( ) => {
APP . searchAutoRefresh = true ;
$button . prop ( 'disabled' , false ) ;
} ) ;
} ) ;
events . REFRESH _FILTER . reg ( ( ) => {
if ( ! APP . searchAutoRefresh ) { return ; }
redraw ( ) ;
} ) ;
cb ( container ) ;
} , { noTitle : true , noHint : true } ) ;
2024-03-05 01:14:59 +08:00
sidebar . addItem ( 'filter' , cb => {
2024-03-16 00:00:31 +08:00
let container = blocks . block ( [ ] , 'cp-support-filter-container' ) ;
2024-03-05 01:14:59 +08:00
let $container = $ ( container ) ;
let redrawTags = ( ) => {
$container . empty ( ) ;
var existing = APP . allTags ;
var list = h ( 'div.cp-tags-list' ) ;
var reset = h ( 'button.btn.btn-cancel.cp-tags-filter-reset' , [
h ( 'i.fa.fa-times' ) ,
Messages . kanban _clearFilter
] ) ;
var hint = h ( 'span' , Messages . kanban _tags ) ;
var tags = h ( 'div.cp-tags-filter' , [
h ( 'span.cp-tags-filter-toggle' , [
hint ,
reset ,
] ) ,
list ,
] ) ;
var $reset = $ ( reset ) ;
var $list = $ ( list ) ;
var $hint = $ ( hint ) ;
var setTagFilterState = function ( bool ) {
$hint . css ( 'visibility' , bool ? 'hidden' : 'visible' ) ;
$reset . css ( 'visibility' , bool ? 'visible' : 'hidden' ) ;
} ;
var getTags = function ( ) {
return $list . find ( 'span.active' ) . map ( function ( ) {
return String ( $ ( this ) . data ( 'tag' ) ) ;
} ) . get ( ) ;
} ;
var commitTags = function ( ) {
var t = getTags ( ) ;
setTagFilterState ( t . length ) ;
APP . filterTags = t ;
events . REFRESH _FILTER . fire ( ) ;
} ;
APP . filterTags = ( APP . filterTags || [ ] ) . filter ( tag => {
return existing . includes ( tag ) ;
} ) ;
var redrawList = function ( allTags ) {
if ( ! Array . isArray ( allTags ) ) { return ; }
$list . empty ( ) ;
$list . removeClass ( 'cp-empty' ) ;
if ( ! allTags . length ) {
$list . addClass ( 'cp-empty' ) ;
$list . append ( h ( 'em' , Messages . kanban _noTags ) ) ;
return ;
}
allTags . forEach ( function ( t ) {
let active = APP . filterTags . includes ( t ) ? '.active' : '' ;
var $tag = $ ( h ( 'span' + active , { 'data-tag' : t } , t ) ) . appendTo ( $list ) ;
Util . onClickEnter ( $tag , function ( ) {
$tag . toggleClass ( 'active' ) ;
commitTags ( ) ;
} ) ;
} ) ;
} ;
redrawList ( existing ) ;
commitTags ( ) ;
Util . onClickEnter ( $reset , function ( ) {
$list . find ( 'span' ) . removeClass ( 'active' ) ;
commitTags ( ) ;
} ) ;
$container . append ( tags ) ;
} ;
events . REFRESH _TAGS . reg ( redrawTags ) ;
cb ( container ) ;
} , { noTitle : true , noHint : true } ) ;
2024-03-01 23:34:27 +08:00
sidebar . addItem ( 'recorded' , cb => {
2024-03-15 23:51:02 +08:00
let empty = blocks . inline ( Messages . support _recordedEmpty ) ;
2024-03-16 00:00:31 +08:00
let list = blocks . table ( [
2024-03-01 23:34:27 +08:00
Messages . support _recordedId ,
Messages . support _recordedContent ,
Messages . kanban _delete
] , [ ] ) ;
let labelledList = blocks . labelledInput ( Messages . support _recordedList , list ) ;
2024-03-06 22:29:28 +08:00
let inputId = blocks . input ( { type : 'text' , class : 'cp-support-recorded-id' ,
maxlength : 20 } ) ;
2024-03-15 23:51:02 +08:00
let inputContent = blocks . textarea ( ) ;
2024-03-01 23:34:27 +08:00
let labelId = blocks . labelledInput ( Messages . support _recordedId , inputId ) ;
let labelContent = blocks . labelledInput ( Messages . support _recordedContent , inputContent ) ;
let create = blocks . button ( 'primary' , 'fa-plus' , Messages . tag _add ) ;
let nav = blocks . nav ( [ create ] ) ;
let form = blocks . form ( [
empty ,
labelledList ,
labelId ,
labelContent ,
] , nav ) ;
let $empty = $ ( empty ) ;
let $list = $ ( labelledList ) . hide ( ) ;
let $create = $ ( create ) ;
let $inputId = $ ( inputId ) . on ( 'input' , ( ) => {
let val = $inputId . val ( ) . toLowerCase ( ) . replace ( / /g , '-' ) . replace ( /[^a-z-_]/g , '' ) ;
$inputId . val ( val ) ;
} ) ;
let refresh = function ( ) { } ;
let edit = ( id , content , remove ) => {
APP . module . execCommand ( 'SET_RECORDED' , { id , content , remove } , function ( obj ) {
$create . removeAttr ( 'disabled' ) ;
if ( obj && obj . error ) {
console . error ( obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
$ ( inputId ) . val ( '' ) ;
$ ( inputContent ) . val ( '' ) ;
events . RECORDED _CHANGE . fire ( ) ;
} ) ;
} ;
refresh = ( ) => {
APP . module . execCommand ( 'GET_RECORDED' , { } , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
let lines = [ ] ;
let messages = obj . messages ;
Object . keys ( messages ) . forEach ( id => {
let del = blocks . button ( 'danger-alt' , 'fa-trash-o' , Messages . kanban _delete ) ;
Util . onClickEnter ( $ ( del ) , ( ) => {
edit ( id , '' , true ) ;
} ) ;
lines . push ( [ id , h ( 'pre' , messages [ id ] . content ) , del ] ) ;
} ) ;
list . updateContent ( lines ) ;
if ( ! lines . length ) {
$list . hide ( ) ;
$empty . show ( ) ;
return ;
}
$list . show ( ) ;
$empty . hide ( ) ;
} ) ;
} ;
Util . onClickEnter ( $create , function ( ) {
$create . attr ( 'disabled' , 'disabled' ) ;
let id = $ ( inputId ) . val ( ) . trim ( ) ;
let content = $ ( inputContent ) . val ( ) . trim ( ) ;
edit ( id , content , false ) ;
} ) ;
events . RECORDED _CHANGE . reg ( refresh ) ;
refresh ( ) ;
cb ( form ) ;
} ) ;
2024-02-29 00:25:32 +08:00
sidebar . addItem ( 'open-ticket' , cb => {
2024-03-01 23:34:27 +08:00
let form = APP . support . makeForm ( { } ) ;
let updateRecorded = ( ) => {
APP . module . execCommand ( 'GET_RECORDED' , { } , function ( obj ) {
if ( obj && obj . error ) { return ; }
form . updateRecorded ( {
all : obj . messages ,
onClick : id => {
APP . module . execCommand ( 'USE_RECORDED' , { id } , ( ) => { } ) ;
}
} ) ;
} ) ;
} ;
events . RECORDED _CHANGE . reg ( updateRecorded ) ;
APP . openTicketCategory . reg ( updateRecorded ) ;
2024-02-29 00:25:32 +08:00
let inputName = blocks . input ( { type : 'text' , readonly : true } ) ;
let inputChan = blocks . input ( { type : 'text' , readonly : true } ) ;
let inputKey = blocks . input ( { type : 'text' , readonly : true } ) ;
let labelName = blocks . labelledInput ( Messages . login _username , inputName ) ;
let labelChan = blocks . labelledInput ( Messages . support _userChannel , inputChan ) ;
let labelKey = blocks . labelledInput ( Messages . support _userKey , inputKey ) ;
let send = blocks . button ( 'primary' , 'fa-paper-plane' , Messages . support _formButton ) ;
let nav = blocks . nav ( [ send ] ) ;
2024-03-01 19:21:30 +08:00
let reset = blocks . button ( 'danger-alt' , 'fa-times' , Messages . form _reset ) ;
2024-03-15 23:51:02 +08:00
let paste = blocks . textarea ( {
2024-03-06 22:29:28 +08:00
class : 'cp-support-newticket-paste' ,
2024-02-29 00:25:32 +08:00
placeholder : Messages . support _pasteUserData
} ) ;
let inputs = h ( 'div.cp-moderation-userdata-inputs' , [ labelName , labelChan , labelKey ] ) ;
2024-03-01 19:21:30 +08:00
let userData = h ( 'div.cp-moderation-userdata' , [ inputs , paste , reset ] ) ;
2024-02-29 00:25:32 +08:00
2024-03-01 19:21:30 +08:00
let $reset = $ ( reset ) . hide ( ) ;
2024-02-29 00:25:32 +08:00
let $paste = $ ( paste ) . on ( 'input' , ( ) => {
let text = $paste . val ( ) . trim ( ) ;
let parsed = Util . tryParse ( text ) ;
$paste . val ( '' ) ;
if ( ! parsed || ! parsed . name || ! parsed . notifications || ! parsed . curvePublic ) {
return void UI . warn ( Messages . error ) ;
}
$ ( inputName ) . val ( parsed . name ) ;
$ ( inputChan ) . val ( parsed . notifications ) ;
$ ( inputKey ) . val ( parsed . curvePublic ) ;
$paste . hide ( ) ;
2024-03-01 19:21:30 +08:00
$reset . show ( ) ;
} ) ;
Util . onClickEnter ( $reset , ( ) => {
$ ( inputName ) . val ( '' ) ;
$ ( inputChan ) . val ( '' ) ;
$ ( inputKey ) . val ( '' ) ;
$reset . hide ( ) ;
$paste . show ( ) ;
2024-03-05 01:14:59 +08:00
setTimeout ( ( ) => { $paste . focus ( ) ; } ) ;
2024-02-29 00:25:32 +08:00
} ) ;
[ inputName , inputChan , inputKey ] . forEach ( input => {
$ ( input ) . on ( 'input' , ( ) => { $paste . show ( ) ; } ) ;
} ) ;
let $send = $ ( send ) ;
Util . onClickEnter ( $send , function ( ) {
let name = $ ( inputName ) . val ( ) . trim ( ) ;
let chan = $ ( inputChan ) . val ( ) . trim ( ) ;
let key = $ ( inputKey ) . val ( ) . trim ( ) ;
let data = APP . support . getFormData ( form ) ;
if ( ! name ) { return void UI . warn ( Messages . login _invalUser ) ; }
if ( ! Hash . isValidChannel ( chan ) ) { return void UI . warn ( Messages . support _invalChan ) ; }
if ( key . length !== 44 ) { return void UI . warn ( Messages . admin _invalKey ) ; }
$send . attr ( 'disabled' , 'disabled' ) ;
APP . module . execCommand ( 'MAKE_TICKET_ADMIN' , {
name : name ,
notifications : chan ,
curvePublic : key ,
channel : Hash . createChannelId ( ) ,
title : data . title ,
ticket : data
} , function ( obj ) {
if ( obj && obj . error ) {
console . error ( obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
refreshAll ( ) ;
sidebar . openCategory ( 'open' ) ;
} ) ;
} ) ;
let div = blocks . form ( [ userData , form ] , nav ) ;
cb ( div ) ;
} ) ;
2024-02-29 01:37:15 +08:00
sidebar . addItem ( 'legacy' , cb => {
if ( ! APP . privateKey ) { return void cb ( false ) ; }
let start = blocks . button ( 'primary' , 'fa-paper-plane' , Messages . support _legacyButton ) ;
2024-03-01 01:36:17 +08:00
let dump = blocks . button ( 'secondary' , 'fa-database' , Messages . support _legacyDump ) ;
2024-02-29 01:37:15 +08:00
let clean = blocks . button ( 'danger' , 'fa-trash-o' , Messages . support _legacyClear ) ;
let content = h ( 'div.cp-support-container' ) ;
2024-03-01 01:36:17 +08:00
let nav = blocks . nav ( [ start , dump , clean ] ) ;
let sortLegacyTickets = contentByHash => {
let all = { } ;
Object . keys ( contentByHash ) . forEach ( key => {
let data = contentByHash [ key ] ;
let content = data . content ;
let id = content . id ;
content . hash = key ;
if ( data . ctime ) { content . time = data . ctime ; }
if ( content . sender && content . sender . curvePublic !== data . author ) { return ; }
all [ id ] = all [ id ] || [ ] ;
all [ id ] . push ( content ) ;
all [ id ] . sort ( ( c1 , c2 ) => {
return c1 . time - c2 . time ;
} ) ;
} ) ;
// sort
let sorted = Object . keys ( all ) . sort ( ( t1 , t2 ) => {
let a = t1 [ 0 ] ;
let b = t2 [ 0 ] ;
return ( a . time || 0 ) - ( b . time || 0 ) ;
} ) ;
return sorted . map ( id => {
return all [ id ] ;
} ) ;
} ;
UI . confirmButton ( dump , { classes : 'btn-secondary' } , function ( ) {
APP . module . execCommand ( 'DUMP_LEGACY' , { } , contentByHash => {
// group by ticket id
let sorted = sortLegacyTickets ( contentByHash ) ;
let dump = '' ;
sorted . forEach ( ( t , i ) => {
if ( ! Array . isArray ( t ) || ! t . length ) { return ; }
let first = t [ 0 ] ;
if ( i ) { dump += '\n\n' ; }
dump += ` ================================
=== === === === === === === === === === ==
ID : # $ { first . id }
Title : $ { first . title }
User : $ { first . sender . name }
Date : $ { new Date ( first . time ) . toISOString ( ) } ` ;
t . forEach ( msg => {
if ( ! msg . message ) {
dump += `
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
CLOSED : $ { new Date ( msg . time ) . toISOString ( ) } ` ;
return ;
}
dump += `
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
From : $ { msg . sender . name }
Date : $ { new Date ( msg . time ) . toISOString ( ) }
-- -
$ { msg . message }
-- -
Attachments : $ { JSON . stringify ( msg . attachments , 0 , 2 ) } ` ;
} ) ;
} ) ;
saveAs ( new Blob ( [ dump ] , { type : 'text/plain' } ) , "cryptpad-support-dump.txt" ) ;
} ) ;
} ) ;
2024-02-29 01:37:15 +08:00
UI . confirmButton ( clean , { classes : 'btn-danger' } , function ( ) {
2024-03-01 01:36:17 +08:00
APP . module . execCommand ( 'CLEAR_LEGACY' , { } , ( ) => {
delete APP . privateKey ;
sidebar . deleteCategory ( 'legacy' ) ;
sidebar . openCategory ( 'open' ) ;
} ) ;
2024-02-29 01:37:15 +08:00
} ) ;
let run = ( ) => {
let $div = $ ( content ) ;
$div . empty ( ) ;
2024-03-01 01:36:17 +08:00
APP . module . execCommand ( 'GET_LEGACY' , { } , contentByHash => {
// group by ticket id
let sorted = sortLegacyTickets ( contentByHash ) ;
sorted . forEach ( ticket => {
if ( ! Array . isArray ( ticket ) || ! ticket . length ) { return ; }
ticket . forEach ( content => {
var id = content . id ;
var $ticket = $div . find ( '.cp-support-list-ticket[data-id="' + id + '"]' ) ;
if ( ! content . message ) {
// A ticket has been closed by the admins...
if ( ! $ticket . length ) { return ; }
$ticket . hide ( ) ;
$ticket . append ( APP . support . makeCloseMessage ( content ) ) ;
return ;
}
$ticket . show ( ) ;
const onMove = function ( ) {
let hashes = [ ] ;
let messages = [ ] ;
ticket . forEach ( content => {
hashes . push ( content . hash ) ;
let clone = Util . clone ( content ) ;
delete clone . hash ;
messages . push ( clone ) ;
} ) ;
APP . module . execCommand ( 'RESTORE_LEGACY' , {
messages , hashes
} , obj => {
if ( obj && obj . error ) {
console . error ( obj . error ) ;
return void UI . warn ( Messages . error ) ;
}
} ) ;
} ;
if ( ! $ticket . length ) {
2024-03-06 22:39:22 +08:00
content . category = 'legacy' ; // Hide invalid features
2024-03-01 01:36:17 +08:00
$ticket = APP . support . makeTicket ( { id , content , onMove } ) ;
$div . append ( $ticket ) ;
}
$ticket . append ( APP . support . makeMessage ( content ) ) ;
} ) ;
} ) ;
2024-02-29 01:37:15 +08:00
} ) ;
} ;
Util . onClickEnter ( $ ( start ) , run ) ;
2024-03-01 01:36:17 +08:00
2024-02-29 01:37:15 +08:00
let div = blocks . form ( [ content ] , nav ) ;
cb ( div ) ;
} ) ;
2024-02-22 00:31:07 +08:00
sidebar . makeLeftside ( categories ) ;
2024-02-12 21:30:33 +08:00
} ;
var createToolbar = function ( ) {
var displayed = [ 'useradmin' , 'newpad' , 'limit' , 'pageTitle' , 'notifications' ] ;
var configTb = {
displayed : displayed ,
sfCommon : common ,
$container : APP . $toolbar ,
pageTitle : Messages . supportPage ,
metadataMgr : common . getMetadataMgr ( ) ,
} ;
APP . toolbar = Toolbar . create ( configTb ) ;
APP . toolbar . $rightside . hide ( ) ;
} ;
nThen ( function ( waitFor ) {
$ ( waitFor ( UI . addLoadingScreen ) ) ;
SFCommon . create ( waitFor ( function ( c ) { APP . common = common = c ; } ) ) ;
} ) . nThen ( function ( waitFor ) {
2024-02-22 00:31:07 +08:00
APP . $container = $ ( '#cp-sidebarlayout-container' ) ;
2024-02-12 21:30:33 +08:00
APP . $toolbar = $ ( '#cp-toolbar' ) ;
2024-02-22 00:31:07 +08:00
sframeChan = common . getSframeChannel ( ) ;
sframeChan . onReady ( waitFor ( ) ) ;
2024-02-22 01:52:12 +08:00
} ) . nThen ( function ( waitFor ) {
common . getAttribute ( [ 'general' , 'disableSupportNotif' ] , waitFor ( function ( err , value ) {
APP . disableSupportNotif = ! ! value ;
} ) ) ;
2024-02-12 21:30:33 +08:00
} ) . nThen ( function ( /*waitFor*/ ) {
createToolbar ( ) ;
var metadataMgr = common . getMetadataMgr ( ) ;
var privateData = metadataMgr . getPrivateData ( ) ;
common . setTabTitle ( Messages . supportPage ) ;
2024-02-27 23:52:30 +08:00
if ( ! ApiConfig . supportMailboxKey ) {
return void UI . errorLoadingScreen ( Messages . support _disabledTitle ) ;
}
2024-02-12 21:30:33 +08:00
APP . privateKey = privateData . supportPrivateKey ;
APP . origin = privateData . origin ;
APP . readOnly = privateData . readOnly ;
2024-02-20 21:00:09 +08:00
APP . module = common . makeUniversal ( 'support' , {
onEvent : ( obj ) => {
let cmd = obj . ev ;
let data = obj . data ;
if ( ! events [ cmd ] ) { return ; }
events [ cmd ] . fire ( data ) ;
}
} ) ;
2024-02-12 21:30:33 +08:00
APP . support = Support . create ( common , true ) ;
2024-02-20 21:00:09 +08:00
let active = privateData . category || 'active' ;
let linkedTicket ;
if ( active . indexOf ( '-' ) !== - 1 ) {
linkedTicket = active . split ( '-' ) [ 1 ] ;
active = active . split ( '-' ) [ 0 ] ;
}
2024-02-22 00:31:07 +08:00
andThen ( common , APP . $container , linkedTicket ) ;
2024-02-12 21:30:33 +08:00
UI . removeLoadingScreen ( ) ;
} ) ;
} ) ;