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 = [ ] ;
var attachItemCallback = function ( ) { }
var quoteItemCallback = function ( ) { }
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-04-04 01:25:59 +00:00
function ajaxError ( xhr , status , er ) {
2017-10-30 09:57:08 +00:00
console . log ( "The AJAX request failed" ) ;
2020-03-22 23:14:08 +00:00
console . log ( "xhr" , xhr ) ;
console . log ( "status" , status ) ;
2020-04-04 01:25:59 +00:00
console . log ( "er" , er ) ;
2019-03-16 11:31:10 +00:00
if ( status == "parsererror" ) console . 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-01-31 10:48:55 +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 ( ) {
2019-03-16 11:31:10 +00:00
console . 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-04 01:25:59 +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 ) {
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-03-09 10:37:41 +00:00
console . 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-04-04 01:25:59 +00:00
console . log ( "data" , data ) ;
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 ) {
2017-11-06 16:24:45 +00:00
console . log ( magic . responseText ) ;
2020-04-04 01:25:59 +00:00
console . log ( e ) ;
2017-11-06 16:24:45 +00:00
}
2020-04-04 01:25:59 +00:00
console . 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 ++ ) {
Added Quick Topic.
Added Attachments.
Added Attachment Media Embeds.
Renamed a load of *Store and *Cache methods to reduce the amount of unneccesary typing.
Added petabytes as a unit and cleaned up a few of the friendly units.
Refactored the username change logic to make it easier to maintain.
Refactored the avatar change logic to make it easier to maintain.
Shadow now uses CSS Variables for most of it's colours. We have plans to transpile this to support older browsers later on!
Snuck some CSS Variables into Tempra Conflux.
Added the GroupCache interface to MemoryGroupStore.
Added the Length method to MemoryGroupStore.
Added support for a site short name.
Added the UploadFiles permission.
Renamed more functions.
Fixed the background for the left gutter on the postbit for Tempra Simple and Shadow.
Added support for if statements operating on int8, int16, int32, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 for the template compiler.
Added support for if statements operating on slices and maps for the template compiler.
Fixed a security exploit in reply editing.
Fixed a bug in the URL detector in the parser where it couldn't find URLs with non-standard ports.
Fixed buttons having blue outlines on focus on Shadow.
Refactored the topic creation logic to make it easier to maintain.
Made a few responsive fixes, but there's still more to do in the following commits!
2017-10-05 10:20:28 +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 ;
Added Quick Topic.
Added Attachments.
Added Attachment Media Embeds.
Renamed a load of *Store and *Cache methods to reduce the amount of unneccesary typing.
Added petabytes as a unit and cleaned up a few of the friendly units.
Refactored the username change logic to make it easier to maintain.
Refactored the avatar change logic to make it easier to maintain.
Shadow now uses CSS Variables for most of it's colours. We have plans to transpile this to support older browsers later on!
Snuck some CSS Variables into Tempra Conflux.
Added the GroupCache interface to MemoryGroupStore.
Added the Length method to MemoryGroupStore.
Added support for a site short name.
Added the UploadFiles permission.
Renamed more functions.
Fixed the background for the left gutter on the postbit for Tempra Simple and Shadow.
Added support for if statements operating on int8, int16, int32, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 for the template compiler.
Added support for if statements operating on slices and maps for the template compiler.
Fixed a security exploit in reply editing.
Fixed a bug in the URL detector in the parser where it couldn't find URLs with non-standard ports.
Fixed buttons having blue outlines on focus on Shadow.
Refactored the topic creation logic to make it easier to maintain.
Made a few responsive fixes, but there's still more to do in the following commits!
2017-10-05 10:20:28 +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-04 01:25:59 +00:00
if ( data . length > lastI ) o [ o . length - 1 ] += data . substring ( lastI ) ;
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 ) {
console . log ( "wsAlertEvent" , dat )
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-04-04 01:25:59 +00:00
conn . onerror = err => {
2018-08-13 10:34:00 +00:00
console . log ( err ) ;
}
2019-03-05 04:46:43 +00:00
// TODO: Sync alerts, topic list, etc.
2018-06-24 13:49:29 +00:00
conn . onopen = ( ) => {
2017-11-06 16:24:45 +00:00
console . 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 ;
console . 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 ] == "{" ) {
2018-06-24 13:49:29 +00:00
console . log ( "json message" ) ;
let data = "" ;
2017-11-06 16:24:45 +00:00
try {
2018-06-24 13:49:29 +00:00
data = JSON . parse ( event . data ) ;
2017-11-06 16:24:45 +00:00
} catch ( err ) {
console . log ( err ) ;
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 ) {
console . log ( "topic in data" ) ;
2020-03-12 01:08:56 +00:00
console . log ( "data" , 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 ) {
2018-06-24 13:49:29 +00:00
console . log ( "empty topic list" ) ;
return ;
2017-06-10 07:58:15 +00:00
}
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" ) ;
2018-06-24 13:49:29 +00:00
console . log ( "Prepending to topic list" ) ;
$ ( ".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" ) ;
for ( let i = 0 ; i < blocks . length ; i ++ ) {
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-03-09 05:16:44 +00:00
console . 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-04-04 01:25:59 +00:00
} else console . 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-03-12 01:08:56 +00:00
//console.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 ) {
if ( ! name . indexOf ( '.' > - 1 ) ) throw ( "This file doesn't have an extension" ) ;
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" ) ;
2019-03-16 11:31:10 +00:00
console . 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-03-06 23:00:35 +00:00
if ( ! noAlerts ) {
2020-03-22 23:14:08 +00:00
notifyOnScriptW ( "tmpl_alert" , e => {
2020-03-12 01:08:56 +00:00
if ( e != undefined ) console . 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 ( ( ) => {
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" ) ;
if ( sAlertList != "" && sAlertMapping != "" && sAlertCount != "" ) {
2020-04-04 02:58:07 +00:00
console . log ( "sAlertList" , sAlertList )
console . log ( "sAlertMapping" , sAlertMapping )
console . log ( "sAlertCount" , sAlertCount )
2020-04-04 02:51:31 +00:00
alertList = JSON . parse ( sAlertList )
alertMapping = JSON . parse ( sAlertMapping )
alertCount = parseInt ( sAlertCount )
2020-04-04 02:58:07 +00:00
console . log ( "alertList" , alertList )
console . log ( "alertMapping" , alertMapping )
console . 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 ( ) {
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 ++ ) {
let block = blocks [ i ] ;
2019-06-04 05:48:12 +00:00
block . classList . remove ( "more_topic_block_active" ) ;
block . 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-03-11 09:28:00 +00:00
console . 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 ] ;
console . 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-04-04 01:25:59 +00:00
console . log ( "data" , dat ) ;
2020-03-11 03:26:33 +00:00
console . log ( "status" , status ) ;
console . 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-03-22 23:14:08 +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-03-22 23:14:08 +00:00
this . outerHTML = Tmpl _paginator ( { PageList : pageList , Page : page , LastPage : lastPage } ) ;
2019-02-10 05:52:26 +00:00
ok = true ;
} ) ;
2020-03-22 23:14:08 +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 ( ) {
$ ( ".pageitem a" ) . unbind ( "click" ) ;
$ ( ".pageitem a" ) . click ( function ( ) {
event . preventDefault ( ) ;
// TODO: Take mostviewed into account
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
fetch ( url + q + "&js=1" , { credentials : "same-origin" } )
2020-03-09 10:37:41 +00:00
. then ( resp => {
2019-03-16 11:31:10 +00:00
if ( ! resp . ok ) throw ( url + q + "&js=1 failed to load" ) ;
return resp . json ( ) ;
2020-04-04 01:25:59 +00:00
} ) . then ( dat => {
if ( ! "Topics" in dat ) throw ( "no Topics in data" ) ;
let topics = dat [ "Topics" ] ;
2019-03-16 11:31:10 +00:00
console . 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-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 ) ;
2020-04-04 01:25:59 +00:00
let obj = { Title : document . title , Url : url + q } ;
history . pushState ( obj , obj . Title , obj . Url ) ;
rebuildPaginator ( dat . LastPage ) ;
2019-02-10 05:52:26 +00:00
rebindPaginator ( ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2019-02-10 05:52:26 +00:00
console . log ( "Unable to get script '" + url + q + "&js=1" + "'" ) ;
2020-04-04 01:25:59 +00:00
console . log ( "e" , 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-04-04 01:25:59 +00:00
fetch ( url + "&js=1" , { credentials : "same-origin" } )
2020-03-09 10:37:41 +00:00
. then ( resp => {
2019-03-16 11:31:10 +00:00
if ( ! resp . ok ) throw ( url + "&js=1 failed to load" ) ;
return resp . json ( ) ;
2020-04-04 01:25:59 +00:00
} ) . then ( dat => {
console . log ( "data" , dat ) ;
if ( ! "Topics" in dat ) throw ( "no Topics in data" ) ;
let topics = dat [ "Topics" ] ;
2019-03-16 11:31:10 +00:00
console . 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 ;
2019-02-10 05:52:26 +00:00
let obj = { Title : document . title , Url : url } ;
history . pushState ( obj , obj . Title , obj . Url ) ;
2020-04-04 01:25:59 +00:00
rebuildPaginator ( dat . 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-04-04 01:25:59 +00:00
} ) . catch ( e => {
2019-02-10 05:52:26 +00:00
console . log ( "Unable to get script '" + url + "&js=1" + "'" ) ;
2020-04-04 01:25:59 +00:00
console . log ( "e" , e ) ;
2019-02-10 05:52:26 +00:00
console . trace ( ) ;
} ) ;
} ) ;
if ( document . getElementById ( "topicsItemList" ) !== null ) rebindPaginator ( ) ;
if ( document . getElementById ( "forumItemList" ) !== null ) rebindPaginator ( ) ;
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 ;
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-03-09 10:37:41 +00:00
. then ( resp => {
2019-03-16 11:31:10 +00:00
if ( ! resp . ok ) throw ( url + q + "&js=1 failed to load" ) ;
return resp . json ( ) ;
2020-03-09 10:37:41 +00:00
} ) . then ( data => {
2019-02-23 06:29:19 +00:00
if ( ! "Topics" in data ) throw ( "no Topics in data" ) ;
let topics = data [ "Topics" ] ;
2019-03-16 11:31:10 +00:00
console . 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-04 01:25:59 +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 } ;
history . pushState ( obj , obj . Title , obj . Url ) ;
rebuildPaginator ( data . LastPage ) ;
rebindPaginator ( ) ;
2020-04-04 01:25:59 +00:00
} ) . catch ( e => {
2019-02-23 06:29:19 +00:00
console . log ( "Unable to get script '" + url + q + "&js=1" + "'" ) ;
2020-04-04 01:25:59 +00:00
console . log ( "e" , e ) ;
2019-02-23 06:29:19 +00:00
console . trace ( ) ;
} ) ;
} ) ;
2020-03-11 07:08:27 +00:00
bindPage ( ) ;
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-03-23 05:31:17 +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' ) ;
let block = bp . find ( '.editable_block' ) . eq ( 0 ) ;
2020-01-31 10:48:55 +00:00
let content = block . find ( 'input' ) . eq ( 0 ) . val ( ) ;
block . 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 ( {
2019-08-31 22:59:00 +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 ;
2017-05-29 14:52:37 +00:00
//console.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>" ;
}
2017-10-30 09:57:08 +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-03-23 05:31:17 +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 ( ) ;
2017-10-30 09:57:08 +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" ) ;
//console.log("href",href);
2017-10-30 09:57:08 +00:00
//console.log(outData);
2020-04-04 01:25:59 +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
2020-03-06 23:00:35 +00:00
$ ( ".create_topic_link" ) . click ( ev => {
ev . preventDefault ( ) ;
2019-09-01 01:37:04 +00:00
$ ( ".topic_create_form" ) . removeClass ( "auto_hide" ) ;
Added Quick Topic.
Added Attachments.
Added Attachment Media Embeds.
Renamed a load of *Store and *Cache methods to reduce the amount of unneccesary typing.
Added petabytes as a unit and cleaned up a few of the friendly units.
Refactored the username change logic to make it easier to maintain.
Refactored the avatar change logic to make it easier to maintain.
Shadow now uses CSS Variables for most of it's colours. We have plans to transpile this to support older browsers later on!
Snuck some CSS Variables into Tempra Conflux.
Added the GroupCache interface to MemoryGroupStore.
Added the Length method to MemoryGroupStore.
Added support for a site short name.
Added the UploadFiles permission.
Renamed more functions.
Fixed the background for the left gutter on the postbit for Tempra Simple and Shadow.
Added support for if statements operating on int8, int16, int32, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 for the template compiler.
Added support for if statements operating on slices and maps for the template compiler.
Fixed a security exploit in reply editing.
Fixed a bug in the URL detector in the parser where it couldn't find URLs with non-standard ports.
Fixed buttons having blue outlines on focus on Shadow.
Refactored the topic creation logic to make it easier to maintain.
Made a few responsive fixes, but there's still more to do in the following commits!
2017-10-05 10:20:28 +00:00
} ) ;
2020-03-06 23:00:35 +00:00
$ ( ".topic_create_form .close_form" ) . click ( ev => {
ev . preventDefault ( ) ;
2019-09-01 01:37:04 +00:00
$ ( ".topic_create_form" ) . addClass ( "auto_hide" ) ;
Added Quick Topic.
Added Attachments.
Added Attachment Media Embeds.
Renamed a load of *Store and *Cache methods to reduce the amount of unneccesary typing.
Added petabytes as a unit and cleaned up a few of the friendly units.
Refactored the username change logic to make it easier to maintain.
Refactored the avatar change logic to make it easier to maintain.
Shadow now uses CSS Variables for most of it's colours. We have plans to transpile this to support older browsers later on!
Snuck some CSS Variables into Tempra Conflux.
Added the GroupCache interface to MemoryGroupStore.
Added the Length method to MemoryGroupStore.
Added support for a site short name.
Added the UploadFiles permission.
Renamed more functions.
Fixed the background for the left gutter on the postbit for Tempra Simple and Shadow.
Added support for if statements operating on int8, int16, int32, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 for the template compiler.
Added support for if statements operating on slices and maps for the template compiler.
Fixed a security exploit in reply editing.
Fixed a bug in the URL detector in the parser where it couldn't find URLs with non-standard ports.
Fixed buttons having blue outlines on focus on Shadow.
Refactored the topic creation logic to make it easier to maintain.
Made a few responsive fixes, but there's still more to do in the following commits!
2017-10-05 10:20:28 +00:00
} ) ;
2017-09-10 16:57:22 +00:00
$ ( "#themeSelectorSelect" ) . change ( function ( ) {
2020-04-04 01:25:59 +00:00
console . log ( "Changing the theme to " + this . options [ this . selectedIndex ] . getAttribute ( "value" ) ) ;
2017-09-10 16:57:22 +00:00
$ . ajax ( {
2019-08-31 22:59:00 +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 ) {
2017-09-10 16:57:22 +00:00
console . log ( "Theme successfully switched" ) ;
2020-04-04 01:25:59 +00:00
console . log ( "dat" , dat ) ;
2020-03-11 07:08:27 +00:00
console . log ( "status" , status ) ;
console . 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 ( ) {
let elems = this . form . elements ;
let s = "" ;
for ( let i = 0 ; i < elems . length ; i ++ ) {
let elem = elems [ i ] ;
if ( elem . nodeName == "SELECT" ) {
2019-10-27 23:13:24 +00:00
s += elem . name + "=" + elem . options [ elem . selectedIndex ] . getAttribute ( "value" ) + "&" ;
2019-05-18 00:33:35 +00:00
}
// TODO: Implement other element types...
}
if ( s . length > 0 ) s = "?" + s . substr ( 0 , s . length - 1 ) ;
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-03-31 12:33:40 +00:00
console . log ( "date" , date ) ;
2018-01-10 03:32:48 +00:00
let minutes = "0" + date . getMinutes ( ) ;
let formattedTime = date . getHours ( ) + ":" + minutes . substr ( - 2 ) ;
2020-03-22 23:14:08 +00:00
console . 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-03-22 23:14:08 +00:00
console . log ( "date" , date ) ;
2019-02-24 01:29:06 +00:00
let day = "0" + date . getDate ( ) ;
let formattedTime = monthList [ date . getMonth ( ) ] + " " + day . substr ( - 2 ) + " " + date . getFullYear ( ) ;
2020-03-11 09:28:00 +00:00
console . 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-03-13 02:56:23 +00:00
$ ( ".rowtopic a,a.rowtopic" ) . click ( function ( ev ) {
2020-03-11 03:26:33 +00:00
let base = this . getAttribute ( "href" ) ;
let href = base + "?i=1" ;
fetch ( href , { credentials : "same-origin" } )
. then ( resp => {
if ( ! resp . ok ) throw ( href + " failed to load" ) ;
2020-03-13 02:56:23 +00:00
let xRes = resp . headers . get ( "x-res" )
2020-03-23 01:42:42 +00:00
if ( xRes != null ) {
for ( let res of xRes . split ( "," ) ) {
let pro ;
if ( stripQ ( getExt ( res ) ) == "css" ) pro = asyncGetSheet ( "/s/" + res )
else pro = asyncGetScript ( "/s/" + res )
pro . then ( ( ) => console . log ( "Loaded " + res ) )
. catch ( e => {
console . log ( "Unable to get '" + res + "'" ) ;
console . log ( "e" , e ) ;
console . trace ( ) ;
} ) ;
}
2020-03-13 02:56:23 +00:00
}
2020-03-11 03:26:33 +00:00
return resp . text ( ) ;
} ) . then ( data => {
2020-03-11 07:08:27 +00:00
document . querySelector ( "#back" ) . outerHTML = data ;
2020-03-11 03:26:33 +00:00
unbindTopic ( ) ;
bindTopic ( ) ;
2020-03-11 07:08:27 +00:00
$ ( ".elapsed" ) . remove ( ) ;
2020-03-11 03:26:33 +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-03-11 03:26:33 +00:00
console . log ( "Unable to get script '" + href + "" + "'" ) ;
2020-04-04 01:25:59 +00:00
console . log ( "e" , e ) ;
2020-03-11 03:26:33 +00:00
console . trace ( ) ;
} ) ;
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
} )
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 ( ) {
bindTopic ( ) ;
}
function unbindPage ( ) {
unbindTopic ( ) ;
}
2020-03-11 03:26:33 +00:00
function bindTopic ( ) {
$ ( ".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" ) ;
let content = block . querySelector ( 'textarea' ) . value ;
block . innerHTML = quickParse ( content ) ;
if ( srcNode != null ) srcNode . innerText = content ;
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" ,
data : { js : 1 , edit _item : content } ,
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 ] ;
let content = document . getElementById ( "input_content" )
2020-03-22 23:14:08 +00:00
console . log ( "content.value" , content . value ) ;
2020-03-11 07:08:27 +00:00
let item ;
2020-04-04 01:25:59 +00:00
if ( content . value == "" ) item = "<blockquote>" + src . innerHTML + "</blockquote>"
else item = "\r\n<blockquote>" + src . innerHTML + "</blockquote>" ;
2020-03-11 07:08:27 +00:00
content . value = content . value + item ;
2020-03-22 23:14:08 +00:00
console . log ( "content.value" , content . 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-04 01:25:59 +00:00
} ) . then ( resp => resp . text ( ) ) . catch ( er => console . error ( "er" , er ) ) . 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" ) ;
console . log ( "all zero" )
return ;
}
$ ( "#poll_results_" + pollID + " .user_content" ) . html ( "<div id='poll_results_chart_" + pollID + "'></div>" ) ;
console . log ( "rawData" , rawData ) ;
console . log ( "series" , data ) ;
Chartist . Pie ( '#poll_results_chart_' + pollID , {
series : data ,
} , {
height : '120px' ,
} ) ;
} )
} ) ;
2020-03-11 09:13:05 +00:00
runHook ( "end_bind_topic" ) ;
2020-03-11 03:26:33 +00:00
}
function unbindTopic ( ) {
$ ( ".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
}