[Plugin] BetterChat

BetterChat

BetterChat is an unofficial addon for TeamSpeak 5 and aims to provide a better chat experience for the TeamSpeak 5 client. It enables support for BBCodes, improving messages sent by TeamSpeak 3 users, and automatic rich embeds for video, audio, image and twitter content. It works in both compact and detailed view.

BBCode support

BetterChat readds support for BBCodes in chat, just like in TeamSpeak 3. Currently the following tags are supported:

Name Syntax Example
bold [b]text[/b] bold
code [code]text[/code] code
color [color=hexcode]text[/color] or [color=color]text[/color] color
italic [i]text[/i] italic
spoiler [spoiler]text[/spoiler] spoiler
strike [s]text[/s] strike
underline [u]text[/u] underline
url [url]link[/url] or [url=link]text[/url] url

Rich Embeds

BetterChat supports automatic embedding of any audio or video content. It also supports image and twitter embedding. Due to technical reasons it is not possible to automatically create embeds for other resources.

Video Embed

Audio Embed

Audio Embed

Twitter Embed

Twitter Embed

Due to technical limitations not all video and audio formats are supported at the moment.

Styling

Custom styling for the image preview can be changed in the style.css.
Rich embeds use the already existing css classes of the TeamSpeak client and should be compatible with already existing themes.

Download and Installation

:warning: The addon needs to be reinstalled after every TeamSpeak update

A download and installation instructions can be found on GitHub including an installer made by FelixVolo. Note that this addon modifies your existing TeamSpeak 5 installation.

Configuration

BetterChat can be enabled and disabled while TeamSpeak is running.
Just go to the settings menu inside TeamSpeak and navigate to Behavior.
There you can toggle specific features, like BBCode support or Rich Embeds, or enable and disable the addon entirely.

Acknowledgements

This addon is inspired by the TeamSpeak UNOFFICIAL Plugin Installer by Gamer9200

9 Likes

So, I didn’t look too deep at your code, but it looks really well written!
It seems you manipulate the actual DOM the same way I do.
For me, this results in some problems with the virtual scroller of the chat when rendering Twitter embeds.
Technically you can access the vuejs virtual scroller content and manipulate the render function directly. This would even eliminate the need for the MutationObserver and greatly improve performance. Seeing your dedication to this project, this would be an awesome next step. :stuck_out_tongue:

Also, your README suggests that MP4 (and others) are supported while in fact, it can’t be, as TeamSpeak’s CEF is compiled without proprietary codec support.

5 Likes

Thank you!

I would be very thankful if you would show me a proof of concept!

Good catch! I thought that the website would only list the available codecs. Technically you can get mp4 support in ts5 if you recompile cef with propietary codecs enabled, although its pretty complicated (but it works).

1 Like

It took some considerable time to figure all this vuejs stuff out, but I got there!
By modifying the component loader in the vendors.js file you can manipulate components before they are created or mounted. In this POC I opted to manipulate the tsv-virtual-list-item component. Keep in mind that the component IDs generated by webpack sadly change per update and thus you will need to reverse the main.js after each update.

POC

vendor.js after formatting

--- a/vendor.js
+++ b/vendor.js
@@ -2,7 +2,125 @@

-function e(r) { 
-  var o = n[r];
-  if (void 0 !== o) return o.exports;
-  var i = (n[r] = { id: r, loaded: !1, exports: {} });
-  return t[r].call(i.exports, i, i.exports, e), (i.loaded = !0), i.exports;
-}
+function oldE(r) {
+  var o = n[r];
+  if (void 0 !== o) return o.exports;
+  var i = (n[r] = { id: r, loaded: !1, exports: {} });
+  return t[r].call(i.exports, i, i.exports, e), (i.loaded = !0), i.exports;
+}
+
+const linkRegex =
+  /[[email protected]:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+
+const contentTypeMap = {
+  jpg: "image/jpeg",
+  jpeg: "image/jpeg",
+  png: "image/png",
+  gif: "image/gif",
+  webp: "image/webp",
+  svg: "image/svg+xml",
+};
+
+function e(r) {
+  let result = oldE(r);
+  if (r === 661163) {
+    result = {
+      s: function () {
+        var e = this,
+          t = e._self._c;
+        e._self._setupProxy;
+
+        const item = e.item;
+
+        const children = [
+          e.item
+            ? t(
+                e.component,
+                e._g(
+                  e._b(
+                    {
+                      key: e.uniqueId,
+                      tag: "component",
+                      style: { opacity: e.finalHeight ? "1" : "0" },
+                      attrs: { role: "listitem" },
+                    },
+                    "component",
+                    e.filteredProps,
+                    !1
+                  ),
+                  e.wrappedListeners
+                )
+              )
+            : e._e(),
+        ];
+
+        try {
+          if (
+            !item.isSystem &&
+            !item.parsedItem?.hideText &&
+            !(item.parsedItem?.attachments?.length || false)
+          ) {
+            const text = item.original || item.body;
+            if (text.match(linkRegex)) {
+              console.error(this);
+              const extension = text.split(".").pop().split("?")[0];
+              const filename = text.split("/").pop().split("?")[0];
+              if (
+                ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension)
+              ) {
+                children.push(
+                  t(
+                    "div",
+                    {
+                      staticClass: "test",
+                      style: { "padding-left": "70px" },
+                    },
+                    [
+                      t("img", {
+                        attrs: { src: text },
+                        style: {
+                          width: "100%",
+                          "max-width": "350px",
+                          cursor: "pointer",
+                        },
+                        on: {
+                          click() {
+                            e.$root.appController.events.onInvokeLightboxEmitter.fire(
+                              {
+                                invocation: {
+                                  url: text,
+                                  thumbnailUrl: text,
+                                  contentType: contentTypeMap[extension],
+                                  contentSize: 0,
+                                  footerActions: [],
+                                  headerLabel: filename,
+                                  bodyLabel: "ᴱᵐᵇᵉᵈ ᵖᵒʷᵉʳᵉᵈ ᵇʸ ᴳᵃᵐᵉʳ⁹²⁰⁰⁰",
+                                },
+                              }
+                            );
+                          },
+                        },
+                      }),
+                    ]
+                  )
+                );
+              }
+            }
+          }
+        } catch (e) {
+          console.error(e);
+        }
+
+        return t(
+          "div",
+          {
+            ref: "itemRef",
+            staticClass: "tsv-virtual-list-item",
+            style: e.itemStyles,
+          },
+          children
+        );
+      },
+      x: result.x,
+    };
+  }
+  return result;
+}

This will crudely embed images and allow you to click on them just like other shared images.
TeamSpeak_FP3MfLvkhp

I actually did this once. But sadly you can neither distribute such a self-compiled version nor can you expect the users to do so. Also I wouldn’t want to compile this for a windows target…

EDIT: changed the POC to do something actually useful.
EDIT2: I just couldn’t stop myself…
Now the embed uses the native overlay to display a larger version of the image when clicked on.

4 Likes

I think this could also be done without modifying the vendor.js. You can access the vue instance with document.body.querySelector("#app").__vue__ or call $0.__vue__ on any other component.

2 Likes

Well, I’m not that proficient with vuejs + webpack compiled and bundled code.
So I don’t think you could patch the component loader this way.
Nonetheless, whether you modify the index.html or vendor.js is not much of a difference, is it?

My main problem was that I needed to modify the component loader before the vendor.js was actually loaded in order for it to actually use the patched version. Otherwise, the original version was just everywhere in the call stack and referenced from everywhere directly.

2 Likes

awesome work!
sadly i couldn’t make it work with channel description, only chats.

twitch instagram twitter facebook