2017-08-18 12:16:56 +00:00
'use strict' ;
2020-04-04 01:25:59 +00:00
var formVars = { } ;
var alertMapping = { } ;
var alertList = [ ] ;
var alertCount = 0 ;
var moreTopicCount = 0 ;
var conn = false ;
var selectedTopics = [ ] ;
2020-04-10 21:48:30 +00:00
var attachItemCallback = ( ) => { }
var quoteItemCallback = ( ) => { }
2020-04-04 01:25:59 +00:00
var baseTitle = document . title ;
var wsBackoff = 0 ;
var noAlerts = false ;
2017-10-30 09:57:08 +00:00
2018-01-14 12:03:20 +00:00
// Topic move
2020-04-04 01:25:59 +00:00
var forumToMoveTo = 0 ;
2018-01-14 12:03:20 +00:00
2019-04-27 06:32:26 +00:00
function pushNotice ( msg ) {
let aBox = document . getElementsByClassName ( "alertbox" ) [ 0 ] ;
2020-04-04 01:25:59 +00:00
let n = document . createElement ( 'div' ) ;
n . innerHTML = Tmpl _notice ( msg ) . trim ( ) ;
aBox . appendChild ( n ) ;
2019-04-27 06:32:26 +00:00
runInitHook ( "after_notice" ) ;
}
2017-10-30 09:57:08 +00:00
// TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts
2020-05-08 23:15:44 +00:00
function ajaxError ( xhr , status , e ) {
2020-07-23 04:02:16 +00:00
log ( "The AJAX request failed" ) ;
log ( "xhr" , xhr ) ;
log ( "status" , status ) ;
log ( "e" , e ) ;
if ( status == "parsererror" ) log ( "The server didn't respond with a valid JSON response" ) ;
2017-10-30 09:57:08 +00:00
console . trace ( ) ;
}
2017-01-31 05:13:38 +00:00
2020-03-06 23:00:35 +00:00
function postLink ( ev ) {
ev . preventDefault ( ) ;
let formAction = $ ( ev . target ) . closest ( 'a' ) . attr ( "href" ) ;
2020-04-10 21:48:30 +00:00
$ . ajax ( { url : formAction , type : "POST" , dataType : "json" , error : ajaxError , data : { js : 1 } } ) ;
2016-12-02 07:38:54 +00:00
}
2017-10-30 09:57:08 +00:00
function bindToAlerts ( ) {
2020-07-23 04:02:16 +00:00
log ( "bindToAlerts" ) ;
2019-03-11 08:47:45 +00:00
$ ( ".alertItem.withAvatar a" ) . unbind ( "click" ) ;
2020-03-06 23:00:35 +00:00
$ ( ".alertItem.withAvatar a" ) . click ( function ( ev ) {
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
2019-03-16 11:31:10 +00:00
$ . ajax ( {
2020-03-09 05:16:44 +00:00
url : "/api/?a=set&m=dismiss-alert" ,
2019-03-16 11:31:10 +00:00
type : "POST" ,
dataType : "json" ,
2019-05-07 02:33:33 +00:00
data : { id : $ ( this ) . attr ( "data-asid" ) } ,
2019-05-11 23:07:24 +00:00
//error: ajaxError,
2019-03-16 11:31:10 +00:00
success : ( ) => {
window . location . href = this . getAttribute ( "href" ) ;
}
} ) ;
2017-08-18 12:16:56 +00:00
} ) ;
}
2020-04-04 01:25:59 +00:00
function addAlert ( msg , notice = false ) {
2018-09-08 12:47:37 +00:00
var mmsg = msg . msg ;
2019-05-07 02:33:33 +00:00
if ( mmsg [ 0 ] == "." ) mmsg = phraseBox [ "alerts" ] [ "alerts" + mmsg ] ;
2018-09-08 12:47:37 +00:00
if ( "sub" in msg ) {
2020-04-10 21:48:30 +00:00
for ( var i = 0 ; i < msg . sub . length ; i ++ ) mmsg = mmsg . replace ( "\{" + i + "\}" , msg . sub [ i ] ) ;
2018-09-08 12:47:37 +00:00
}
2020-03-22 23:14:08 +00:00
let aItem = Tmpl _alert ( {
2019-05-07 02:33:33 +00:00
ASID : msg . id ,
2018-09-08 12:47:37 +00:00
Path : msg . path ,
2020-03-12 03:29:19 +00:00
Avatar : msg . img || "" ,
2018-09-08 12:47:37 +00:00
Message : mmsg
} )
2019-05-07 02:33:33 +00:00
//alertMapping[msg.id] = aItem;
2019-03-16 11:31:10 +00:00
let div = document . createElement ( 'div' ) ;
div . innerHTML = aItem . trim ( ) ;
2019-05-07 02:33:33 +00:00
alertMapping [ msg . id ] = div . firstChild ;
alertList . push ( msg . id ) ;
2018-09-08 12:47:37 +00:00
if ( notice ) {
// TODO: Add some sort of notification queue to avoid flooding the end-user with notices?
// TODO: Use the site name instead of "Something Happened"
2020-04-04 01:25:59 +00:00
if ( Notification . permission === "granted" ) {
2018-09-08 12:47:37 +00:00
var n = new Notification ( "Something Happened" , {
body : mmsg ,
2020-03-12 03:29:19 +00:00
icon : msg . img ,
2018-09-08 12:47:37 +00:00
} ) ;
2020-04-04 01:25:59 +00:00
setTimeout ( n . close . bind ( n ) , 8000 ) ;
2018-09-08 12:47:37 +00:00
}
}
runInitHook ( "after_add_alert" ) ;
}
2018-05-14 08:56:56 +00:00
2018-09-08 12:47:37 +00:00
function updateAlertList ( menuAlerts ) {
2020-07-23 04:02:16 +00:00
log ( "enter updateAlertList" ) ;
log ( "alertList:" , alertList ) ;
log ( "alertMapping:" , alertMapping ) ;
log ( "alertCount:" , alertCount ) ;
2018-09-08 12:47:37 +00:00
let alertListNode = menuAlerts . getElementsByClassName ( "alertList" ) [ 0 ] ;
let alertCounterNode = menuAlerts . getElementsByClassName ( "alert_counter" ) [ 0 ] ;
2017-08-17 11:13:49 +00:00
alertCounterNode . textContent = "0" ;
2018-09-08 12:47:37 +00:00
2019-03-16 11:31:10 +00:00
alertListNode . innerHTML = "" ;
let any = false ;
let j = 0 ;
2020-04-04 01:25:59 +00:00
for ( var i = 0 ; i < alertList . length && j < 8 ; i ++ ) {
2019-03-16 11:31:10 +00:00
any = true ;
alertListNode . appendChild ( alertMapping [ alertList [ i ] ] ) ;
//outList += alertMapping[alertList[i]];
j ++ ;
2018-09-08 12:47:37 +00:00
}
2019-03-16 11:31:10 +00:00
if ( ! any ) alertListNode . innerHTML = "<div class='alertItem'>" + phraseBox [ "alerts" ] [ "alerts.no_alerts" ] + "</div>" ;
2018-09-08 12:47:37 +00:00
2020-04-04 01:25:59 +00:00
if ( alertCount != 0 ) {
2018-09-08 12:47:37 +00:00
alertCounterNode . textContent = alertCount ;
menuAlerts . classList . add ( "has_alerts" ) ;
2019-03-16 11:31:10 +00:00
let nTitle = "(" + alertCount + ") " + baseTitle ;
if ( document . title != nTitle ) document . title = nTitle ;
2018-09-08 12:47:37 +00:00
} else {
menuAlerts . classList . remove ( "has_alerts" ) ;
2019-03-16 11:31:10 +00:00
if ( document . title != baseTitle ) document . title = baseTitle ;
2018-09-08 12:47:37 +00:00
}
bindToAlerts ( ) ;
2020-07-23 04:02:16 +00:00
log ( "alertCount" , alertCount )
2020-04-04 01:25:59 +00:00
runInitHook ( "after_update_alert_list" , alertCount ) ;
2018-09-08 12:47:37 +00:00
}
function setAlertError ( menuAlerts , msg ) {
2020-03-31 12:33:40 +00:00
let n = menuAlerts . getElementsByClassName ( "alertList" ) [ 0 ] ;
n . innerHTML = "<div class='alertItem'>" + msg + "</div>" ;
2018-09-08 12:47:37 +00:00
}
var alertsInitted = false ;
2019-05-11 23:07:24 +00:00
var lastTc = 0 ;
2020-03-31 12:33:40 +00:00
function loadAlerts ( menuAlerts , eTc = false ) {
2018-09-08 12:47:37 +00:00
if ( ! alertsInitted ) return ;
2019-05-11 23:07:24 +00:00
let tc = "" ;
2020-04-04 01:25:59 +00:00
if ( eTc && lastTc != 0 ) tc = "&t=" + lastTc + "&c=" + alertCount ;
2017-03-03 16:28:49 +00:00
$ . ajax ( {
2020-03-11 03:26:33 +00:00
type : 'get' ,
dataType : 'json' ,
2020-04-04 01:25:59 +00:00
url : '/api/?m=alerts' + tc ,
2020-03-12 03:29:19 +00:00
success : data => {
2017-11-06 16:24:45 +00:00
if ( "errmsg" in data ) {
2018-09-08 12:47:37 +00:00
setAlertError ( menuAlerts , data . errmsg )
2017-11-06 16:24:45 +00:00
return ;
}
2020-04-04 01:25:59 +00:00
if ( ! eTc ) {
alertList = [ ] ;
alertMapping = { } ;
}
2019-05-11 23:07:24 +00:00
if ( ! data . hasOwnProperty ( "msgs" ) ) data = { "msgs" : [ ] , "count" : alertCount , "tc" : lastTc } ;
/ * e l s e i f ( d a t a . c o u n t ! = ( a l e r t C o u n t + d a t a . m s g s . l e n g t h ) ) t c = f a l s e ;
2020-04-04 01:25:59 +00:00
if ( eTc && lastTc != 0 ) {
2019-05-11 23:07:24 +00:00
for ( var i in data . msgs ) wsAlertEvent ( data . msgs [ i ] ) ;
} else { * /
2020-07-23 04:02:16 +00:00
log ( "data" , data ) ;
2020-04-04 01:25:59 +00:00
for ( var i in data . msgs ) addAlert ( data . msgs [ i ] ) ;
alertCount = data . count ;
updateAlertList ( menuAlerts ) ;
2020-04-04 02:51:31 +00:00
try {
localStorage . setItem ( "alertList" , JSON . stringify ( alertList ) ) ;
localStorage . setItem ( "alertMapping" , JSON . stringify ( alertMapping ) ) ;
localStorage . setItem ( "alertCount" , alertCount ) ;
} catch ( e ) {
localStorage . clear ( ) ;
}
2019-05-11 23:07:24 +00:00
//}
lastTc = data . tc ;
2017-11-06 16:24:45 +00:00
} ,
2020-04-04 01:25:59 +00:00
error : ( magic , status , er ) => {
2018-09-19 06:09:03 +00:00
let errtxt = "Unable to get the alerts" ;
2017-11-06 16:24:45 +00:00
try {
2020-04-04 01:25:59 +00:00
let dat = JSON . parse ( magic . responseText ) ;
if ( "errmsg" in dat ) errtxt = dat . errmsg ;
} catch ( e ) {
2020-07-23 04:02:16 +00:00
log ( magic . responseText ) ;
log ( e ) ;
2017-11-06 16:24:45 +00:00
}
2020-07-23 04:02:16 +00:00
log ( "er" , er ) ;
2018-09-08 12:47:37 +00:00
setAlertError ( menuAlerts , errtxt ) ;
2017-11-06 16:24:45 +00:00
}
} ) ;
2017-03-03 16:28:49 +00:00
}
2017-08-15 13:47:56 +00:00
function SplitN ( data , ch , n ) {
2020-04-04 01:25:59 +00:00
var o = [ ] ;
if ( data . length === 0 ) return o ;
2017-08-15 13:47:56 +00:00
2020-03-06 23:00:35 +00:00
var lastI = 0 ;
2017-08-15 13:47:56 +00:00
var j = 0 ;
var lastN = 1 ;
2020-04-04 01:25:59 +00:00
for ( let i = 0 ; i < data . length ; i ++ ) {
2020-04-10 21:48:30 +00:00
if ( data [ i ] === ch ) {
2020-04-04 01:25:59 +00:00
o [ j ++ ] = data . substring ( lastI , i ) ;
2020-03-06 23:00:35 +00:00
lastI = i ;
2020-04-10 21:48:30 +00:00
if ( lastN === n ) break ;
2017-08-15 13:47:56 +00:00
lastN ++ ;
2017-05-11 13:04:43 +00:00
}
}
2020-04-10 21:48:30 +00:00
if ( data . length > lastI ) o [ o . length - 1 ] += data . substring ( lastI ) ;
2020-04-04 01:25:59 +00:00
return o ;
2017-08-15 13:47:56 +00:00
}
2017-05-29 14:52:37 +00:00
2020-04-04 01:25:59 +00:00
function wsAlertEvent ( dat ) {
2020-07-23 04:02:16 +00:00
log ( "wsAlertEvent" , dat )
2020-04-04 01:25:59 +00:00
addAlert ( dat , true ) ;
2019-03-16 11:31:10 +00:00
alertCount ++ ;
let aTmp = alertList ;
alertList = [ alertList [ alertList . length - 1 ] ] ;
aTmp = aTmp . slice ( 0 , - 1 ) ;
2020-04-04 01:25:59 +00:00
for ( let i = 0 ; i < aTmp . length ; i ++ ) alertList . push ( aTmp [ i ] ) ;
2018-06-24 13:49:29 +00:00
// TODO: Add support for other alert feeds like PM Alerts
2020-04-04 01:25:59 +00:00
let n = document . getElementById ( "general_alerts" ) ;
2018-11-30 03:02:20 +00:00
// TODO: Make sure we update alertCount here
2019-05-11 23:07:24 +00:00
lastTc = 0 ;
2020-04-04 01:25:59 +00:00
updateAlertList ( n /*,alist*/ ) ;
2018-06-24 13:49:29 +00:00
}
2020-03-12 03:29:19 +00:00
function runWebSockets ( resume = false ) {
2020-01-31 10:48:55 +00:00
let s = "" ;
if ( window . location . protocol == "https:" ) s = "s" ;
conn = new WebSocket ( "ws" + s + "://" + document . location . host + "/ws/" ) ;
2017-06-10 07:58:15 +00:00
2020-07-19 03:26:56 +00:00
conn . onerror = e => {
2020-07-23 04:02:16 +00:00
log ( e ) ;
2018-08-13 10:34:00 +00:00
}
2019-03-05 04:46:43 +00:00
// TODO: Sync alerts, topic list, etc.
2018-06-24 13:49:29 +00:00
conn . onopen = ( ) => {
2020-07-23 04:02:16 +00:00
log ( "The WebSockets connection was opened" ) ;
2019-05-09 06:58:55 +00:00
if ( resume ) conn . send ( "resume " + document . location . pathname + " " + Math . round ( ( new Date ( ) ) . getTime ( ) / 1000 ) + '\r' ) ;
else conn . send ( "page " + document . location . pathname + '\r' ) ;
2017-11-06 16:24:45 +00:00
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
2019-03-16 11:31:10 +00:00
if ( me . User . ID > 0 ) Notification . requestPermission ( ) ;
2017-11-06 16:24:45 +00:00
}
2018-08-13 10:34:00 +00:00
2018-06-24 13:49:29 +00:00
conn . onclose = ( ) => {
2017-11-06 16:24:45 +00:00
conn = false ;
2020-07-23 04:02:16 +00:00
log ( "The WebSockets connection was closed" ) ;
2019-03-21 22:59:41 +00:00
let backoff = 0.8 ;
2019-03-16 23:14:47 +00:00
if ( wsBackoff < 0 ) wsBackoff = 0 ;
2019-03-21 22:59:41 +00:00
else if ( wsBackoff > 12 ) backoff = 11 ;
else if ( wsBackoff > 5 ) backoff = 5 ;
2019-03-16 23:14:47 +00:00
wsBackoff ++ ;
2019-03-05 05:53:02 +00:00
setTimeout ( ( ) => {
2020-03-06 23:00:35 +00:00
if ( ! noAlerts ) {
2020-04-04 01:25:59 +00:00
let nl = document . getElementsByClassName ( "menu_alerts" ) ;
for ( var i = 0 ; i < nl . length ; i ++ ) loadAlerts ( nl [ i ] , true ) ;
2020-03-06 23:00:35 +00:00
}
2019-03-21 22:59:41 +00:00
runWebSockets ( true ) ;
} , backoff * 60 * 1000 ) ;
2019-03-16 23:14:47 +00:00
if ( wsBackoff > 0 ) {
2019-03-21 22:59:41 +00:00
if ( wsBackoff <= 5 ) setTimeout ( ( ) => wsBackoff -- , 5.5 * 60 * 1000 ) ;
else if ( wsBackoff <= 12 ) setTimeout ( ( ) => wsBackoff -- , 11.5 * 60 * 1000 ) ;
else setTimeout ( ( ) => wsBackoff -- , 20 * 60 * 1000 ) ;
2019-03-16 23:14:47 +00:00
}
2017-11-06 16:24:45 +00:00
}
2018-08-13 10:34:00 +00:00
2018-06-24 13:49:29 +00:00
conn . onmessage = ( event ) => {
2020-03-06 23:00:35 +00:00
if ( ! noAlerts && event . data [ 0 ] == "{" ) {
2020-07-23 04:02:16 +00:00
log ( "json message" ) ;
2018-06-24 13:49:29 +00:00
let data = "" ;
2017-11-06 16:24:45 +00:00
try {
2018-06-24 13:49:29 +00:00
data = JSON . parse ( event . data ) ;
2020-07-14 22:01:11 +00:00
} catch ( e ) {
2020-07-23 04:02:16 +00:00
log ( e ) ;
2018-06-24 13:49:29 +00:00
return ;
2017-11-06 16:24:45 +00:00
}
2017-08-18 12:16:56 +00:00
2020-04-04 01:25:59 +00:00
if ( "msg" in data ) wsAlertEvent ( data ) ;
2018-11-30 03:02:20 +00:00
else if ( "event" in data ) {
2020-04-04 01:25:59 +00:00
if ( data . event == "dismiss-alert" ) {
2018-09-08 13:50:15 +00:00
Object . keys ( alertMapping ) . forEach ( ( key ) => {
2019-05-11 23:07:24 +00:00
if ( key != data . id ) return ;
alertCount -- ;
let index = - 1 ;
2020-03-06 23:00:35 +00:00
for ( var i = 0 ; i < alertList . length ; i ++ ) {
2019-05-11 23:07:24 +00:00
if ( alertList [ i ] == key ) {
alertList [ i ] = 0 ;
index = i ;
2018-09-08 12:47:37 +00:00
}
2019-05-11 23:07:24 +00:00
}
if ( index == - 1 ) return ;
2018-09-08 13:50:15 +00:00
2020-04-04 01:25:59 +00:00
for ( var i = index ; ( i + 1 ) < alertList . length ; i ++ ) alertList [ i ] = alertList [ i + 1 ] ;
2019-05-11 23:07:24 +00:00
alertList . splice ( alertList . length - 1 , 1 ) ;
delete alertMapping [ key ] ;
// TODO: Add support for other alert feeds like PM Alerts
2020-03-12 01:08:56 +00:00
let generalAlerts = document . getElementById ( "general_alerts" ) ;
2020-04-04 01:25:59 +00:00
if ( alertList . length < 8 ) loadAlerts ( generalAlerts , true ) ;
2019-05-11 23:07:24 +00:00
else updateAlertList ( generalAlerts ) ;
2018-09-08 12:47:37 +00:00
} ) ;
}
2018-06-24 13:49:29 +00:00
} else if ( "Topics" in data ) {
2020-07-23 04:02:16 +00:00
log ( "topic in data" ) ;
log ( "data" , data ) ;
2020-07-19 03:26:56 +00:00
// TODO: Handle desyncs more gracefully?
// TODO: Send less unneccessary data?
2018-06-24 13:49:29 +00:00
let topic = data . Topics [ 0 ] ;
2020-03-22 23:14:08 +00:00
if ( topic === undefined ) {
2020-07-23 04:02:16 +00:00
log ( "empty topic list" ) ;
2018-06-24 13:49:29 +00:00
return ;
2017-06-10 07:58:15 +00:00
}
2020-07-19 03:26:56 +00:00
if ( "mod" in data ) {
topic . CanMod = data . mod == 1 || data . mod [ 0 ] == 1 ;
if ( data . lock == 1 ) {
$ ( ".val_lock" ) . each ( function ( ) {
this . classList . remove ( "auto_hide" ) ;
} ) ;
}
if ( data . move == 1 ) {
$ ( ".val_move" ) . each ( function ( ) {
this . classList . remove ( "auto_hide" ) ;
} ) ;
}
}
2018-06-30 03:40:50 +00:00
// TODO: Fix the data race where the function hasn't been loaded yet
2020-03-22 23:14:08 +00:00
let renTopic = Tmpl _topics _topic ( topic ) ;
2018-06-30 03:40:50 +00:00
$ ( ".topic_row[data-tid='" + topic . ID + "']" ) . addClass ( "ajax_topic_dupe" ) ;
2018-06-24 13:49:29 +00:00
let node = $ ( renTopic ) ;
2018-06-30 03:40:50 +00:00
node . addClass ( "new_item hide_ajax_topic" ) ;
2020-07-23 04:02:16 +00:00
log ( "Prepending to topic list" ) ;
2018-06-24 13:49:29 +00:00
$ ( ".topic_list" ) . prepend ( node ) ;
2018-06-30 03:40:50 +00:00
moreTopicCount ++ ;
2020-04-04 01:25:59 +00:00
let blocks = document . getElementsByClassName ( "more_topic_block_initial" ) ;
2020-07-19 03:26:56 +00:00
for ( let i = 0 ; i < blocks . length ; i ++ ) {
2020-04-04 01:25:59 +00:00
let block = blocks [ i ] ;
block . classList . remove ( "more_topic_block_initial" ) ;
block . classList . add ( "more_topic_block_active" ) ;
2018-06-30 03:40:50 +00:00
2020-07-23 04:02:16 +00:00
log ( "phraseBox" , phraseBox ) ;
2020-04-04 01:25:59 +00:00
let msgBox = block . getElementsByClassName ( "more_topics" ) [ 0 ] ;
2018-06-30 03:40:50 +00:00
msgBox . innerText = phraseBox [ "topic_list" ] [ "topic_list.changed_topics" ] . replace ( "%d" , moreTopicCount ) ;
}
2020-07-23 04:02:16 +00:00
} else log ( "unknown message" , data ) ;
2017-11-06 16:24:45 +00:00
}
2017-06-10 07:58:15 +00:00
2020-03-12 01:08:56 +00:00
let messages = event . data . split ( '\r' ) ;
2020-04-04 01:25:59 +00:00
for ( var i = 0 ; i < messages . length ; i ++ ) {
2018-03-31 05:25:27 +00:00
let message = messages [ i ] ;
2020-07-23 04:02:16 +00:00
//log("message",message);
2018-08-11 15:53:42 +00:00
let msgblocks = SplitN ( message , " " , 3 ) ;
if ( msgblocks . length < 3 ) continue ;
2018-03-31 05:25:27 +00:00
if ( message . startsWith ( "set " ) ) {
2019-01-21 12:27:59 +00:00
let oldInnerHTML = document . querySelector ( msgblocks [ 1 ] ) . innerHTML ;
if ( msgblocks [ 2 ] == oldInnerHTML ) continue ;
2017-11-06 16:24:45 +00:00
document . querySelector ( msgblocks [ 1 ] ) . innerHTML = msgblocks [ 2 ] ;
2018-03-31 05:25:27 +00:00
} else if ( message . startsWith ( "set-class " ) ) {
2019-01-21 12:27:59 +00:00
// Fix to stop the inspector from getting all jittery
let oldClassName = document . querySelector ( msgblocks [ 1 ] ) . className ;
if ( msgblocks [ 2 ] == oldClassName ) continue ;
2017-11-06 16:24:45 +00:00
document . querySelector ( msgblocks [ 1 ] ) . className = msgblocks [ 2 ] ;
2017-05-11 13:04:43 +00:00
}
}
}
2017-11-06 16:24:45 +00:00
}
2020-03-13 02:56:23 +00:00
// TODO: Surely, there's a prettier and more elegant way of doing this?
function getExt ( name ) {
2020-07-26 05:14:08 +00:00
if ( ! ( name . indexOf ( '.' ) > - 1 ) ) throw ( "This file doesn't have an extension" ) ;
2020-03-13 02:56:23 +00:00
return name . split ( '.' ) . pop ( ) ;
}
2018-08-11 15:53:42 +00:00
( ( ) => {
2018-08-13 10:34:00 +00:00
addInitHook ( "pre_init" , ( ) => {
2020-03-08 00:50:23 +00:00
runInitHook ( "pre_global" ) ;
2020-07-23 04:02:16 +00:00
log ( "before notify on alert" )
2018-08-13 10:34:00 +00:00
// We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :(
2020-07-23 11:13:55 +00:00
log ( "noAlerts:" , noAlerts ) ;
2020-03-06 23:00:35 +00:00
if ( ! noAlerts ) {
2020-03-22 23:14:08 +00:00
notifyOnScriptW ( "tmpl_alert" , e => {
2020-07-23 04:02:16 +00:00
if ( e != undefined ) log ( "failed alert? why?" , e )
2019-03-16 11:31:10 +00:00
} , ( ) => {
2020-04-04 02:51:31 +00:00
if ( ! Tmpl _alert ) throw ( "tmpl func not found" ) ;
2018-11-30 03:02:20 +00:00
addInitHook ( "after_phrases" , ( ) => {
// TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred
$ ( document ) . ready ( ( ) => {
2020-07-23 11:13:55 +00:00
log ( "checking local storage cache" ) ;
2018-11-30 03:02:20 +00:00
alertsInitted = true ;
2020-04-04 02:51:31 +00:00
let al = document . getElementsByClassName ( "menu_alerts" ) ;
let sAlertList = localStorage . getItem ( "alertList" ) ;
let sAlertMapping = localStorage . getItem ( "alertMapping" ) ;
let sAlertCount = localStorage . getItem ( "alertCount" ) ;
2020-04-04 03:01:34 +00:00
if ( sAlertList != null && sAlertList != "" &&
2020-07-23 04:02:16 +00:00
sAlertMapping != null && sAlertMapping != "" && sAlertCount != null && sAlertCount != "" && sAlertCount != "0"
2020-04-04 03:01:34 +00:00
) {
2020-07-23 04:02:16 +00:00
log ( "sAlertList" , sAlertList )
log ( "sAlertMapping" , sAlertMapping )
log ( "sAlertCount" , sAlertCount )
2020-04-04 02:51:31 +00:00
alertList = JSON . parse ( sAlertList )
alertMapping = JSON . parse ( sAlertMapping )
alertCount = parseInt ( sAlertCount )
2020-07-23 04:02:16 +00:00
log ( "alertList" , alertList )
log ( "alertMapping" , alertMapping )
log ( "alertCount" , alertCount )
2020-04-04 02:51:31 +00:00
for ( var i = 0 ; i < al . length ; i ++ ) loadAlerts ( al [ i ] , true ) ;
} else for ( var i = 0 ; i < al . length ; i ++ ) loadAlerts ( al [ i ] ) ;
2018-11-30 03:02:20 +00:00
if ( window [ "WebSocket" ] ) runWebSockets ( ) ;
} ) ;
2018-08-11 15:53:42 +00:00
} ) ;
2018-08-13 10:34:00 +00:00
} ) ;
2020-03-06 23:00:35 +00:00
} else {
addInitHook ( "after_phrases" , ( ) => {
$ ( document ) . ready ( ( ) => {
if ( window [ "WebSocket" ] ) runWebSockets ( ) ;
} ) ;
} ) ;
}
2018-08-11 15:53:42 +00:00
2018-08-13 10:34:00 +00:00
$ ( document ) . ready ( mainInit ) ;
} ) ;
2018-08-11 15:53:42 +00:00
} ) ( ) ;
2019-02-10 05:52:26 +00:00
// TODO: Use these in .filter_item and pass back an item count from the backend to work with here
// Ported from common/parser.go
2020-03-22 23:14:08 +00:00
function PageOffset ( count , page , perPage ) {
2019-02-10 05:52:26 +00:00
let offset = 0 ;
let lastPage = LastPage ( count , perPage )
2019-05-11 23:07:24 +00:00
if ( page > 1 ) offset = ( perPage * page ) - perPage ;
else if ( page == - 1 ) {
page = lastPage ;
offset = ( perPage * page ) - perPage ;
} else page = 1 ;
2019-02-10 05:52:26 +00:00
// We don't want the offset to overflow the slices, if everything's in memory
2019-02-24 01:29:06 +00:00
//if(offset >= (count - 1)) offset = 0;
2020-04-04 01:25:59 +00:00
return { Offset : offset , Page : page , LastPage : lastPage } ;
2019-02-10 05:52:26 +00:00
}
2020-03-22 23:14:08 +00:00
function LastPage ( count , perPage ) {
2019-02-10 05:52:26 +00:00
return ( count / perPage ) + 1
}
2020-03-22 23:14:08 +00:00
function Paginate ( currentPage , lastPage , maxPages ) {
2019-06-04 05:48:12 +00:00
let diff = lastPage - currentPage ;
let pre = 3 ;
if ( diff < 3 ) pre = maxPages - diff ;
let page = currentPage - pre ;
if ( page < 0 ) page = 0 ;
2020-04-04 01:25:59 +00:00
let o = [ ] ;
while ( o . length < maxPages && page < lastPage ) {
2019-02-10 05:52:26 +00:00
page ++ ;
2020-04-04 01:25:59 +00:00
o . push ( page ) ;
2019-02-10 05:52:26 +00:00
}
2020-04-04 01:25:59 +00:00
return o ;
2019-02-10 05:52:26 +00:00
}
2018-08-11 15:53:42 +00:00
function mainInit ( ) {
2020-07-23 11:13:55 +00:00
log ( "enter mainInit" ) ;
2018-08-13 10:34:00 +00:00
runInitHook ( "start_init" ) ;
2017-05-29 14:52:37 +00:00
2020-03-09 10:37:41 +00:00
$ ( ".more_topics" ) . click ( ev => {
2020-03-06 23:00:35 +00:00
ev . preventDefault ( ) ;
2020-04-04 01:25:59 +00:00
let blocks = document . getElementsByClassName ( "more_topic_block_active" ) ;
for ( let i = 0 ; i < blocks . length ; i ++ ) {
2020-07-19 03:26:56 +00:00
let bl = blocks [ i ] ;
bl . classList . remove ( "more_topic_block_active" ) ;
bl . classList . add ( "more_topic_block_initial" ) ;
2018-06-30 03:40:50 +00:00
}
$ ( ".ajax_topic_dupe" ) . fadeOut ( "slow" , function ( ) {
$ ( this ) . remove ( ) ;
} ) ;
$ ( ".hide_ajax_topic" ) . removeClass ( "hide_ajax_topic" ) ; // TODO: Do Fade
moreTopicCount = 0 ;
} )
2020-03-09 05:16:44 +00:00
$ ( ".add_like,.remove_like" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
2020-01-31 10:48:55 +00:00
//$(this).unbind("click");
2018-03-31 05:25:27 +00:00
let target = this . closest ( "a" ) . getAttribute ( "href" ) ;
2020-07-23 04:02:16 +00:00
log ( "target" , target ) ;
2020-01-31 10:48:55 +00:00
2018-09-08 12:47:37 +00:00
let controls = this . closest ( ".controls" ) ;
2018-03-31 05:25:27 +00:00
let hadLikes = controls . classList . contains ( "has_likes" ) ;
let likeCountNode = controls . getElementsByClassName ( "like_count" ) [ 0 ] ;
2020-07-23 04:02:16 +00:00
log ( "likeCountNode" , likeCountNode ) ;
2020-01-31 10:48:55 +00:00
if ( this . classList . contains ( "add_like" ) ) {
this . classList . remove ( "add_like" ) ;
this . classList . add ( "remove_like" ) ;
if ( ! hadLikes ) controls . classList . add ( "has_likes" ) ;
2020-04-04 01:25:59 +00:00
this . closest ( "a" ) . setAttribute ( "href" , target . replace ( "like" , "unlike" ) ) ;
2020-01-31 10:48:55 +00:00
likeCountNode . innerHTML = parseInt ( likeCountNode . innerHTML ) + 1 ;
} else {
this . classList . remove ( "remove_like" ) ;
this . classList . add ( "add_like" ) ;
let likeCount = parseInt ( likeCountNode . innerHTML ) ;
if ( likeCount == 1 ) controls . classList . remove ( "has_likes" ) ;
2020-04-04 01:25:59 +00:00
this . closest ( "a" ) . setAttribute ( "href" , target . replace ( "unlike" , "like" ) ) ;
2020-01-31 10:48:55 +00:00
likeCountNode . innerHTML = parseInt ( likeCountNode . innerHTML ) - 1 ;
}
//let likeButton = this;
2018-03-31 05:25:27 +00:00
$ . ajax ( {
2020-04-04 01:25:59 +00:00
url : target ,
type : "POST" ,
dataType : "json" ,
2019-09-30 10:15:50 +00:00
data : { js : 1 } ,
2018-03-31 05:25:27 +00:00
error : ajaxError ,
2020-04-04 01:25:59 +00:00
success : function ( dat , status , xhr ) {
if ( "success" in dat && dat [ "success" ] == "1" ) return ;
2018-03-31 05:25:27 +00:00
// addNotice("Failed to add a like: {err}")
2020-03-09 05:16:44 +00:00
//likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML)-1;
2020-07-23 04:02:16 +00:00
log ( "data" , dat ) ;
log ( "status" , status ) ;
log ( "xhr" , xhr ) ;
2018-03-31 05:25:27 +00:00
}
} ) ;
} ) ;
2020-03-09 05:16:44 +00:00
$ ( ".link_label" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
2020-04-04 01:25:59 +00:00
let linkSel = $ ( '#' + $ ( this ) . attr ( "data-for" ) ) ;
if ( ! linkSel . hasClass ( "link_opened" ) ) {
2020-03-09 05:16:44 +00:00
ev . stopPropagation ( ) ;
2020-04-04 01:25:59 +00:00
linkSel . addClass ( "link_opened" ) ;
2018-09-27 09:41:35 +00:00
}
2018-09-26 07:46:30 +00:00
} ) ;
2019-02-10 05:52:26 +00:00
function rebuildPaginator ( lastPage ) {
let urlParams = new URLSearchParams ( window . location . search ) ;
let page = urlParams . get ( 'page' ) ;
if ( page == "" ) page = 1 ;
2019-06-04 05:48:12 +00:00
let pageList = Paginate ( page , lastPage , 5 )
2020-05-08 23:15:44 +00:00
//$(".pageset").html(Tmpl_paginator({PageList:pageList,Page:page,LastPage:lastPage}));
2019-02-10 05:52:26 +00:00
let ok = false ;
$ ( ".pageset" ) . each ( function ( ) {
2020-05-08 23:15:44 +00:00
this . outerHTML = Tmpl _paginator ( { PageList : pageList , Page : page , LastPage : lastPage } ) ;
2019-02-10 05:52:26 +00:00
ok = true ;
} ) ;
2020-05-08 23:15:44 +00:00
if ( ! ok ) $ ( Tmpl _paginator ( { PageList : pageList , Page : page , LastPage : lastPage } ) ) . insertAfter ( "#topic_list" ) ;
2019-02-10 05:52:26 +00:00
}
function rebindPaginator ( ) {
2020-05-08 23:15:44 +00:00
// TODO: Take mostviewed into account
// TODO: Get this to work with topics too
2019-02-10 05:52:26 +00:00
$ ( ".pageitem a" ) . unbind ( "click" ) ;
2020-05-08 23:15:44 +00:00
$ ( ".pageitem a" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
2019-02-10 05:52:26 +00:00
let url = "//" + window . location . host + window . location . pathname ;
let urlParams = new URLSearchParams ( window . location . search ) ;
urlParams . set ( "page" , new URLSearchParams ( this . getAttribute ( "href" ) ) . get ( "page" ) ) ;
let q = "?" ;
for ( let item of urlParams . entries ( ) ) q += item [ 0 ] + "=" + item [ 1 ] + "&" ;
if ( q . length > 1 ) q = q . slice ( 0 , - 1 ) ;
// TODO: Try to de-duplicate some of these fetch calls
2020-05-08 23:15:44 +00:00
fetch ( url + q + "&js=1" , { credentials : "same-origin" } )
2020-08-20 22:18:54 +00:00
. then ( r => {
if ( ! r . ok ) throw ( url + q + "&js=1 failed to load" ) ;
return r . json ( ) ;
} ) . then ( d => {
if ( ! "Topics" in d ) throw ( "no Topics in data" ) ;
let topics = d [ "Topics" ] ;
2020-07-23 04:02:16 +00:00
log ( "ajax navigated to different page" ) ;
2019-02-10 05:52:26 +00:00
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "" ;
2020-05-08 23:15:44 +00:00
for ( let i = 0 ; i < topics . length ; i ++ ) out += Tmpl _topics _topic ( topics [ i ] ) ;
2019-02-10 05:52:26 +00:00
$ ( ".topic_list" ) . html ( out ) ;
2020-04-10 21:48:30 +00:00
let obj = { Title : document . title , Url : url + q } ;
2020-04-04 01:25:59 +00:00
history . pushState ( obj , obj . Title , obj . Url ) ;
2020-08-20 22:18:54 +00:00
rebuildPaginator ( d . LastPage ) ;
2019-02-10 05:52:26 +00:00
rebindPaginator ( ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2020-08-20 22:18:54 +00:00
log ( "Unable to get script " + url + q + "&js=1" , e ) ;
2019-02-10 05:52:26 +00:00
console . trace ( ) ;
} ) ;
} ) ;
}
// TODO: Render a headless topics.html instead of the individual topic rows and a bit of JS glue
2020-03-09 10:37:41 +00:00
$ ( ".filter_item" ) . click ( function ( ev ) {
2019-02-10 05:52:26 +00:00
if ( ! window . location . pathname . startsWith ( "/topics/" ) ) return
2020-03-09 10:37:41 +00:00
ev . preventDefault ( ) ;
2019-02-10 05:52:26 +00:00
let that = this ;
let fid = this . getAttribute ( "data-fid" ) ;
// TODO: Take mostviewed into account
let url = "//" + window . location . host + "/topics/?fids=" + fid ;
2020-08-20 22:18:54 +00:00
fetch ( url + "&js=1" , { credentials : "same-origin" } )
. then ( r => {
if ( ! r . ok ) throw ( url + "&js=1 failed to load" ) ;
return r . json ( ) ;
} ) . then ( d => {
log ( "data" , d ) ;
if ( ! "Topics" in d ) throw ( "no Topics in data" ) ;
let topics = d [ "Topics" ] ;
2020-07-23 04:02:16 +00:00
log ( "ajax navigated to " + that . innerText ) ;
2019-02-10 05:52:26 +00:00
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "" ;
2020-04-04 01:25:59 +00:00
for ( let i = 0 ; i < topics . length ; i ++ ) out += Tmpl _topics _topic ( topics [ i ] ) ;
2019-02-10 05:52:26 +00:00
$ ( ".topic_list" ) . html ( out ) ;
2019-02-28 07:28:17 +00:00
//$(".topic_list").addClass("single_forum");
2019-02-10 05:52:26 +00:00
2019-03-16 11:31:10 +00:00
baseTitle = that . innerText ;
if ( alertCount > 0 ) document . title = "(" + alertCount + ") " + baseTitle ;
else document . title = baseTitle ;
2020-04-10 21:48:30 +00:00
let obj = { Title : document . title , Url : url } ;
history . pushState ( obj , obj . Title , obj . Url ) ;
2020-08-20 22:18:54 +00:00
rebuildPaginator ( d . LastPage )
2019-02-10 05:52:26 +00:00
rebindPaginator ( ) ;
$ ( ".filter_item" ) . each ( function ( ) {
this . classList . remove ( "filter_selected" ) ;
} ) ;
that . classList . add ( "filter_selected" ) ;
$ ( ".topic_list_title h1" ) . text ( that . innerText ) ;
2020-08-20 22:18:54 +00:00
$ ( ".link_select .link_option .link_recent" ) . attr ( "href" , "//" + window . location . host + "/topics/?fids=" + fid ) ;
$ ( ".link_select .link_option .link_most_viewed" ) . attr ( "href" , "//" + window . location . host + "/topics/most-viewed/?fids=" + fid ) ;
2020-07-23 11:13:55 +00:00
unbindPage ( ) ;
bindPage ( ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2020-08-20 22:18:54 +00:00
log ( "Unable to get script " + url + "&js=1" , e ) ;
2019-02-10 05:52:26 +00:00
console . trace ( ) ;
} ) ;
} ) ;
2020-08-20 22:18:54 +00:00
if ( document . getElementById ( "topicsItemList" ) !== null ) rebindPaginator ( ) ;
if ( document . getElementById ( "forumItemList" ) !== null ) rebindPaginator ( ) ;
2019-02-10 05:52:26 +00:00
2019-02-23 06:29:19 +00:00
// TODO: Show a search button when JS is disabled?
$ ( ".widget_search_input" ) . keypress ( function ( e ) {
2020-03-11 09:28:00 +00:00
if ( e . keyCode != '13' ) return ;
2020-05-08 23:15:44 +00:00
// TODO: Only fire on /topics/
2019-02-23 06:29:19 +00:00
event . preventDefault ( ) ;
// TODO: Take mostviewed into account
let url = "//" + window . location . host + window . location . pathname ;
let urlParams = new URLSearchParams ( window . location . search ) ;
urlParams . set ( "q" , this . value ) ;
let q = "?" ;
for ( let item of urlParams . entries ( ) ) q += item [ 0 ] + "=" + item [ 1 ] + "&" ;
if ( q . length > 1 ) q = q . slice ( 0 , - 1 ) ;
// TODO: Try to de-duplicate some of these fetch calls
2020-04-04 01:25:59 +00:00
fetch ( url + q + "&js=1" , { credentials : "same-origin" } )
2020-08-20 22:18:54 +00:00
. then ( r => {
if ( ! r . ok ) throw ( url + q + "&js=1 failed to load" ) ;
return r . json ( ) ;
} ) . then ( d => {
if ( ! "Topics" in d ) throw ( "no Topics in data" ) ;
let topics = d [ "Topics" ] ;
2020-07-23 04:02:16 +00:00
log ( "ajax navigated to search page" ) ;
2019-02-23 06:29:19 +00:00
// TODO: Fix the data race where the function hasn't been loaded yet
let out = "" ;
2020-04-10 21:48:30 +00:00
for ( let i = 0 ; i < topics . length ; i ++ ) out += Tmpl _topics _topic ( topics [ i ] ) ;
2019-02-23 06:29:19 +00:00
$ ( ".topic_list" ) . html ( out ) ;
2019-03-16 11:31:10 +00:00
baseTitle = phraseBox [ "topic_list" ] [ "topic_list.search_head" ] ;
2019-02-24 01:29:06 +00:00
$ ( ".topic_list_title h1" ) . text ( phraseBox [ "topic_list" ] [ "topic_list.search_head" ] ) ;
2019-03-16 11:31:10 +00:00
if ( alertCount > 0 ) document . title = "(" + alertCount + ") " + baseTitle ;
else document . title = baseTitle ;
2019-02-23 06:29:19 +00:00
let obj = { Title : document . title , Url : url + q } ;
2020-04-10 21:48:30 +00:00
history . pushState ( obj , obj . Title , obj . Url ) ;
2020-08-20 22:18:54 +00:00
rebuildPaginator ( d . LastPage ) ;
2019-02-23 06:29:19 +00:00
rebindPaginator ( ) ;
2020-12-18 01:04:07 +00:00
$ ( ".link_select .link_option .link_recent" ) . attr ( "href" , url + q ) ;
$ ( ".link_select .link_option .link_most_viewed" ) . attr ( "href" , url + q ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2020-08-20 22:18:54 +00:00
log ( "Unable to get script " + url + q + "&js=1" , e ) ;
2019-02-23 06:29:19 +00:00
console . trace ( ) ;
} ) ;
} ) ;
2020-07-23 11:13:55 +00:00
runInitHook ( "before_init_bind_page" ) ;
2020-03-11 07:08:27 +00:00
bindPage ( ) ;
2020-07-19 03:26:56 +00:00
runInitHook ( "after_init_bind_page" ) ;
2017-05-29 14:52:37 +00:00
2020-03-09 10:37:41 +00:00
$ ( ".edit_field" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
let bp = $ ( this ) . closest ( '.editable_parent' ) ;
let block = bp . find ( '.editable_block' ) . eq ( 0 ) ;
2020-07-23 04:02:16 +00:00
block . html ( "<input name='edit_field'value='" + block . text ( ) + "'type='text'><a href='" + $ ( this ) . closest ( 'a' ) . attr ( "href" ) + "'><button class='submit_edit'type='submit'>Update</button></a>" ) ;
2017-05-29 14:52:37 +00:00
2020-03-09 10:37:41 +00:00
$ ( ".submit_edit" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
let bp = $ ( this ) . closest ( '.editable_parent' ) ;
2020-07-14 22:01:11 +00:00
let bl = bp . find ( '.editable_block' ) . eq ( 0 ) ;
let content = bl . find ( 'input' ) . eq ( 0 ) . val ( ) ;
bl . html ( content ) ;
2017-05-29 14:52:37 +00:00
2017-10-16 07:32:58 +00:00
let formAction = $ ( this ) . closest ( 'a' ) . attr ( "href" ) ;
2016-12-06 10:26:48 +00:00
$ . ajax ( {
2020-07-14 22:01:11 +00:00
url : formAction + "?s=" + me . User . S ,
2020-04-04 01:25:59 +00:00
type : "POST" ,
dataType : "json" ,
2017-10-30 09:57:08 +00:00
error : ajaxError ,
2020-01-31 10:48:55 +00:00
data : { js : 1 , edit _item : content }
2016-12-06 10:26:48 +00:00
} ) ;
} ) ;
} ) ;
2017-05-29 14:52:37 +00:00
2020-03-09 10:37:41 +00:00
$ ( ".edit_fields" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
2020-03-11 09:28:00 +00:00
if ( $ ( this ) . find ( "input" ) . length !== 0 ) return ;
2020-07-23 04:02:16 +00:00
//log("clicked .edit_fields");
2020-04-04 01:25:59 +00:00
var bp = $ ( this ) . closest ( '.editable_parent' ) ;
bp . find ( '.hide_on_edit' ) . addClass ( "edit_opened" ) ;
bp . find ( '.show_on_edit' ) . addClass ( "edit_opened" ) ;
bp . find ( '.editable_block' ) . show ( ) ;
bp . find ( '.editable_block' ) . each ( function ( ) {
2017-10-30 09:57:08 +00:00
var fieldName = this . getAttribute ( "data-field" ) ;
var fieldType = this . getAttribute ( "data-type" ) ;
2018-01-28 14:30:24 +00:00
if ( fieldType == "list" ) {
2017-10-30 09:57:08 +00:00
var fieldValue = this . getAttribute ( "data-value" ) ;
2018-02-22 02:27:17 +00:00
if ( fieldName in formVars ) var it = formVars [ fieldName ] ;
2017-01-31 05:13:38 +00:00
else var it = [ 'No' , 'Yes' ] ;
var itLen = it . length ;
var out = "" ;
2020-04-04 01:25:59 +00:00
for ( var i = 0 ; i < itLen ; i ++ ) {
2017-09-25 00:48:35 +00:00
var sel = "" ;
2017-10-30 09:57:08 +00:00
if ( fieldValue == i || fieldValue == it [ i ] ) {
2017-05-29 14:52:37 +00:00
sel = "selected " ;
2020-04-04 01:25:59 +00:00
this . classList . remove ( fieldName + '_' + it [ i ] ) ;
2017-05-29 14:52:37 +00:00
this . innerHTML = "" ;
2017-09-25 00:48:35 +00:00
}
2017-01-31 05:13:38 +00:00
out += "<option " + sel + "value='" + i + "'>" + it [ i ] + "</option>" ;
}
2020-07-23 04:02:16 +00:00
this . innerHTML = "<select data-field='" + fieldName + "'name='" + fieldName + "'>" + out + "</select>" ;
2017-01-31 05:13:38 +00:00
}
2017-10-30 09:57:08 +00:00
else if ( fieldType == "hidden" ) { }
2020-07-23 04:02:16 +00:00
else this . innerHTML = "<input name='" + fieldName + "'value='" + this . textContent + "'type='text'>" ;
2017-01-31 05:13:38 +00:00
} ) ;
2017-05-29 14:52:37 +00:00
// Remove any handlers already attached to the submitter
$ ( ".submit_edit" ) . unbind ( "click" ) ;
2020-03-09 10:37:41 +00:00
$ ( ".submit_edit" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
2020-01-31 10:48:55 +00:00
var outData = { js : 1 }
2020-03-09 10:37:41 +00:00
var bp = $ ( this ) . closest ( '.editable_parent' ) ;
bp . find ( '.editable_block' ) . each ( function ( ) {
2017-10-30 09:57:08 +00:00
var fieldName = this . getAttribute ( "data-field" ) ;
var fieldType = this . getAttribute ( "data-type" ) ;
if ( fieldType == "list" ) {
2017-05-29 14:52:37 +00:00
var newContent = $ ( this ) . find ( 'select :selected' ) . text ( ) ;
2020-07-14 22:01:11 +00:00
this . classList . add ( fieldName + '_' + newContent ) ;
2017-05-29 14:52:37 +00:00
this . innerHTML = "" ;
2017-10-30 09:57:08 +00:00
} else if ( fieldType == "hidden" ) {
2017-06-05 11:57:27 +00:00
var newContent = $ ( this ) . val ( ) ;
2017-05-29 14:52:37 +00:00
} else {
var newContent = $ ( this ) . find ( 'input' ) . eq ( 0 ) . val ( ) ;
this . innerHTML = newContent ;
}
this . setAttribute ( "data-value" , newContent ) ;
2017-10-30 09:57:08 +00:00
outData [ fieldName ] = newContent ;
2017-01-31 05:13:38 +00:00
} ) ;
2017-05-29 14:52:37 +00:00
2020-04-04 01:25:59 +00:00
let href = $ ( this ) . closest ( 'a' ) . attr ( "href" ) ;
2020-07-23 04:02:16 +00:00
//log("href",href);
//log(outData);
2020-07-14 22:01:11 +00:00
$ . ajax ( { url : href + "?s=" + me . User . S , type : "POST" , dataType : "json" , data : outData , error : ajaxError } ) ;
2020-03-09 10:37:41 +00:00
bp . find ( '.hide_on_edit' ) . removeClass ( "edit_opened" ) ;
bp . find ( '.show_on_edit' ) . removeClass ( "edit_opened" ) ;
2017-01-31 05:13:38 +00:00
} ) ;
} ) ;
2017-05-29 14:52:37 +00:00
2018-03-31 05:25:27 +00:00
$ ( this ) . click ( ( ) => {
2017-03-03 16:28:49 +00:00
$ ( ".selectedAlert" ) . removeClass ( "selectedAlert" ) ;
2017-06-05 11:57:27 +00:00
$ ( "#back" ) . removeClass ( "alertActive" ) ;
2018-09-27 09:41:35 +00:00
$ ( ".link_select" ) . removeClass ( "link_opened" ) ;
2017-06-05 11:57:27 +00:00
} ) ;
2018-09-27 09:41:35 +00:00
2017-06-05 11:57:27 +00:00
$ ( ".alert_bell" ) . click ( function ( ) {
2020-04-04 01:25:59 +00:00
let menuAlerts = $ ( this ) . parent ( ) ;
2017-10-30 09:57:08 +00:00
if ( menuAlerts . hasClass ( "selectedAlert" ) ) {
2017-06-05 11:57:27 +00:00
event . stopPropagation ( ) ;
2017-10-30 09:57:08 +00:00
menuAlerts . removeClass ( "selectedAlert" ) ;
2017-06-05 11:57:27 +00:00
$ ( "#back" ) . removeClass ( "alertActive" ) ;
}
2017-03-03 16:28:49 +00:00
} ) ;
2020-03-06 23:00:35 +00:00
$ ( ".menu_alerts" ) . click ( function ( ev ) {
ev . stopPropagation ( ) ;
2017-03-01 11:36:50 +00:00
if ( $ ( this ) . hasClass ( "selectedAlert" ) ) return ;
2020-04-04 01:25:59 +00:00
if ( ! conn ) loadAlerts ( this , true ) ;
2017-06-05 11:57:27 +00:00
this . className += " selectedAlert" ;
document . getElementById ( "back" ) . className += " alertActive"
2017-03-01 11:36:50 +00:00
} ) ;
2020-03-06 23:00:35 +00:00
$ ( ".link_select" ) . click ( ev => ev . stopPropagation ( ) ) ;
2017-05-29 14:52:37 +00:00
2020-03-06 23:00:35 +00:00
$ ( "input,textarea,select,option" ) . keyup ( ev => ev . stopPropagation ( ) )
2017-06-16 10:41:30 +00:00
2017-09-10 16:57:22 +00:00
$ ( "#themeSelectorSelect" ) . change ( function ( ) {
2020-07-23 04:02:16 +00:00
log ( "Changing the theme to " + this . options [ this . selectedIndex ] . getAttribute ( "value" ) ) ;
2017-09-10 16:57:22 +00:00
$ . ajax ( {
2020-04-10 21:48:30 +00:00
url : this . form . getAttribute ( "action" ) + "?s=" + me . User . S ,
2020-04-04 01:25:59 +00:00
type : "POST" ,
dataType : "json" ,
2019-10-27 08:58:35 +00:00
data : { "theme" : this . options [ this . selectedIndex ] . getAttribute ( "value" ) , js : 1 } ,
2017-10-30 09:57:08 +00:00
error : ajaxError ,
2020-04-04 01:25:59 +00:00
success : function ( dat , status , xhr ) {
2020-07-23 04:02:16 +00:00
log ( "Theme successfully switched" ) ;
log ( "dat" , dat ) ;
log ( "status" , status ) ;
log ( "xhr" , xhr ) ;
2017-09-10 16:57:22 +00:00
window . location . reload ( ) ;
}
} ) ;
} ) ;
2018-01-08 08:53:51 +00:00
// The time range selector for the time graphs in the Control Panel
2019-05-18 00:33:35 +00:00
$ ( ".autoSubmitRedirect" ) . change ( function ( ) {
2020-04-11 08:12:37 +00:00
let els = this . form . elements ;
2019-05-18 00:33:35 +00:00
let s = "" ;
2020-04-11 08:12:37 +00:00
for ( let i = 0 ; i < els . length ; i ++ ) {
let el = els [ i ] ;
if ( el . nodeName == "SELECT" ) {
s += el . name + "=" + el . options [ el . selectedIndex ] . getAttribute ( "value" ) + "&" ;
2019-05-18 00:33:35 +00:00
}
// TODO: Implement other element types...
}
2020-04-10 21:48:30 +00:00
if ( s . length > 0 ) s = "?" + s . substr ( 0 , s . length - 1 ) ;
2019-05-18 00:33:35 +00:00
2020-04-11 08:12:37 +00:00
window . location = this . form . getAttribute ( "action" ) + s ; // Do a redirect as a form submission refuses to work properly
2018-01-08 08:53:51 +00:00
} ) ;
2018-01-10 03:32:48 +00:00
$ ( ".unix_to_24_hour_time" ) . each ( function ( ) {
let unixTime = this . innerText ;
let date = new Date ( unixTime * 1000 ) ;
2020-07-23 04:02:16 +00:00
log ( "date" , date ) ;
2020-04-11 08:12:37 +00:00
let mins = "0" + date . getMinutes ( ) ;
let formattedTime = date . getHours ( ) + ":" + mins . substr ( - 2 ) ;
2020-07-23 04:02:16 +00:00
log ( "formattedTime" , formattedTime ) ;
2019-02-24 01:29:06 +00:00
this . innerText = formattedTime ;
} ) ;
$ ( ".unix_to_date" ) . each ( function ( ) {
// TODO: Localise this
let monthList = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
let date = new Date ( this . innerText * 1000 ) ;
2020-07-23 04:02:16 +00:00
log ( "date" , date ) ;
2020-04-10 21:48:30 +00:00
let day = "0" + date . getDate ( ) ;
let formattedTime = monthList [ date . getMonth ( ) ] + " " + day . substr ( - 2 ) + " " + date . getFullYear ( ) ;
2020-07-23 04:02:16 +00:00
log ( "formattedTime" , formattedTime ) ;
2018-01-10 03:32:48 +00:00
this . innerText = formattedTime ;
} ) ;
2019-11-06 21:15:43 +00:00
$ ( "spoiler" ) . addClass ( "hide_spoil" ) ;
2020-03-06 23:00:35 +00:00
$ ( ".hide_spoil" ) . click ( function ( ev ) {
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
2019-11-06 21:15:43 +00:00
$ ( this ) . removeClass ( "hide_spoil" ) ;
$ ( this ) . unbind ( "click" ) ;
} ) ;
2020-03-06 23:00:35 +00:00
this . onkeyup = function ( ev ) {
2020-03-31 12:33:40 +00:00
if ( ev . which == 37 ) this . querySelectorAll ( "#prevFloat a" ) [ 0 ] . click ( ) ;
if ( ev . which == 39 ) this . querySelectorAll ( "#nextFloat a" ) [ 0 ] . click ( ) ;
2017-02-16 06:47:55 +00:00
} ;
2018-01-08 08:53:51 +00:00
2020-03-13 02:56:23 +00:00
function asyncGetSheet ( src ) {
return new Promise ( ( resolve , reject ) => {
let res = document . createElement ( 'link' ) ;
res . async = true ;
const onloadHandler = ( e , isAbort ) => {
if ( isAbort || ! res . readyState || /loaded|complete/ . test ( res . readyState ) ) {
res . onload = null ;
res . onreadystatechange = null ;
res = undefined ;
isAbort ? reject ( e ) : resolve ( ) ;
}
2019-04-10 07:40:47 +00:00
}
2020-03-13 02:56:23 +00:00
res . onerror = ( e ) => {
reject ( e ) ;
} ;
res . onload = onloadHandler ;
res . onreadystatechange = onloadHandler ;
res . href = src ;
res . rel = "stylesheet" ;
res . type = "text/css" ;
const prior = document . getElementsByTagName ( 'link' ) [ 0 ] ;
prior . parentNode . insertBefore ( res , prior ) ;
} ) ;
}
2019-04-10 07:40:47 +00:00
2020-03-13 02:56:23 +00:00
function stripQ ( name ) {
return name . split ( '?' ) [ 0 ] ;
}
2018-02-22 02:27:17 +00:00
2020-07-14 22:01:11 +00:00
function loadArb ( base , href , h = null ) {
2020-04-10 21:48:30 +00:00
fetch ( href , { credentials : "same-origin" } )
2020-03-11 03:26:33 +00:00
. then ( resp => {
if ( ! resp . ok ) throw ( href + " failed to load" ) ;
2020-04-10 21:48:30 +00:00
let xr = resp . headers . get ( "x-res" )
if ( xr != null ) {
for ( let res of xr . split ( "," ) ) {
2020-03-23 01:42:42 +00:00
let pro ;
2020-07-30 23:40:14 +00:00
if ( stripQ ( getExt ( res ) ) == "css" ) pro = asyncGetSheet ( pre + res )
else pro = asyncGetScript ( pre + res )
2020-07-23 04:02:16 +00:00
pro . then ( ( ) => log ( "Loaded " + res ) )
2020-03-23 01:42:42 +00:00
. catch ( e => {
2020-07-30 23:40:14 +00:00
log ( "Unable to get " + res , e ) ;
2020-03-23 01:42:42 +00:00
console . trace ( ) ;
} ) ;
}
2020-03-13 02:56:23 +00:00
}
2020-03-11 03:26:33 +00:00
return resp . text ( ) ;
2020-08-20 22:18:54 +00:00
} ) . then ( d => {
document . querySelector ( "#back" ) . outerHTML = d ;
if ( h !== null ) h ( d ) ;
2020-03-11 07:08:27 +00:00
$ ( ".elapsed" ) . remove ( ) ;
2020-04-10 21:48:30 +00:00
let obj = { Title : document . title , Url : base } ;
history . pushState ( obj , obj . Title , obj . Url ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2020-08-20 22:18:54 +00:00
log ( "Unable to get script " + href , e ) ;
2020-03-11 03:26:33 +00:00
console . trace ( ) ;
} ) ;
2020-04-10 21:48:30 +00:00
}
2020-07-14 22:01:11 +00:00
/ * $ ( " . r o w t o p i c a , a . r o w t o p i c , a . f o r u m _ p o s t e r " ) . c l i c k ( f u n c t i o n ( e v ) {
2020-04-10 21:48:30 +00:00
let base = this . getAttribute ( "href" ) ;
2020-05-08 23:15:44 +00:00
loadArb ( base , base + "?i=1" , ( ) => {
unbindTopic ( ) ;
bindTopic ( ) ;
} ) ;
2020-03-11 03:26:33 +00:00
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
2020-07-14 22:01:11 +00:00
} ) * /
2020-04-11 08:12:37 +00:00
$ ( "a" ) . click ( function ( ev ) {
let base = this . getAttribute ( "href" ) ;
if ( base != "/topics/" ) return ;
2020-05-08 23:15:44 +00:00
loadArb ( base , base + "?i=1" , ( ) => {
unbindPage ( ) ;
bindPage ( ) ;
} ) ;
2020-04-11 08:12:37 +00:00
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
} )
2020-03-11 03:26:33 +00:00
2019-04-25 06:02:51 +00:00
runInitHook ( "almost_end_init" ) ;
2018-08-13 10:34:00 +00:00
runInitHook ( "end_init" ) ;
2020-03-11 03:26:33 +00:00
}
2020-03-11 07:08:27 +00:00
function bindPage ( ) {
2020-07-23 11:13:55 +00:00
log ( "enter bindPage" ) ;
2020-05-08 23:15:44 +00:00
$ ( ".create_topic_link" ) . click ( ev => {
ev . preventDefault ( ) ;
$ ( ".topic_create_form" ) . removeClass ( "auto_hide" ) ;
} ) ;
$ ( ".topic_create_form .close_form" ) . click ( ev => {
ev . preventDefault ( ) ;
$ ( ".topic_create_form" ) . addClass ( "auto_hide" ) ;
} ) ;
2020-03-11 07:08:27 +00:00
bindTopic ( ) ;
2020-07-23 11:13:55 +00:00
runInitHook ( "end_bind_page" )
2020-03-11 07:08:27 +00:00
}
function unbindPage ( ) {
2020-07-23 11:13:55 +00:00
log ( "enter unbindPage" ) ;
2020-05-08 23:15:44 +00:00
$ ( ".create_topic_link" ) . unbind ( "click" ) ;
$ ( ".topic_create_form .close_form" ) . unbind ( "click" ) ;
2020-03-11 07:08:27 +00:00
unbindTopic ( ) ;
2020-07-23 11:13:55 +00:00
runHook ( "end_unbind_page" )
2020-03-11 07:08:27 +00:00
}
2020-03-11 03:26:33 +00:00
function bindTopic ( ) {
2020-07-23 11:13:55 +00:00
log ( "enter bindTopic" ) ;
2020-03-11 03:26:33 +00:00
$ ( ".open_edit" ) . click ( ev => {
ev . preventDefault ( ) ;
$ ( '.hide_on_edit' ) . addClass ( "edit_opened" ) ;
$ ( '.show_on_edit' ) . addClass ( "edit_opened" ) ;
runHook ( "open_edit" ) ;
} ) ;
$ ( ".topic_item .submit_edit" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
let nameInput = $ ( ".topic_name_input" ) . val ( ) ;
$ ( ".topic_name" ) . html ( nameInput ) ;
$ ( ".topic_name" ) . attr ( nameInput ) ;
let contentInput = $ ( '.topic_content_input' ) . val ( ) ;
$ ( ".topic_content" ) . html ( quickParse ( contentInput ) ) ;
let statusInput = $ ( '.topic_status_input' ) . val ( ) ;
$ ( ".topic_status_e:not(.open_edit)" ) . html ( statusInput ) ;
$ ( '.hide_on_edit' ) . removeClass ( "edit_opened" ) ;
$ ( '.show_on_edit' ) . removeClass ( "edit_opened" ) ;
runHook ( "close_edit" ) ;
$ . ajax ( {
url : this . form . getAttribute ( "action" ) ,
2020-03-11 09:28:00 +00:00
type : "POST" ,
dataType : "json" ,
2020-03-11 03:26:33 +00:00
data : {
name : nameInput ,
status : statusInput ,
content : contentInput ,
js : 1
} ,
error : ajaxError ,
2020-04-04 01:25:59 +00:00
success : ( dat , status , xhr ) => {
if ( "Content" in dat ) $ ( ".topic_content" ) . html ( dat [ "Content" ] ) ;
2020-03-11 03:26:33 +00:00
}
} ) ;
} ) ;
$ ( ".delete_item" ) . click ( function ( ev ) {
postLink ( ev ) ;
$ ( this ) . closest ( '.deletable_block' ) . remove ( ) ;
} ) ;
// Miniature implementation of the parser to avoid sending as much data back and forth
function quickParse ( m ) {
2020-03-22 02:37:32 +00:00
const r = ( o , n ) => {
m = m . replace ( o , n )
}
r ( ":)" , "😀" )
r ( ":(" , "😞" )
r ( ":D" , "😃" )
r ( ":P" , "😛" )
r ( ":O" , "😲" )
r ( ":p" , "😛" )
r ( ":o" , "😲" )
r ( ";)" , "😉" )
r ( "\n" , "<br>" )
2020-03-11 03:26:33 +00:00
return m
}
$ ( ".edit_item" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
let bp = this . closest ( '.editable_parent' ) ;
$ ( bp ) . find ( '.hide_on_edit' ) . addClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.show_on_edit' ) . addClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.hide_on_block_edit' ) . addClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.show_on_block_edit' ) . addClass ( "edit_opened" ) ;
let srcNode = bp . querySelector ( ".edit_source" ) ;
let block = bp . querySelector ( '.editable_block' ) ;
block . classList . add ( "in_edit" ) ;
let src = "" ;
if ( srcNode != null ) src = srcNode . innerText ;
else src = block . innerHTML ;
2020-03-22 23:14:08 +00:00
block . innerHTML = Tmpl _topic _c _edit _post ( {
2020-03-11 03:26:33 +00:00
ID : bp . getAttribute ( "id" ) . slice ( "post-" . length ) ,
Source : src ,
Ref : this . closest ( 'a' ) . getAttribute ( "href" )
} )
runHook ( "edit_item_pre_bind" ) ;
$ ( ".submit_edit" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
$ ( bp ) . find ( '.hide_on_edit' ) . removeClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.show_on_edit' ) . removeClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.hide_on_block_edit' ) . removeClass ( "edit_opened" ) ;
$ ( bp ) . find ( '.show_on_block_edit' ) . removeClass ( "edit_opened" ) ;
block . classList . remove ( "in_edit" ) ;
2020-04-10 21:48:30 +00:00
let con = block . querySelector ( 'textarea' ) . value ;
block . innerHTML = quickParse ( con ) ;
if ( srcNode != null ) srcNode . innerText = con ;
2020-03-11 03:26:33 +00:00
let formAction = this . closest ( 'a' ) . getAttribute ( "href" ) ;
// TODO: Bounce the parsed post back and set innerHTML to it?
$ . ajax ( {
url : formAction ,
type : "POST" ,
dataType : "json" ,
2020-04-10 21:48:30 +00:00
data : { js : 1 , edit _item : con } ,
2020-03-11 03:26:33 +00:00
error : ajaxError ,
2020-04-04 01:25:59 +00:00
success : ( dat , status , xhr ) => {
if ( "Content" in dat ) block . innerHTML = dat [ "Content" ] ;
2020-03-11 03:26:33 +00:00
}
} ) ;
} ) ;
} ) ;
2020-03-11 07:08:27 +00:00
$ ( ".quote_item" ) . click ( function ( ev ) {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
let src = this . closest ( ".post_item" ) . getElementsByClassName ( "edit_source" ) [ 0 ] ;
2020-04-10 21:48:30 +00:00
let con = document . getElementById ( "input_content" )
2020-07-23 04:02:16 +00:00
log ( "con.value" , con . value ) ;
2020-03-11 07:08:27 +00:00
let item ;
2020-04-10 21:48:30 +00:00
if ( con . value == "" ) item = "<blockquote>" + src . innerHTML + "</blockquote>"
2020-04-04 01:25:59 +00:00
else item = "\r\n<blockquote>" + src . innerHTML + "</blockquote>" ;
2020-04-10 21:48:30 +00:00
con . value = con . value + item ;
2020-07-23 04:02:16 +00:00
log ( "con.value" , con . value ) ;
2020-03-11 07:08:27 +00:00
// For custom / third party text editors
quoteItemCallback ( src . innerHTML , item ) ;
} ) ;
2020-03-11 09:13:05 +00:00
2020-04-04 01:25:59 +00:00
//id="poll_results_{pollid}" class="poll_results auto_hide"
2020-03-13 02:56:23 +00:00
$ ( ".poll_results_button" ) . click ( function ( ) {
let pollID = $ ( this ) . attr ( "data-poll-id" ) ;
2020-04-04 01:25:59 +00:00
$ ( "#poll_results_" + pollID ) . removeClass ( "auto_hide" ) ;
fetch ( "/poll/results/" + pollID , {
2020-03-13 02:56:23 +00:00
credentials : 'same-origin'
2020-04-10 21:48:30 +00:00
} ) . then ( resp => resp . text ( ) ) . catch ( e => console . error ( "e" , e ) ) . then ( rawData => {
2020-03-13 02:56:23 +00:00
// TODO: Make sure the received data is actually a list of integers
let data = JSON . parse ( rawData ) ;
let allZero = true ;
2020-04-04 01:25:59 +00:00
for ( let i = 0 ; i < data . length ; i ++ ) {
if ( data [ i ] != "0" ) allZero = false ;
2020-03-13 02:56:23 +00:00
}
if ( allZero ) {
$ ( "#poll_results_" + pollID + " .poll_no_results" ) . removeClass ( "auto_hide" ) ;
2020-07-23 04:02:16 +00:00
log ( "all zero" )
2020-03-13 02:56:23 +00:00
return ;
}
$ ( "#poll_results_" + pollID + " .user_content" ) . html ( "<div id='poll_results_chart_" + pollID + "'></div>" ) ;
2020-07-23 04:02:16 +00:00
log ( "rawData" , rawData ) ;
log ( "series" , data ) ;
2020-03-13 02:56:23 +00:00
Chartist . Pie ( '#poll_results_chart_' + pollID , {
series : data ,
} , {
height : '120px' ,
} ) ;
} )
} ) ;
2020-07-23 11:13:55 +00:00
runInitHook ( "end_bind_topic" ) ;
2020-03-11 03:26:33 +00:00
}
function unbindTopic ( ) {
2020-07-23 11:13:55 +00:00
log ( "enter unbindTopic" ) ;
2020-03-11 03:26:33 +00:00
$ ( ".open_edit" ) . unbind ( "click" ) ;
$ ( ".topic_item .submit_edit" ) . unbind ( "click" ) ;
$ ( ".delete_item" ) . unbind ( "click" ) ;
$ ( ".edit_item" ) . unbind ( "click" ) ;
$ ( ".submit_edit" ) . unbind ( "click" ) ;
2020-03-11 07:08:27 +00:00
$ ( ".quote_item" ) . unbind ( "click" ) ;
2020-03-13 02:56:23 +00:00
$ ( ".poll_results_button" ) . unbind ( "click" ) ;
2020-03-11 09:13:05 +00:00
runHook ( "end_unbind_topic" ) ;
2020-03-09 10:37:41 +00:00
}