diff --git a/docker-compose.yaml b/docker-compose.yaml index 85a2414c01..3806c6a8f5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -89,6 +89,7 @@ services: ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" API_URL: back:50051 DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS + #Map editor PUBLIC_MAP_STORAGE_URL: "http://map-storage.workadventure.localhost" INTERNAL_MAP_STORAGE_URL: "http://map-storage:3000" ENABLE_OPENAPI_ENDPOINT: "true" diff --git a/docs/schema/1.0/wam.json b/docs/schema/1.0/wam.json index 3c6d142a39..5360ea8fec 100644 --- a/docs/schema/1.0/wam.json +++ b/docs/schema/1.0/wam.json @@ -207,6 +207,36 @@ "type" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/id" + }, + "buttonLabel": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/buttonLabel" + }, + "hideButtonLabel": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/hideButtonLabel" + }, + "type": { + "type": "string", + "const": "entityDescriptionProperties" + }, + "description": { + "type": "string" + }, + "searchable": { + "type": "boolean", + "default": false + } + }, + "required": [ + "id", + "type" + ], + "additionalProperties": false } ] } @@ -442,6 +472,36 @@ "speakerZoneName" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/id" + }, + "buttonLabel": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/buttonLabel" + }, + "hideButtonLabel": { + "$ref": "#/definitions/WAMFileFormat/properties/entities/additionalProperties/properties/properties/items/anyOf/0/properties/hideButtonLabel" + }, + "type": { + "type": "string", + "const": "areaDescriptionProperties" + }, + "description": { + "type": "string" + }, + "searchable": { + "type": "boolean", + "default": false + } + }, + "required": [ + "id", + "type" + ], + "additionalProperties": false } ] } @@ -520,20 +580,112 @@ "type": "object", "properties": { "name": { - "type": "string", + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "string" + } + ] + }, + { + "type": "null" + } + ], "description": "The name of the map." }, "description": { - "type": "string", + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "string" + } + ] + }, + { + "type": "null" + } + ], "description": "A description of the map. Can be used in social networks when sharing a link to the map." }, "copyright": { - "type": "string", + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "string" + } + ] + }, + { + "type": "null" + } + ], "description": "Copyright notice for this map. Can be a link to a license. Parts of this map like tilesets or images can have their own copyright." }, "thumbnail": { - "type": "string", + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "string" + } + ] + }, + { + "type": "null" + } + ], "description": "URL to a thumbnail image. This image will be used in social networks when sharing a link to the map." + }, + "areasSearchable": { + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "number" + } + ] + }, + { + "type": "null" + } + ], + "description": "Number of areas define as searchable by the map editor for the exploration mode." + }, + "entitiesSearchable": { + "anyOf": [ + { + "anyOf": [ + { + "not": {} + }, + { + "type": "number" + } + ] + }, + { + "type": "null" + } + ], + "description": "Number of entities define as searchable by the map editor for the exploration mode." } }, "additionalProperties": false, diff --git a/libs/map-editor/src/Commands/Command.ts b/libs/map-editor/src/Commands/Command.ts index 1d04154c87..2a580b16dc 100644 --- a/libs/map-editor/src/Commands/Command.ts +++ b/libs/map-editor/src/Commands/Command.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from "uuid"; +import { WAMFileFormat } from "../types"; export abstract class Command { public readonly commandId: string; @@ -7,6 +8,6 @@ export abstract class Command { this.commandId = commandId ?? uuidv4(); } - public abstract execute(): Promise; + public abstract execute(): Promise; //public abstract undo(): Promise; } diff --git a/libs/map-editor/src/Commands/Entity/UpdateEntityCommand.ts b/libs/map-editor/src/Commands/Entity/UpdateEntityCommand.ts index 8718458b71..0e0adb2cbd 100644 --- a/libs/map-editor/src/Commands/Entity/UpdateEntityCommand.ts +++ b/libs/map-editor/src/Commands/Entity/UpdateEntityCommand.ts @@ -1,4 +1,4 @@ -import type { WAMEntityData } from "../../types"; +import type { WAMEntityData, WAMFileFormat } from "../../types"; import type { GameMap } from "../../GameMap/GameMap"; import { Command } from "../Command"; @@ -29,10 +29,10 @@ export class UpdateEntityCommand extends Command { this.newConfig = structuredClone(dataToModify); } - public execute(): Promise { + public execute(): Promise { if (!this.gameMap.getGameMapEntities()?.updateEntity(this.entityId, this.newConfig)) { throw new Error(`MapEditorError: Could not execute UpdateEntity Command. Entity ID: ${this.entityId}`); } - return Promise.resolve(); + return Promise.resolve(this.gameMap.getGameMapEntities()?.wamFile); } } diff --git a/libs/map-editor/src/Commands/WAM/UpdateWAMMetadataCommand.ts b/libs/map-editor/src/Commands/WAM/UpdateWAMMetadataCommand.ts new file mode 100644 index 0000000000..12c2c215f9 --- /dev/null +++ b/libs/map-editor/src/Commands/WAM/UpdateWAMMetadataCommand.ts @@ -0,0 +1,34 @@ +import type { ModifiyWAMMetadataMessage } from "@workadventure/messages"; +import { WAMFileFormat } from "../../types"; +import { Command } from "../Command"; + +export class UpdateWAMMetadataCommand extends Command { + constructor( + protected wam: WAMFileFormat, + protected modifiyWAMMetadataMessage: ModifiyWAMMetadataMessage, + id?: string + ) { + super(id); + } + + execute(): Promise { + if (!this.wam.metadata) { + this.wam.metadata = {}; + } + this.wam.metadata.name = this.modifiyWAMMetadataMessage.name; + this.wam.metadata.description = this.modifiyWAMMetadataMessage.description; + this.wam.metadata.thumbnail = this.modifiyWAMMetadataMessage.thumbnail; + this.wam.metadata.copyright = this.modifiyWAMMetadataMessage.copyright; + + if (!this.wam.vendor) { + this.wam.vendor = { + tags: Array(), + }; + } + (this.wam.vendor as { tags: Array }).tags = + this.modifiyWAMMetadataMessage.tags && this.modifiyWAMMetadataMessage.tags.length > 0 + ? this.modifiyWAMMetadataMessage.tags.split(",") + : []; + return Promise.resolve(this.wam); + } +} diff --git a/libs/map-editor/src/GameMap/GameMapEntities.ts b/libs/map-editor/src/GameMap/GameMapEntities.ts index d3b8a9e265..06bfb312ff 100644 --- a/libs/map-editor/src/GameMap/GameMapEntities.ts +++ b/libs/map-editor/src/GameMap/GameMapEntities.ts @@ -45,4 +45,8 @@ export class GameMapEntities { public getEntities(): Record { return this.wam.entities; } + + get wamFile(): WAMFileFormat { + return this.wam; + } } diff --git a/libs/map-editor/src/index.ts b/libs/map-editor/src/index.ts index c897001022..d4e81ccd9f 100644 --- a/libs/map-editor/src/index.ts +++ b/libs/map-editor/src/index.ts @@ -9,6 +9,7 @@ export * from "./Commands/Entity/UpdateEntityCommand"; export * from "./Commands/Entity/CreateEntityCommand"; export * from "./Commands/Entity/DeleteEntityCommand"; export * from "./Commands/WAM/UpdateWAMSettingCommand"; +export * from "./Commands/WAM/UpdateWAMMetadataCommand"; export * from "./Commands/Command"; // MapFetcher is not exported because it is using Node imports that are not available in the browser //export * from "./MapFetcher"; diff --git a/libs/map-editor/src/types.ts b/libs/map-editor/src/types.ts index 847160fd55..b451f832aa 100644 --- a/libs/map-editor/src/types.ts +++ b/libs/map-editor/src/types.ts @@ -106,6 +106,18 @@ export const ListenerMegaphonePropertyData = PropertyBase.extend({ chatEnabled: z.boolean().default(false), }); +export const EntityDescriptionPropertyData = PropertyBase.extend({ + type: z.literal("entityDescriptionProperties"), + description: z.string().optional(), + searchable: z.boolean().default(false), +}); + +export const AreaDescriptionPropertyData = PropertyBase.extend({ + type: z.literal("areaDescriptionProperties"), + description: z.string().optional(), + searchable: z.boolean().default(false), +}); + export const AreaDataProperty = z.discriminatedUnion("type", [ StartPropertyData, ExitPropertyData, @@ -116,6 +128,7 @@ export const AreaDataProperty = z.discriminatedUnion("type", [ OpenWebsitePropertyData, SpeakerMegaphonePropertyData, ListenerMegaphonePropertyData, + AreaDescriptionPropertyData, ]); export const AreaDataProperties = z.array(AreaDataProperty); @@ -142,6 +155,7 @@ export const EntityDataProperty = z.discriminatedUnion("type", [ JitsiRoomPropertyData, PlayAudioPropertyData, OpenWebsitePropertyData, + EntityDescriptionPropertyData, ]); export const EntityDataProperties = z.array(EntityDataProperty); @@ -187,24 +201,38 @@ export const WAMEntityData = EntityData.omit({ prefab: true, id: true }); export type WAMEntityData = z.infer; export const WAMMetadata = z.object({ - name: z.string().optional().describe("The name of the map."), + name: z.string().optional().nullable().describe("The name of the map."), description: z .string() .optional() + .nullable() .describe("A description of the map. Can be used in social networks when sharing a link to the map."), copyright: z .string() .optional() + .nullable() .describe( "Copyright notice for this map. Can be a link to a license. Parts of this map like tilesets or images can have their own copyright." ), thumbnail: z .string() .optional() + .nullable() .describe( "URL to a thumbnail image. This image will be used in social networks when sharing a link to the map." ), + areasSearchable: z + .number() + .optional() + .nullable() + .describe("Number of areas define as searchable by the map editor for the exploration mode."), + entitiesSearchable: z + .number() + .optional() + .nullable() + .describe("Number of entities define as searchable by the map editor for the exploration mode."), }); +export type WAMMetadata = z.infer; export const WAMVendor = z .unknown() @@ -288,6 +316,8 @@ export type MapsCacheSingleMapFormat = z.infer; export type MapsCacheFileFormat = z.infer; export type SpeakerMegaphonePropertyData = z.infer; export type ListenerMegaphonePropertyData = z.infer; +export type EntityDescriptionPropertyData = z.infer; +export type AreaDescriptionPropertyData = z.infer; export enum GameMapProperties { ALLOW_API = "allowApi", diff --git a/libs/map-editor/tests/WAMMetadata.test.ts b/libs/map-editor/tests/WAMMetadata.test.ts new file mode 100644 index 0000000000..dac4568ecc --- /dev/null +++ b/libs/map-editor/tests/WAMMetadata.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, assert } from "vitest"; +import { UpdateWAMMetadataCommand, WAMFileFormat } from "../src"; + +describe("WAM Metadata", () => { + const defaultWamFile: WAMFileFormat = { + version: "1.0.0", + mapUrl: "testMapUrl", + entities: {}, + areas: [], + entityCollections: [], + }; + const dataToModify = { + name: "Test of room", + thumbnail: "Test of room thumbnail", + description: "Test of room description", + copyright: "Test of copyright", + tags: "admin,member", + }; + it("should change WAM file loaded when WAMMetadata receive", async () => { + const wamFile: WAMFileFormat = { ...defaultWamFile }; + const command = new UpdateWAMMetadataCommand(wamFile, dataToModify, "test-uuid"); + await command.execute(); + expect(wamFile.metadata).toBeDefined(); + if (wamFile.metadata) { + expect(wamFile.metadata.name).toEqual(dataToModify.name); + expect(wamFile.metadata.description).toEqual(dataToModify.description); + expect(wamFile.metadata.copyright).toEqual(dataToModify.copyright); + expect(wamFile.metadata.thumbnail).toEqual(dataToModify.thumbnail); + } else { + assert.fail("wamFile.metadata is not defined"); + } + + if (wamFile.vendor) { + expect((wamFile.vendor as { tags: string[] }).tags).toBeDefined(); + if ((wamFile.vendor as { tags: string[] }).tags) { + expect((wamFile.vendor as { tags: string[] }).tags).toEqual(dataToModify.tags.split(",")); + } else { + assert.fail("wamFile.vendor.tags error"); + } + } else { + assert.fail("wamFile.vendor is not defined"); + } + }); +}); diff --git a/map-storage/src/MapStorageServer.ts b/map-storage/src/MapStorageServer.ts index c556d9435a..d90612af2c 100644 --- a/map-storage/src/MapStorageServer.ts +++ b/map-storage/src/MapStorageServer.ts @@ -6,12 +6,12 @@ import { CreateEntityCommand, DeleteAreaCommand, DeleteEntityCommand, - EntityData, EntityDataProperties, UpdateAreaCommand, UpdateEntityCommand, UpdateWAMSettingCommand, WAMEntityData, + UpdateWAMMetadataCommand, } from "@workadventure/map-editor"; import { EditMapCommandMessage, @@ -118,7 +118,7 @@ const mapStorageServer: MapStorageServer = { // NOTE: protobuf does not distinguish between null and empty array, we cannot create optional repeated value. // Because of that, we send additional "modifyProperties" flag set properties value as "undefined" so they won't get erased // by [] value which was supposed to be null. - const dataToModify: AtLeast = structuredClone(message); + const dataToModify: AtLeast = structuredClone(message); if (!message.modifyProperties) { dataToModify.properties = undefined; } @@ -126,6 +126,7 @@ const mapStorageServer: MapStorageServer = { if (area) { await mapsManager.executeCommand( mapKey, + mapUrl.host, new UpdateAreaCommand(gameMap, dataToModify, commandId) ); } else { @@ -141,13 +142,18 @@ const mapStorageServer: MapStorageServer = { }; await mapsManager.executeCommand( mapKey, + mapUrl.host, new CreateAreaCommand(gameMap, areaObjectConfig, commandId) ); break; } case "deleteAreaMessage": { const message = editMapMessage.deleteAreaMessage; - await mapsManager.executeCommand(mapKey, new DeleteAreaCommand(gameMap, message.id, commandId)); + await mapsManager.executeCommand( + mapKey, + mapUrl.host, + new DeleteAreaCommand(gameMap, message.id, commandId) + ); break; } case "modifyEntityMessage": { @@ -164,6 +170,7 @@ const mapStorageServer: MapStorageServer = { if (entity) { await mapsManager.executeCommand( mapKey, + mapUrl.host, new UpdateEntityCommand(gameMap, message.id, dataToModify, commandId) ); } else { @@ -175,6 +182,7 @@ const mapStorageServer: MapStorageServer = { const message = editMapMessage.createEntityMessage; await mapsManager.executeCommand( mapKey, + mapUrl.host, new CreateEntityCommand( gameMap, message.id, @@ -194,7 +202,11 @@ const mapStorageServer: MapStorageServer = { } case "deleteEntityMessage": { const message = editMapMessage.deleteEntityMessage; - await mapsManager.executeCommand(mapKey, new DeleteEntityCommand(gameMap, message.id, commandId)); + await mapsManager.executeCommand( + mapKey, + mapUrl.host, + new DeleteEntityCommand(gameMap, message.id, commandId) + ); break; } case "updateWAMSettingsMessage": { @@ -205,13 +217,30 @@ const mapStorageServer: MapStorageServer = { throw new Error("WAM is not defined"); } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - await mapsManager.executeCommand(mapKey, new UpdateWAMSettingCommand(wam, message, commandId)); + await mapsManager.executeCommand( + mapKey, + mapUrl.host, + new UpdateWAMSettingCommand(wam, message, commandId) + ); break; } case "errorCommandMessage": { // Nothing to do, this message will never come from client break; } + case "modifiyWAMMetadataMessage": { + const message = editMapMessage.modifiyWAMMetadataMessage; + const wam = gameMap.getWam(); + if (!wam) { + throw new Error("WAM is not defined"); + } + await mapsManager.executeCommand( + mapKey, + mapUrl.host, + new UpdateWAMMetadataCommand(wam, message, commandId) + ); + break; + } default: { const _exhaustiveCheck: never = editMapMessage; } diff --git a/map-storage/src/MapsManager.ts b/map-storage/src/MapsManager.ts index fb5df0d6b8..2e87fe6bae 100644 --- a/map-storage/src/MapsManager.ts +++ b/map-storage/src/MapsManager.ts @@ -3,6 +3,9 @@ import { EditMapCommandMessage } from "@workadventure/messages"; import { ITiledMap } from "@workadventure/tiled-map-type-guard"; import * as Sentry from "@sentry/node"; import { fileSystem } from "./fileSystem"; +import { MapListService } from "./Services/MapListService"; +import { WebHookService } from "./Services/WebHookService"; +import { WEB_HOOK_URL } from "./Enum/EnvironmentVariable"; class MapsManager { private loadedMaps: Map; @@ -11,6 +14,8 @@ class MapsManager { private saveMapIntervals: Map; private mapLastChangeTimestamp: Map; + private mapListService: MapListService; + /** * Attempt to save the map from memory to file every time interval */ @@ -29,9 +34,10 @@ class MapsManager { this.loadedMapsCommandsQueue = new Map(); this.saveMapIntervals = new Map(); this.mapLastChangeTimestamp = new Map(); + this.mapListService = new MapListService(fileSystem, new WebHookService(WEB_HOOK_URL)); } - public async executeCommand(mapKey: string, command: Command): Promise { + public async executeCommand(mapKey: string, domain: string, command: Command): Promise { const gameMap = this.getGameMap(mapKey); if (!gameMap) { throw new Error('Could not find GameMap with key "' + mapKey + '"'); @@ -40,7 +46,11 @@ class MapsManager { if (!this.saveMapIntervals.has(mapKey)) { this.startSavingMapInterval(mapKey, this.AUTO_SAVE_INTERVAL_MS); } - await command.execute(); + const wamFile = await command.execute(); + if (wamFile != undefined) + this.mapListService + .updateWAMFileInCache(domain, mapKey.replace(domain, ""), wamFile) + .catch((e) => console.error(e)); } public getCommandsNewerThan(mapKey: string, commandId: string | undefined): EditMapCommandMessage[] { diff --git a/map-storage/src/Services/MapListService.ts b/map-storage/src/Services/MapListService.ts index 65157f19b0..ecfea5984a 100644 --- a/map-storage/src/Services/MapListService.ts +++ b/map-storage/src/Services/MapListService.ts @@ -1,4 +1,9 @@ -import { MapsCacheFileFormat, WAMFileFormat } from "@workadventure/map-editor"; +import { + AreaDescriptionPropertyData, + EntityDescriptionPropertyData, + MapsCacheFileFormat, + WAMFileFormat, +} from "@workadventure/map-editor"; import { WAMVersionHash } from "@workadventure/map-editor/src/WAMVersionHash"; import pLimit from "p-limit"; import * as Sentry from "@sentry/node"; @@ -111,9 +116,30 @@ export class MapListService { if (wamFilePath.startsWith("/")) { wamFilePath = wamFilePath.substring(1); } + cacheFile.maps[wamFilePath] = { mapUrl: wamFile.mapUrl, - metadata: wamFile.metadata, + metadata: { + ...wamFile.metadata, + areasSearchable: wamFile.areas.reduce((nb, area) => { + if ( + area.properties && + area.properties.find((property) => (property as AreaDescriptionPropertyData).searchable) + ) { + return nb + 1; + } + return nb; + }, 0), + entitiesSearchable: Object.values(wamFile.entities).reduce((nb, entity) => { + if ( + entity.properties && + entity.properties.find((property) => (property as EntityDescriptionPropertyData).searchable) + ) { + return nb + 1; + } + return nb; + }, 0), + }, vendor: wamFile.vendor, }; await this.writeCacheFileNoLimit(domain, cacheFile); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index eae7524a34..609cdf8fe3 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -144,6 +144,7 @@ message EditMapMessage { DeleteEntityMessage deleteEntityMessage = 6; UpdateWAMSettingsMessage updateWAMSettingsMessage = 7; ErrorCommandMessage errorCommandMessage = 8; + ModifiyWAMMetadataMessage modifiyWAMMetadataMessage = 9; } } @@ -273,6 +274,14 @@ message ReceivedEventMessage { optional int32 senderId = 3; } +message ModifiyWAMMetadataMessage{ + string name = 1; + optional string description = 2; + optional string copyright = 3; + optional string thumbnail = 4; + optional string tags = 5; +} + /************ BI-DIRECTIONAL MESSAGES **************/ message ItemEventMessage { @@ -403,6 +412,11 @@ message RoomShortDescription { string name = 1; string roomUrl = 2; optional string wamUrl = 3; + optional string description = 4; + optional string copyright = 5; + optional string thumbnail = 6; + optional int32 areasSearchable = 7; + optional int32 entitiesSearchable = 8; } message RoomsFromSameWorldAnswer { diff --git a/play/public/resources/objects/cloud.mp3 b/play/public/resources/objects/cloud.mp3 new file mode 100644 index 0000000000..1990a49017 Binary files /dev/null and b/play/public/resources/objects/cloud.mp3 differ diff --git a/play/src/front/Administration/AnalyticsClient.ts b/play/src/front/Administration/AnalyticsClient.ts index bc9fd6a1e6..389f0dfb97 100644 --- a/play/src/front/Administration/AnalyticsClient.ts +++ b/play/src/front/Administration/AnalyticsClient.ts @@ -673,5 +673,29 @@ class AnalyticsClient { }) .catch((e) => console.error(e)); } + + openExplorationMode(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture(`wa_map-exploration-open`); + }) + .catch((e) => console.error(e)); + } + + closeExplorationMode(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture(`wa_map-exploration-close`); + }) + .catch((e) => console.error(e)); + } + + openedRoomList(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-opened-room-list"); + }) + .catch((e) => console.error(e)); + } } export const analyticsClient = new AnalyticsClient(); diff --git a/play/src/front/Components/ActionBar/ActionBar.svelte b/play/src/front/Components/ActionBar/ActionBar.svelte index 7f4424689c..153231eb57 100644 --- a/play/src/front/Components/ActionBar/ActionBar.svelte +++ b/play/src/front/Components/ActionBar/ActionBar.svelte @@ -31,7 +31,7 @@ import followImg from "../images/follow.png"; import lockOpenImg from "../images/lock-opened.png"; import lockCloseImg from "../images/lock-closed.png"; - import mapBuilder from "../images/maps-builder.png"; + import mapBuilder from "../images/maps-builder.svg"; import screenshareOn from "../images/screenshare-on.png"; import screenshareOff from "../images/screenshare-off.png"; import emojiPickOn from "../images/emoji-on.png"; @@ -40,6 +40,7 @@ import hammerImg from "../images/hammer.png"; import megaphoneImg from "../images/megaphone.svg"; import WorkAdventureImg from "../images/icon-workadventure-white.png"; + import worldImg from "../images/world.svg"; import { LayoutMode } from "../../WebRtc/LayoutManager"; import { embedScreenLayoutStore } from "../../Stores/EmbedScreensStore"; import { followRoleStore, followStateStore, followUsersStore } from "../../Stores/FollowStore"; @@ -62,7 +63,6 @@ additionnalButtonsMenu, addClassicButtonActionBarEvent, addActionButtonActionBarEvent, - mapEditorActivated, } from "../../Stores/MenuStore"; import { emoteDataStore, @@ -80,7 +80,7 @@ import { peerStore } from "../../Stores/PeerStore"; import { StringUtils } from "../../Utils/StringUtils"; import Tooltip from "../Util/Tooltip.svelte"; - import { modalIframeStore, modalVisibilityStore } from "../../Stores/ModalStore"; + import { modalIframeStore, modalVisibilityStore, roomListVisibilityStore } from "../../Stores/ModalStore"; import { userHasAccessToBackOfficeStore } from "../../Stores/GameStore"; import { AddButtonActionBarEvent } from "../../Api/Events/Ui/ButtonActionBarEvent"; import { Emoji } from "../../Stores/Utils/emojiSchema"; @@ -189,6 +189,7 @@ function toggleMapEditorMode() { if (isMobile) return; + if ($mapEditorModeStore) gameManager.getCurrentGameScene().getMapEditorModeManager().equipTool(undefined); analyticsClient.toggleMapEditor(!$mapEditorModeStore); mapEditorModeStore.switchMode(!$mapEditorModeStore); } @@ -385,6 +386,13 @@ mapEditorModeStore.set(false); } }); + + function showRoomList() { + resetChatVisibility(); + resetModalVisibility(); + + roomListVisibilityStore.set(true); + } @@ -811,34 +819,28 @@ {$LL.menu.icon.open.menu()} - {#if $mapEditorActivated} - -
+
+ {#if isMobile} + + {:else} + + {/if} + -
- {/if} + toggle-map-editor + +
{#if $userHasAccessToBackOfficeStore}
+
+ + + +
analyticsClient.openedRoomList()} + on:click={showRoomList} + class="bottom-action-button" + > + + + +
+
+ {#if $addActionButtonActionBarEvent.length > 0}
{#each $addActionButtonActionBarEvent as button (button.id)} diff --git a/play/src/front/Components/EmbedScreens/Layouts/MozaicLayout.svelte b/play/src/front/Components/EmbedScreens/Layouts/MozaicLayout.svelte index b029d58fc6..5acadee96d 100644 --- a/play/src/front/Components/EmbedScreens/Layouts/MozaicLayout.svelte +++ b/play/src/front/Components/EmbedScreens/Layouts/MozaicLayout.svelte @@ -41,7 +41,7 @@ {/if}
-
+
{#if $myCameraStore && !displayFullMedias} {/if} diff --git a/play/src/front/Components/Exploration/Explorer.svelte b/play/src/front/Components/Exploration/Explorer.svelte new file mode 100644 index 0000000000..91eafa835b --- /dev/null +++ b/play/src/front/Components/Exploration/Explorer.svelte @@ -0,0 +1,397 @@ + + +
+
+

{$LL.mapEditor.explorer.title()}

+ {#if !showSearchMode} +

+ {$LL.mapEditor.explorer.description()} +

+ {/if} +
+
+
+ {#if !showSearchMode} +
+ +
+ {/if} +
+ +
+
+ + {#if showSearchMode} +
+ +
+ +
+ { + addFilter("jitsiRoomProperty"); + }} + /> + { + addFilter("playAudio"); + }} + /> + { + addFilter("openWebsite"); + }} + /> + { + addFilter("speakerMegaphone"); + }} + /> + { + addFilter("listenerMegaphone"); + }} + /> + { + addFilter("exit"); + }} + /> + { + addFilter("silent"); + }} + /> + { + addFilter("focusable"); + }} + /> +
+ + +
+ link icon + {#if $entitessListFiltered.size > 0} + {$entitessListFiltered.size} + {$LL.mapEditor.explorer.entitiesFound($entitessListFiltered.size > 1)} + {:else} +

{$LL.mapEditor.explorer.noEntitiesFound()}

+ {/if} + +
+ + {#if entityListActive && $entitessListFiltered.size > 0} +
+ {#each [...$entitessListFiltered] as [key, entity] (key)} + +
highlightEntity(entity)} + on:mouseleave={() => unhighlightEntity(entity)} + on:click={() => mapExplorationObjectSelectedStore.set(entity)} + class="item tw-p-4 tw-rounded-2xl tw-flex tw-flex-row tw-justify-around tw-items-center tw-cursor-pointer" + > + link icon + {entity.getEntityData().name ?? entity.getPrefab().name} +
+ {/each} +
+ {/if} + + +
+ link icon + {#if $areasListFiltered.size > 0} + {$areasListFiltered.size} + {$LL.mapEditor.explorer.areasFound($areasListFiltered.size > 1)} + {:else} +

{$LL.mapEditor.explorer.noAreasFound()}

+ {/if} + +
+ {#if areaListActive && $areasListFiltered.size > 0} +
+ {#if $areasListFiltered.size > 0} + {#each [...$areasListFiltered] as [key, area] (key)} + +
highlightArea(area)} + on:mouseleave={() => unhighlightArea(area)} + on:click={() => mapExplorationObjectSelectedStore.set(area)} + class="item tw-p-4 tw-rounded-2xl tw-flex tw-flex-row tw-justify-around tw-items-center tw-cursor-pointer" + > + link icon + + {area.getAreaData().name || "No name"} + +
+ {/each} + {/if} +
+ {/if} + {/if} +
+
+ + diff --git a/play/src/front/Components/Exploration/MapList.svelte b/play/src/front/Components/Exploration/MapList.svelte new file mode 100644 index 0000000000..11d973df30 --- /dev/null +++ b/play/src/front/Components/Exploration/MapList.svelte @@ -0,0 +1,135 @@ + + + + + diff --git a/play/src/front/Components/Input/InputTags.svelte b/play/src/front/Components/Input/InputTags.svelte index 03d23ee2e4..e5514b21f8 100644 --- a/play/src/front/Components/Input/InputTags.svelte +++ b/play/src/front/Components/Input/InputTags.svelte @@ -25,13 +25,13 @@ function handleChange() { options = options.map((i) => { delete i.created; - return { ...i, label: i.label.toLocaleUpperCase() }; + return { ...i, label: i.label.toLowerCase() }; }); } -
-