@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.
- package/editor.css +1 -0
- package/editor.js +33891 -0
- package/package.json +8 -4
- package/prefabs/admin-tools/composite.json +77 -0
- package/prefabs/admin-tools/data.json +28 -0
- package/prefabs/admin-tools/icons/admin-panel-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-moderation-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-rewards-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-smart-item-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-text-announcement-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-video-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-tool-background.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-chat-message.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-check.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-close-button.png +0 -0
- package/prefabs/admin-tools/icons/moderation/admin-panel-verified-user.png +0 -0
- package/prefabs/admin-tools/icons/moderation/ban.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-back.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-forward.png +0 -0
- package/prefabs/admin-tools/icons/moderation/close.png +0 -0
- package/prefabs/admin-tools/icons/moderation/error.png +0 -0
- package/prefabs/admin-tools/icons/moderation/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/moderation-control-icon.png +0 -0
- package/prefabs/admin-tools/icons/rewards/close.png +0 -0
- package/prefabs/admin-tools/icons/rewards/send.png +0 -0
- package/prefabs/admin-tools/icons/rewards-control.png +0 -0
- package/prefabs/admin-tools/icons/smart-item-control.png +0 -0
- package/prefabs/admin-tools/icons/text-announcement-control.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-down.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-up.png +0 -0
- package/prefabs/admin-tools/icons/video/copy-to-clipboard.png +0 -0
- package/prefabs/admin-tools/icons/video/error.png +0 -0
- package/prefabs/admin-tools/icons/video/eye-off.png +0 -0
- package/prefabs/admin-tools/icons/video/eye.png +0 -0
- package/prefabs/admin-tools/icons/video/help.png +0 -0
- package/prefabs/admin-tools/icons/video/info.png +0 -0
- package/prefabs/admin-tools/icons/video/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/video/star.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-dcl-cast.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-live.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-loop.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-mute.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-pause-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-play-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-video-icon.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-minus-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-plus-button.png +0 -0
- package/prefabs/admin-tools/icons/video-control.png +0 -0
- package/prefabs/admin-tools/scripts/actions.ts +206 -0
- package/prefabs/admin-tools/scripts/admin.tsx +253 -0
- package/prefabs/admin-tools/scripts/api.ts +125 -0
- package/prefabs/admin-tools/scripts/components.ts +190 -0
- package/prefabs/admin-tools/scripts/icons.ts +53 -0
- package/prefabs/admin-tools/scripts/message-bus.ts +297 -0
- package/prefabs/admin-tools/scripts/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/Moderation.tsx +155 -0
- package/prefabs/admin-tools/scripts/tabs/Rewards.tsx +136 -0
- package/prefabs/admin-tools/scripts/tabs/SmartItemActions.tsx +167 -0
- package/prefabs/admin-tools/scripts/tabs/TextAnnouncements.tsx +195 -0
- package/prefabs/admin-tools/scripts/tabs/VideoControl.tsx +156 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/Overlay.tsx +94 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/icons.ts +15 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/state.ts +71 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/ui.tsx +37 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/AddUserInput.tsx +184 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/RemoveAdminConfirmation.tsx +93 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/UsersList.tsx +228 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/api.ts +35 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/icons.ts +23 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/kick.ts +95 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/ui.tsx +172 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/utils.ts +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Button.tsx +52 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Captcha.tsx +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/api.ts +138 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/claim.ts +99 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/controller.ts +103 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/icons.ts +13 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/state.ts +32 -0
- package/prefabs/admin-tools/scripts/tabs/smart-items/selection.ts +90 -0
- package/prefabs/admin-tools/scripts/tabs/video/DclCast.tsx +226 -0
- package/prefabs/admin-tools/scripts/tabs/video/LiveStream.tsx +284 -0
- package/prefabs/admin-tools/scripts/tabs/video/SpeakerShowcase.tsx +112 -0
- package/prefabs/admin-tools/scripts/tabs/video/VideoUrl.tsx +107 -0
- package/prefabs/admin-tools/scripts/tabs/video/Volume.tsx +69 -0
- package/prefabs/admin-tools/scripts/tabs/video/api.ts +172 -0
- package/prefabs/admin-tools/scripts/tabs/video/controls.ts +108 -0
- package/prefabs/admin-tools/scripts/tabs/video/icons.ts +59 -0
- package/prefabs/admin-tools/scripts/tabs/video/speakers.ts +43 -0
- package/prefabs/admin-tools/scripts/tabs/video/state.ts +82 -0
- package/prefabs/admin-tools/scripts/tabs/video/ui.tsx +243 -0
- package/prefabs/admin-tools/scripts/types.ts +46 -0
- package/prefabs/admin-tools/scripts/ui.tsx +122 -0
- package/prefabs/admin-tools/thumbnail.png +0 -0
- package/prefabs/announcer/ai.md +76 -0
- package/prefabs/announcer/composite.json +31 -0
- package/prefabs/announcer/data.json +35 -0
- package/prefabs/announcer/scripts/announcer.tsx +79 -0
- package/prefabs/announcer/scripts/pure/toast.ts +30 -0
- package/prefabs/announcer/thumbnail.png +0 -0
- package/prefabs/black-chair/HighBackChair.glb +0 -0
- package/prefabs/black-chair/composite.json +74 -0
- package/prefabs/black-chair/data.json +30 -0
- package/prefabs/black-chair/scripts/seat.ts +191 -0
- package/prefabs/black-chair/thumbnail.png +0 -0
- package/prefabs/catalog.json +1 -0
- package/prefabs/classic-bench/Bench_01.glb +0 -0
- package/prefabs/classic-bench/composite.json +100 -0
- package/prefabs/classic-bench/data.json +34 -0
- package/prefabs/classic-bench/file1.png +0 -0
- package/prefabs/classic-bench/scripts/seat.ts +191 -0
- package/prefabs/classic-bench/thumbnail.png +0 -0
- package/prefabs/classic-bench-armrests/Bench 1.glb +0 -0
- package/prefabs/classic-bench-armrests/composite.json +100 -0
- package/prefabs/classic-bench-armrests/data.json +30 -0
- package/prefabs/classic-bench-armrests/scripts/seat.ts +191 -0
- package/prefabs/classic-bench-armrests/thumbnail.png +0 -0
- package/prefabs/classroom-chair/Chair_02.glb +0 -0
- package/prefabs/classroom-chair/composite.json +74 -0
- package/prefabs/classroom-chair/data.json +34 -0
- package/prefabs/classroom-chair/file1.png +0 -0
- package/prefabs/classroom-chair/scripts/seat.ts +191 -0
- package/prefabs/classroom-chair/thumbnail.png +0 -0
- package/prefabs/court-chair/Chair_Wood_01.glb +0 -0
- package/prefabs/court-chair/FanstasyPack_TX.png +0 -0
- package/prefabs/court-chair/composite.json +74 -0
- package/prefabs/court-chair/data.json +34 -0
- package/prefabs/court-chair/scripts/seat.ts +191 -0
- package/prefabs/court-chair/thumbnail.png +0 -0
- package/prefabs/curved-couch/CurvedCouch.glb +0 -0
- package/prefabs/curved-couch/composite.json +100 -0
- package/prefabs/curved-couch/data.json +30 -0
- package/prefabs/curved-couch/scripts/seat.ts +191 -0
- package/prefabs/curved-couch/thumbnail.png +0 -0
- package/prefabs/game-flow/ai.md +94 -0
- package/prefabs/game-flow/composite.json +45 -0
- package/prefabs/game-flow/data.json +27 -0
- package/prefabs/game-flow/scripts/game-flow.ts +168 -0
- package/prefabs/game-flow/scripts/pure/flow.ts +185 -0
- package/prefabs/game-flow/thumbnail.png +0 -0
- package/prefabs/health-respawn/ai.md +76 -0
- package/prefabs/health-respawn/composite.json +31 -0
- package/prefabs/health-respawn/data.json +38 -0
- package/prefabs/health-respawn/scripts/health-respawn.ts +98 -0
- package/prefabs/health-respawn/scripts/health.ts +22 -0
- package/prefabs/health-respawn/scripts/pure/health.ts +60 -0
- package/prefabs/health-respawn/thumbnail.png +0 -0
- package/prefabs/high-stool/HighStool.glb +0 -0
- package/prefabs/high-stool/composite.json +74 -0
- package/prefabs/high-stool/data.json +30 -0
- package/prefabs/high-stool/scripts/seat.ts +191 -0
- package/prefabs/high-stool/thumbnail.png +0 -0
- package/prefabs/large-couch/ThreeSeater_Couch.glb +0 -0
- package/prefabs/large-couch/composite.json +100 -0
- package/prefabs/large-couch/data.json +30 -0
- package/prefabs/large-couch/scripts/seat.ts +191 -0
- package/prefabs/large-couch/thumbnail.png +0 -0
- package/prefabs/leaderboard/ai.md +75 -0
- package/prefabs/leaderboard/board-panel.glb +0 -0
- package/prefabs/leaderboard/composite.json +82 -0
- package/prefabs/leaderboard/data.json +35 -0
- package/prefabs/leaderboard/scripts/leaderboard.ts +66 -0
- package/prefabs/leaderboard/scripts/pure/board.ts +71 -0
- package/prefabs/leaderboard/thumbnail.png +0 -0
- package/prefabs/loveseat/Couch_01.glb +0 -0
- package/prefabs/loveseat/composite.json +100 -0
- package/prefabs/loveseat/data.json +34 -0
- package/prefabs/loveseat/file1.png +0 -0
- package/prefabs/loveseat/scripts/seat.ts +191 -0
- package/prefabs/loveseat/thumbnail.png +0 -0
- package/prefabs/modern-sofa/ModernSofa.glb +0 -0
- package/prefabs/modern-sofa/composite.json +100 -0
- package/prefabs/modern-sofa/data.json +30 -0
- package/prefabs/modern-sofa/scripts/seat.ts +191 -0
- package/prefabs/modern-sofa/thumbnail.png +0 -0
- package/prefabs/outdoor-chair/Chair_07.glb +0 -0
- package/prefabs/outdoor-chair/composite.json +74 -0
- package/prefabs/outdoor-chair/data.json +34 -0
- package/prefabs/outdoor-chair/file1.png +0 -0
- package/prefabs/outdoor-chair/scripts/seat.ts +191 -0
- package/prefabs/outdoor-chair/thumbnail.png +0 -0
- package/prefabs/rustic-bench/Bench_01.glb +0 -0
- package/prefabs/rustic-bench/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-bench/composite.json +100 -0
- package/prefabs/rustic-bench/data.json +34 -0
- package/prefabs/rustic-bench/scripts/seat.ts +191 -0
- package/prefabs/rustic-bench/thumbnail.png +0 -0
- package/prefabs/rustic-round-stool/Chairwood_01.glb +0 -0
- package/prefabs/rustic-round-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-round-stool/composite.json +74 -0
- package/prefabs/rustic-round-stool/data.json +34 -0
- package/prefabs/rustic-round-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-round-stool/thumbnail.png +0 -0
- package/prefabs/rustic-square-stool/Chairwood_02.glb +0 -0
- package/prefabs/rustic-square-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-square-stool/composite.json +74 -0
- package/prefabs/rustic-square-stool/data.json +34 -0
- package/prefabs/rustic-square-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-square-stool/thumbnail.png +0 -0
- package/prefabs/server-clock/ai.md +67 -0
- package/prefabs/server-clock/composite.json +45 -0
- package/prefabs/server-clock/data.json +24 -0
- package/prefabs/server-clock/scripts/clock-hud.tsx +55 -0
- package/prefabs/server-clock/scripts/server-clock.ts +71 -0
- package/prefabs/server-clock/thumbnail.png +0 -0
- package/prefabs/simple-chair/Chair_03.glb +0 -0
- package/prefabs/simple-chair/composite.json +74 -0
- package/prefabs/simple-chair/data.json +34 -0
- package/prefabs/simple-chair/file1.png +0 -0
- package/prefabs/simple-chair/scripts/seat.ts +191 -0
- package/prefabs/simple-chair/thumbnail.png +0 -0
- package/prefabs/sit-spot/composite.json +45 -0
- package/prefabs/sit-spot/data.json +30 -0
- package/prefabs/sit-spot/scripts/seat.ts +191 -0
- package/prefabs/sit-spot/thumbnail.png +0 -0
- package/prefabs/sit-spot-edge/composite.json +42 -0
- package/prefabs/sit-spot-edge/data.json +32 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge1_emote.glb +0 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge2_emote.glb +0 -0
- package/prefabs/sit-spot-edge/scripts/seat.ts +191 -0
- package/prefabs/sit-spot-edge/thumbnail.png +0 -0
- package/prefabs/small-couch/TwoSeater_Couch.glb +0 -0
- package/prefabs/small-couch/composite.json +100 -0
- package/prefabs/small-couch/data.json +30 -0
- package/prefabs/small-couch/scripts/seat.ts +191 -0
- package/prefabs/small-couch/thumbnail.png +0 -0
- package/prefabs/spawner/ai.md +109 -0
- package/prefabs/spawner/composite.json +73 -0
- package/prefabs/spawner/data.json +22 -0
- package/prefabs/spawner/scripts/spawner.ts +380 -0
- package/prefabs/spawner/thumbnail.png +0 -0
- package/prefabs/steampunk-bench/Steampunk Bench.glb +0 -0
- package/prefabs/steampunk-bench/composite.json +100 -0
- package/prefabs/steampunk-bench/data.json +30 -0
- package/prefabs/steampunk-bench/scripts/seat.ts +191 -0
- package/prefabs/steampunk-bench/thumbnail.png +0 -0
- package/prefabs/steampunk-chair/Steampunk Chair.glb +0 -0
- package/prefabs/steampunk-chair/composite.json +74 -0
- package/prefabs/steampunk-chair/data.json +30 -0
- package/prefabs/steampunk-chair/scripts/seat.ts +191 -0
- package/prefabs/steampunk-chair/thumbnail.png +0 -0
- package/prefabs/tall-stool/Stool_D.glb +0 -0
- package/prefabs/tall-stool/composite.json +74 -0
- package/prefabs/tall-stool/data.json +30 -0
- package/prefabs/tall-stool/scripts/seat.ts +191 -0
- package/prefabs/tall-stool/thumbnail.png +0 -0
- package/prefabs/trigger-zone/ai.md +115 -0
- package/prefabs/trigger-zone/composite.json +52 -0
- package/prefabs/trigger-zone/data.json +31 -0
- package/prefabs/trigger-zone/scripts/trigger-zone.ts +89 -0
- package/prefabs/trigger-zone/thumbnail.png +0 -0
- package/prefabs/video-screen/composite.json +98 -0
- package/prefabs/video-screen/data.json +21 -0
- package/prefabs/video-screen/thumbnail.png +0 -0
- package/prefabs/video-screen/video_player.glb +0 -0
- package/prefabs/wooden-chair/Chair 1.glb +0 -0
- package/prefabs/wooden-chair/composite.json +74 -0
- package/prefabs/wooden-chair/data.json +30 -0
- package/prefabs/wooden-chair/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair/thumbnail.png +0 -0
- package/prefabs/wooden-chair-armrests/Chair 3.glb +0 -0
- package/prefabs/wooden-chair-armrests/composite.json +74 -0
- package/prefabs/wooden-chair-armrests/data.json +30 -0
- package/prefabs/wooden-chair-armrests/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair-armrests/thumbnail.png +0 -0
- package/scene/about +17 -0
- package/scene/bafkreias2w4xuqaic6sdcvblrisayi6xg7drfowzpt5ubjkmrk3mx6czc4 +1 -0
- package/scene/bafkreiby2gmthtauf7cckttphmhpecsueyypi2hzlke4lcodg7fm4cm2sq +23 -0
- package/scene/bafkreidhwqwajqk4nsuffef5fkdrgmp5ql6vnn2knpqsuqalyosv5o6lk4 +1 -0
- package/scene/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +0 -0
- package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +90 -0
- package/web-build/esbuild.wasm +0 -0
- package/web-build/index.js +3 -0
- package/web-build/sdk-snapshot.json +1 -0
- package/web-build/worker.js +701 -0
- 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
|
+
}
|