@dcl-regenesislabs/web-editor 0.0.0-stage → 0.1.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.
Files changed (277) hide show
  1. package/editor.css +1 -0
  2. package/editor.js +33891 -0
  3. package/package.json +8 -4
  4. package/prefabs/admin-tools/composite.json +77 -0
  5. package/prefabs/admin-tools/data.json +28 -0
  6. package/prefabs/admin-tools/icons/admin-panel-control-button.png +0 -0
  7. package/prefabs/admin-tools/icons/admin-panel-moderation-control-button.png +0 -0
  8. package/prefabs/admin-tools/icons/admin-panel-rewards-control-button.png +0 -0
  9. package/prefabs/admin-tools/icons/admin-panel-smart-item-control-button.png +0 -0
  10. package/prefabs/admin-tools/icons/admin-panel-text-announcement-control-button.png +0 -0
  11. package/prefabs/admin-tools/icons/admin-panel-video-control-button.png +0 -0
  12. package/prefabs/admin-tools/icons/admin-tool-background.png +0 -0
  13. package/prefabs/admin-tools/icons/announcements/text-announcement-chat-message.png +0 -0
  14. package/prefabs/admin-tools/icons/announcements/text-announcement-check.png +0 -0
  15. package/prefabs/admin-tools/icons/announcements/text-announcement-close-button.png +0 -0
  16. package/prefabs/admin-tools/icons/moderation/admin-panel-verified-user.png +0 -0
  17. package/prefabs/admin-tools/icons/moderation/ban.png +0 -0
  18. package/prefabs/admin-tools/icons/moderation/chevron-back.png +0 -0
  19. package/prefabs/admin-tools/icons/moderation/chevron-forward.png +0 -0
  20. package/prefabs/admin-tools/icons/moderation/close.png +0 -0
  21. package/prefabs/admin-tools/icons/moderation/error.png +0 -0
  22. package/prefabs/admin-tools/icons/moderation/person-outline.png +0 -0
  23. package/prefabs/admin-tools/icons/moderation-control-icon.png +0 -0
  24. package/prefabs/admin-tools/icons/rewards/close.png +0 -0
  25. package/prefabs/admin-tools/icons/rewards/send.png +0 -0
  26. package/prefabs/admin-tools/icons/rewards-control.png +0 -0
  27. package/prefabs/admin-tools/icons/smart-item-control.png +0 -0
  28. package/prefabs/admin-tools/icons/text-announcement-control.png +0 -0
  29. package/prefabs/admin-tools/icons/video/chevron-down.png +0 -0
  30. package/prefabs/admin-tools/icons/video/chevron-up.png +0 -0
  31. package/prefabs/admin-tools/icons/video/copy-to-clipboard.png +0 -0
  32. package/prefabs/admin-tools/icons/video/error.png +0 -0
  33. package/prefabs/admin-tools/icons/video/eye-off.png +0 -0
  34. package/prefabs/admin-tools/icons/video/eye.png +0 -0
  35. package/prefabs/admin-tools/icons/video/help.png +0 -0
  36. package/prefabs/admin-tools/icons/video/info.png +0 -0
  37. package/prefabs/admin-tools/icons/video/person-outline.png +0 -0
  38. package/prefabs/admin-tools/icons/video/star.png +0 -0
  39. package/prefabs/admin-tools/icons/video/video-control-dcl-cast.png +0 -0
  40. package/prefabs/admin-tools/icons/video/video-control-live.png +0 -0
  41. package/prefabs/admin-tools/icons/video/video-control-loop.png +0 -0
  42. package/prefabs/admin-tools/icons/video/video-control-mute.png +0 -0
  43. package/prefabs/admin-tools/icons/video/video-control-pause-button.png +0 -0
  44. package/prefabs/admin-tools/icons/video/video-control-play-button.png +0 -0
  45. package/prefabs/admin-tools/icons/video/video-control-video-icon.png +0 -0
  46. package/prefabs/admin-tools/icons/video/video-control-volume-minus-button.png +0 -0
  47. package/prefabs/admin-tools/icons/video/video-control-volume-plus-button.png +0 -0
  48. package/prefabs/admin-tools/icons/video-control.png +0 -0
  49. package/prefabs/admin-tools/scripts/actions.ts +206 -0
  50. package/prefabs/admin-tools/scripts/admin.tsx +253 -0
  51. package/prefabs/admin-tools/scripts/api.ts +125 -0
  52. package/prefabs/admin-tools/scripts/components.ts +190 -0
  53. package/prefabs/admin-tools/scripts/icons.ts +53 -0
  54. package/prefabs/admin-tools/scripts/message-bus.ts +297 -0
  55. package/prefabs/admin-tools/scripts/state.ts +69 -0
  56. package/prefabs/admin-tools/scripts/tabs/Moderation.tsx +155 -0
  57. package/prefabs/admin-tools/scripts/tabs/Rewards.tsx +136 -0
  58. package/prefabs/admin-tools/scripts/tabs/SmartItemActions.tsx +167 -0
  59. package/prefabs/admin-tools/scripts/tabs/TextAnnouncements.tsx +195 -0
  60. package/prefabs/admin-tools/scripts/tabs/VideoControl.tsx +156 -0
  61. package/prefabs/admin-tools/scripts/tabs/announcements/Overlay.tsx +94 -0
  62. package/prefabs/admin-tools/scripts/tabs/announcements/icons.ts +15 -0
  63. package/prefabs/admin-tools/scripts/tabs/announcements/state.ts +71 -0
  64. package/prefabs/admin-tools/scripts/tabs/announcements/ui.tsx +37 -0
  65. package/prefabs/admin-tools/scripts/tabs/moderation/AddUserInput.tsx +184 -0
  66. package/prefabs/admin-tools/scripts/tabs/moderation/RemoveAdminConfirmation.tsx +93 -0
  67. package/prefabs/admin-tools/scripts/tabs/moderation/UsersList.tsx +228 -0
  68. package/prefabs/admin-tools/scripts/tabs/moderation/api.ts +35 -0
  69. package/prefabs/admin-tools/scripts/tabs/moderation/icons.ts +23 -0
  70. package/prefabs/admin-tools/scripts/tabs/moderation/kick.ts +95 -0
  71. package/prefabs/admin-tools/scripts/tabs/moderation/state.ts +69 -0
  72. package/prefabs/admin-tools/scripts/tabs/moderation/ui.tsx +172 -0
  73. package/prefabs/admin-tools/scripts/tabs/moderation/utils.ts +88 -0
  74. package/prefabs/admin-tools/scripts/tabs/rewards/Button.tsx +52 -0
  75. package/prefabs/admin-tools/scripts/tabs/rewards/Captcha.tsx +88 -0
  76. package/prefabs/admin-tools/scripts/tabs/rewards/api.ts +138 -0
  77. package/prefabs/admin-tools/scripts/tabs/rewards/claim.ts +99 -0
  78. package/prefabs/admin-tools/scripts/tabs/rewards/controller.ts +103 -0
  79. package/prefabs/admin-tools/scripts/tabs/rewards/icons.ts +13 -0
  80. package/prefabs/admin-tools/scripts/tabs/rewards/state.ts +32 -0
  81. package/prefabs/admin-tools/scripts/tabs/smart-items/selection.ts +90 -0
  82. package/prefabs/admin-tools/scripts/tabs/video/DclCast.tsx +226 -0
  83. package/prefabs/admin-tools/scripts/tabs/video/LiveStream.tsx +284 -0
  84. package/prefabs/admin-tools/scripts/tabs/video/SpeakerShowcase.tsx +112 -0
  85. package/prefabs/admin-tools/scripts/tabs/video/VideoUrl.tsx +107 -0
  86. package/prefabs/admin-tools/scripts/tabs/video/Volume.tsx +69 -0
  87. package/prefabs/admin-tools/scripts/tabs/video/api.ts +172 -0
  88. package/prefabs/admin-tools/scripts/tabs/video/controls.ts +108 -0
  89. package/prefabs/admin-tools/scripts/tabs/video/icons.ts +59 -0
  90. package/prefabs/admin-tools/scripts/tabs/video/speakers.ts +43 -0
  91. package/prefabs/admin-tools/scripts/tabs/video/state.ts +82 -0
  92. package/prefabs/admin-tools/scripts/tabs/video/ui.tsx +243 -0
  93. package/prefabs/admin-tools/scripts/types.ts +46 -0
  94. package/prefabs/admin-tools/scripts/ui.tsx +122 -0
  95. package/prefabs/admin-tools/thumbnail.png +0 -0
  96. package/prefabs/announcer/ai.md +76 -0
  97. package/prefabs/announcer/composite.json +31 -0
  98. package/prefabs/announcer/data.json +35 -0
  99. package/prefabs/announcer/scripts/announcer.tsx +79 -0
  100. package/prefabs/announcer/scripts/pure/toast.ts +30 -0
  101. package/prefabs/announcer/thumbnail.png +0 -0
  102. package/prefabs/black-chair/HighBackChair.glb +0 -0
  103. package/prefabs/black-chair/composite.json +74 -0
  104. package/prefabs/black-chair/data.json +30 -0
  105. package/prefabs/black-chair/scripts/seat.ts +191 -0
  106. package/prefabs/black-chair/thumbnail.png +0 -0
  107. package/prefabs/catalog.json +1 -0
  108. package/prefabs/classic-bench/Bench_01.glb +0 -0
  109. package/prefabs/classic-bench/composite.json +100 -0
  110. package/prefabs/classic-bench/data.json +34 -0
  111. package/prefabs/classic-bench/file1.png +0 -0
  112. package/prefabs/classic-bench/scripts/seat.ts +191 -0
  113. package/prefabs/classic-bench/thumbnail.png +0 -0
  114. package/prefabs/classic-bench-armrests/Bench 1.glb +0 -0
  115. package/prefabs/classic-bench-armrests/composite.json +100 -0
  116. package/prefabs/classic-bench-armrests/data.json +30 -0
  117. package/prefabs/classic-bench-armrests/scripts/seat.ts +191 -0
  118. package/prefabs/classic-bench-armrests/thumbnail.png +0 -0
  119. package/prefabs/classroom-chair/Chair_02.glb +0 -0
  120. package/prefabs/classroom-chair/composite.json +74 -0
  121. package/prefabs/classroom-chair/data.json +34 -0
  122. package/prefabs/classroom-chair/file1.png +0 -0
  123. package/prefabs/classroom-chair/scripts/seat.ts +191 -0
  124. package/prefabs/classroom-chair/thumbnail.png +0 -0
  125. package/prefabs/court-chair/Chair_Wood_01.glb +0 -0
  126. package/prefabs/court-chair/FanstasyPack_TX.png +0 -0
  127. package/prefabs/court-chair/composite.json +74 -0
  128. package/prefabs/court-chair/data.json +34 -0
  129. package/prefabs/court-chair/scripts/seat.ts +191 -0
  130. package/prefabs/court-chair/thumbnail.png +0 -0
  131. package/prefabs/curved-couch/CurvedCouch.glb +0 -0
  132. package/prefabs/curved-couch/composite.json +100 -0
  133. package/prefabs/curved-couch/data.json +30 -0
  134. package/prefabs/curved-couch/scripts/seat.ts +191 -0
  135. package/prefabs/curved-couch/thumbnail.png +0 -0
  136. package/prefabs/game-flow/ai.md +94 -0
  137. package/prefabs/game-flow/composite.json +45 -0
  138. package/prefabs/game-flow/data.json +27 -0
  139. package/prefabs/game-flow/scripts/game-flow.ts +168 -0
  140. package/prefabs/game-flow/scripts/pure/flow.ts +185 -0
  141. package/prefabs/game-flow/thumbnail.png +0 -0
  142. package/prefabs/health-respawn/ai.md +76 -0
  143. package/prefabs/health-respawn/composite.json +31 -0
  144. package/prefabs/health-respawn/data.json +38 -0
  145. package/prefabs/health-respawn/scripts/health-respawn.ts +98 -0
  146. package/prefabs/health-respawn/scripts/health.ts +22 -0
  147. package/prefabs/health-respawn/scripts/pure/health.ts +60 -0
  148. package/prefabs/health-respawn/thumbnail.png +0 -0
  149. package/prefabs/high-stool/HighStool.glb +0 -0
  150. package/prefabs/high-stool/composite.json +74 -0
  151. package/prefabs/high-stool/data.json +30 -0
  152. package/prefabs/high-stool/scripts/seat.ts +191 -0
  153. package/prefabs/high-stool/thumbnail.png +0 -0
  154. package/prefabs/large-couch/ThreeSeater_Couch.glb +0 -0
  155. package/prefabs/large-couch/composite.json +100 -0
  156. package/prefabs/large-couch/data.json +30 -0
  157. package/prefabs/large-couch/scripts/seat.ts +191 -0
  158. package/prefabs/large-couch/thumbnail.png +0 -0
  159. package/prefabs/leaderboard/ai.md +75 -0
  160. package/prefabs/leaderboard/board-panel.glb +0 -0
  161. package/prefabs/leaderboard/composite.json +82 -0
  162. package/prefabs/leaderboard/data.json +35 -0
  163. package/prefabs/leaderboard/scripts/leaderboard.ts +66 -0
  164. package/prefabs/leaderboard/scripts/pure/board.ts +71 -0
  165. package/prefabs/leaderboard/thumbnail.png +0 -0
  166. package/prefabs/loveseat/Couch_01.glb +0 -0
  167. package/prefabs/loveseat/composite.json +100 -0
  168. package/prefabs/loveseat/data.json +34 -0
  169. package/prefabs/loveseat/file1.png +0 -0
  170. package/prefabs/loveseat/scripts/seat.ts +191 -0
  171. package/prefabs/loveseat/thumbnail.png +0 -0
  172. package/prefabs/modern-sofa/ModernSofa.glb +0 -0
  173. package/prefabs/modern-sofa/composite.json +100 -0
  174. package/prefabs/modern-sofa/data.json +30 -0
  175. package/prefabs/modern-sofa/scripts/seat.ts +191 -0
  176. package/prefabs/modern-sofa/thumbnail.png +0 -0
  177. package/prefabs/outdoor-chair/Chair_07.glb +0 -0
  178. package/prefabs/outdoor-chair/composite.json +74 -0
  179. package/prefabs/outdoor-chair/data.json +34 -0
  180. package/prefabs/outdoor-chair/file1.png +0 -0
  181. package/prefabs/outdoor-chair/scripts/seat.ts +191 -0
  182. package/prefabs/outdoor-chair/thumbnail.png +0 -0
  183. package/prefabs/rustic-bench/Bench_01.glb +0 -0
  184. package/prefabs/rustic-bench/PiratesPack_TX.png.png +0 -0
  185. package/prefabs/rustic-bench/composite.json +100 -0
  186. package/prefabs/rustic-bench/data.json +34 -0
  187. package/prefabs/rustic-bench/scripts/seat.ts +191 -0
  188. package/prefabs/rustic-bench/thumbnail.png +0 -0
  189. package/prefabs/rustic-round-stool/Chairwood_01.glb +0 -0
  190. package/prefabs/rustic-round-stool/PiratesPack_TX.png.png +0 -0
  191. package/prefabs/rustic-round-stool/composite.json +74 -0
  192. package/prefabs/rustic-round-stool/data.json +34 -0
  193. package/prefabs/rustic-round-stool/scripts/seat.ts +191 -0
  194. package/prefabs/rustic-round-stool/thumbnail.png +0 -0
  195. package/prefabs/rustic-square-stool/Chairwood_02.glb +0 -0
  196. package/prefabs/rustic-square-stool/PiratesPack_TX.png.png +0 -0
  197. package/prefabs/rustic-square-stool/composite.json +74 -0
  198. package/prefabs/rustic-square-stool/data.json +34 -0
  199. package/prefabs/rustic-square-stool/scripts/seat.ts +191 -0
  200. package/prefabs/rustic-square-stool/thumbnail.png +0 -0
  201. package/prefabs/server-clock/ai.md +67 -0
  202. package/prefabs/server-clock/composite.json +45 -0
  203. package/prefabs/server-clock/data.json +24 -0
  204. package/prefabs/server-clock/scripts/clock-hud.tsx +55 -0
  205. package/prefabs/server-clock/scripts/server-clock.ts +71 -0
  206. package/prefabs/server-clock/thumbnail.png +0 -0
  207. package/prefabs/simple-chair/Chair_03.glb +0 -0
  208. package/prefabs/simple-chair/composite.json +74 -0
  209. package/prefabs/simple-chair/data.json +34 -0
  210. package/prefabs/simple-chair/file1.png +0 -0
  211. package/prefabs/simple-chair/scripts/seat.ts +191 -0
  212. package/prefabs/simple-chair/thumbnail.png +0 -0
  213. package/prefabs/sit-spot/composite.json +45 -0
  214. package/prefabs/sit-spot/data.json +30 -0
  215. package/prefabs/sit-spot/scripts/seat.ts +191 -0
  216. package/prefabs/sit-spot/thumbnail.png +0 -0
  217. package/prefabs/sit-spot-edge/composite.json +42 -0
  218. package/prefabs/sit-spot-edge/data.json +32 -0
  219. package/prefabs/sit-spot-edge/emotes/Sit_Edge1_emote.glb +0 -0
  220. package/prefabs/sit-spot-edge/emotes/Sit_Edge2_emote.glb +0 -0
  221. package/prefabs/sit-spot-edge/scripts/seat.ts +191 -0
  222. package/prefabs/sit-spot-edge/thumbnail.png +0 -0
  223. package/prefabs/small-couch/TwoSeater_Couch.glb +0 -0
  224. package/prefabs/small-couch/composite.json +100 -0
  225. package/prefabs/small-couch/data.json +30 -0
  226. package/prefabs/small-couch/scripts/seat.ts +191 -0
  227. package/prefabs/small-couch/thumbnail.png +0 -0
  228. package/prefabs/spawner/ai.md +109 -0
  229. package/prefabs/spawner/composite.json +73 -0
  230. package/prefabs/spawner/data.json +22 -0
  231. package/prefabs/spawner/scripts/spawner.ts +380 -0
  232. package/prefabs/spawner/thumbnail.png +0 -0
  233. package/prefabs/steampunk-bench/Steampunk Bench.glb +0 -0
  234. package/prefabs/steampunk-bench/composite.json +100 -0
  235. package/prefabs/steampunk-bench/data.json +30 -0
  236. package/prefabs/steampunk-bench/scripts/seat.ts +191 -0
  237. package/prefabs/steampunk-bench/thumbnail.png +0 -0
  238. package/prefabs/steampunk-chair/Steampunk Chair.glb +0 -0
  239. package/prefabs/steampunk-chair/composite.json +74 -0
  240. package/prefabs/steampunk-chair/data.json +30 -0
  241. package/prefabs/steampunk-chair/scripts/seat.ts +191 -0
  242. package/prefabs/steampunk-chair/thumbnail.png +0 -0
  243. package/prefabs/tall-stool/Stool_D.glb +0 -0
  244. package/prefabs/tall-stool/composite.json +74 -0
  245. package/prefabs/tall-stool/data.json +30 -0
  246. package/prefabs/tall-stool/scripts/seat.ts +191 -0
  247. package/prefabs/tall-stool/thumbnail.png +0 -0
  248. package/prefabs/trigger-zone/ai.md +115 -0
  249. package/prefabs/trigger-zone/composite.json +52 -0
  250. package/prefabs/trigger-zone/data.json +31 -0
  251. package/prefabs/trigger-zone/scripts/trigger-zone.ts +89 -0
  252. package/prefabs/trigger-zone/thumbnail.png +0 -0
  253. package/prefabs/video-screen/composite.json +98 -0
  254. package/prefabs/video-screen/data.json +21 -0
  255. package/prefabs/video-screen/thumbnail.png +0 -0
  256. package/prefabs/video-screen/video_player.glb +0 -0
  257. package/prefabs/wooden-chair/Chair 1.glb +0 -0
  258. package/prefabs/wooden-chair/composite.json +74 -0
  259. package/prefabs/wooden-chair/data.json +30 -0
  260. package/prefabs/wooden-chair/scripts/seat.ts +191 -0
  261. package/prefabs/wooden-chair/thumbnail.png +0 -0
  262. package/prefabs/wooden-chair-armrests/Chair 3.glb +0 -0
  263. package/prefabs/wooden-chair-armrests/composite.json +74 -0
  264. package/prefabs/wooden-chair-armrests/data.json +30 -0
  265. package/prefabs/wooden-chair-armrests/scripts/seat.ts +191 -0
  266. package/prefabs/wooden-chair-armrests/thumbnail.png +0 -0
  267. package/scene/about +17 -0
  268. package/scene/bafkreias2w4xuqaic6sdcvblrisayi6xg7drfowzpt5ubjkmrk3mx6czc4 +1 -0
  269. package/scene/bafkreiby2gmthtauf7cckttphmhpecsueyypi2hzlke4lcodg7fm4cm2sq +23 -0
  270. package/scene/bafkreidhwqwajqk4nsuffef5fkdrgmp5ql6vnn2knpqsuqalyosv5o6lk4 +1 -0
  271. package/scene/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +0 -0
  272. package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +90 -0
  273. package/web-build/esbuild.wasm +0 -0
  274. package/web-build/index.js +3 -0
  275. package/web-build/sdk-snapshot.json +1 -0
  276. package/web-build/worker.js +701 -0
  277. package/README.md +0 -3
@@ -0,0 +1,297 @@
1
+ // Wallet-validated admin command bus — port of @dcl/asset-packs
2
+ // src/admin-toolkit-ui/admin-message-bus.ts.
3
+ //
4
+ // Why a second transport: admin-controlled components (VideoPlayer, the
5
+ // announcement component) are attractive targets for a forged CRDT write. So
6
+ // 1. VideoPlayer is dropped from SyncComponents on admin-controlled entities,
7
+ // which stops attacker CRDT writes from propagating over the binary channel;
8
+ // 2. admin commands travel on the TEXT comms channel instead, and every
9
+ // receiver checks the sender's wallet against the scene admin list;
10
+ // 3. each client seeds its own copy of the state from VideoScreen.defaultURL (a
11
+ // deploy-time value that CRDT cannot touch) and a per-frame system reverts
12
+ // anything that drifts from it;
13
+ // 4. late joiners ask existing participants for the current state.
14
+ //
15
+ // Sending goes through communicationsController.send() rather than
16
+ // MessageBus.emit(): the class's internal flush queue drops follow-up messages.
17
+ // Receiving uses MessageBus.on(), which hooks the global comms observable.
18
+ import { engine, SyncComponents, VideoPlayer, type Entity } from '@dcl/sdk/ecs'
19
+ import { MessageBus } from '@dcl/sdk/message-bus'
20
+ import { onEnterScene } from '@dcl/sdk/players'
21
+ import { send as commsSend } from '~system/CommunicationsController'
22
+ import { TextAnnouncements, VideoScreen } from './components'
23
+ import { isPreview, type SceneAdmin } from './api'
24
+
25
+ const MSG = {
26
+ SET_VIDEO: 'admin:set-video',
27
+ SET_ANNOUNCEMENT: 'admin:set-announcement',
28
+ CLEAR_ANNOUNCEMENT: 'admin:clear-announcement',
29
+ REQUEST_STATE: 'admin:request-state',
30
+ SYNC_STATE: 'admin:sync-state',
31
+ SYNC_ADMINS: 'admin:sync-admins'
32
+ } as const
33
+
34
+ export interface VideoState {
35
+ src: string
36
+ playing: boolean
37
+ volume: number
38
+ loop: boolean
39
+ // write-only: SET_VIDEO forwards it to the player (restart seeks to 0), it is
40
+ // never part of the snapshot a late joiner receives.
41
+ position?: number
42
+ }
43
+
44
+ export interface AnnouncementState {
45
+ text: string
46
+ author: string
47
+ id: string
48
+ }
49
+
50
+ export interface AdminMessageBus {
51
+ setVideo: (entity: Entity, props: Partial<VideoState>) => void
52
+ setAnnouncement: (text: string, author?: string, id?: string) => void
53
+ clearAnnouncement: () => void
54
+ syncAdmins: () => void
55
+ updateAdminList: (admins: SceneAdmin[]) => void
56
+ }
57
+
58
+ export interface AdminMessageBusOptions {
59
+ self: Entity
60
+ admins: SceneAdmin[]
61
+ videoEntities: Entity[]
62
+ onRefetchAdmins: () => void
63
+ }
64
+
65
+ type Payload = Record<string, unknown>
66
+ type Handler = (payload: Payload, sender: string) => void
67
+
68
+ function asRecord(value: unknown): Payload {
69
+ return typeof value === 'object' && value !== null ? (value as Payload) : {}
70
+ }
71
+
72
+ function str(value: unknown, fallback = ''): string {
73
+ return typeof value === 'string' ? value : fallback
74
+ }
75
+
76
+ function num(value: unknown, fallback: number): number {
77
+ return typeof value === 'number' && !Number.isNaN(value) ? value : fallback
78
+ }
79
+
80
+ function bool(value: unknown, fallback: boolean): boolean {
81
+ return typeof value === 'boolean' ? value : fallback
82
+ }
83
+
84
+ let instance: AdminMessageBus | null = null
85
+
86
+ export function getAdminMessageBus(): AdminMessageBus | null {
87
+ return instance
88
+ }
89
+
90
+ export function initAdminMessageBus(options: AdminMessageBusOptions): AdminMessageBus {
91
+ const { self, videoEntities, onRefetchAdmins } = options
92
+ let sceneAdmins = options.admins
93
+
94
+ const authoritativeVideo = new Map<Entity, VideoState>()
95
+ let authoritativeAnnouncement: AnnouncementState = { text: '', author: '', id: '' }
96
+ let adminHasActed = false
97
+
98
+ const isAdminWallet = (sender: string): boolean => {
99
+ if (isPreview()) return true
100
+ if (sender === 'self') return true
101
+ return sceneAdmins.some((admin) => admin.address.toLowerCase() === sender.toLowerCase())
102
+ }
103
+
104
+ const handlers = new Map<string, Handler>()
105
+ const receiver = new MessageBus()
106
+
107
+ const onMessage = (type: string, handler: Handler): void => {
108
+ handlers.set(type, handler)
109
+ receiver.on(type, (value: unknown, sender: string) => handler(asRecord(value), sender))
110
+ }
111
+
112
+ const onAdminMessage = (type: string, handler: Handler): void => {
113
+ onMessage(type, (payload, sender) => {
114
+ if (!isAdminWallet(sender)) return
115
+ handler(payload, sender)
116
+ })
117
+ }
118
+
119
+ const emitMessage = (type: string, payload: Payload): void => {
120
+ handlers.get(type)?.(payload, 'self')
121
+ commsSend({ message: JSON.stringify({ message: type, payload }) }).catch(() => {})
122
+ }
123
+
124
+ for (const entity of videoEntities) {
125
+ const sync = SyncComponents.getMutableOrNull(entity)
126
+ if (sync === null) continue
127
+ sync.componentIds = sync.componentIds.filter((id) => id !== VideoPlayer.componentId)
128
+ }
129
+
130
+ for (const entity of videoEntities) {
131
+ const video = VideoPlayer.getOrNull(entity)
132
+ if (video === null) continue
133
+ const screen = VideoScreen.getOrNull(entity)
134
+ const src = screen?.defaultURL !== undefined && screen.defaultURL !== '' ? screen.defaultURL : video.src
135
+ authoritativeVideo.set(entity, {
136
+ src,
137
+ playing: video.playing ?? false,
138
+ volume: video.volume ?? 1,
139
+ loop: video.loop ?? false
140
+ })
141
+ if (video.src !== src) VideoPlayer.getMutable(entity).src = src
142
+ }
143
+
144
+ onAdminMessage(MSG.SET_VIDEO, (payload) => {
145
+ const entity = num(payload.entity, 0) as Entity
146
+ const current = VideoPlayer.getOrNull(entity)
147
+ if (current === null) return
148
+ const updated: VideoState = {
149
+ src: str(payload.src, current.src),
150
+ playing: bool(payload.playing, current.playing ?? false),
151
+ volume: num(payload.volume, current.volume ?? 1),
152
+ loop: bool(payload.loop, current.loop ?? false)
153
+ }
154
+ authoritativeVideo.set(entity, updated)
155
+ const video = VideoPlayer.getMutable(entity)
156
+ video.src = updated.src
157
+ video.playing = updated.playing
158
+ video.volume = updated.volume
159
+ video.loop = updated.loop
160
+ if (payload.position !== undefined) video.position = num(payload.position, 0)
161
+ adminHasActed = true
162
+ })
163
+
164
+ const applyAnnouncement = (next: AnnouncementState): void => {
165
+ authoritativeAnnouncement = next
166
+ const announcements = TextAnnouncements.getMutableOrNull(self)
167
+ if (announcements === null) return
168
+ announcements.text = next.text
169
+ announcements.author = next.author
170
+ announcements.id = next.id
171
+ adminHasActed = true
172
+ }
173
+
174
+ onAdminMessage(MSG.SET_ANNOUNCEMENT, (payload) => {
175
+ applyAnnouncement({
176
+ text: str(payload.text),
177
+ author: str(payload.author),
178
+ id: str(payload.id)
179
+ })
180
+ })
181
+
182
+ onAdminMessage(MSG.CLEAR_ANNOUNCEMENT, () => {
183
+ applyAnnouncement({ text: '', author: '', id: '' })
184
+ })
185
+
186
+ const broadcastCurrentState = (): void => {
187
+ const video: Payload[] = []
188
+ for (const entity of videoEntities) {
189
+ const current = VideoPlayer.getOrNull(entity)
190
+ if (current === null) continue
191
+ const screen = VideoScreen.getOrNull(entity)
192
+ if (current.src === (screen?.defaultURL ?? '')) continue
193
+ video.push({
194
+ entity: entity as number,
195
+ src: current.src,
196
+ playing: current.playing ?? false,
197
+ volume: current.volume ?? 1,
198
+ loop: current.loop ?? false
199
+ })
200
+ }
201
+ const announcements = TextAnnouncements.getOrNull(self)
202
+ const announcement =
203
+ announcements !== null && announcements.id !== ''
204
+ ? { text: announcements.text, author: announcements.author ?? '', id: announcements.id }
205
+ : null
206
+ if (video.length === 0 && announcement === null) return
207
+ emitMessage(MSG.SYNC_STATE, { video, announcement })
208
+ }
209
+
210
+ onAdminMessage(MSG.SYNC_STATE, (payload) => {
211
+ const list = Array.isArray(payload.video) ? payload.video : []
212
+ for (const raw of list) {
213
+ const item = asRecord(raw)
214
+ const entity = num(item.entity, 0) as Entity
215
+ const video = VideoPlayer.getMutableOrNull(entity)
216
+ if (video === null) continue
217
+ const next: VideoState = {
218
+ src: str(item.src, video.src),
219
+ playing: bool(item.playing, video.playing ?? false),
220
+ volume: num(item.volume, video.volume ?? 1),
221
+ loop: bool(item.loop, video.loop ?? false)
222
+ }
223
+ authoritativeVideo.set(entity, next)
224
+ video.src = next.src
225
+ video.playing = next.playing
226
+ video.volume = next.volume
227
+ video.loop = next.loop
228
+ }
229
+ const announcement = asRecord(payload.announcement)
230
+ if (str(announcement.id) !== '') {
231
+ applyAnnouncement({
232
+ text: str(announcement.text),
233
+ author: str(announcement.author),
234
+ id: str(announcement.id)
235
+ })
236
+ }
237
+ adminHasActed = true
238
+ })
239
+
240
+ onMessage(MSG.REQUEST_STATE, () => {
241
+ if (adminHasActed) broadcastCurrentState()
242
+ })
243
+
244
+ onAdminMessage(MSG.SYNC_ADMINS, () => {
245
+ onRefetchAdmins()
246
+ })
247
+
248
+ onEnterScene(() => {
249
+ if (adminHasActed) broadcastCurrentState()
250
+ })
251
+
252
+ emitMessage(MSG.REQUEST_STATE, {})
253
+
254
+ // Forged CRDT writes bypass SyncComponents (they can be injected straight into
255
+ // LiveKit) but land here. Admin commands update the maps below before
256
+ // touching the components, so a legitimate change is never reverted.
257
+ engine.addSystem(() => {
258
+ for (const [entity, authoritative] of authoritativeVideo) {
259
+ const video = VideoPlayer.getOrNull(entity)
260
+ if (video === null || video.src === authoritative.src) continue
261
+ const mutable = VideoPlayer.getMutable(entity)
262
+ mutable.src = authoritative.src
263
+ mutable.playing = authoritative.playing
264
+ }
265
+ const announcements = TextAnnouncements.getOrNull(self)
266
+ if (
267
+ announcements !== null &&
268
+ authoritativeAnnouncement.id !== '' &&
269
+ announcements.id !== authoritativeAnnouncement.id
270
+ ) {
271
+ const mutable = TextAnnouncements.getMutable(self)
272
+ mutable.text = authoritativeAnnouncement.text
273
+ mutable.author = authoritativeAnnouncement.author
274
+ mutable.id = authoritativeAnnouncement.id
275
+ }
276
+ })
277
+
278
+ instance = {
279
+ setVideo(entity, props) {
280
+ emitMessage(MSG.SET_VIDEO, { entity: entity as number, ...props })
281
+ },
282
+ setAnnouncement(text, author, id) {
283
+ emitMessage(MSG.SET_ANNOUNCEMENT, { text, author: author ?? '', id: id ?? '' })
284
+ },
285
+ clearAnnouncement() {
286
+ emitMessage(MSG.CLEAR_ANNOUNCEMENT, {})
287
+ },
288
+ syncAdmins() {
289
+ emitMessage(MSG.SYNC_ADMINS, {})
290
+ },
291
+ updateAdminList(admins) {
292
+ sceneAdmins = admins
293
+ }
294
+ }
295
+
296
+ return instance
297
+ }
@@ -0,0 +1,69 @@
1
+ // Mutable panel state. react-ecs re-renders the whole tree every frame off these
2
+ // values, so tabs mutate this object directly rather than holding hook state —
3
+ // same model as the Creator Hub's admin-toolkit-ui/index.tsx `state`.
4
+ import type { Entity } from '@dcl/sdk/ecs'
5
+ import type { SceneAdmin, SceneBanUser } from './api'
6
+
7
+ export enum TabId {
8
+ NONE = 'none',
9
+ SMART_ITEMS = 'smart_items',
10
+ TEXT_ANNOUNCEMENTS = 'text_announcements',
11
+ VIDEO = 'video',
12
+ MODERATION = 'moderation',
13
+ REWARDS = 'rewards'
14
+ }
15
+
16
+ export interface SmartItemUiState {
17
+ visible: boolean
18
+ selectedAction: string
19
+ }
20
+
21
+ export interface AdminState {
22
+ panelOpen: boolean
23
+ activeTab: TabId
24
+ admins: SceneAdmin[]
25
+ bans: SceneBanUser[]
26
+ smartItems: {
27
+ selectedIndex: number | undefined
28
+ byEntity: Map<Entity, SmartItemUiState>
29
+ }
30
+ video: {
31
+ selectedIndex: number | undefined
32
+ }
33
+ textAnnouncements: {
34
+ draft: string
35
+ announcements: Array<{ id: string; text: string; author: string; timestamp: number }>
36
+ maxAnnouncements: number
37
+ }
38
+ rewards: {
39
+ selectedIndex: number | undefined
40
+ }
41
+ }
42
+
43
+ export function createAdminState(): AdminState {
44
+ return {
45
+ panelOpen: false,
46
+ activeTab: TabId.NONE,
47
+ admins: [],
48
+ bans: [],
49
+ smartItems: { selectedIndex: undefined, byEntity: new Map() },
50
+ video: { selectedIndex: undefined },
51
+ textAnnouncements: { draft: '', announcements: [], maxAnnouncements: 4 },
52
+ rewards: { selectedIndex: undefined }
53
+ }
54
+ }
55
+
56
+ // react-ecs renders synchronously inside a system, so a state change made from a
57
+ // click handler is only visible next frame. Deferring the "close then open" of a
58
+ // tab through this queue is what makes tab switching re-mount the panel body,
59
+ // which the Hub relies on to reset per-tab widgets.
60
+ export const nextTickQueue: Array<() => void> = []
61
+
62
+ export function nextTick(fn: () => void): void {
63
+ nextTickQueue.push(fn)
64
+ }
65
+
66
+ export function drainNextTick(): void {
67
+ const fn = nextTickQueue.shift()
68
+ if (fn !== undefined) fn()
69
+ }
@@ -0,0 +1,155 @@
1
+ import ReactEcs, { Label, UiEntity } from '@dcl/sdk/react-ecs'
2
+ import type { SceneAdmin, SceneBanUser } from '../api'
3
+ import { TabId } from '../state'
4
+ import type { TabComponent, TabProps, TabSpec } from '../types'
5
+ import { Card, CardHeader } from '../ui'
6
+ import { AddUserInput, PermissionType } from './moderation/AddUserInput'
7
+ import { moderationIcons } from './moderation/icons'
8
+ import { RemoveAdminConfirmation } from './moderation/RemoveAdminConfirmation'
9
+ import {
10
+ currentMessage,
11
+ moderationView,
12
+ ModerationView,
13
+ openView
14
+ } from './moderation/state'
15
+ import { BADGE_GRAY, BLACK, Button, Divider } from './moderation/ui'
16
+ import { UsersList, UserListType } from './moderation/UsersList'
17
+ import { canManageAdmins, refreshAdmins, refreshBans } from './moderation/utils'
18
+
19
+ const MANAGE_HINT =
20
+ 'Only the scene owner can change the admin list. Enable "Allow non-owners to manage the admin list" on the Admin Tools item to open it up.'
21
+
22
+ export const Moderation: TabComponent = (props: TabProps) => {
23
+ const icons = moderationIcons(props.assetBase)
24
+ const canManage = canManageAdmins(props.config, props.state.admins, props.player)
25
+ const message = currentMessage()
26
+
27
+ ReactEcs.useEffect(() => {
28
+ openView(ModerationView.MAIN)
29
+ }, [])
30
+
31
+ const loadAdmins = (): void => {
32
+ void refreshAdmins(props.state, props.bus)
33
+ }
34
+ const syncAdmins = (): void => {
35
+ void refreshAdmins(props.state, props.bus, true)
36
+ }
37
+ const loadBans = (): void => {
38
+ void refreshBans(props.state)
39
+ }
40
+
41
+ const admins: Array<SceneAdmin | SceneBanUser> = props.state.admins
42
+ const bans: Array<SceneAdmin | SceneBanUser> = props.state.bans
43
+
44
+ const body = (): ReactEcs.JSX.Element => {
45
+ const pendingRemoval = moderationView.adminToRemove
46
+ if (moderationView.view === ModerationView.CONFIRM_REMOVE && pendingRemoval !== null) {
47
+ return (
48
+ <RemoveAdminConfirmation admin={pendingRemoval} icons={icons} onChanged={syncAdmins} />
49
+ )
50
+ }
51
+
52
+ if (
53
+ moderationView.view === ModerationView.ADMIN_LIST ||
54
+ moderationView.view === ModerationView.CONFIRM_REMOVE
55
+ ) {
56
+ return (
57
+ <UsersList
58
+ type={UserListType.ADMIN}
59
+ users={admins}
60
+ icons={icons}
61
+ canManage={canManage}
62
+ onChanged={syncAdmins}
63
+ />
64
+ )
65
+ }
66
+
67
+ if (moderationView.view === ModerationView.BAN_LIST) {
68
+ return (
69
+ <UsersList
70
+ type={UserListType.BAN}
71
+ users={bans}
72
+ icons={icons}
73
+ canManage
74
+ onChanged={loadBans}
75
+ />
76
+ )
77
+ }
78
+
79
+ return (
80
+ <UiEntity uiTransform={{ width: '100%', height: 'auto', flexDirection: 'column' }}>
81
+ <AddUserInput
82
+ type={PermissionType.ADMIN}
83
+ icons={icons}
84
+ admins={props.state.admins}
85
+ disabled={!canManage}
86
+ disabledHint={MANAGE_HINT}
87
+ allowKick={false}
88
+ onChanged={syncAdmins}
89
+ />
90
+ <Button
91
+ label="View Admin List"
92
+ variant="secondary"
93
+ icon={icons.verified}
94
+ onClick={() => {
95
+ loadAdmins()
96
+ openView(ModerationView.ADMIN_LIST)
97
+ }}
98
+ uiTransform={{ width: 230, height: 40, margin: { top: 12 } }}
99
+ />
100
+ <Divider />
101
+ <AddUserInput
102
+ type={PermissionType.BAN}
103
+ icons={icons}
104
+ admins={props.state.admins}
105
+ disabled={false}
106
+ disabledHint=""
107
+ allowKick
108
+ onChanged={loadBans}
109
+ />
110
+ <Button
111
+ label="View Ban List"
112
+ variant="secondary"
113
+ icon={icons.ban}
114
+ onClick={() => {
115
+ loadBans()
116
+ openView(ModerationView.BAN_LIST)
117
+ }}
118
+ uiTransform={{ width: 230, height: 40, margin: { top: 12 } }}
119
+ />
120
+ </UiEntity>
121
+ )
122
+ }
123
+
124
+ return (
125
+ <Card>
126
+ <UiEntity uiTransform={{ width: '100%', height: 'auto', flexDirection: 'column' }}>
127
+ <CardHeader icon={props.icons.headerModeration} title="PERMISSIONS & MODERATION" />
128
+ {body()}
129
+ {message === '' ? null : (
130
+ <UiEntity
131
+ uiTransform={{
132
+ width: '100%',
133
+ height: 34,
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ borderRadius: 8,
137
+ margin: { top: 14 }
138
+ }}
139
+ uiBackground={{ color: BADGE_GRAY }}
140
+ >
141
+ <Label value={message} fontSize={13} color={BLACK} />
142
+ </UiEntity>
143
+ )}
144
+ </UiEntity>
145
+ </Card>
146
+ )
147
+ }
148
+
149
+ export const moderationTab: TabSpec = {
150
+ id: TabId.MODERATION,
151
+ icon: 'tabModeration',
152
+ isEnabled: (config) => config.moderationControl.isEnabled,
153
+ hiddenInPreview: true,
154
+ Component: Moderation
155
+ }
@@ -0,0 +1,136 @@
1
+ import ReactEcs, { Dropdown, Label, UiEntity } from '@dcl/sdk/react-ecs'
2
+ import { Color4 } from '@dcl/sdk/math'
3
+ import { TabId } from '../state'
4
+ import type { TabComponent, TabProps, TabSpec } from '../types'
5
+ import { Card, CardHeader, TEXT_DIM } from '../ui'
6
+ import { RewardsButton } from './rewards/Button'
7
+ import { CaptchaPrompt } from './rewards/Captcha'
8
+ import { clear, release, selectedEntity, submit } from './rewards/controller'
9
+ import { rewardsIcons } from './rewards/icons'
10
+ import { clearCaptcha, rewardsTabState, setStatus, type StatusTone } from './rewards/state'
11
+
12
+ const STATUS_COLOR: Record<StatusTone, Color4> = {
13
+ info: TEXT_DIM,
14
+ error: Color4.create(1, 100 / 255, 100 / 255, 1),
15
+ success: Color4.create(120 / 255, 220 / 255, 150 / 255, 1)
16
+ }
17
+
18
+ export const Rewards: TabComponent = (props: TabProps) => {
19
+ const items = props.config.rewardsControl.rewardItems ?? []
20
+ const icons = rewardsIcons(props.assetBase)
21
+ const selectedIndex = props.state.rewards.selectedIndex
22
+ const entity = selectedEntity(items, selectedIndex)
23
+ const pending = rewardsTabState.pending
24
+
25
+ return (
26
+ <Card>
27
+ <CardHeader icon={props.icons.headerRewards} title="AIRDROPS" />
28
+
29
+ {items.length === 0 ? (
30
+ <Label
31
+ value="No airdrops linked yet. Add reward dispensers from the Admin Tools inspector."
32
+ fontSize={14}
33
+ color={TEXT_DIM}
34
+ textAlign="middle-left"
35
+ uiTransform={{ width: '100%', height: 'auto' }}
36
+ />
37
+ ) : (
38
+ <UiEntity uiTransform={{ width: '100%', flexDirection: 'column' }}>
39
+ <UiEntity
40
+ uiTransform={{
41
+ width: '100%',
42
+ flexDirection: 'column',
43
+ margin: { bottom: 24 }
44
+ }}
45
+ >
46
+ <Label
47
+ value="<b>Selected Airdrop</b>"
48
+ fontSize={16}
49
+ color={Color4.White()}
50
+ textAlign="middle-left"
51
+ uiTransform={{ margin: { bottom: 12 }, height: 'auto' }}
52
+ />
53
+ <Dropdown
54
+ key="rewards-item-selector"
55
+ acceptEmpty
56
+ emptyLabel="Select your airdrop"
57
+ options={items.map((item) => item.customName)}
58
+ selectedIndex={selectedIndex ?? -1}
59
+ onChange={(index) => {
60
+ props.state.rewards.selectedIndex = index
61
+ clearCaptcha()
62
+ setStatus('')
63
+ }}
64
+ textAlign="middle-left"
65
+ fontSize={14}
66
+ uiTransform={{ width: '100%', height: 40 }}
67
+ uiBackground={{ color: Color4.White() }}
68
+ color={Color4.Black()}
69
+ />
70
+ </UiEntity>
71
+
72
+ {pending !== null && entity !== null ? (
73
+ <CaptchaPrompt
74
+ captcha={pending.captcha}
75
+ answer={rewardsTabState.answer}
76
+ busy={rewardsTabState.busy}
77
+ closeIcon={icons.close}
78
+ onChange={(value) => {
79
+ rewardsTabState.answer = value
80
+ }}
81
+ onSubmit={submit}
82
+ onClose={() => {
83
+ clearCaptcha()
84
+ setStatus('Airdrop release cancelled.')
85
+ }}
86
+ />
87
+ ) : (
88
+ <UiEntity uiTransform={{ width: '100%', flexDirection: 'column' }}>
89
+ <Label
90
+ value="<b>Actions</b>"
91
+ fontSize={16}
92
+ color={Color4.White()}
93
+ textAlign="middle-left"
94
+ uiTransform={{ margin: { bottom: 12 }, height: 'auto' }}
95
+ />
96
+ <UiEntity uiTransform={{ flexDirection: 'row' }}>
97
+ <RewardsButton
98
+ label={rewardsTabState.busy ? 'Releasing…' : 'Release'}
99
+ icon={icons.send}
100
+ disabled={entity === null || rewardsTabState.busy}
101
+ onClick={() => {
102
+ if (entity !== null) release(entity)
103
+ }}
104
+ />
105
+ <RewardsButton
106
+ label="Clear"
107
+ disabled={entity === null || rewardsTabState.busy}
108
+ onClick={() => {
109
+ if (entity !== null) clear(entity)
110
+ }}
111
+ />
112
+ </UiEntity>
113
+ </UiEntity>
114
+ )}
115
+
116
+ {rewardsTabState.status === '' ? null : (
117
+ <Label
118
+ value={rewardsTabState.status}
119
+ fontSize={13}
120
+ color={STATUS_COLOR[rewardsTabState.tone]}
121
+ textAlign="middle-left"
122
+ uiTransform={{ width: '100%', height: 'auto', margin: { top: 16 } }}
123
+ />
124
+ )}
125
+ </UiEntity>
126
+ )}
127
+ </Card>
128
+ )
129
+ }
130
+
131
+ export const rewardsTab: TabSpec = {
132
+ id: TabId.REWARDS,
133
+ icon: 'tabRewards',
134
+ isEnabled: (config) => config.rewardsControl.isEnabled,
135
+ Component: Rewards
136
+ }