mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-26 22:50:26 +01:00
tgchat (#52426)
Replaces goonchat with a tgui based chat panel
Fixes #52898
Fixes #52663
It is as fast as goonchat was (if not faster in certain circumstances), and is very extensible. It has all the necessary code for sorting messages into categories, which means that one of the next features will be multiple tab support.
Additional features that you will get with tgchat right now:
Massively faster server-side performance compared to goonchat, especially if batching multiple messages to one client.
Message persistence across rounds and reconnects. (All messages are stored client-side in IndexedDB)
More robust scroll tracking. If you scroll up, it will not change the scroll position on new messages like goonchat did.
Multiple message combining. (Currently set to combine up to 5 messages over last 5 seconds).
If using the highlighting feature, it highlights the whole message as well as the matching word.
"Now playing" widget, with preview of the song title, a knob for adjusting the volume and a stop button.
Architecture is as following:
```
to_chat() -+
|
SSchat
(queue, batching)
|
window.send_message()
|
v
+-------------+
| tgui-panel |
|+-----------+|
|| tgchat ||
|+-----------+|
+-------------+
```
Subsystem is basically goonchat, but without all the garbage that slows the servers down (string concatenation, double urlencoding, sanitizing, etc). Now, instead of all that, it's being slowed down by json_encode in /datum/tgui_window/proc/send_message, which IMO is completely worth it, and allows sending various templates and widgets to tgchat.
/datum/tgui_window abstracts the whole window away from you, establishes a nice message-passing interface between DM and JS, with two message queues on each side, automatically loads js/css assets for you, basically does everything. You as a developer only have to worry about sending/receiving messages and write javascript.
tgui-panel is a slimmed down version of tgui, and functions as a container for various widgets, and tgchat is one of them. It of course can be expanded with more stuff.
It's also a separate entry point and a JS bundle, so it's not bloating the main tgui bundle, and is currently sitting at about 230kB.
This commit is contained in:
@@ -130,6 +130,13 @@
|
||||
*/
|
||||
/client/var/list/tgui_windows = list()
|
||||
|
||||
/**
|
||||
* global
|
||||
*
|
||||
* TRUE if cache was reloaded by tgui dev server at least once.
|
||||
*/
|
||||
/client/var/tgui_cache_reloaded = FALSE
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
@@ -159,16 +166,29 @@
|
||||
/**
|
||||
* Middleware for /client/Topic.
|
||||
*
|
||||
* return bool Whether the topic is passed (TRUE), or cancelled (FALSE).
|
||||
* return bool If TRUE, prevents propagation of the topic call.
|
||||
*/
|
||||
/proc/tgui_Topic(href_list)
|
||||
// Skip non-tgui topics
|
||||
if(!href_list["tgui"])
|
||||
return TRUE
|
||||
return FALSE
|
||||
var/type = href_list["type"]
|
||||
// Unconditionally collect tgui logs
|
||||
if(type == "log")
|
||||
log_tgui(usr, href_list["message"])
|
||||
// Reload all tgui windows
|
||||
if(type == "cacheReloaded")
|
||||
if(!check_rights(R_ADMIN) || usr.client.tgui_cache_reloaded)
|
||||
return TRUE
|
||||
// Mark as reloaded
|
||||
usr.client.tgui_cache_reloaded = TRUE
|
||||
// Notify windows
|
||||
var/list/windows = usr.client.tgui_windows
|
||||
for(var/window_id in windows)
|
||||
var/datum/tgui_window/window = windows[window_id]
|
||||
if (window.status == TGUI_WINDOW_READY)
|
||||
window.on_message(type, null, href_list)
|
||||
return TRUE
|
||||
// Locate window
|
||||
var/window_id = href_list["window_id"]
|
||||
var/datum/tgui_window/window
|
||||
@@ -177,7 +197,7 @@
|
||||
if(!window)
|
||||
log_tgui(usr, "Error: Couldn't find the window datum, force closing.")
|
||||
SStgui.force_close_window(usr, window_id)
|
||||
return FALSE
|
||||
return TRUE
|
||||
// Decode payload
|
||||
var/payload
|
||||
if(href_list["payload"])
|
||||
@@ -185,4 +205,4 @@
|
||||
// Pass message to window
|
||||
if(window)
|
||||
window.on_message(type, payload, href_list)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
@@ -80,16 +80,19 @@
|
||||
opened_at = world.time
|
||||
window.acquire_lock(src)
|
||||
if(!window.is_ready())
|
||||
window.initialize(inline_assets = list(
|
||||
get_asset_datum(/datum/asset/simple/tgui),
|
||||
))
|
||||
window.initialize(
|
||||
fancy = user.client.prefs.tgui_fancy,
|
||||
inline_assets = list(
|
||||
get_asset_datum(/datum/asset/simple/tgui_common),
|
||||
get_asset_datum(/datum/asset/simple/tgui),
|
||||
))
|
||||
else
|
||||
window.send_message("ping")
|
||||
|
||||
var/flushqueue = window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
|
||||
var/flush_queue = window.send_asset(get_asset_datum(
|
||||
/datum/asset/simple/namespaced/fontawesome))
|
||||
for(var/datum/asset/asset in src_object.ui_assets(user))
|
||||
flushqueue |= window.send_asset(asset)
|
||||
if (flushqueue)
|
||||
flush_queue |= window.send_asset(asset)
|
||||
if (flush_queue)
|
||||
user.client.browse_queue_flush()
|
||||
window.send_message("update", get_payload(
|
||||
with_data = TRUE,
|
||||
@@ -206,9 +209,13 @@
|
||||
"fancy" = user.client.prefs.tgui_fancy,
|
||||
"locked" = user.client.prefs.tgui_lock,
|
||||
),
|
||||
"client" = list(
|
||||
"ckey" = user.client.ckey,
|
||||
"address" = user.client.address,
|
||||
"computer_id" = user.client.computer_id,
|
||||
),
|
||||
"user" = list(
|
||||
"name" = "[user]",
|
||||
"ckey" = "[user.ckey]",
|
||||
"observer" = isobserver(user),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
var/client/client
|
||||
var/pooled
|
||||
var/pool_index
|
||||
var/is_browser = FALSE
|
||||
var/status = TGUI_WINDOW_CLOSED
|
||||
var/locked = FALSE
|
||||
var/datum/tgui/locked_by
|
||||
var/datum/subscriber_object
|
||||
var/subscriber_delegate
|
||||
var/fatally_errored = FALSE
|
||||
var/message_queue
|
||||
var/sent_assets = list()
|
||||
// Vars passed to initialize proc (and saved for later)
|
||||
var/inline_assets
|
||||
var/fancy
|
||||
|
||||
/**
|
||||
* public
|
||||
@@ -26,9 +32,9 @@
|
||||
/datum/tgui_window/New(client/client, id, pooled = FALSE)
|
||||
src.id = id
|
||||
src.client = client
|
||||
src.client.tgui_windows[id] = src
|
||||
src.pooled = pooled
|
||||
if(pooled)
|
||||
client.tgui_windows[id] = src
|
||||
src.pool_index = TGUI_WINDOW_INDEX(id)
|
||||
|
||||
/**
|
||||
@@ -39,18 +45,24 @@
|
||||
* will be put into the queue until the window finishes loading.
|
||||
*
|
||||
* optional inline_assets list List of assets to inline into the html.
|
||||
* optional inline_html string Custom HTML to inject.
|
||||
* optional fancy bool If TRUE, will hide the window titlebar.
|
||||
*/
|
||||
/datum/tgui_window/proc/initialize(inline_assets = list())
|
||||
/datum/tgui_window/proc/initialize(
|
||||
inline_assets = list(),
|
||||
inline_html = "",
|
||||
fancy = FALSE)
|
||||
log_tgui(client, "[id]/initialize")
|
||||
if(!client)
|
||||
return
|
||||
src.inline_assets = inline_assets
|
||||
src.fancy = fancy
|
||||
status = TGUI_WINDOW_LOADING
|
||||
fatally_errored = FALSE
|
||||
message_queue = null
|
||||
// Build window options
|
||||
var/options = "file=[id].html;can_minimize=0;auto_format=0;"
|
||||
// Remove titlebar and resize handles for a fancy window
|
||||
if(client.prefs.tgui_fancy)
|
||||
if(fancy)
|
||||
options += "titlebar=0;can_resize=0;"
|
||||
else
|
||||
options += "titlebar=1;can_resize=1;"
|
||||
@@ -72,10 +84,14 @@
|
||||
asset.send(client)
|
||||
html = replacetextEx(html, "<!-- tgui:styles -->\n", inline_styles)
|
||||
html = replacetextEx(html, "<!-- tgui:scripts -->\n", inline_scripts)
|
||||
// Inject custom HTML
|
||||
html = replacetextEx(html, "<!-- tgui:html -->\n", inline_html)
|
||||
// Open the window
|
||||
client << browse(html, "window=[id];[options]")
|
||||
// Instruct the client to signal UI when the window is closed.
|
||||
winset(client, id, "on-close=\"uiclose [id]\"")
|
||||
// Detect whether the control is a browser
|
||||
is_browser = winexists(client, id) == "BROWSER"
|
||||
|
||||
/**
|
||||
* public
|
||||
@@ -107,8 +123,8 @@
|
||||
* Acquire the window lock. Pool will not be able to provide this window
|
||||
* to other UIs for the duration of the lock.
|
||||
*
|
||||
* Can be given an optional tgui datum, which will hook its on_message
|
||||
* callback into the message stream.
|
||||
* Can be given an optional tgui datum, which will be automatically
|
||||
* subscribed to incoming messages via the on_message proc.
|
||||
*
|
||||
* optional ui /datum/tgui
|
||||
*/
|
||||
@@ -117,6 +133,8 @@
|
||||
locked_by = ui
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
* Release the window lock.
|
||||
*/
|
||||
/datum/tgui_window/proc/release_lock()
|
||||
@@ -126,6 +144,28 @@
|
||||
locked = FALSE
|
||||
locked_by = null
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
* Subscribes the datum to consume window messages on a specified proc.
|
||||
*
|
||||
* Note, that this supports only one subscriber, because code for that
|
||||
* is simpler and therefore faster. If necessary, this can be rewritten
|
||||
* to support multiple subscribers.
|
||||
*/
|
||||
/datum/tgui_window/proc/subscribe(datum/object, delegate)
|
||||
subscriber_object = object
|
||||
subscriber_delegate = delegate
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
* Unsubscribes the datum. Do not forget to call this when cleaning up.
|
||||
*/
|
||||
/datum/tgui_window/proc/unsubscribe(datum/object)
|
||||
subscriber_object = null
|
||||
subscriber_delegate = null
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
@@ -159,25 +199,40 @@
|
||||
* required payload list Message payload
|
||||
* optional force bool Send regardless of the ready status.
|
||||
*/
|
||||
/datum/tgui_window/proc/send_message(type, list/payload, force)
|
||||
/datum/tgui_window/proc/send_message(type, payload, force)
|
||||
if(!client)
|
||||
return
|
||||
var/message = json_encode(list(
|
||||
"type" = type,
|
||||
"payload" = payload,
|
||||
))
|
||||
// Strip #255/improper.
|
||||
message = replacetext(message, "\proper", "")
|
||||
message = replacetext(message, "\improper", "")
|
||||
// Pack for sending via output()
|
||||
message = url_encode(message)
|
||||
var/message = TGUI_CREATE_MESSAGE(type, payload)
|
||||
// Place into queue if window is still loading
|
||||
if(!force && status != TGUI_WINDOW_READY)
|
||||
if(!message_queue)
|
||||
message_queue = list()
|
||||
message_queue += list(message)
|
||||
return
|
||||
client << output(message, "[id].browser:update")
|
||||
client << output(message, is_browser \
|
||||
? "[id]:update" \
|
||||
: "[id].browser:update")
|
||||
|
||||
/**
|
||||
* public
|
||||
*
|
||||
* Sends a raw payload to tgui window.
|
||||
*
|
||||
* required message string JSON+urlencoded blob to send.
|
||||
* optional force bool Send regardless of the ready status.
|
||||
*/
|
||||
/datum/tgui_window/proc/send_raw_message(message, force)
|
||||
if(!client)
|
||||
return
|
||||
// Place into queue if window is still loading
|
||||
if(!force && status != TGUI_WINDOW_READY)
|
||||
if(!message_queue)
|
||||
message_queue = list()
|
||||
message_queue += list(message)
|
||||
return
|
||||
client << output(message, is_browser \
|
||||
? "[id]:update" \
|
||||
: "[id].browser:update")
|
||||
|
||||
/**
|
||||
* public
|
||||
@@ -191,12 +246,12 @@
|
||||
/datum/tgui_window/proc/send_asset(datum/asset/asset)
|
||||
if(!client || !asset)
|
||||
return
|
||||
sent_assets |= list(asset)
|
||||
. = asset.send(client)
|
||||
if(istype(asset, /datum/asset/spritesheet))
|
||||
var/datum/asset/spritesheet/spritesheet = asset
|
||||
send_message("asset/stylesheet", spritesheet.css_filename())
|
||||
send_message("asset/mappings", asset.get_url_mappings())
|
||||
sent_assets += list(asset)
|
||||
return asset.send(client)
|
||||
|
||||
/**
|
||||
* private
|
||||
@@ -207,7 +262,9 @@
|
||||
if(!client || !message_queue)
|
||||
return
|
||||
for(var/message in message_queue)
|
||||
client << output(message, "[id].browser:update")
|
||||
client << output(message, is_browser \
|
||||
? "[id]:update" \
|
||||
: "[id].browser:update")
|
||||
message_queue = null
|
||||
|
||||
/**
|
||||
@@ -215,26 +272,45 @@
|
||||
*
|
||||
* Callback for handling incoming tgui messages.
|
||||
*/
|
||||
/datum/tgui_window/proc/on_message(type, list/payload, list/href_list)
|
||||
switch(type)
|
||||
if("ready")
|
||||
// Status can be READY if user has refreshed the window.
|
||||
if(status == TGUI_WINDOW_READY)
|
||||
// Resend the assets
|
||||
for(var/asset in sent_assets)
|
||||
send_asset(asset)
|
||||
status = TGUI_WINDOW_READY
|
||||
if("log")
|
||||
if(href_list["fatal"])
|
||||
fatally_errored = TRUE
|
||||
/datum/tgui_window/proc/on_message(type, payload, href_list)
|
||||
// Status can be READY if user has refreshed the window.
|
||||
if(type == "ready" && status == TGUI_WINDOW_READY)
|
||||
// Resend the assets
|
||||
for(var/asset in sent_assets)
|
||||
send_asset(asset)
|
||||
// Mark this window as fatally errored which prevents it from
|
||||
// being suspended.
|
||||
if(type == "log" && href_list["fatal"])
|
||||
fatally_errored = TRUE
|
||||
// Mark window as ready since we received this message from somewhere
|
||||
if(status != TGUI_WINDOW_READY)
|
||||
status = TGUI_WINDOW_READY
|
||||
flush_message_queue()
|
||||
// Pass message to UI that requested the lock
|
||||
if(locked && locked_by)
|
||||
locked_by.on_message(type, payload, href_list)
|
||||
flush_message_queue()
|
||||
return
|
||||
var/prevent_default = locked_by.on_message(type, payload, href_list)
|
||||
if(prevent_default)
|
||||
return
|
||||
// Pass message to the subscriber
|
||||
else if(subscriber_object)
|
||||
var/prevent_default = call(
|
||||
subscriber_object,
|
||||
subscriber_delegate)(type, payload, href_list)
|
||||
if(prevent_default)
|
||||
return
|
||||
// If not locked, handle these message types
|
||||
switch(type)
|
||||
if("ping")
|
||||
send_message("pingReply", payload)
|
||||
if("suspend")
|
||||
close(can_be_suspended = TRUE)
|
||||
if("close")
|
||||
close(can_be_suspended = FALSE)
|
||||
if("openLink")
|
||||
client << link(href_list["url"])
|
||||
if("cacheReloaded")
|
||||
// Reinitialize
|
||||
initialize(inline_assets = inline_assets, fancy = fancy)
|
||||
// Resend the assets
|
||||
for(var/asset in sent_assets)
|
||||
send_asset(asset)
|
||||
|
||||
Reference in New Issue
Block a user