[Plugin] BetterChat v2.1.0

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 any website, including dedicated 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 rich embeds for any website, including dedicated embeds for video, audio, image and twitter content. Due to technical limitations, not all video and audio formats are supported at the moment.

Video Embed

Audio Embed

Audio Embed

Twitter Embed

Twitter Embed

Generic Embed

Generic Embed

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.

Installation

Notes:

  • Manual installation only works for TeamSpeak 5 beta 70
  • Depending on your installation directory you may need elevated permission privileges
  • The installation process needs to be repeated after each TeamSpeak update
  • Please check the compatibility section before installing BetterChat

Installer

Installation steps:

  1. Download the installer for your operating system from here
  2. Start the installer
  3. Select your TeamSpeak installation directory
  4. Select “BetterChat”
  5. Click on “Install”

Manual

Installation steps:

  1. Navigate to your TeamSpeak installation directory
  2. Navigate to ./html/client_ui/
  3. Open index.html in that directory with an editor and copy all contents from the index.html of this repository before </head> and save the file
  4. Create a new folder named betterchat
  5. Copy all contents from src/ into the newly created folder
  6. And you are done! You can now start TeamSpeak

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.

Compatibility

TeamSpeak Windows Linux MacOS
Beta 74 2.1.0+ 2.1.0+ 2.1.0+
Beta 73 1.2.0+ 1.2.0+ 1.2.0+
Beta 72 1.1.0+ 1.1.0+ n/a
Beta 71 :x: :x: n/a
Beta 70 1.0.0 - 1.0.4 1.0.0 - 1.0.4 1.0.0 - 1.0.4

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.

We updated the Installer and BetterChat to be compatible with the latest version of TeamSpeak (5.0.0-beta72). We also fixed the issue with the vuejs virtual scroller (hopefully).

3 Likes

We updated the Installer and BetterChat to be compatible with the latest version of TeamSpeak (5.0.0-beta73).

1 Like

We updated the Installer and BetterChat to be compatible with the latest version of TeamSpeak (5.0.0-beta74).

1 Like
twitch instagram twitter facebook