@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,167 @@
|
|
|
1
|
+
import ReactEcs, { Dropdown, Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
3
|
+
import { dispatchAction, getActions, SUPPORTED_ACTIONS } from '../actions'
|
|
4
|
+
import type { ActionEntry } from '../components'
|
|
5
|
+
import { TabId } from '../state'
|
|
6
|
+
import type { TabComponent, TabProps, TabSpec } from '../types'
|
|
7
|
+
import { Card, CardHeader, TextButton, TEXT_DIM } from '../ui'
|
|
8
|
+
import {
|
|
9
|
+
selectedItem,
|
|
10
|
+
selectSmartItem,
|
|
11
|
+
setVisible,
|
|
12
|
+
smartItemList,
|
|
13
|
+
smartItemsUi,
|
|
14
|
+
uiStateFor,
|
|
15
|
+
type SmartItemEntry
|
|
16
|
+
} from './smart-items/selection'
|
|
17
|
+
|
|
18
|
+
const MUTED = Color4.create(187 / 255, 187 / 255, 187 / 255, 1)
|
|
19
|
+
|
|
20
|
+
function FieldLabel(props: { value: string }): ReactEcs.JSX.Element {
|
|
21
|
+
return (
|
|
22
|
+
<Label
|
|
23
|
+
value={`<b>${props.value}</b>`}
|
|
24
|
+
fontSize={16}
|
|
25
|
+
color={Color4.White()}
|
|
26
|
+
textAlign="middle-left"
|
|
27
|
+
uiTransform={{ width: '100%', height: 24, margin: { bottom: 12 } }}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function play(props: TabProps, item: SmartItemEntry, action: ActionEntry): void {
|
|
33
|
+
const ran = dispatchAction(item.entity, action.name)
|
|
34
|
+
smartItemsUi.lastPlayed = action.name
|
|
35
|
+
smartItemsUi.lastPlayedFailed = !ran || !SUPPORTED_ACTIONS.includes(action.type)
|
|
36
|
+
uiStateFor(props.state, item).selectedAction = action.name
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SmartItemActions: TabComponent = (props: TabProps) => {
|
|
40
|
+
const items = smartItemList(props.config, props.self)
|
|
41
|
+
const item = selectedItem(props.state, items)
|
|
42
|
+
const actions = item === undefined ? [] : getActions(item.entity)
|
|
43
|
+
const ui = item === undefined ? undefined : uiStateFor(props.state, item)
|
|
44
|
+
const actionIndex =
|
|
45
|
+
ui === undefined ? -1 : actions.findIndex((action) => action.name === ui.selectedAction)
|
|
46
|
+
const action = actionIndex >= 0 ? actions[actionIndex] : undefined
|
|
47
|
+
const unsupported = action !== undefined && !SUPPORTED_ACTIONS.includes(action.type)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader icon={props.icons.headerSmartItems} title="SMART ITEM ACTIONS" />
|
|
52
|
+
|
|
53
|
+
{items.length === 0 ? (
|
|
54
|
+
<Label
|
|
55
|
+
value="No smart items linked yet. Add them from the Admin Tools inspector."
|
|
56
|
+
fontSize={14}
|
|
57
|
+
color={TEXT_DIM}
|
|
58
|
+
textAlign="middle-left"
|
|
59
|
+
uiTransform={{ width: '100%', height: 'auto' }}
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<UiEntity uiTransform={{ flexDirection: 'column', width: '100%' }}>
|
|
63
|
+
<UiEntity uiTransform={{ flexDirection: 'column', margin: { bottom: 24 } }}>
|
|
64
|
+
<FieldLabel value="Selected Smart Item" />
|
|
65
|
+
<Dropdown
|
|
66
|
+
key="smart-item-selector"
|
|
67
|
+
acceptEmpty
|
|
68
|
+
emptyLabel="Select Smart Item"
|
|
69
|
+
options={items.map((entry) => entry.customName)}
|
|
70
|
+
selectedIndex={props.state.smartItems.selectedIndex ?? -1}
|
|
71
|
+
onChange={(index) => selectSmartItem(props.state, items, index)}
|
|
72
|
+
textAlign="middle-left"
|
|
73
|
+
fontSize={14}
|
|
74
|
+
color={Color4.Black()}
|
|
75
|
+
uiBackground={{ color: Color4.White() }}
|
|
76
|
+
uiTransform={{ width: '100%', height: 40 }}
|
|
77
|
+
/>
|
|
78
|
+
</UiEntity>
|
|
79
|
+
|
|
80
|
+
<UiEntity uiTransform={{ flexDirection: 'column', margin: { bottom: 16 } }}>
|
|
81
|
+
<FieldLabel value="Actions" />
|
|
82
|
+
{actions.length === 0 ? (
|
|
83
|
+
<Label
|
|
84
|
+
value={
|
|
85
|
+
item === undefined
|
|
86
|
+
? 'Pick a smart item to see its actions.'
|
|
87
|
+
: 'This smart item has no actions authored on it.'
|
|
88
|
+
}
|
|
89
|
+
fontSize={14}
|
|
90
|
+
color={TEXT_DIM}
|
|
91
|
+
textAlign="middle-left"
|
|
92
|
+
uiTransform={{ width: '100%', height: 22 }}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
<Dropdown
|
|
96
|
+
key={`smart-item-actions-${item === undefined ? 'none' : (item.entity as number)}`}
|
|
97
|
+
acceptEmpty
|
|
98
|
+
emptyLabel="Select Action"
|
|
99
|
+
options={actions.map((entry) => entry.name)}
|
|
100
|
+
selectedIndex={actionIndex}
|
|
101
|
+
disabled={item === undefined}
|
|
102
|
+
onChange={(index) => {
|
|
103
|
+
if (item === undefined || index < 0 || index >= actions.length) return
|
|
104
|
+
uiStateFor(props.state, item).selectedAction = actions[index].name
|
|
105
|
+
smartItemsUi.lastPlayed = ''
|
|
106
|
+
smartItemsUi.lastPlayedFailed = false
|
|
107
|
+
}}
|
|
108
|
+
textAlign="middle-left"
|
|
109
|
+
fontSize={14}
|
|
110
|
+
color={Color4.Black()}
|
|
111
|
+
uiBackground={{ color: item === undefined ? Color4.Gray() : Color4.White() }}
|
|
112
|
+
uiTransform={{ width: '100%', height: 40 }}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</UiEntity>
|
|
116
|
+
|
|
117
|
+
<UiEntity uiTransform={{ flexDirection: 'row', height: 40 }}>
|
|
118
|
+
<TextButton
|
|
119
|
+
label="Play Action"
|
|
120
|
+
disabled={item === undefined || action === undefined}
|
|
121
|
+
onClick={() => {
|
|
122
|
+
if (item !== undefined && action !== undefined) play(props, item, action)
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<TextButton
|
|
126
|
+
label={ui !== undefined && ui.visible ? 'Hide Entity' : 'Show Entity'}
|
|
127
|
+
disabled={item === undefined}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
if (item !== undefined && ui !== undefined) setVisible(props.state, item, !ui.visible)
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
</UiEntity>
|
|
133
|
+
|
|
134
|
+
<UiEntity
|
|
135
|
+
uiTransform={{
|
|
136
|
+
width: '100%',
|
|
137
|
+
minHeight: 24,
|
|
138
|
+
margin: { top: 10 },
|
|
139
|
+
display: unsupported || smartItemsUi.lastPlayed !== '' ? 'flex' : 'none'
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<Label
|
|
143
|
+
value={
|
|
144
|
+
unsupported
|
|
145
|
+
? `"${action?.type ?? ''}" is not one of the action types this panel can run.`
|
|
146
|
+
: smartItemsUi.lastPlayedFailed
|
|
147
|
+
? `Could not play "${smartItemsUi.lastPlayed}".`
|
|
148
|
+
: `Played "${smartItemsUi.lastPlayed}".`
|
|
149
|
+
}
|
|
150
|
+
fontSize={13}
|
|
151
|
+
color={MUTED}
|
|
152
|
+
textAlign="middle-left"
|
|
153
|
+
uiTransform={{ width: '100%', height: 'auto' }}
|
|
154
|
+
/>
|
|
155
|
+
</UiEntity>
|
|
156
|
+
</UiEntity>
|
|
157
|
+
)}
|
|
158
|
+
</Card>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const smartItemActionsTab: TabSpec = {
|
|
163
|
+
id: TabId.SMART_ITEMS,
|
|
164
|
+
icon: 'tabSmartItems',
|
|
165
|
+
isEnabled: (config) => config.smartItemsControl.isEnabled,
|
|
166
|
+
Component: SmartItemActions
|
|
167
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import ReactEcs, { Input, Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
3
|
+
import { TextAnnouncements as TextAnnouncementsComponent } from '../components'
|
|
4
|
+
import { TabId } from '../state'
|
|
5
|
+
import type { TabComponent, TabProps, TabSpec } from '../types'
|
|
6
|
+
import { Card, CardHeader, TextButton, TEXT_DIM } from '../ui'
|
|
7
|
+
import { AnnouncementOverlay } from './announcements/Overlay'
|
|
8
|
+
import { announcementIcons } from './announcements/icons'
|
|
9
|
+
import { PrimaryButton } from './announcements/ui'
|
|
10
|
+
import {
|
|
11
|
+
ANNOUNCEMENT_MAX_LENGTH,
|
|
12
|
+
announcementId,
|
|
13
|
+
announcementUi,
|
|
14
|
+
cooldownRemaining,
|
|
15
|
+
feedbackText,
|
|
16
|
+
recordAnnouncement,
|
|
17
|
+
recordSend
|
|
18
|
+
} from './announcements/state'
|
|
19
|
+
|
|
20
|
+
const PLACEHOLDER_COLOR = Color4.create(160 / 255, 155 / 255, 168 / 255, 1)
|
|
21
|
+
const MUTED = Color4.create(187 / 255, 187 / 255, 187 / 255, 1)
|
|
22
|
+
|
|
23
|
+
function resetInput(props: TabProps): void {
|
|
24
|
+
props.state.textAnnouncements.draft = ''
|
|
25
|
+
announcementUi.inputResetSeq += 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function send(props: TabProps): void {
|
|
29
|
+
if (props.bus === null) return
|
|
30
|
+
const text = props.state.textAnnouncements.draft.trim()
|
|
31
|
+
if (text === '') {
|
|
32
|
+
announcementUi.feedback = 'empty'
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const author = props.player?.name ?? ''
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const throttledFor = cooldownRemaining(author, now)
|
|
39
|
+
if (throttledFor > 0) {
|
|
40
|
+
announcementUi.feedback = 'throttled'
|
|
41
|
+
announcementUi.throttledFor = throttledFor
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const showAuthor = props.config.textAnnouncementControl.showAuthorOnEachAnnouncement
|
|
46
|
+
props.bus.setAnnouncement(
|
|
47
|
+
text.slice(0, ANNOUNCEMENT_MAX_LENGTH),
|
|
48
|
+
showAuthor ? author : '',
|
|
49
|
+
announcementId(now, author)
|
|
50
|
+
)
|
|
51
|
+
recordSend(author, now)
|
|
52
|
+
resetInput(props)
|
|
53
|
+
announcementUi.feedback = 'sent'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clear(props: TabProps): void {
|
|
57
|
+
if (props.bus === null) return
|
|
58
|
+
props.bus.clearAnnouncement()
|
|
59
|
+
props.state.textAnnouncements.announcements = []
|
|
60
|
+
announcementUi.dismissed.clear()
|
|
61
|
+
resetInput(props)
|
|
62
|
+
announcementUi.feedback = 'cleared'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function History(props: TabProps): ReactEcs.JSX.Element | null {
|
|
66
|
+
const entries = props.state.textAnnouncements.announcements
|
|
67
|
+
if (entries.length === 0) return null
|
|
68
|
+
return (
|
|
69
|
+
<UiEntity uiTransform={{ flexDirection: 'column', width: '100%', margin: { top: 8 } }}>
|
|
70
|
+
<Label
|
|
71
|
+
value="<b>Recent</b>"
|
|
72
|
+
fontSize={14}
|
|
73
|
+
color={TEXT_DIM}
|
|
74
|
+
textAlign="middle-left"
|
|
75
|
+
uiTransform={{ width: '100%', height: 22 }}
|
|
76
|
+
/>
|
|
77
|
+
{entries.map((entry) => (
|
|
78
|
+
<Label
|
|
79
|
+
key={entry.id}
|
|
80
|
+
value={entry.author === '' ? entry.text : `${entry.text} <b>- ${entry.author}</b>`}
|
|
81
|
+
fontSize={13}
|
|
82
|
+
color={MUTED}
|
|
83
|
+
textAlign="middle-left"
|
|
84
|
+
uiTransform={{ width: '100%', height: 20 }}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
</UiEntity>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const TextAnnouncements: TabComponent = (props: TabProps) => {
|
|
92
|
+
const live = TextAnnouncementsComponent.getOrNull(props.self)
|
|
93
|
+
if (live !== null && live.text !== '') {
|
|
94
|
+
recordAnnouncement(props.state, {
|
|
95
|
+
id: live.id,
|
|
96
|
+
text: live.text,
|
|
97
|
+
author: live.author ?? '',
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const icons = announcementIcons(props.assetBase)
|
|
103
|
+
const ready = props.bus !== null
|
|
104
|
+
const draft = props.state.textAnnouncements.draft
|
|
105
|
+
const feedback = announcementUi.feedback
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Card>
|
|
109
|
+
<CardHeader icon={props.icons.headerTextAnnouncements} title="TEXT ANNOUNCEMENTS" />
|
|
110
|
+
<UiEntity uiTransform={{ flexDirection: 'column', width: '100%' }}>
|
|
111
|
+
<Label
|
|
112
|
+
value="<b>Message window</b>"
|
|
113
|
+
fontSize={16}
|
|
114
|
+
color={Color4.White()}
|
|
115
|
+
textAlign="middle-left"
|
|
116
|
+
uiTransform={{ width: '100%', height: 24, margin: { bottom: 12 } }}
|
|
117
|
+
/>
|
|
118
|
+
<Input
|
|
119
|
+
key={`announcement-input-${announcementUi.inputResetSeq}`}
|
|
120
|
+
placeholder="Write your announcement here"
|
|
121
|
+
placeholderColor={PLACEHOLDER_COLOR}
|
|
122
|
+
color={Color4.Black()}
|
|
123
|
+
fontSize={16}
|
|
124
|
+
disabled={!ready}
|
|
125
|
+
textAlign="top-left"
|
|
126
|
+
uiBackground={{ color: Color4.White() }}
|
|
127
|
+
uiTransform={{ width: '100%', height: 80, margin: { bottom: 16 } }}
|
|
128
|
+
onChange={(value) => {
|
|
129
|
+
props.state.textAnnouncements.draft = value
|
|
130
|
+
announcementUi.feedback = undefined
|
|
131
|
+
}}
|
|
132
|
+
onSubmit={(value) => {
|
|
133
|
+
props.state.textAnnouncements.draft = value
|
|
134
|
+
send(props)
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
<UiEntity
|
|
138
|
+
uiTransform={{
|
|
139
|
+
width: '100%',
|
|
140
|
+
height: 40,
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
alignItems: 'center',
|
|
143
|
+
margin: { bottom: 10 }
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<Label
|
|
147
|
+
value={`${draft.length} / ${ANNOUNCEMENT_MAX_LENGTH}`}
|
|
148
|
+
fontSize={14}
|
|
149
|
+
color={draft.length > ANNOUNCEMENT_MAX_LENGTH ? Color4.Red() : MUTED}
|
|
150
|
+
textAlign="middle-left"
|
|
151
|
+
uiTransform={{ flexGrow: 1 }}
|
|
152
|
+
/>
|
|
153
|
+
<TextButton
|
|
154
|
+
label="Clear Announcements"
|
|
155
|
+
disabled={!ready}
|
|
156
|
+
onClick={() => clear(props)}
|
|
157
|
+
/>
|
|
158
|
+
<PrimaryButton label="Share" disabled={!ready} onClick={() => send(props)} />
|
|
159
|
+
</UiEntity>
|
|
160
|
+
<UiEntity
|
|
161
|
+
uiTransform={{
|
|
162
|
+
width: '100%',
|
|
163
|
+
minHeight: 30,
|
|
164
|
+
flexDirection: 'row',
|
|
165
|
+
alignItems: 'center',
|
|
166
|
+
display: feedback === undefined ? 'none' : 'flex'
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<UiEntity
|
|
170
|
+
uiTransform={{ width: 24, height: 24, margin: { right: 8 } }}
|
|
171
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: icons.check } }}
|
|
172
|
+
/>
|
|
173
|
+
<Label
|
|
174
|
+
value={feedback === undefined ? '' : feedbackText(feedback, announcementUi.throttledFor)}
|
|
175
|
+
fontSize={14}
|
|
176
|
+
color={MUTED}
|
|
177
|
+
textAlign="middle-left"
|
|
178
|
+
uiTransform={{ flexGrow: 1 }}
|
|
179
|
+
/>
|
|
180
|
+
</UiEntity>
|
|
181
|
+
<History {...props} />
|
|
182
|
+
</UiEntity>
|
|
183
|
+
</Card>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const textAnnouncementsTab: TabSpec = {
|
|
188
|
+
id: TabId.TEXT_ANNOUNCEMENTS,
|
|
189
|
+
icon: 'tabTextAnnouncements',
|
|
190
|
+
isEnabled: (config) => config.textAnnouncementControl.isEnabled,
|
|
191
|
+
Component: TextAnnouncements
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { AnnouncementOverlay }
|
|
195
|
+
export type { AnnouncementOverlayProps } from './announcements/Overlay'
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import ReactEcs, { Dropdown, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import { VideoPlayer } from '@dcl/sdk/ecs'
|
|
3
|
+
import { TabId } from '../state'
|
|
4
|
+
import type { TabComponent, TabProps, TabSpec } from '../types'
|
|
5
|
+
import { Card, CardHeader } from '../ui'
|
|
6
|
+
import { DclCast } from './video/DclCast'
|
|
7
|
+
import { LiveStream } from './video/LiveStream'
|
|
8
|
+
import { VideoUrl } from './video/VideoUrl'
|
|
9
|
+
import { videoIcons } from './video/icons'
|
|
10
|
+
import { videoTab, type MediaSource } from './video/state'
|
|
11
|
+
import {
|
|
12
|
+
createControls,
|
|
13
|
+
enforceMute,
|
|
14
|
+
isCastSource,
|
|
15
|
+
isVideoUrl,
|
|
16
|
+
LIVEKIT_STREAM_SRC,
|
|
17
|
+
managedScreens
|
|
18
|
+
} from './video/controls'
|
|
19
|
+
import { Button, COLORS, Column, Hint, Row, SectionLabel } from './video/ui'
|
|
20
|
+
|
|
21
|
+
function sourceOf(src: string): MediaSource {
|
|
22
|
+
if (isVideoUrl(src)) return 'video-url'
|
|
23
|
+
if (src === '' || src === LIVEKIT_STREAM_SRC) return 'live'
|
|
24
|
+
return 'dcl-cast'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const VideoControl: TabComponent = (props: TabProps) => {
|
|
28
|
+
const icons = videoIcons(props.assetBase)
|
|
29
|
+
const screens = managedScreens(props.config)
|
|
30
|
+
const soundDisabled = props.config.videoControl.disableVideoPlayersSound
|
|
31
|
+
const selected = Math.min(Math.max(props.state.video.selectedIndex ?? 0, 0), screens.length - 1)
|
|
32
|
+
const screen = screens[selected]
|
|
33
|
+
const entity = screen?.entity
|
|
34
|
+
const src = entity === undefined ? '' : (VideoPlayer.getOrNull(entity)?.src ?? '')
|
|
35
|
+
|
|
36
|
+
ReactEcs.useEffect(() => {
|
|
37
|
+
if (soundDisabled) enforceMute(screens)
|
|
38
|
+
}, [screens.length, soundDisabled])
|
|
39
|
+
|
|
40
|
+
ReactEcs.useEffect(() => {
|
|
41
|
+
const stillCasting =
|
|
42
|
+
videoTab.source === 'dcl-cast' && (isCastSource(src) || src === videoTab.activeTrackSid)
|
|
43
|
+
videoTab.source = stillCasting ? 'dcl-cast' : sourceOf(src)
|
|
44
|
+
}, [entity])
|
|
45
|
+
|
|
46
|
+
if (entity === undefined) {
|
|
47
|
+
return (
|
|
48
|
+
<Card>
|
|
49
|
+
<CardHeader icon={props.icons.headerVideo} title="VIDEO SCREENS" />
|
|
50
|
+
<Hint text="No video screens linked yet. Add them from the Admin Tools inspector, or turn on 'Link all video players'." />
|
|
51
|
+
</Card>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const source = videoTab.source ?? sourceOf(src)
|
|
56
|
+
const urlActive = isVideoUrl(src)
|
|
57
|
+
const liveActive = src === LIVEKIT_STREAM_SRC && source !== 'dcl-cast'
|
|
58
|
+
const castActive =
|
|
59
|
+
src !== '' &&
|
|
60
|
+
(isCastSource(src) ? source === 'dcl-cast' : src === videoTab.activeTrackSid)
|
|
61
|
+
const controls = createControls(props.bus, entity, soundDisabled)
|
|
62
|
+
|
|
63
|
+
const pick = (value: MediaSource): void => {
|
|
64
|
+
videoTab.source = value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sourceButton = (
|
|
68
|
+
value: MediaSource,
|
|
69
|
+
label: string,
|
|
70
|
+
icon: string,
|
|
71
|
+
active: boolean
|
|
72
|
+
): ReactEcs.JSX.Element => (
|
|
73
|
+
<Column uiTransform={{ width: '31%' }}>
|
|
74
|
+
<Button
|
|
75
|
+
label={label}
|
|
76
|
+
icon={icon}
|
|
77
|
+
fontSize={14}
|
|
78
|
+
variant={source === value ? 'primary' : 'secondary'}
|
|
79
|
+
uiTransform={{ width: '100%', height: 36, borderRadius: 6 }}
|
|
80
|
+
onClick={() => pick(value)}
|
|
81
|
+
/>
|
|
82
|
+
<UiEntity
|
|
83
|
+
uiTransform={{ width: '100%', height: 4, margin: { top: 6 }, borderRadius: 2 }}
|
|
84
|
+
uiBackground={{ color: active ? COLORS.success : COLORS.transparent }}
|
|
85
|
+
/>
|
|
86
|
+
</Column>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Column>
|
|
91
|
+
<Card>
|
|
92
|
+
<CardHeader icon={props.icons.headerVideo} title="VIDEO SCREENS" />
|
|
93
|
+
{screens.length > 1 ? (
|
|
94
|
+
<Column>
|
|
95
|
+
<SectionLabel text="Current Screen" uiTransform={{ margin: { bottom: 6 } }} />
|
|
96
|
+
<Dropdown
|
|
97
|
+
options={screens.map((item) => item.customName)}
|
|
98
|
+
selectedIndex={selected}
|
|
99
|
+
onChange={(index) => {
|
|
100
|
+
props.state.video.selectedIndex = index
|
|
101
|
+
}}
|
|
102
|
+
textAlign="middle-left"
|
|
103
|
+
fontSize={15}
|
|
104
|
+
color={COLORS.black}
|
|
105
|
+
uiBackground={{ color: COLORS.offWhite }}
|
|
106
|
+
uiTransform={{ width: '100%', height: 36 }}
|
|
107
|
+
/>
|
|
108
|
+
</Column>
|
|
109
|
+
) : null}
|
|
110
|
+
<SectionLabel text="Media Source" />
|
|
111
|
+
<Row uiTransform={{ justifyContent: 'space-between', margin: { top: 4 } }}>
|
|
112
|
+
{sourceButton('video-url', 'VIDEO URL', icons.sourceVideo, urlActive)}
|
|
113
|
+
{sourceButton('dcl-cast', 'DCL CAST', icons.sourceCast, castActive)}
|
|
114
|
+
{sourceButton('live', 'STREAM', icons.sourceLive, liveActive)}
|
|
115
|
+
</Row>
|
|
116
|
+
</Card>
|
|
117
|
+
<Card>
|
|
118
|
+
{source === 'video-url' ? (
|
|
119
|
+
<VideoUrl
|
|
120
|
+
entity={entity}
|
|
121
|
+
icons={icons}
|
|
122
|
+
controls={controls}
|
|
123
|
+
soundDisabled={soundDisabled}
|
|
124
|
+
/>
|
|
125
|
+
) : null}
|
|
126
|
+
{source === 'live' ? (
|
|
127
|
+
<LiveStream
|
|
128
|
+
self={props.self}
|
|
129
|
+
entity={entity}
|
|
130
|
+
icons={icons}
|
|
131
|
+
controls={controls}
|
|
132
|
+
soundDisabled={soundDisabled}
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
{source === 'dcl-cast' ? (
|
|
136
|
+
<DclCast
|
|
137
|
+
self={props.self}
|
|
138
|
+
entity={entity}
|
|
139
|
+
icons={icons}
|
|
140
|
+
controls={controls}
|
|
141
|
+
soundDisabled={soundDisabled}
|
|
142
|
+
playerAddress={props.player?.userId}
|
|
143
|
+
active={castActive}
|
|
144
|
+
/>
|
|
145
|
+
) : null}
|
|
146
|
+
</Card>
|
|
147
|
+
</Column>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const videoControlTab: TabSpec = {
|
|
152
|
+
id: TabId.VIDEO,
|
|
153
|
+
icon: 'tabVideo',
|
|
154
|
+
isEnabled: (config) => config.videoControl.isEnabled,
|
|
155
|
+
Component: VideoControl
|
|
156
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ReactEcs, { Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import type { Entity } from '@dcl/sdk/ecs'
|
|
3
|
+
import { TextAnnouncements as TextAnnouncementsComponent } from '../../components'
|
|
4
|
+
import type { AdminToolsValue } from '../../components'
|
|
5
|
+
import type { AdminState } from '../../state'
|
|
6
|
+
import { announcementIcons } from './icons'
|
|
7
|
+
import { announcementUi, recordAnnouncement } from './state'
|
|
8
|
+
|
|
9
|
+
export interface AnnouncementOverlayProps {
|
|
10
|
+
self: Entity
|
|
11
|
+
config: AdminToolsValue
|
|
12
|
+
assetBase: string
|
|
13
|
+
state: AdminState
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AnnouncementOverlay(props: AnnouncementOverlayProps): ReactEcs.JSX.Element | null {
|
|
17
|
+
const announcement = TextAnnouncementsComponent.getOrNull(props.self)
|
|
18
|
+
if (announcement === null || announcement.text === '') return null
|
|
19
|
+
|
|
20
|
+
recordAnnouncement(props.state, {
|
|
21
|
+
id: announcement.id,
|
|
22
|
+
text: announcement.text,
|
|
23
|
+
author: announcement.author ?? '',
|
|
24
|
+
timestamp: Date.now()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (announcementUi.dismissed.has(announcement.id)) return null
|
|
28
|
+
|
|
29
|
+
const icons = announcementIcons(props.assetBase)
|
|
30
|
+
const author = announcement.author ?? ''
|
|
31
|
+
const showAuthor =
|
|
32
|
+
props.config.textAnnouncementControl.showAuthorOnEachAnnouncement && author !== ''
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<UiEntity
|
|
36
|
+
uiTransform={{
|
|
37
|
+
positionType: 'absolute',
|
|
38
|
+
flexDirection: 'column',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'flex-end',
|
|
41
|
+
width: '100%',
|
|
42
|
+
height: '100%'
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<UiEntity
|
|
46
|
+
key={announcement.id}
|
|
47
|
+
uiTransform={{
|
|
48
|
+
flexDirection: 'column',
|
|
49
|
+
width: 400,
|
|
50
|
+
height: 150,
|
|
51
|
+
margin: { bottom: 10 },
|
|
52
|
+
padding: { top: 10, right: 10, bottom: 10, left: 10 },
|
|
53
|
+
pointerFilter: 'block'
|
|
54
|
+
}}
|
|
55
|
+
uiBackground={{ color: { r: 0.15, g: 0.15, b: 0.15, a: 0.95 } }}
|
|
56
|
+
>
|
|
57
|
+
<UiEntity
|
|
58
|
+
uiTransform={{ alignSelf: 'center', justifyContent: 'center', width: 50, height: 50 }}
|
|
59
|
+
uiBackground={{
|
|
60
|
+
textureMode: 'stretch',
|
|
61
|
+
texture: { src: icons.chatMessage },
|
|
62
|
+
color: { r: 1, g: 1, b: 1, a: 1 }
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
<Label
|
|
66
|
+
value={announcement.text}
|
|
67
|
+
fontSize={18}
|
|
68
|
+
uiTransform={{ width: '100%', flexGrow: 1 }}
|
|
69
|
+
/>
|
|
70
|
+
{showAuthor ? (
|
|
71
|
+
<Label
|
|
72
|
+
value={`- ${author}`}
|
|
73
|
+
fontSize={14}
|
|
74
|
+
color={{ r: 0.7, g: 0.7, b: 0.7, a: 1 }}
|
|
75
|
+
textAlign="bottom-right"
|
|
76
|
+
uiTransform={{ width: '100%' }}
|
|
77
|
+
/>
|
|
78
|
+
) : null}
|
|
79
|
+
<UiEntity
|
|
80
|
+
uiTransform={{
|
|
81
|
+
width: 24,
|
|
82
|
+
height: 24,
|
|
83
|
+
positionType: 'absolute',
|
|
84
|
+
position: { top: 5, right: 5 }
|
|
85
|
+
}}
|
|
86
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: icons.close } }}
|
|
87
|
+
onMouseDown={() => {
|
|
88
|
+
announcementUi.dismissed.add(announcement.id)
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</UiEntity>
|
|
92
|
+
</UiEntity>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { iconPath } from '../../icons'
|
|
2
|
+
|
|
3
|
+
export interface AnnouncementIcons {
|
|
4
|
+
check: string
|
|
5
|
+
close: string
|
|
6
|
+
chatMessage: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function announcementIcons(base: string): AnnouncementIcons {
|
|
10
|
+
return {
|
|
11
|
+
check: iconPath(base, 'announcements/text-announcement-check.png'),
|
|
12
|
+
close: iconPath(base, 'announcements/text-announcement-close-button.png'),
|
|
13
|
+
chatMessage: iconPath(base, 'announcements/text-announcement-chat-message.png')
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Announcement state the shared AdminState does not carry: the per-author rate
|
|
2
|
+
// tracker (the Creator Hub declares `messageRateTracker` but never reads it — it
|
|
3
|
+
// is enforced here), the composer's transient feedback line, and the set of
|
|
4
|
+
// announcement ids the local player dismissed on the overlay.
|
|
5
|
+
import type { AdminState } from '../../state'
|
|
6
|
+
|
|
7
|
+
export const ANNOUNCEMENT_MAX_LENGTH = 90
|
|
8
|
+
export const ANNOUNCEMENT_COOLDOWN_MS = 3000
|
|
9
|
+
|
|
10
|
+
export type AnnouncementFeedback = 'sent' | 'cleared' | 'empty' | 'throttled'
|
|
11
|
+
|
|
12
|
+
export interface AnnouncementUiState {
|
|
13
|
+
feedback: AnnouncementFeedback | undefined
|
|
14
|
+
throttledFor: number
|
|
15
|
+
inputResetSeq: number
|
|
16
|
+
rateTracker: Map<string, number>
|
|
17
|
+
dismissed: Set<string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const announcementUi: AnnouncementUiState = {
|
|
21
|
+
feedback: undefined,
|
|
22
|
+
throttledFor: 0,
|
|
23
|
+
inputResetSeq: 0,
|
|
24
|
+
rateTracker: new Map(),
|
|
25
|
+
dismissed: new Set()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cooldownRemaining(author: string, now: number): number {
|
|
29
|
+
const last = announcementUi.rateTracker.get(author)
|
|
30
|
+
if (last === undefined) return 0
|
|
31
|
+
return Math.max(0, ANNOUNCEMENT_COOLDOWN_MS - (now - last))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function recordSend(author: string, now: number): void {
|
|
35
|
+
announcementUi.rateTracker.set(author, now)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function announcementId(timestamp: number, author: string): string {
|
|
39
|
+
return `${timestamp}-${author}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AnnouncementRecord {
|
|
43
|
+
id: string
|
|
44
|
+
text: string
|
|
45
|
+
author: string
|
|
46
|
+
timestamp: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The panel history has to pick up announcements made by *other* admins too, so
|
|
50
|
+
// it is fed from the synced component rather than from the send handler.
|
|
51
|
+
export function recordAnnouncement(state: AdminState, entry: AnnouncementRecord): void {
|
|
52
|
+
if (entry.id === '' || entry.text === '') return
|
|
53
|
+
const list = state.textAnnouncements.announcements
|
|
54
|
+
if (list.some((known) => known.id === entry.id)) return
|
|
55
|
+
list.unshift(entry)
|
|
56
|
+
const max = Math.max(1, state.textAnnouncements.maxAnnouncements)
|
|
57
|
+
if (list.length > max) list.length = max
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function feedbackText(feedback: AnnouncementFeedback, throttledFor: number): string {
|
|
61
|
+
switch (feedback) {
|
|
62
|
+
case 'sent':
|
|
63
|
+
return 'Message sent!'
|
|
64
|
+
case 'cleared':
|
|
65
|
+
return 'Message cleared!'
|
|
66
|
+
case 'empty':
|
|
67
|
+
return 'Write an announcement first.'
|
|
68
|
+
default:
|
|
69
|
+
return `Too fast — try again in ${Math.ceil(throttledFor / 1000)}s.`
|
|
70
|
+
}
|
|
71
|
+
}
|