From 68215dd54486fccc4ea9014a3fc1038b643e67a9 Mon Sep 17 00:00:00 2001 From: Wouter Koppenol Date: Fri, 31 May 2019 13:45:44 +0200 Subject: [PATCH] #37 Updated echo server configuration, added ability to switch colors and have it be live updated. Fixed an issue where anonymous users couldn't view any routes. --- .env.example | 1 + app/Events/BrushlineChangedEvent.php | 3 +- app/Events/BrushlineDeletedEvent.php | 3 +- app/Events/KillZoneChangedEvent.php | 3 +- app/Events/KillZoneDeletedEvent.php | 3 +- app/Events/MapCommentChangedEvent.php | 3 +- app/Events/MapCommentDeletedEvent.php | 3 +- app/Events/PathChangedEvent.php | 3 +- app/Events/PathDeletedEvent.php | 3 +- app/Events/UserColorChangedEvent.php | 58 ++++++++++ app/Http/Controllers/ProfileController.php | 34 +++++- app/Policies/DungeonRoutePolicy.php | 2 +- app/Providers/EchoServerServiceProvider.php | 30 +++++ app/Service/EchoServerConfigService.php | 70 ++++++++++++ .../EchoServerConfigServiceInterface.php | 14 +++ app/Service/EchoServerHttpApiInterface.php | 15 +++ app/Service/EchoServerHttpApiService.php | 105 ++++++++++++++++++ config/app.php | 7 ++ .../echo/live/laravel-echo-server.json | 7 +- .../echo/local/laravel-echo-server.json | 12 +- .../echo/staging/laravel-echo-server.json | 4 +- docs/echo_server.md | 11 +- resources/assets/js/custom/dungeonmap.js | 5 +- .../js/custom/mapcontrols/echocontrols.js | 51 +++++++-- .../brushlinemapobjectgroup.js | 2 +- .../mapobjectgroups/killzonemapobjectgroup.js | 2 +- .../mapcommentmapobjectgroup.js | 2 +- .../mapobjectgroups/pathmapobjectgroup.js | 2 +- resources/views/common/forms/login.blade.php | 2 +- resources/views/common/maps/map.blade.php | 15 ++- routes/channels.php | 2 +- update_echo_clients.sh | 21 ++++ update_live.sh | 3 + 33 files changed, 457 insertions(+), 44 deletions(-) create mode 100644 app/Events/UserColorChangedEvent.php create mode 100644 app/Providers/EchoServerServiceProvider.php create mode 100644 app/Service/EchoServerConfigService.php create mode 100644 app/Service/EchoServerConfigServiceInterface.php create mode 100644 app/Service/EchoServerHttpApiInterface.php create mode 100644 app/Service/EchoServerHttpApiService.php rename laravel-echo-server-live.json => config/echo/live/laravel-echo-server.json (85%) rename laravel-echo-server-local.json => config/echo/local/laravel-echo-server.json (66%) rename laravel-echo-server-staging.json => config/echo/staging/laravel-echo-server.json (89%) create mode 100644 update_echo_clients.sh diff --git a/.env.example b/.env.example index b243fcec4..f1a4068d4 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ APP_DEBUG=true APP_LOG_LEVEL=debug APP_LOG_DISCORD_WEBHOOK= APP_URL=http://keystone.test +APP_TYPE=local LOG_CHANNEL=daily DB_CONNECTION=mysql diff --git a/app/Events/BrushlineChangedEvent.php b/app/Events/BrushlineChangedEvent.php index 0b25843ec..7732dc819 100644 --- a/app/Events/BrushlineChangedEvent.php +++ b/app/Events/BrushlineChangedEvent.php @@ -5,6 +5,7 @@ use App\Models\Brushline; use App\Models\DungeonRoute; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, Brushline $brushline) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/BrushlineDeletedEvent.php b/app/Events/BrushlineDeletedEvent.php index 85cc2efc4..1407f4d49 100644 --- a/app/Events/BrushlineDeletedEvent.php +++ b/app/Events/BrushlineDeletedEvent.php @@ -5,6 +5,7 @@ use App\Models\Brushline; use App\Models\DungeonRoute; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, Brushline $brushline) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/KillZoneChangedEvent.php b/app/Events/KillZoneChangedEvent.php index 1fab7c4d8..2b4b5b05f 100644 --- a/app/Events/KillZoneChangedEvent.php +++ b/app/Events/KillZoneChangedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\KillZone; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, KillZone $killZone) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/KillZoneDeletedEvent.php b/app/Events/KillZoneDeletedEvent.php index ad402b043..f2c3a6dcc 100644 --- a/app/Events/KillZoneDeletedEvent.php +++ b/app/Events/KillZoneDeletedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\KillZone; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, KillZone $killZone) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/MapCommentChangedEvent.php b/app/Events/MapCommentChangedEvent.php index 984c52af0..f92197978 100644 --- a/app/Events/MapCommentChangedEvent.php +++ b/app/Events/MapCommentChangedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\MapComment; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, MapComment $mapComment) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/MapCommentDeletedEvent.php b/app/Events/MapCommentDeletedEvent.php index c02e1fc68..2eb7e5dd3 100644 --- a/app/Events/MapCommentDeletedEvent.php +++ b/app/Events/MapCommentDeletedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\MapComment; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, MapComment $mapComment) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/PathChangedEvent.php b/app/Events/PathChangedEvent.php index 84444d293..1db779299 100644 --- a/app/Events/PathChangedEvent.php +++ b/app/Events/PathChangedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\Path; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, Path $path) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/PathDeletedEvent.php b/app/Events/PathDeletedEvent.php index 817e6d15a..995ba889f 100644 --- a/app/Events/PathDeletedEvent.php +++ b/app/Events/PathDeletedEvent.php @@ -5,6 +5,7 @@ use App\Models\DungeonRoute; use App\Models\Path; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -40,7 +41,7 @@ public function __construct(DungeonRoute $dungeonroute, Path $path) */ public function broadcastOn() { - return new PrivateChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); } public function broadcastAs() diff --git a/app/Events/UserColorChangedEvent.php b/app/Events/UserColorChangedEvent.php new file mode 100644 index 000000000..f6199adf4 --- /dev/null +++ b/app/Events/UserColorChangedEvent.php @@ -0,0 +1,58 @@ +_dungeonroute = $dungeonroute; + $this->_user = $user; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PresenceChannel(sprintf('route-edit.%s', $this->_dungeonroute->public_key)); + } + + public function broadcastAs() + { + return 'user-color-changed'; + } + + public function broadcastWith() + { + return [ + 'name' => $this->_user->name, + 'color' => $this->_user->echo_color + ]; + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 1f0e11a32..2800009eb 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -2,6 +2,9 @@ namespace App\Http\Controllers; +use App\Events\UserColorChangedEvent; +use App\Models\DungeonRoute; +use App\Service\EchoServerHttpApiService; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -17,9 +20,11 @@ public function edit(Request $request) /** * @param Request $request * @param User $user + * @param EchoServerHttpApiService $echoServerHttpApiService * @return \Illuminate\Http\RedirectResponse + * @throws \Exception */ - public function update(Request $request, User $user) + public function update(Request $request, User $user, EchoServerHttpApiService $echoServerHttpApiService) { // Allow username change once! if ($user->isOAuth()) { @@ -54,6 +59,33 @@ public function update(Request $request, User $user) if (!$emailExists && !$nameExists) { if ($user->save()) { \Session::flash('status', __('Profile updated')); + + // Propagate changes to any channel the user may be in + foreach ($echoServerHttpApiService->getChannels() as $channel) { + $assoc = get_object_vars($channel); + $channelName = array_keys($assoc)[0]; + + $routeKey = str_replace('presence-route-edit.', '', $channelName); + + $userInChannel = false; + // Check if the user is in this channel.. + foreach ($echoServerHttpApiService->getChannelUsers($channelName) as $users) { + + foreach ($users as $channelUser) { + if ($channelUser->id === $user->id) { + $userInChannel = true; + break; + } + } + } + + if ($userInChannel) { + /** @var DungeonRoute $dungeonRoute */ + $dungeonRoute = DungeonRoute::where('public_key', $routeKey)->firstOrFail(); + // Broadcast that channel that the user's color has changed + broadcast(new UserColorChangedEvent($dungeonRoute, $user)); + } + } } else { abort(500, __('An unexpected error occurred trying to save your profile')); } diff --git a/app/Policies/DungeonRoutePolicy.php b/app/Policies/DungeonRoutePolicy.php index 40dde45b3..8e842fb5a 100644 --- a/app/Policies/DungeonRoutePolicy.php +++ b/app/Policies/DungeonRoutePolicy.php @@ -17,7 +17,7 @@ class DungeonRoutePolicy * @param \App\Models\DungeonRoute $dungeonroute * @return mixed */ - public function view(User $user, DungeonRoute $dungeonroute) + public function view(?User $user, DungeonRoute $dungeonroute) { // Everyone can view dungeon routes (for now) return $dungeonroute->published; diff --git a/app/Providers/EchoServerServiceProvider.php b/app/Providers/EchoServerServiceProvider.php new file mode 100644 index 000000000..265928bd4 --- /dev/null +++ b/app/Providers/EchoServerServiceProvider.php @@ -0,0 +1,30 @@ +app->bind('App\Service\EchoServerConfigServiceInterface', 'App\Service\EchoServerConfigService'); + $this->app->bind('App\Service\EchoServerHttpApiServiceInterface', 'App\Service\EchoServerHttpApiService'); + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/app/Service/EchoServerConfigService.php b/app/Service/EchoServerConfigService.php new file mode 100644 index 000000000..3ee72bc2b --- /dev/null +++ b/app/Service/EchoServerConfigService.php @@ -0,0 +1,70 @@ +getConfig())->clients; + } + + /** + * @param $appId string + * @return \stdClass + * @throws \Exception + */ + public function getClient($appId) + { + $result = false; + foreach ($this->getConfig()->clients as $client) { + if ($client->appId === $appId) { + $result = $client; + break; + } + } + + return $result; + } + + /** + * @param $index int + * @return \stdClass + * @throws \Exception + */ + public function getClientAt($index) + { + return ($this->getConfig())->clients[$index]; + } + +} \ No newline at end of file diff --git a/app/Service/EchoServerConfigServiceInterface.php b/app/Service/EchoServerConfigServiceInterface.php new file mode 100644 index 000000000..cdb7f0693 --- /dev/null +++ b/app/Service/EchoServerConfigServiceInterface.php @@ -0,0 +1,14 @@ +_configService = $configService; + $port = ($this->_configService->getConfig())->port; + + // Make sure we don't have a trailing slash in the app_url + $appUrl = trim(env('APP_URL'), '/'); + + $this->_client = new Client([ + // Base URI is used with relative requests + 'base_uri' => sprintf('%s:%s', $appUrl, $port), + // You can set any number of default request options. + 'timeout' => 2.0 + ]); + } + + /** + * @param $uri + * @param string $appId + * @return bool|mixed + * @throws \Exception + */ + private function _doRequest($uri, $appId = '') + { + $result = false; + + // Find the client based on the app ID + $client = $appId === '' ? $this->_configService->getClientAt(0) : $this->_configService->getClient($appId); + + // Perform the API request with the correct auth key + $response = $this->_client->get(sprintf('apps/%s/%s', $client->appId, $uri), ['query' => ['auth_key' => $client->key]]); + if ($response->getStatusCode() === StatusCode::OK) { + $result = json_decode((string)$response->getBody()); + } + + return $result; + } + + /** + * @param $appId + * @return mixed + * @throws \Exception + */ + public function getStatus($appId = '') + { + return $this->_doRequest('status', $appId); + } + + /** + * @param $appId + * @return mixed + * @throws \Exception + */ + public function getChannels($appId = '') + { + return $this->_doRequest('channels', $appId); + } + + /** + * @param $appId + * @param $channelName + * @return mixed + * @throws \Exception + */ + public function getChannelInfo($channelName, $appId = '') + { + return $this->_doRequest(sprintf('channels/%s', $channelName), $appId); + } + + /** + * @param $appId + * @param $channelName + * @return mixed + * @throws \Exception + */ + public function getChannelUsers($channelName, $appId = '') + { + return $this->_doRequest(sprintf('channels/%s/users', $channelName), $appId); + } + +} \ No newline at end of file diff --git a/config/app.php b/config/app.php index 1596e3348..7c66db7d6 100644 --- a/config/app.php +++ b/config/app.php @@ -1,5 +1,7 @@ { - console.log('here', users); for (let index in users) { if (users.hasOwnProperty(index)) { self._addUser(users[index]); @@ -47,35 +46,48 @@ class EchoControls extends MapControl { self._setStatus('connected'); }) .joining(user => { - console.log('joining', user); self._addUser(user); }) .leaving(user => { - console.log('leaving', user); self._removeUser(user); + }) + .listen('.user-color-changed', (e) => { + self._setUserColor(e.name, e.color); }); } + /** + * Sets the status of the controls. + * @param status string Either 'connecting' or 'connected'. + * @private + */ _setStatus(status) { + console.assert(this instanceof EchoControls, 'this is not EchoControls', this); + let $connecting = $('.connecting'); + let $connected = $('.connected'); switch (status) { case 'connecting': - $('.connecting').show(); - $('.connected').hide(); + $connecting.show(); + $connected.hide(); break; case 'connected': - $('.connecting').hide(); - $('.connected').show(); + $connecting.hide(); + $connected.show(); break; } } + /** + * Adds a user to the status bar. + * @param user Object + * @private + */ _addUser(user) { + console.assert(this instanceof EchoControls, 'this is not EchoControls', this); let template = Handlebars.templates['map_controls_route_echo_member_template']; // May be unset when not our own user, but this confuses handlebars - if (typeof user.self === 'undefined') { - user.self = false; - } + user.self = user.name === this.map.options.username; let data = getHandlebarsDefaultVariables(); @@ -87,11 +99,28 @@ class EchoControls extends MapControl { refreshTooltips(); } + /** + * Removes a user from the status bar. + * @param user Object + * @private + */ _removeUser(user) { + console.assert(this instanceof EchoControls, 'this is not EchoControls', this); // Remove element $('.echo_user_' + user.name).remove(); } + /** + * Sets the display color of a user. + * @param name string + * @param color string + * @private + */ + _setUserColor(name, color) { + console.assert(this instanceof EchoControls, 'this is not EchoControls', this); + $('.echo_user_' + name).css('background-color', color); + } + /** * Adds the Control to the current LeafletMap */ @@ -110,7 +139,7 @@ class EchoControls extends MapControl { // Add the leaflet draw control to the sidebar let container = this._mapControl.getContainer(); $(container).removeClass('leaflet-control'); - let $targetContainer = $('#edit_route_echo_container'); + let $targetContainer = $('#route_echo_container'); $targetContainer.append(container); } diff --git a/resources/assets/js/custom/mapobjectgroups/brushlinemapobjectgroup.js b/resources/assets/js/custom/mapobjectgroups/brushlinemapobjectgroup.js index 67b1f7e6d..4034e9523 100644 --- a/resources/assets/js/custom/mapobjectgroups/brushlinemapobjectgroup.js +++ b/resources/assets/js/custom/mapobjectgroups/brushlinemapobjectgroup.js @@ -7,7 +7,7 @@ class BrushlineMapObjectGroup extends MapObjectGroup { this.title = 'Hide/show brushlines'; this.fa_class = 'fa-paint-brush'; - window.Echo.private('route-edit.' + this.manager.map.getDungeonRoute().publicKey) + window.Echo.join('route-edit.' + this.manager.map.getDungeonRoute().publicKey) .listen('.brushline-changed', (e) => { self._restoreObject(e.brushline); }) diff --git a/resources/assets/js/custom/mapobjectgroups/killzonemapobjectgroup.js b/resources/assets/js/custom/mapobjectgroups/killzonemapobjectgroup.js index 8e4bddf46..9d87ae24b 100644 --- a/resources/assets/js/custom/mapobjectgroups/killzonemapobjectgroup.js +++ b/resources/assets/js/custom/mapobjectgroups/killzonemapobjectgroup.js @@ -7,7 +7,7 @@ class KillZoneMapObjectGroup extends MapObjectGroup { this.title = 'Hide/show killzone'; this.fa_class = 'fa-bullseye'; - window.Echo.private('route-edit.' + this.manager.map.getDungeonRoute().publicKey) + window.Echo.join('route-edit.' + this.manager.map.getDungeonRoute().publicKey) .listen('.killzone-changed', (e) => { self._restoreObject(e.killzone); }) diff --git a/resources/assets/js/custom/mapobjectgroups/mapcommentmapobjectgroup.js b/resources/assets/js/custom/mapobjectgroups/mapcommentmapobjectgroup.js index 29d4f9d9f..902f5e402 100644 --- a/resources/assets/js/custom/mapobjectgroups/mapcommentmapobjectgroup.js +++ b/resources/assets/js/custom/mapobjectgroups/mapcommentmapobjectgroup.js @@ -7,7 +7,7 @@ class MapCommentMapObjectGroup extends MapObjectGroup { this.title = 'Hide/show map comments'; this.fa_class = 'fa-comment'; - window.Echo.private('route-edit.' + this.manager.map.getDungeonRoute().publicKey) + window.Echo.join('route-edit.' + this.manager.map.getDungeonRoute().publicKey) .listen('.mapcomment-changed', (e) => { self._restoreObject(e.mapcomment); }) diff --git a/resources/assets/js/custom/mapobjectgroups/pathmapobjectgroup.js b/resources/assets/js/custom/mapobjectgroups/pathmapobjectgroup.js index b0cff3e18..22613b568 100644 --- a/resources/assets/js/custom/mapobjectgroups/pathmapobjectgroup.js +++ b/resources/assets/js/custom/mapobjectgroups/pathmapobjectgroup.js @@ -7,7 +7,7 @@ class PathMapObjectGroup extends MapObjectGroup { this.title = 'Hide/show route'; this.fa_class = 'fa-route'; - window.Echo.private('route-edit.' + this.manager.map.getDungeonRoute().publicKey) + window.Echo.join('route-edit.' + this.manager.map.getDungeonRoute().publicKey) .listen('.path-changed', (e) => { self._restoreObject(e.path); }) diff --git a/resources/views/common/forms/login.blade.php b/resources/views/common/forms/login.blade.php index ce60d36af..a7c63920a 100644 --- a/resources/views/common/forms/login.blade.php +++ b/resources/views/common/forms/login.blade.php @@ -39,7 +39,7 @@
diff --git a/resources/views/common/maps/map.blade.php b/resources/views/common/maps/map.blade.php index 64a066ec5..a65a9e192 100644 --- a/resources/views/common/maps/map.blade.php +++ b/resources/views/common/maps/map.blade.php @@ -1,4 +1,5 @@ @include('common.general.inline', ['path' => 'common/maps/map', 'options' => [ + 'username' => Auth::check() ? Auth::user()->name : '', + // Only activate Echo when we are a member of the team in which this route is a member of + 'echo' => $dungeonroute->team === null ? false : $dungeonroute->team->isUserMember($user), 'floorId' => $floorId, 'edit' => $edit, 'try' => $tryMode, @@ -289,13 +293,12 @@ class="selectpicker dungeon_floor_switch_edit_popup_target_floor" data-width="30 +
+ +
+
+
@if($edit) - -