@thestatic-tv/dcl-sdk 1.0.7 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,12 +17,22 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ ChatUIModule: () => ChatUIModule,
23
34
  GuideModule: () => GuideModule,
35
+ GuideUIModule: () => GuideUIModule,
24
36
  HeartbeatModule: () => HeartbeatModule,
25
37
  InteractionsModule: () => InteractionsModule,
26
38
  KEY_TYPE_CHANNEL: () => KEY_TYPE_CHANNEL,
@@ -124,8 +136,8 @@ function getPlayerWallet() {
124
136
  return cachedWallet;
125
137
  }
126
138
  try {
127
- const { getPlayer } = require("@dcl/sdk/players");
128
- const player = getPlayer();
139
+ const { getPlayer: getPlayer2 } = require("@dcl/sdk/players");
140
+ const player = getPlayer2();
129
141
  return player?.userId ?? null;
130
142
  } catch {
131
143
  return null;
@@ -136,8 +148,8 @@ function getPlayerDisplayName() {
136
148
  return cachedDisplayName;
137
149
  }
138
150
  try {
139
- const { getPlayer } = require("@dcl/sdk/players");
140
- const player = getPlayer();
151
+ const { getPlayer: getPlayer2 } = require("@dcl/sdk/players");
152
+ const player = getPlayer2();
141
153
  return player?.name ?? null;
142
154
  } catch {
143
155
  return null;
@@ -186,6 +198,50 @@ function dclClearInterval(timerId) {
186
198
  timers.delete(timerId);
187
199
  }
188
200
  }
201
+ var timeouts = /* @__PURE__ */ new Map();
202
+ var timeoutSystemAdded = false;
203
+ function ensureTimeoutSystem() {
204
+ if (timeoutSystemAdded) return;
205
+ import_ecs.engine.addSystem((dt) => {
206
+ const toRemove = [];
207
+ for (const timeout of timeouts.values()) {
208
+ if (!timeout.active) continue;
209
+ timeout.elapsedTime += dt;
210
+ if (timeout.elapsedTime >= timeout.delaySeconds) {
211
+ timeout.active = false;
212
+ toRemove.push(timeout.id);
213
+ try {
214
+ timeout.callback();
215
+ } catch (e) {
216
+ console.error("[StaticTV Timer] Timeout callback error:", e);
217
+ }
218
+ }
219
+ }
220
+ for (const id of toRemove) {
221
+ timeouts.delete(id);
222
+ }
223
+ });
224
+ timeoutSystemAdded = true;
225
+ }
226
+ function dclSetTimeout(callback, delayMs) {
227
+ ensureTimeoutSystem();
228
+ const id = nextTimerId++;
229
+ timeouts.set(id, {
230
+ id,
231
+ callback,
232
+ delaySeconds: delayMs / 1e3,
233
+ elapsedTime: 0,
234
+ active: true
235
+ });
236
+ return id;
237
+ }
238
+ function dclClearTimeout(timeoutId) {
239
+ const timeout = timeouts.get(timeoutId);
240
+ if (timeout) {
241
+ timeout.active = false;
242
+ timeouts.delete(timeoutId);
243
+ }
244
+ }
189
245
 
190
246
  // src/modules/session.ts
191
247
  var SessionModule = class {
@@ -498,6 +554,1548 @@ var InteractionsModule = class {
498
554
  }
499
555
  };
500
556
 
557
+ // src/ui/guide-ui.tsx
558
+ var import_react_ecs2 = __toESM(require("@dcl/sdk/react-ecs"));
559
+ var import_math3 = require("@dcl/sdk/math");
560
+ var import_RestrictedActions = require("~system/RestrictedActions");
561
+
562
+ // src/ui/theme.ts
563
+ var import_math = require("@dcl/sdk/math");
564
+ var THEME = {
565
+ colors: {
566
+ panel: import_math.Color4.create(0.05, 0.05, 0.08, 0.98),
567
+ panelDark: import_math.Color4.create(0.04, 0.04, 0.06, 1),
568
+ panelBorder: import_math.Color4.create(0.2, 0.2, 0.2, 1),
569
+ inputBackground: import_math.Color4.create(0.1, 0.1, 0.12, 1),
570
+ buttonBackground: import_math.Color4.create(0.15, 0.15, 0.18, 1),
571
+ buttonHover: import_math.Color4.create(0.2, 0.2, 0.24, 1),
572
+ activeBackground: import_math.Color4.create(0, 1, 1, 0.1),
573
+ cyan: import_math.Color4.create(0, 1, 1, 1),
574
+ magenta: import_math.Color4.create(1, 0, 1, 1),
575
+ yellow: import_math.Color4.create(1, 0.8, 0, 1),
576
+ red: import_math.Color4.create(1, 0.2, 0.2, 1),
577
+ green: import_math.Color4.create(0, 1, 0, 1),
578
+ white: import_math.Color4.White(),
579
+ gray: import_math.Color4.create(0.5, 0.5, 0.5, 1),
580
+ grayText: import_math.Color4.create(0.35, 0.35, 0.35, 1),
581
+ darkGray: import_math.Color4.create(0.15, 0.15, 0.15, 1)
582
+ }
583
+ };
584
+ var UI_DIMENSIONS = {
585
+ // Guide UI
586
+ guide: {
587
+ width: 900,
588
+ height: "55%",
589
+ bottom: 55,
590
+ right: 20,
591
+ sidebar: {
592
+ width: 300,
593
+ buttonHeight: 42,
594
+ fontSize: 16,
595
+ headerSize: 24,
596
+ subHeaderSize: 14
597
+ },
598
+ content: {
599
+ cardHeight: 150,
600
+ cardWidth: 260,
601
+ headerSize: 20,
602
+ bodySize: 16,
603
+ padding: 15
604
+ }
605
+ },
606
+ // Chat UI
607
+ chat: {
608
+ width: 340,
609
+ height: 480,
610
+ bottom: 55,
611
+ right: 20,
612
+ messagesPerPage: 5,
613
+ channelsPerPage: 6
614
+ },
615
+ // Shared
616
+ closeButton: {
617
+ size: 40,
618
+ fontSize: 24
619
+ }
620
+ };
621
+ var DEFAULT_CHAT_THEME = {
622
+ header: 22,
623
+ channelButton: 14,
624
+ channelDropdown: 14,
625
+ systemMessage: 14,
626
+ chatUsername: 16,
627
+ chatTimestamp: 11,
628
+ chatMessage: 16,
629
+ input: 16,
630
+ sendButton: 14,
631
+ userInfo: 14,
632
+ authStatus: 12,
633
+ notification: 18,
634
+ closeButton: 16
635
+ };
636
+ function scaleChatTheme(theme, fontScale) {
637
+ return {
638
+ header: Math.round(theme.header * fontScale),
639
+ channelButton: Math.round(theme.channelButton * fontScale),
640
+ channelDropdown: Math.round(theme.channelDropdown * fontScale),
641
+ systemMessage: Math.round(theme.systemMessage * fontScale),
642
+ chatUsername: Math.round(theme.chatUsername * fontScale),
643
+ chatTimestamp: Math.round(theme.chatTimestamp * fontScale),
644
+ chatMessage: Math.round(theme.chatMessage * fontScale),
645
+ input: Math.round(theme.input * fontScale),
646
+ sendButton: Math.round(theme.sendButton * fontScale),
647
+ userInfo: Math.round(theme.userInfo * fontScale),
648
+ authStatus: Math.round(theme.authStatus * fontScale),
649
+ notification: Math.round(theme.notification * fontScale),
650
+ closeButton: Math.round(theme.closeButton * fontScale)
651
+ };
652
+ }
653
+
654
+ // src/ui/components.tsx
655
+ var import_react_ecs = __toESM(require("@dcl/sdk/react-ecs"));
656
+ var import_math2 = require("@dcl/sdk/math");
657
+ var PanelHeader = (props) => {
658
+ const fontSize = props.fontSize || 14;
659
+ const fontScale = props.fontScale || 1;
660
+ const scaledFontSize = Math.round(fontSize * fontScale);
661
+ const currentPos = props.position || "right";
662
+ return import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
663
+ uiTransform: {
664
+ height: 36,
665
+ minHeight: 36,
666
+ flexShrink: 0,
667
+ width: "100%",
668
+ flexDirection: "row",
669
+ justifyContent: "space-between",
670
+ alignItems: "center",
671
+ padding: { left: 10, right: 6 }
672
+ },
673
+ uiBackground: { color: THEME.colors.panelDark },
674
+ children: [
675
+ // Title
676
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
677
+ uiText: { value: props.title, fontSize: scaledFontSize, color: THEME.colors.cyan }
678
+ }),
679
+ // Controls
680
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
681
+ uiTransform: { flexDirection: "row", gap: 4, alignItems: "center" },
682
+ children: [
683
+ // Position controls
684
+ ...props.showPositionControls ? [
685
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
686
+ key: "pos-left",
687
+ uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
688
+ uiBackground: { color: currentPos === "left" ? import_math2.Color4.create(0, 0.3, 0.3, 1) : THEME.colors.buttonBackground },
689
+ onMouseDown: () => props.onPositionChange?.("left"),
690
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u25C0", fontSize: 10, color: THEME.colors.cyan } })]
691
+ }),
692
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
693
+ key: "pos-center",
694
+ uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
695
+ uiBackground: { color: currentPos === "center" ? import_math2.Color4.create(0, 0.3, 0.3, 1) : THEME.colors.buttonBackground },
696
+ onMouseDown: () => props.onPositionChange?.("center"),
697
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u25CF", fontSize: 10, color: THEME.colors.cyan } })]
698
+ }),
699
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
700
+ key: "pos-right",
701
+ uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
702
+ uiBackground: { color: currentPos === "right" ? import_math2.Color4.create(0, 0.3, 0.3, 1) : THEME.colors.buttonBackground },
703
+ onMouseDown: () => props.onPositionChange?.("right"),
704
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u25B6", fontSize: 10, color: THEME.colors.cyan } })]
705
+ }),
706
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "pos-spacer", uiTransform: { width: 6 } })
707
+ ] : [],
708
+ // Font controls
709
+ ...props.showFontControls ? [
710
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
711
+ key: "font-down",
712
+ uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
713
+ uiBackground: { color: THEME.colors.buttonBackground },
714
+ onMouseDown: () => props.onFontScaleDown?.(),
715
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: THEME.colors.cyan } })]
716
+ }),
717
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
718
+ key: "font-up",
719
+ uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
720
+ uiBackground: { color: THEME.colors.buttonBackground },
721
+ onMouseDown: () => props.onFontScaleUp?.(),
722
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "+", fontSize: 14, color: THEME.colors.cyan } })]
723
+ }),
724
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "font-spacer", uiTransform: { width: 6 } })
725
+ ] : [],
726
+ // Close button
727
+ ...props.onClose ? [
728
+ import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
729
+ key: "close",
730
+ uiTransform: { width: 24, height: 22, justifyContent: "center", alignItems: "center" },
731
+ uiBackground: { color: THEME.colors.buttonBackground },
732
+ onMouseDown: () => props.onClose?.(),
733
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "X", fontSize: 12, color: THEME.colors.red } })]
734
+ })
735
+ ] : []
736
+ ]
737
+ })
738
+ ]
739
+ });
740
+ };
741
+ function truncateText(text, maxLength) {
742
+ if (text.length <= maxLength) return text;
743
+ return text.substring(0, maxLength - 3) + "...";
744
+ }
745
+
746
+ // src/ui/guide-ui.tsx
747
+ var GuideUIModule = class {
748
+ constructor(client, config = {}) {
749
+ // Visibility state
750
+ this._isVisible = false;
751
+ // Data state
752
+ this.videos = [];
753
+ this.featuredPlaylists = [];
754
+ this.liveVideos = [];
755
+ // UI state
756
+ this.activeTab = "SIGNALS";
757
+ this.currentPage = 0;
758
+ this.itemsPerPage = 6;
759
+ this.searchQuery = "";
760
+ this.uiScale = 1;
761
+ // Current video tracking (for "PLAYING" indicator)
762
+ this._currentVideoId = null;
763
+ // =============================================================================
764
+ // --- UI RENDERING ---
765
+ // =============================================================================
766
+ /**
767
+ * Get the guide UI component for rendering
768
+ */
769
+ this.getComponent = () => {
770
+ if (!this._isVisible) return null;
771
+ const windowW = this.s(UI_DIMENSIONS.guide.width);
772
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
773
+ uiTransform: {
774
+ width: windowW,
775
+ height: UI_DIMENSIONS.guide.height,
776
+ positionType: "absolute",
777
+ position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.guide.right },
778
+ flexDirection: "row",
779
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
780
+ borderColor: THEME.colors.panelBorder
781
+ },
782
+ uiBackground: { color: THEME.colors.panel },
783
+ children: [
784
+ this.renderLeftPanel(),
785
+ this.renderRightPanel(),
786
+ this.renderCloseButton()
787
+ ]
788
+ });
789
+ };
790
+ this.client = client;
791
+ this.config = config;
792
+ this.uiScale = config.uiScale || 1;
793
+ this._currentVideoId = config.currentVideoId || null;
794
+ }
795
+ /**
796
+ * Initialize the guide - fetch channel data
797
+ */
798
+ async init() {
799
+ await this.fetchGuideData();
800
+ this.client.log("GuideUI initialized");
801
+ }
802
+ /**
803
+ * Show the guide UI
804
+ */
805
+ show() {
806
+ this._isVisible = true;
807
+ this.fetchGuideData().catch(() => {
808
+ });
809
+ }
810
+ /**
811
+ * Hide the guide UI
812
+ */
813
+ hide() {
814
+ this._isVisible = false;
815
+ this.activeTab = "SIGNALS";
816
+ this.searchQuery = "";
817
+ this.currentPage = 0;
818
+ }
819
+ /**
820
+ * Toggle guide visibility
821
+ */
822
+ toggle() {
823
+ if (this._isVisible) {
824
+ this.hide();
825
+ } else {
826
+ this.show();
827
+ }
828
+ }
829
+ /**
830
+ * Check if guide is visible
831
+ */
832
+ get isVisible() {
833
+ return this._isVisible;
834
+ }
835
+ /**
836
+ * Set the currently playing video ID (for "PLAYING" indicator)
837
+ */
838
+ set currentVideoId(id) {
839
+ this._currentVideoId = id;
840
+ }
841
+ /**
842
+ * Get the currently playing video ID
843
+ */
844
+ get currentVideoId() {
845
+ return this._currentVideoId;
846
+ }
847
+ /**
848
+ * Get all videos
849
+ */
850
+ getVideos() {
851
+ return this.videos;
852
+ }
853
+ /**
854
+ * Get live videos only
855
+ */
856
+ getLiveVideos() {
857
+ return this.liveVideos;
858
+ }
859
+ /**
860
+ * Refresh guide data
861
+ */
862
+ async refresh() {
863
+ await this.fetchGuideData();
864
+ }
865
+ // =============================================================================
866
+ // --- INTERNAL DATA FETCHING ---
867
+ // =============================================================================
868
+ getBaseUrl() {
869
+ const config = this.client.getConfig();
870
+ const baseUrl = config.baseUrl || "https://thestatic.tv/api/v1/dcl";
871
+ return baseUrl.replace("/api/v1/dcl", "");
872
+ }
873
+ async fetchGuideData() {
874
+ try {
875
+ const baseUrl = this.getBaseUrl();
876
+ const response = await fetch(`${baseUrl}/api/v1/guide`);
877
+ if (!response.ok) {
878
+ this.client.log("Guide API request failed");
879
+ return;
880
+ }
881
+ const data = await response.json();
882
+ const videos = [];
883
+ const liveVideos = [];
884
+ if (data.videoMap) {
885
+ for (const [slug, video] of Object.entries(data.videoMap)) {
886
+ const v = video;
887
+ const mapped = {
888
+ id: v.id || slug,
889
+ name: v.name || v.title || "Unknown",
890
+ src: v.src || v.liveStreamUrl || "",
891
+ isLive: v.isLive === true || v.type === "live",
892
+ isCreator: v.isCreator || v.type === "live",
893
+ channelId: v.channelId || slug,
894
+ duration: v.duration,
895
+ requiresAuth: v.requiresAuth
896
+ };
897
+ videos.push(mapped);
898
+ if (mapped.isLive) {
899
+ liveVideos.push(mapped);
900
+ }
901
+ }
902
+ }
903
+ const playlists = [];
904
+ if (data.featuredPlaylists) {
905
+ for (const pl of data.featuredPlaylists) {
906
+ playlists.push({
907
+ name: pl.name,
908
+ videos: (pl.videos || []).map((v) => ({
909
+ id: v.id,
910
+ name: v.name || v.title || "Unknown",
911
+ src: v.src || v.liveStreamUrl || "",
912
+ isLive: v.isLive === true,
913
+ isCreator: v.isCreator,
914
+ channelId: v.channelId || v.id,
915
+ duration: v.duration,
916
+ requiresAuth: v.requiresAuth
917
+ }))
918
+ });
919
+ }
920
+ }
921
+ this.videos = videos;
922
+ this.liveVideos = liveVideos;
923
+ this.featuredPlaylists = playlists;
924
+ this.client.log(`Guide loaded: ${videos.length} videos, ${playlists.length} playlists`);
925
+ } catch (e) {
926
+ this.client.log(`Failed to fetch guide data: ${e}`);
927
+ }
928
+ }
929
+ // =============================================================================
930
+ // --- UTILITIES ---
931
+ // =============================================================================
932
+ s(value) {
933
+ return Math.round(value * this.uiScale);
934
+ }
935
+ handleVideoSelect(video) {
936
+ if (this.config.onVideoSelect) {
937
+ this.config.onVideoSelect(video);
938
+ }
939
+ }
940
+ handleChannelClick(video) {
941
+ const baseUrl = this.getBaseUrl();
942
+ const channelUrl = `${baseUrl}/${video.channelId || video.id}`;
943
+ (0, import_RestrictedActions.openExternalUrl)({ url: channelUrl });
944
+ }
945
+ renderLeftPanel() {
946
+ const sidebarW = this.s(UI_DIMENSIONS.guide.sidebar.width);
947
+ const liveCount = this.liveVideos.length;
948
+ const signalCount = this.videos.filter((v) => !v.isLive).length;
949
+ const nodesPlaylist = this.featuredPlaylists.find((p) => p.name === "NODES");
950
+ const nodesCount = nodesPlaylist ? nodesPlaylist.videos.filter((v) => v.isCreator).length : 0;
951
+ const displayedPlaylists = this.featuredPlaylists.filter((p) => p.name !== "NODES").slice(0, 6);
952
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
953
+ uiTransform: {
954
+ width: sidebarW,
955
+ minWidth: sidebarW,
956
+ maxWidth: sidebarW,
957
+ height: "100%",
958
+ flexDirection: "column",
959
+ border: { right: 1 },
960
+ borderColor: THEME.colors.panelBorder,
961
+ justifyContent: "flex-start"
962
+ },
963
+ uiBackground: { color: import_math3.Color4.create(0, 0, 0, 0.4) },
964
+ children: [
965
+ // Header & Search
966
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
967
+ uiTransform: { width: "100%", flexDirection: "column", padding: 10 },
968
+ children: [
969
+ // Title
970
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
971
+ uiText: {
972
+ value: "THE STATIC TV",
973
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
974
+ color: THEME.colors.cyan,
975
+ textAlign: "middle-center"
976
+ },
977
+ uiTransform: { height: this.s(40), marginBottom: 10 }
978
+ }),
979
+ // Random Signal button
980
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
981
+ uiTransform: {
982
+ width: "100%",
983
+ height: this.s(40),
984
+ marginBottom: 10,
985
+ justifyContent: "center",
986
+ alignItems: "center",
987
+ border: { top: 1, bottom: 1, left: 1, right: 1 },
988
+ borderColor: THEME.colors.cyan
989
+ },
990
+ uiBackground: { color: import_math3.Color4.create(0, 0.2, 0.2, 0.3) },
991
+ onMouseDown: () => {
992
+ if (this.videos.length > 0) {
993
+ const randomVideo = this.videos[Math.floor(Math.random() * this.videos.length)];
994
+ this.handleVideoSelect(randomVideo);
995
+ }
996
+ },
997
+ children: [
998
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
999
+ uiText: { value: ">> RANDOM SIGNAL <<", fontSize: this.s(14), color: THEME.colors.cyan }
1000
+ })
1001
+ ]
1002
+ }),
1003
+ // Search input
1004
+ import_react_ecs2.default.createElement(import_react_ecs2.Input, {
1005
+ placeholder: "SCAN FREQ...",
1006
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.fontSize),
1007
+ uiTransform: {
1008
+ width: "100%",
1009
+ height: this.s(40),
1010
+ border: { top: 1, bottom: 1, left: 1, right: 1 },
1011
+ borderColor: THEME.colors.grayText
1012
+ },
1013
+ color: THEME.colors.cyan,
1014
+ placeholderColor: THEME.colors.grayText,
1015
+ uiBackground: { color: import_math3.Color4.create(0, 0, 0, 1) },
1016
+ onChange: (val) => {
1017
+ this.searchQuery = val;
1018
+ this.currentPage = 0;
1019
+ }
1020
+ })
1021
+ ]
1022
+ }),
1023
+ // Menu items
1024
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1025
+ uiTransform: { flexGrow: 1, flexDirection: "column", width: "100%", padding: { top: 5 } },
1026
+ children: [
1027
+ // Section header
1028
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1029
+ uiTransform: { padding: { left: 10, bottom: 8, top: 5 } },
1030
+ uiText: { value: "GLOBAL NETWORK", fontSize: this.s(12), color: THEME.colors.cyan }
1031
+ }),
1032
+ // Menu buttons
1033
+ this.renderMenuButton("LIVE NOW", liveCount, THEME.colors.red, liveCount > 0 ? "\u25CF" : void 0, "ACTIVE STREAMS"),
1034
+ this.renderMenuButton("SIGNALS", signalCount, THEME.colors.white, void 0, "ALL VIDEOS"),
1035
+ this.renderMenuButton("NODES", nodesCount, THEME.colors.magenta, void 0, "CHANNELS & CREATORS"),
1036
+ // Divider
1037
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1038
+ uiTransform: { height: 1, width: "90%", margin: { top: 12, bottom: 8, left: 5 } },
1039
+ uiBackground: { color: THEME.colors.panelBorder }
1040
+ }),
1041
+ // Archives section
1042
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1043
+ uiTransform: { padding: { left: 10, bottom: 8 } },
1044
+ uiText: { value: "ARCHIVES", fontSize: this.s(12), color: THEME.colors.cyan }
1045
+ }),
1046
+ // Playlist buttons
1047
+ ...displayedPlaylists.map(
1048
+ (pl) => this.renderMenuButton(pl.name.toUpperCase(), pl.videos.length, THEME.colors.white, void 0, "PLAYLIST")
1049
+ )
1050
+ ]
1051
+ })
1052
+ ]
1053
+ });
1054
+ }
1055
+ renderMenuButton(label, count, color, badge, subtitle) {
1056
+ const isActive = this.activeTab === label;
1057
+ const buttonHeight = this.s(UI_DIMENSIONS.guide.sidebar.buttonHeight);
1058
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1059
+ key: label,
1060
+ uiTransform: {
1061
+ width: "100%",
1062
+ height: buttonHeight,
1063
+ marginBottom: 2,
1064
+ flexDirection: "row",
1065
+ alignItems: "center",
1066
+ justifyContent: "space-between",
1067
+ padding: { left: 10, right: 10 },
1068
+ border: { left: 4 },
1069
+ borderColor: isActive ? color : import_math3.Color4.Clear()
1070
+ },
1071
+ uiBackground: { color: isActive ? import_math3.Color4.create(color.r, color.g, color.b, 0.15) : import_math3.Color4.Clear() },
1072
+ onMouseDown: () => {
1073
+ this.activeTab = label;
1074
+ this.currentPage = 0;
1075
+ },
1076
+ children: [
1077
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1078
+ uiTransform: { flexDirection: "column", alignItems: "flex-start", justifyContent: "center", gap: 0 },
1079
+ children: [
1080
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1081
+ uiTransform: { height: this.s(18) },
1082
+ uiText: {
1083
+ value: `${label} [${count}]`,
1084
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.fontSize),
1085
+ color: isActive ? color : THEME.colors.white,
1086
+ textAlign: "middle-left"
1087
+ }
1088
+ }),
1089
+ ...subtitle ? [
1090
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1091
+ key: "subtitle",
1092
+ uiTransform: { height: this.s(12), marginTop: -2 },
1093
+ uiText: {
1094
+ value: subtitle,
1095
+ fontSize: this.s(9),
1096
+ color: THEME.colors.grayText,
1097
+ textAlign: "middle-left"
1098
+ }
1099
+ })
1100
+ ] : []
1101
+ ]
1102
+ }),
1103
+ ...badge ? [
1104
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1105
+ key: "badge",
1106
+ uiText: { value: badge, fontSize: this.s(UI_DIMENSIONS.guide.sidebar.fontSize), color: THEME.colors.red }
1107
+ })
1108
+ ] : []
1109
+ ]
1110
+ });
1111
+ }
1112
+ renderRightPanel() {
1113
+ let videosToShow = [];
1114
+ const nodePl = this.featuredPlaylists.find((p) => p.name === "NODES");
1115
+ if (this.activeTab === "SIGNALS") {
1116
+ videosToShow = this.videos.filter((v) => !v.isLive);
1117
+ } else if (this.activeTab === "NODES") {
1118
+ videosToShow = nodePl ? nodePl.videos.filter((v) => v.isCreator) : [];
1119
+ } else if (this.activeTab === "LIVE NOW") {
1120
+ videosToShow = this.liveVideos;
1121
+ } else {
1122
+ const pl = this.featuredPlaylists.find((p) => p.name.toUpperCase() === this.activeTab);
1123
+ if (pl) videosToShow = pl.videos;
1124
+ }
1125
+ if (this.searchQuery) {
1126
+ videosToShow = videosToShow.filter(
1127
+ (v) => v.name?.toLowerCase().includes(this.searchQuery.toLowerCase())
1128
+ );
1129
+ }
1130
+ const start = this.currentPage * this.itemsPerPage;
1131
+ const paged = videosToShow.slice(start, start + this.itemsPerPage);
1132
+ const totalPages = Math.ceil(videosToShow.length / this.itemsPerPage);
1133
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1134
+ uiTransform: {
1135
+ flexGrow: 1,
1136
+ flexDirection: "column",
1137
+ height: "100%",
1138
+ padding: this.s(UI_DIMENSIONS.guide.content.padding)
1139
+ },
1140
+ children: [
1141
+ this.renderCardGrid(paged),
1142
+ // Pagination
1143
+ ...totalPages > 1 ? [
1144
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1145
+ key: "pagination",
1146
+ uiTransform: {
1147
+ width: "100%",
1148
+ height: this.s(40),
1149
+ flexShrink: 0,
1150
+ flexDirection: "row",
1151
+ justifyContent: "center",
1152
+ alignItems: "center",
1153
+ gap: 15
1154
+ },
1155
+ children: [
1156
+ ...this.currentPage > 0 ? [
1157
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1158
+ key: "prev",
1159
+ uiText: {
1160
+ value: "< PREV",
1161
+ fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize),
1162
+ color: THEME.colors.cyan
1163
+ },
1164
+ onMouseDown: () => this.currentPage--
1165
+ })
1166
+ ] : [],
1167
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1168
+ uiText: {
1169
+ value: `${this.currentPage + 1}/${totalPages}`,
1170
+ fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize),
1171
+ color: THEME.colors.grayText
1172
+ }
1173
+ }),
1174
+ ...this.currentPage < totalPages - 1 ? [
1175
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1176
+ key: "next",
1177
+ uiText: {
1178
+ value: "NEXT >",
1179
+ fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize),
1180
+ color: THEME.colors.cyan
1181
+ },
1182
+ onMouseDown: () => this.currentPage++
1183
+ })
1184
+ ] : []
1185
+ ]
1186
+ })
1187
+ ] : []
1188
+ ]
1189
+ });
1190
+ }
1191
+ renderCardGrid(videos) {
1192
+ if (videos.length === 0) {
1193
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1194
+ uiTransform: { width: "100%", flexGrow: 1, justifyContent: "center", alignItems: "center" },
1195
+ uiText: { value: "NO SIGNAL", fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize), color: THEME.colors.gray }
1196
+ });
1197
+ }
1198
+ const rows = [];
1199
+ for (let i = 0; i < videos.length; i += 2) {
1200
+ rows.push(videos.slice(i, i + 2));
1201
+ }
1202
+ const cardH = this.s(UI_DIMENSIONS.guide.content.cardHeight);
1203
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1204
+ uiTransform: {
1205
+ width: "100%",
1206
+ flexGrow: 1,
1207
+ flexDirection: "column",
1208
+ alignItems: "flex-start",
1209
+ gap: 8
1210
+ },
1211
+ children: rows.map(
1212
+ (row, rowIndex) => import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1213
+ key: `row-${rowIndex}`,
1214
+ uiTransform: {
1215
+ width: "100%",
1216
+ height: cardH + 8,
1217
+ flexDirection: "row",
1218
+ gap: 10
1219
+ },
1220
+ children: row.map((v) => this.renderGuideCard(v))
1221
+ })
1222
+ )
1223
+ });
1224
+ }
1225
+ renderGuideCard(video) {
1226
+ const cardW = this.s(UI_DIMENSIONS.guide.content.cardWidth);
1227
+ const cardH = this.s(UI_DIMENSIONS.guide.content.cardHeight);
1228
+ const isActive = this._currentVideoId === video.id;
1229
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1230
+ key: video.id,
1231
+ uiTransform: {
1232
+ width: cardW,
1233
+ minWidth: cardW,
1234
+ maxWidth: cardW,
1235
+ height: cardH,
1236
+ minHeight: cardH,
1237
+ flexShrink: 0,
1238
+ flexDirection: "column",
1239
+ margin: 4
1240
+ },
1241
+ uiBackground: { color: isActive ? import_math3.Color4.create(0, 0.4, 0, 0.5) : import_math3.Color4.create(0.15, 0.15, 0.15, 0.8) },
1242
+ children: [
1243
+ // Main click area
1244
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1245
+ uiTransform: {
1246
+ width: "100%",
1247
+ height: "70%",
1248
+ justifyContent: "center",
1249
+ alignItems: "center",
1250
+ flexDirection: "column"
1251
+ },
1252
+ onMouseDown: () => this.handleVideoSelect(video),
1253
+ children: [
1254
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1255
+ uiText: {
1256
+ value: truncateText(video.name || "Unknown", 22),
1257
+ fontSize: this.s(UI_DIMENSIONS.guide.content.headerSize),
1258
+ color: THEME.colors.white
1259
+ }
1260
+ }),
1261
+ ...isActive ? [
1262
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1263
+ key: "playing",
1264
+ uiText: {
1265
+ value: "PLAYING",
1266
+ fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize),
1267
+ color: THEME.colors.green
1268
+ }
1269
+ })
1270
+ ] : video.isLive ? [
1271
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1272
+ key: "live",
1273
+ uiTransform: { padding: { left: 4, right: 4, top: 2, bottom: 2 }, marginTop: 4 },
1274
+ uiBackground: { color: THEME.colors.red },
1275
+ children: [
1276
+ import_react_ecs2.default.createElement(import_react_ecs2.Label, { value: "LIVE", fontSize: this.s(10), color: THEME.colors.white })
1277
+ ]
1278
+ })
1279
+ ] : []
1280
+ ]
1281
+ }),
1282
+ // Channel link
1283
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1284
+ uiTransform: {
1285
+ width: "100%",
1286
+ height: "30%",
1287
+ justifyContent: "center",
1288
+ alignItems: "center",
1289
+ padding: 4
1290
+ },
1291
+ uiBackground: { color: import_math3.Color4.create(0, 0, 0, 0.3) },
1292
+ onMouseDown: () => this.handleChannelClick(video),
1293
+ children: [
1294
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1295
+ uiText: {
1296
+ value: "CHANNEL",
1297
+ fontSize: this.s(UI_DIMENSIONS.guide.content.bodySize),
1298
+ color: THEME.colors.cyan
1299
+ }
1300
+ })
1301
+ ]
1302
+ })
1303
+ ]
1304
+ });
1305
+ }
1306
+ renderCloseButton() {
1307
+ const closeSize = this.s(UI_DIMENSIONS.closeButton.size);
1308
+ return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1309
+ uiTransform: {
1310
+ positionType: "absolute",
1311
+ position: { top: -5, right: -5 },
1312
+ width: closeSize,
1313
+ height: closeSize,
1314
+ justifyContent: "center",
1315
+ alignItems: "center"
1316
+ },
1317
+ uiBackground: { color: THEME.colors.panel },
1318
+ onMouseDown: () => this.hide(),
1319
+ children: [
1320
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1321
+ uiText: { value: "X", fontSize: this.s(UI_DIMENSIONS.closeButton.fontSize), color: THEME.colors.red }
1322
+ })
1323
+ ]
1324
+ });
1325
+ }
1326
+ };
1327
+
1328
+ // src/ui/chat-ui.tsx
1329
+ var import_react_ecs3 = __toESM(require("@dcl/sdk/react-ecs"));
1330
+ var import_math4 = require("@dcl/sdk/math");
1331
+ var import_SignedFetch = require("~system/SignedFetch");
1332
+ var import_players = require("@dcl/sdk/players");
1333
+ var import_RestrictedActions2 = require("~system/RestrictedActions");
1334
+ var FIREBASE_API_KEY = "AIzaSyCX5jViDWUSagUmAX7S4OXgXyJZ9shaC5Y";
1335
+ var POLL_INTERVAL_ACTIVE = 2500;
1336
+ var POLL_INTERVAL_PASSIVE = 2e4;
1337
+ var ChatUIModule = class {
1338
+ constructor(client, config = {}) {
1339
+ // Visibility state
1340
+ this._isVisible = false;
1341
+ this._unreadCount = 0;
1342
+ // Authentication state
1343
+ this.idToken = null;
1344
+ this.isSigningIn = false;
1345
+ this.isRegisteredUser = false;
1346
+ this.currentUsername = "Guest";
1347
+ this.currentUserId = "";
1348
+ // Channel state
1349
+ this.channels = [{ id: "global-community", name: "GLOBAL COMMUNITY" }];
1350
+ this.currentChannelId = "global-community";
1351
+ this.showChannelDropdown = false;
1352
+ this.channelPage = 0;
1353
+ // Message state
1354
+ this.realMessages = [];
1355
+ this.systemMessages = [];
1356
+ this.inputText = "";
1357
+ this.chatScrollOffset = 0;
1358
+ // UI preferences
1359
+ this.position = "right";
1360
+ this.fontScale = 1;
1361
+ // Timers
1362
+ this.chatTimerId = null;
1363
+ this.playerInfoTimerId = null;
1364
+ this.isInitialized = false;
1365
+ // =============================================================================
1366
+ // --- UI RENDERING ---
1367
+ // =============================================================================
1368
+ /**
1369
+ * Get the chat UI component for rendering
1370
+ */
1371
+ this.getComponent = () => {
1372
+ if (!this._isVisible) return null;
1373
+ const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.fontScale);
1374
+ const positionStyle = this.getPositionStyle();
1375
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1376
+ uiTransform: {
1377
+ width: UI_DIMENSIONS.chat.width,
1378
+ height: UI_DIMENSIONS.chat.height,
1379
+ positionType: "absolute",
1380
+ ...positionStyle,
1381
+ flexDirection: "column",
1382
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
1383
+ borderColor: THEME.colors.panelBorder
1384
+ },
1385
+ uiBackground: { color: THEME.colors.panel },
1386
+ children: [
1387
+ this.renderHeader(),
1388
+ this.renderChannelButton(scaledTheme),
1389
+ this.renderMessagesArea(scaledTheme),
1390
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1391
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1392
+ uiBackground: { color: THEME.colors.panelBorder }
1393
+ }),
1394
+ this.renderUserInfoBar(scaledTheme),
1395
+ this.renderInputArea(scaledTheme),
1396
+ this.renderChannelDropdown(scaledTheme)
1397
+ ]
1398
+ });
1399
+ };
1400
+ this.client = client;
1401
+ this.config = config;
1402
+ this.position = config.position || "right";
1403
+ this.fontScale = config.fontScale || 1;
1404
+ }
1405
+ /**
1406
+ * Initialize the chat system
1407
+ */
1408
+ async init() {
1409
+ if (this.isInitialized) return;
1410
+ this.isInitialized = true;
1411
+ this.generateBootSequence();
1412
+ this.fetchPlayerInfo();
1413
+ await this.fetchChannels();
1414
+ this.performAuth();
1415
+ this.startChatLoop();
1416
+ this.client.log("ChatUI initialized");
1417
+ }
1418
+ /**
1419
+ * Show the chat UI
1420
+ */
1421
+ show() {
1422
+ this._isVisible = true;
1423
+ this._unreadCount = 0;
1424
+ this.chatScrollOffset = 0;
1425
+ this.resetChatTimer();
1426
+ }
1427
+ /**
1428
+ * Hide the chat UI
1429
+ */
1430
+ hide() {
1431
+ this._isVisible = false;
1432
+ }
1433
+ /**
1434
+ * Toggle chat visibility
1435
+ */
1436
+ toggle() {
1437
+ if (this._isVisible) {
1438
+ this.hide();
1439
+ } else {
1440
+ this.show();
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Check if chat is visible
1445
+ */
1446
+ get isVisible() {
1447
+ return this._isVisible;
1448
+ }
1449
+ /**
1450
+ * Get unread message count
1451
+ */
1452
+ get unreadCount() {
1453
+ return this._unreadCount;
1454
+ }
1455
+ /**
1456
+ * Get current channel ID
1457
+ */
1458
+ get currentChannel() {
1459
+ return this.currentChannelId;
1460
+ }
1461
+ /**
1462
+ * Switch to a different channel
1463
+ */
1464
+ setChannel(channelId) {
1465
+ if (this.currentChannelId !== channelId) {
1466
+ this.switchChannel(channelId);
1467
+ }
1468
+ }
1469
+ /**
1470
+ * Cleanup resources
1471
+ */
1472
+ destroy() {
1473
+ if (this.chatTimerId !== null) {
1474
+ dclClearTimeout(this.chatTimerId);
1475
+ this.chatTimerId = null;
1476
+ }
1477
+ if (this.playerInfoTimerId !== null) {
1478
+ dclClearTimeout(this.playerInfoTimerId);
1479
+ this.playerInfoTimerId = null;
1480
+ }
1481
+ }
1482
+ // =============================================================================
1483
+ // --- INTERNAL API LOGIC ---
1484
+ // =============================================================================
1485
+ getBaseUrl() {
1486
+ const config = this.client.getConfig();
1487
+ const baseUrl = config.baseUrl || "https://thestatic.tv/api/v1/dcl";
1488
+ return baseUrl.replace("/api/v1/dcl", "");
1489
+ }
1490
+ async performAuth() {
1491
+ if (this.isSigningIn || this.idToken) return;
1492
+ this.isSigningIn = true;
1493
+ try {
1494
+ const baseUrl = this.getBaseUrl();
1495
+ const response = await (0, import_SignedFetch.signedFetch)({
1496
+ url: `${baseUrl}/api/auth/dcl`,
1497
+ init: {
1498
+ method: "POST",
1499
+ headers: { "Content-Type": "application/json" },
1500
+ body: JSON.stringify({})
1501
+ }
1502
+ });
1503
+ if (!response.ok) throw new Error("Backend refused login");
1504
+ const authData = JSON.parse(response.body);
1505
+ if (authData.username) this.currentUsername = authData.username;
1506
+ this.isRegisteredUser = !!authData.isRegistered;
1507
+ const exchangeRes = await fetch(
1508
+ `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`,
1509
+ {
1510
+ method: "POST",
1511
+ headers: { "Content-Type": "application/json" },
1512
+ body: JSON.stringify({ token: authData.token, returnSecureToken: true })
1513
+ }
1514
+ );
1515
+ const exchangeData = await exchangeRes.json();
1516
+ if (exchangeData.idToken) {
1517
+ this.idToken = exchangeData.idToken;
1518
+ if (this.inputText && this.isRegisteredUser) {
1519
+ this.handleSend();
1520
+ }
1521
+ }
1522
+ this.client.log("Chat auth successful");
1523
+ } catch (e) {
1524
+ this.client.log(`Chat auth failed: ${e}`);
1525
+ } finally {
1526
+ this.isSigningIn = false;
1527
+ }
1528
+ }
1529
+ async fetchChannels() {
1530
+ try {
1531
+ const baseChannels = [{ id: "global-community", name: "GLOBAL COMMUNITY" }];
1532
+ const baseUrl = this.getBaseUrl();
1533
+ const response = await fetch(`${baseUrl}/api/v1/guide`);
1534
+ if (response.ok) {
1535
+ const guideData = await response.json();
1536
+ if (guideData.videoMap) {
1537
+ const creatorChannels = [];
1538
+ for (const [slug, video] of Object.entries(guideData.videoMap)) {
1539
+ const v = video;
1540
+ if ((v.type === "live" || v.isCreator === true) && slug !== "global-community") {
1541
+ creatorChannels.push({
1542
+ id: slug,
1543
+ name: (v.name || v.creator_name || slug).toUpperCase()
1544
+ });
1545
+ }
1546
+ }
1547
+ creatorChannels.sort((a, b) => a.name.localeCompare(b.name));
1548
+ this.channels = [...baseChannels, ...creatorChannels];
1549
+ }
1550
+ }
1551
+ } catch (e) {
1552
+ this.client.log(`Failed to fetch channels: ${e}`);
1553
+ }
1554
+ }
1555
+ async fetchMessages() {
1556
+ try {
1557
+ const baseUrl = this.getBaseUrl();
1558
+ const response = await fetch(`${baseUrl}/api/chat/history?channelId=${this.currentChannelId}`);
1559
+ const data = await response.json();
1560
+ if (data.messages) {
1561
+ const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1e3;
1562
+ const recentMsgs = data.messages.filter(
1563
+ (m) => this.parseServerTime(m.createdAt) > twentyFourHoursAgo
1564
+ );
1565
+ const sortedMsgs = [...recentMsgs].sort(
1566
+ (a, b) => this.parseServerTime(a.createdAt) - this.parseServerTime(b.createdAt)
1567
+ );
1568
+ const lastOldMsg = this.realMessages[this.realMessages.length - 1];
1569
+ if (!this._isVisible && sortedMsgs.length > 0) {
1570
+ if (lastOldMsg) {
1571
+ const lastOldTime = this.parseServerTime(lastOldMsg.createdAt);
1572
+ const newMsgsCount = sortedMsgs.filter(
1573
+ (m) => this.parseServerTime(m.createdAt) > lastOldTime
1574
+ ).length;
1575
+ this._unreadCount += newMsgsCount;
1576
+ } else {
1577
+ this._unreadCount = sortedMsgs.length;
1578
+ }
1579
+ }
1580
+ const lastNewMsg = sortedMsgs[sortedMsgs.length - 1];
1581
+ if (lastNewMsg && lastOldMsg && lastNewMsg.id !== lastOldMsg.id) {
1582
+ this.chatScrollOffset = 0;
1583
+ }
1584
+ this.realMessages = sortedMsgs;
1585
+ }
1586
+ } catch (e) {
1587
+ }
1588
+ }
1589
+ async handleSend() {
1590
+ if (!this.inputText.trim()) return;
1591
+ if (!this.idToken) {
1592
+ await this.performAuth();
1593
+ return;
1594
+ }
1595
+ if (!this.isRegisteredUser) return;
1596
+ const message = this.inputText.trim();
1597
+ this.inputText = "";
1598
+ try {
1599
+ const baseUrl = this.getBaseUrl();
1600
+ const response = await (0, import_SignedFetch.signedFetch)({
1601
+ url: `${baseUrl}/api/chat/send`,
1602
+ init: {
1603
+ method: "POST",
1604
+ headers: {
1605
+ "Content-Type": "application/json",
1606
+ Authorization: `Bearer ${this.idToken}`
1607
+ },
1608
+ body: JSON.stringify({ channelId: this.currentChannelId, text: message })
1609
+ }
1610
+ });
1611
+ if (response.ok) {
1612
+ await this.fetchMessages();
1613
+ this.resetChatTimer();
1614
+ } else if (response.status === 401) {
1615
+ this.idToken = null;
1616
+ this.performAuth();
1617
+ }
1618
+ } catch (e) {
1619
+ this.client.log(`Failed to send message: ${e}`);
1620
+ }
1621
+ }
1622
+ generateBootSequence() {
1623
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1624
+ this.systemMessages = [
1625
+ { id: "sys-1", username: "SYSTEM", text: "ESTABLISHING SECURE CONNECTION...", uid: "system", createdAt: now, isSystem: true },
1626
+ { id: "sys-2", username: "SYSTEM", text: "ENCRYPTION KEYS EXCHANGED.", uid: "system", createdAt: now, isSystem: true },
1627
+ { id: "sys-3", username: "SYSTEM", text: `CONNECTED TO: ${this.currentChannelId.toUpperCase()}`, uid: "system", createdAt: now, isSystem: true }
1628
+ ];
1629
+ }
1630
+ async fetchPlayerInfo() {
1631
+ if (this.currentUserId) return;
1632
+ try {
1633
+ const player = await (0, import_players.getPlayer)();
1634
+ if (player && player.userId) {
1635
+ this.currentUserId = player.userId;
1636
+ if (this.currentUsername === "Guest") {
1637
+ this.currentUsername = player.name;
1638
+ }
1639
+ }
1640
+ } catch (e) {
1641
+ }
1642
+ }
1643
+ switchChannel(channelId) {
1644
+ this.currentChannelId = channelId;
1645
+ this.realMessages = [];
1646
+ this.showChannelDropdown = false;
1647
+ this.chatScrollOffset = 0;
1648
+ this.channelPage = 0;
1649
+ this.generateBootSequence();
1650
+ this.resetChatTimer();
1651
+ }
1652
+ startChatLoop() {
1653
+ this.chatLoop();
1654
+ }
1655
+ chatLoop() {
1656
+ const startTime = Date.now();
1657
+ this.fetchMessages().finally(() => {
1658
+ const duration = Date.now() - startTime;
1659
+ const targetInterval = this._isVisible ? POLL_INTERVAL_ACTIVE : POLL_INTERVAL_PASSIVE;
1660
+ const nextDelay = Math.max(0, targetInterval - duration);
1661
+ if (this.chatTimerId !== null) {
1662
+ dclClearTimeout(this.chatTimerId);
1663
+ }
1664
+ this.chatTimerId = dclSetTimeout(() => this.chatLoop(), nextDelay);
1665
+ });
1666
+ }
1667
+ resetChatTimer() {
1668
+ if (this.chatTimerId !== null) {
1669
+ dclClearTimeout(this.chatTimerId);
1670
+ }
1671
+ this.chatLoop();
1672
+ }
1673
+ // =============================================================================
1674
+ // --- UTILITIES ---
1675
+ // =============================================================================
1676
+ parseServerTime(input) {
1677
+ if (!input) return 0;
1678
+ if (typeof input === "number") return input;
1679
+ if (typeof input === "string") {
1680
+ const d = new Date(input);
1681
+ if (!isNaN(d.getTime())) return d.getTime();
1682
+ }
1683
+ return 0;
1684
+ }
1685
+ formatTime(isoString) {
1686
+ const ms = this.parseServerTime(isoString);
1687
+ if (ms === 0) return "";
1688
+ try {
1689
+ const date = new Date(ms);
1690
+ return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
1691
+ } catch (e) {
1692
+ return "";
1693
+ }
1694
+ }
1695
+ getPositionStyle() {
1696
+ const base = { bottom: UI_DIMENSIONS.chat.bottom };
1697
+ switch (this.position) {
1698
+ case "left":
1699
+ return { ...base, left: 20 };
1700
+ case "center":
1701
+ return { ...base, left: 400 };
1702
+ // Approximate center
1703
+ case "right":
1704
+ default:
1705
+ return { ...base, right: UI_DIMENSIONS.chat.right };
1706
+ }
1707
+ }
1708
+ renderHeader() {
1709
+ return PanelHeader({
1710
+ title: "LIVE CHAT",
1711
+ fontSize: 14,
1712
+ fontScale: this.fontScale,
1713
+ showPositionControls: true,
1714
+ position: this.position,
1715
+ onPositionChange: (pos) => {
1716
+ this.position = pos;
1717
+ },
1718
+ showFontControls: true,
1719
+ onFontScaleUp: () => {
1720
+ this.fontScale = Math.min(1.4, this.fontScale + 0.1);
1721
+ },
1722
+ onFontScaleDown: () => {
1723
+ this.fontScale = Math.max(0.7, this.fontScale - 0.1);
1724
+ },
1725
+ onClose: () => this.hide()
1726
+ });
1727
+ }
1728
+ renderChannelButton(theme) {
1729
+ const currentChannel = this.channels.find((c) => c.id === this.currentChannelId);
1730
+ const displayName = currentChannel?.name || this.currentChannelId;
1731
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1732
+ uiTransform: {
1733
+ height: 40,
1734
+ width: "100%",
1735
+ padding: { left: 15, right: 15 },
1736
+ flexDirection: "row",
1737
+ alignItems: "center",
1738
+ justifyContent: "space-between",
1739
+ flexShrink: 0
1740
+ },
1741
+ uiBackground: { color: THEME.colors.inputBackground },
1742
+ onMouseDown: () => {
1743
+ this.showChannelDropdown = !this.showChannelDropdown;
1744
+ },
1745
+ children: [
1746
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
1747
+ value: `// ${truncateText(displayName, 24)}`,
1748
+ fontSize: theme.channelButton,
1749
+ color: THEME.colors.white
1750
+ }),
1751
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
1752
+ value: this.showChannelDropdown ? "\u25B2" : "\u25BC",
1753
+ fontSize: 10,
1754
+ color: THEME.colors.cyan
1755
+ })
1756
+ ]
1757
+ });
1758
+ }
1759
+ renderMessagesArea(theme) {
1760
+ const allMessages = [...this.systemMessages, ...this.realMessages];
1761
+ const totalMessages = allMessages.length;
1762
+ const maxOffset = Math.max(0, totalMessages - UI_DIMENSIONS.chat.messagesPerPage);
1763
+ if (this.chatScrollOffset > maxOffset) this.chatScrollOffset = maxOffset;
1764
+ if (this.chatScrollOffset < 0) this.chatScrollOffset = 0;
1765
+ const startIdx = Math.max(0, totalMessages - UI_DIMENSIONS.chat.messagesPerPage - this.chatScrollOffset);
1766
+ const endIdx = totalMessages - this.chatScrollOffset;
1767
+ const visibleMessages = allMessages.slice(startIdx, endIdx);
1768
+ const canScrollUp = this.chatScrollOffset < maxOffset;
1769
+ const canScrollDown = this.chatScrollOffset > 0;
1770
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1771
+ uiTransform: { flexGrow: 1, width: "100%", flexDirection: "row", overflow: "hidden" },
1772
+ children: [
1773
+ // Messages
1774
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1775
+ uiTransform: {
1776
+ flexGrow: 1,
1777
+ flexDirection: "column",
1778
+ padding: { left: 12, right: 8, top: 8, bottom: 8 },
1779
+ overflow: "hidden"
1780
+ },
1781
+ children: visibleMessages.map(
1782
+ (msg) => msg.isSystem ? this.renderSystemMessage(msg, theme) : this.renderChatMessage(msg, theme)
1783
+ )
1784
+ }),
1785
+ // Scroll controls
1786
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1787
+ uiTransform: {
1788
+ width: 28,
1789
+ height: "100%",
1790
+ flexDirection: "column",
1791
+ justifyContent: "center",
1792
+ alignItems: "center",
1793
+ gap: 8
1794
+ },
1795
+ children: [
1796
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1797
+ uiTransform: { width: 26, height: 36, justifyContent: "center", alignItems: "center" },
1798
+ uiBackground: { color: canScrollUp ? THEME.colors.buttonBackground : import_math4.Color4.create(0.1, 0.1, 0.1, 0.3) },
1799
+ onMouseDown: () => {
1800
+ if (canScrollUp) this.chatScrollOffset += 1;
1801
+ },
1802
+ children: [
1803
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1804
+ uiText: { value: "\u25B2", fontSize: 14, color: canScrollUp ? THEME.colors.cyan : THEME.colors.gray }
1805
+ })
1806
+ ]
1807
+ }),
1808
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1809
+ uiTransform: { width: 26, height: 24, justifyContent: "center", alignItems: "center" },
1810
+ children: [
1811
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1812
+ uiText: {
1813
+ value: totalMessages > UI_DIMENSIONS.chat.messagesPerPage ? `${this.chatScrollOffset}` : "-",
1814
+ fontSize: 10,
1815
+ color: THEME.colors.gray
1816
+ }
1817
+ })
1818
+ ]
1819
+ }),
1820
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1821
+ uiTransform: { width: 26, height: 36, justifyContent: "center", alignItems: "center" },
1822
+ uiBackground: { color: canScrollDown ? THEME.colors.buttonBackground : import_math4.Color4.create(0.1, 0.1, 0.1, 0.3) },
1823
+ onMouseDown: () => {
1824
+ if (canScrollDown) this.chatScrollOffset -= 1;
1825
+ },
1826
+ children: [
1827
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1828
+ uiText: { value: "\u25BC", fontSize: 14, color: canScrollDown ? THEME.colors.cyan : THEME.colors.gray }
1829
+ })
1830
+ ]
1831
+ })
1832
+ ]
1833
+ })
1834
+ ]
1835
+ });
1836
+ }
1837
+ renderSystemMessage(msg, theme) {
1838
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1839
+ key: msg.id,
1840
+ uiTransform: { width: "100%", height: 24, marginBottom: 4 },
1841
+ uiText: { value: msg.text, fontSize: theme.systemMessage, color: THEME.colors.green, textAlign: "middle-left" }
1842
+ });
1843
+ }
1844
+ renderChatMessage(msg, theme) {
1845
+ const time = this.formatTime(msg.createdAt);
1846
+ let displayName = msg.username || "Anonymous";
1847
+ const isCurrentUser = this.currentUserId && msg.uid === this.currentUserId || msg.username === this.currentUserId;
1848
+ if (isCurrentUser) displayName = this.currentUsername;
1849
+ let accentColor = THEME.colors.cyan;
1850
+ if (displayName.toLowerCase().includes("admin")) accentColor = THEME.colors.magenta;
1851
+ else if (isCurrentUser) accentColor = THEME.colors.green;
1852
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1853
+ key: msg.id,
1854
+ uiTransform: { width: "100%", flexDirection: "column", marginBottom: 16 },
1855
+ children: [
1856
+ // Header row
1857
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1858
+ uiTransform: {
1859
+ width: "100%",
1860
+ height: 24,
1861
+ flexDirection: "row",
1862
+ justifyContent: "space-between",
1863
+ alignItems: "center",
1864
+ marginBottom: 4
1865
+ },
1866
+ children: [
1867
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1868
+ uiText: { value: displayName, fontSize: theme.chatUsername, color: accentColor, textAlign: "middle-left" }
1869
+ }),
1870
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1871
+ uiText: { value: time, fontSize: theme.chatTimestamp, color: THEME.colors.gray, textAlign: "middle-right" }
1872
+ })
1873
+ ]
1874
+ }),
1875
+ // Message content
1876
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1877
+ uiTransform: { width: "100%", flexDirection: "row", alignItems: "stretch", paddingLeft: 8 },
1878
+ children: [
1879
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1880
+ uiTransform: { width: 3, minHeight: 28 },
1881
+ uiBackground: { color: accentColor }
1882
+ }),
1883
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1884
+ uiTransform: { flexGrow: 1, paddingLeft: 12, paddingTop: 4, paddingBottom: 4 },
1885
+ uiText: {
1886
+ value: msg.text,
1887
+ fontSize: theme.chatMessage,
1888
+ color: THEME.colors.white,
1889
+ textWrap: "wrap",
1890
+ textAlign: "middle-left"
1891
+ }
1892
+ })
1893
+ ]
1894
+ })
1895
+ ]
1896
+ });
1897
+ }
1898
+ renderUserInfoBar(theme) {
1899
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1900
+ uiTransform: {
1901
+ width: "100%",
1902
+ height: 34,
1903
+ flexShrink: 0,
1904
+ padding: { left: 15, right: 15 },
1905
+ flexDirection: "row",
1906
+ justifyContent: "space-between",
1907
+ alignItems: "center"
1908
+ },
1909
+ uiBackground: { color: import_math4.Color4.create(0.03, 0.03, 0.05, 1) },
1910
+ children: [
1911
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
1912
+ value: `// ${this.currentUsername || "Guest"}`,
1913
+ fontSize: theme.userInfo,
1914
+ color: THEME.colors.yellow
1915
+ }),
1916
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
1917
+ value: this.isRegisteredUser ? "\u25CF CITIZEN" : "\u25CB GUEST",
1918
+ fontSize: theme.authStatus,
1919
+ color: this.isRegisteredUser ? THEME.colors.green : THEME.colors.gray
1920
+ })
1921
+ ]
1922
+ });
1923
+ }
1924
+ renderInputArea(theme) {
1925
+ const baseUrl = this.getBaseUrl();
1926
+ if (!this.isRegisteredUser) {
1927
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1928
+ uiTransform: {
1929
+ height: 75,
1930
+ minHeight: 75,
1931
+ flexShrink: 0,
1932
+ flexDirection: "column",
1933
+ justifyContent: "center",
1934
+ alignItems: "center",
1935
+ padding: 5,
1936
+ borderColor: THEME.colors.panelBorder,
1937
+ border: { top: 1 }
1938
+ },
1939
+ uiBackground: { color: import_math4.Color4.create(0.08, 0.08, 0.1, 1) },
1940
+ children: [
1941
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
1942
+ value: "ACCESS RESTRICTED: CITIZENS ONLY",
1943
+ fontSize: 12,
1944
+ color: THEME.colors.red,
1945
+ textAlign: "middle-center",
1946
+ uiTransform: { marginBottom: 5 }
1947
+ }),
1948
+ import_react_ecs3.default.createElement(import_react_ecs3.Button, {
1949
+ value: "REGISTER IDENTITY",
1950
+ fontSize: 14,
1951
+ uiTransform: { width: 200, height: 30 },
1952
+ uiBackground: { color: THEME.colors.cyan },
1953
+ color: import_math4.Color4.Black(),
1954
+ onMouseDown: () => {
1955
+ (0, import_RestrictedActions2.openExternalUrl)({ url: `${baseUrl}/apply` });
1956
+ }
1957
+ })
1958
+ ]
1959
+ });
1960
+ }
1961
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1962
+ uiTransform: {
1963
+ height: 75,
1964
+ minHeight: 75,
1965
+ flexShrink: 0,
1966
+ flexDirection: "row",
1967
+ padding: 15,
1968
+ borderColor: THEME.colors.panelBorder,
1969
+ border: { top: 1 }
1970
+ },
1971
+ uiBackground: { color: import_math4.Color4.create(0.08, 0.08, 0.1, 1) },
1972
+ children: [
1973
+ import_react_ecs3.default.createElement(import_react_ecs3.Input, {
1974
+ onChange: (v) => {
1975
+ this.inputText = v;
1976
+ },
1977
+ onSubmit: () => this.handleSend(),
1978
+ value: this.inputText,
1979
+ placeholder: "Transmit message...",
1980
+ fontSize: theme.input,
1981
+ placeholderColor: THEME.colors.grayText,
1982
+ color: THEME.colors.white,
1983
+ uiBackground: { color: import_math4.Color4.create(0.12, 0.12, 0.14, 1) },
1984
+ uiTransform: { flexGrow: 1, height: "100%", margin: { right: 10 } }
1985
+ }),
1986
+ import_react_ecs3.default.createElement(import_react_ecs3.Button, {
1987
+ value: this.isSigningIn ? "..." : "SEND",
1988
+ onMouseDown: () => this.handleSend(),
1989
+ uiTransform: { width: 80, height: "100%" },
1990
+ fontSize: theme.sendButton,
1991
+ uiBackground: { color: THEME.colors.cyan },
1992
+ color: import_math4.Color4.Black()
1993
+ })
1994
+ ]
1995
+ });
1996
+ }
1997
+ renderChannelDropdown(theme) {
1998
+ if (!this.showChannelDropdown) return null;
1999
+ const startIdx = this.channelPage * UI_DIMENSIONS.chat.channelsPerPage;
2000
+ const endIdx = startIdx + UI_DIMENSIONS.chat.channelsPerPage;
2001
+ const visibleChannels = this.channels.slice(startIdx, endIdx);
2002
+ const totalPages = Math.ceil(this.channels.length / UI_DIMENSIONS.chat.channelsPerPage);
2003
+ return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2004
+ uiTransform: {
2005
+ width: "100%",
2006
+ height: 260,
2007
+ flexDirection: "column",
2008
+ positionType: "absolute",
2009
+ position: { top: 95, left: 0 }
2010
+ },
2011
+ uiBackground: { color: import_math4.Color4.create(0.08, 0.1, 0.14, 1) },
2012
+ children: [
2013
+ // Top border
2014
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2015
+ uiTransform: { width: "100%", height: 2, flexShrink: 0 },
2016
+ uiBackground: { color: THEME.colors.cyan }
2017
+ }),
2018
+ // Channel list
2019
+ ...visibleChannels.map(
2020
+ (channel) => import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2021
+ key: channel.id,
2022
+ uiTransform: {
2023
+ height: 38,
2024
+ width: "100%",
2025
+ padding: { left: 15, right: 15 },
2026
+ alignItems: "center"
2027
+ },
2028
+ uiBackground: {
2029
+ color: channel.id === this.currentChannelId ? import_math4.Color4.create(0, 0.8, 0.8, 0.25) : import_math4.Color4.create(0.08, 0.1, 0.14, 1)
2030
+ },
2031
+ onMouseDown: () => this.switchChannel(channel.id),
2032
+ children: [
2033
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
2034
+ value: channel.id === this.currentChannelId ? `\u25B8 ${channel.name}` : channel.name,
2035
+ fontSize: theme.channelDropdown,
2036
+ color: channel.id === this.currentChannelId ? THEME.colors.cyan : THEME.colors.gray
2037
+ })
2038
+ ]
2039
+ })
2040
+ ),
2041
+ // Pagination
2042
+ ...totalPages > 1 ? [
2043
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2044
+ key: "pagination",
2045
+ uiTransform: {
2046
+ width: "100%",
2047
+ height: 30,
2048
+ flexDirection: "row",
2049
+ justifyContent: "center",
2050
+ alignItems: "center",
2051
+ gap: 10,
2052
+ marginTop: 5
2053
+ },
2054
+ children: [
2055
+ ...this.channelPage > 0 ? [
2056
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2057
+ key: "prev",
2058
+ onMouseDown: () => this.channelPage--,
2059
+ children: [
2060
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
2061
+ value: "<< PREV",
2062
+ fontSize: 10,
2063
+ color: THEME.colors.cyan
2064
+ })
2065
+ ]
2066
+ })
2067
+ ] : [],
2068
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
2069
+ value: `${this.channelPage + 1}/${totalPages}`,
2070
+ fontSize: 10,
2071
+ color: THEME.colors.gray
2072
+ }),
2073
+ ...this.channelPage < totalPages - 1 ? [
2074
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2075
+ key: "next",
2076
+ onMouseDown: () => this.channelPage++,
2077
+ children: [
2078
+ import_react_ecs3.default.createElement(import_react_ecs3.Label, {
2079
+ value: "NEXT >>",
2080
+ fontSize: 10,
2081
+ color: THEME.colors.cyan
2082
+ })
2083
+ ]
2084
+ })
2085
+ ] : []
2086
+ ]
2087
+ })
2088
+ ] : [],
2089
+ // Bottom border
2090
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2091
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
2092
+ uiBackground: { color: THEME.colors.panelBorder }
2093
+ })
2094
+ ]
2095
+ });
2096
+ }
2097
+ };
2098
+
501
2099
  // src/StaticTVClient.ts
502
2100
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
503
2101
  var KEY_TYPE_CHANNEL = "channel";
@@ -546,10 +2144,14 @@ var StaticTVClient = class {
546
2144
  this.guide = new GuideModule(this);
547
2145
  this.heartbeat = new HeartbeatModule(this);
548
2146
  this.interactions = new InteractionsModule(this);
2147
+ this.guideUI = new GuideUIModule(this, config.guideUI);
2148
+ this.chatUI = new ChatUIModule(this, config.chatUI);
549
2149
  } else {
550
2150
  this.guide = null;
551
2151
  this.heartbeat = null;
552
2152
  this.interactions = null;
2153
+ this.guideUI = null;
2154
+ this.chatUI = null;
553
2155
  }
554
2156
  if (this.config.autoStartSession) {
555
2157
  fetchUserData().then(() => {
@@ -615,13 +2217,18 @@ var StaticTVClient = class {
615
2217
  if (this.heartbeat) {
616
2218
  this.heartbeat.stopWatching();
617
2219
  }
2220
+ if (this.chatUI) {
2221
+ this.chatUI.destroy();
2222
+ }
618
2223
  await this.session.endSession();
619
2224
  this.log("StaticTVClient destroyed");
620
2225
  }
621
2226
  };
622
2227
  // Annotate the CommonJS export names for ESM import in node:
623
2228
  0 && (module.exports = {
2229
+ ChatUIModule,
624
2230
  GuideModule,
2231
+ GuideUIModule,
625
2232
  HeartbeatModule,
626
2233
  InteractionsModule,
627
2234
  KEY_TYPE_CHANNEL,