From c66d9d3c5eda563842c683827da1abf445b65483 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 29 Feb 2024 22:58:51 +0100 Subject: [PATCH 1/1] guessing game controls --- app/Http/Controllers/ChannelController.php | 78 ++++++++- app/Models/Channel.php | 23 ++- app/Models/GuessingGuess.php | 10 ++ app/Models/GuessingWinner.php | 10 ++ app/TwitchBot/ChatCommand.php | 7 +- app/TwitchBot/GuessingCancelCommand.php | 2 +- resources/js/app/Routes.js | 9 + .../js/components/common/ChannelSelect.js | 11 +- resources/js/components/common/Icon.js | 1 + .../js/components/twitch-bot/Controls.js | 19 ++- .../twitch-bot/GuessingGameControls.js | 76 +++++++++ .../js/components/twitch-bot/GuessingGuess.js | 34 ++++ .../components/twitch-bot/GuessingWinner.js | 35 ++++ resources/js/helpers/Channel.js | 11 ++ resources/js/i18n/de.js | 7 + resources/js/i18n/en.js | 7 + resources/js/pages/GuessingGameControls.js | 157 ++++++++++++++++++ resources/sass/app.scss | 1 + resources/sass/channels.scss | 7 + routes/api.php | 2 + routes/channels.php | 6 + 21 files changed, 500 insertions(+), 13 deletions(-) create mode 100644 resources/js/components/twitch-bot/GuessingGameControls.js create mode 100644 resources/js/components/twitch-bot/GuessingGuess.js create mode 100644 resources/js/components/twitch-bot/GuessingWinner.js create mode 100644 resources/js/helpers/Channel.js create mode 100644 resources/js/pages/GuessingGameControls.js create mode 100644 resources/sass/channels.scss diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index cbd0110..4cc49c1 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -138,6 +138,82 @@ class ChannelController extends Controller { return $channel->toJson(); } + public function controlGuessingGame(Request $request, Channel $channel, $name) { + $this->authorize('editRestream', $channel); + + $validatedData = $request->validate([ + 'action' => 'required|in:cancel,solve,start,stop', + 'solution' => '', + ]); + + switch ($validatedData['action']) { + case 'cancel': + $channel->clearGuessing(); + $msg = $channel->getGuessingSetting('cancel_message'); + if (!empty($msg)) { + TwitchBotCommand::chat($channel->twitch_chat, $msg); + } + break; + case 'solve': + if ($channel->hasActiveGuessing() && $channel->isValidGuess($validatedData['solution'])) { + $winners = $channel->solveGuessing($validatedData['solution']); + $names = []; + foreach ($winners as $winner) { + if ($winner->score > 0) { + $names[] = $winner->uname; + } + } + if (empty($names)) { + $msg = $channel->getGuessingSetting('no_winners_message'); + } else { + $msg = $channel->getGuessingSetting('winners_message'); + $msg = str_replace('{names}', $channel->listAnd($names), $msg); + } + if (!empty($msg)) { + TwitchBotCommand::chat($channel->twitch_chat, $msg); + } + $channel->clearGuessing(); + } + break; + case 'start': + if (!$channel->hasActiveGuessing()) { + $channel->startGuessing($name); + $msg = $channel->getGuessingSetting('start_message'); + if (!empty($msg)) { + TwitchBotCommand::chat($channel->twitch_chat, $msg); + } + } + break; + case 'stop': + if ($channel->hasActiveGuessing()) { + $channel->stopGuessing(); + $msg = $channel->getGuessingSetting('stop_message'); + if (!empty($msg)) { + TwitchBotCommand::chat($channel->twitch_chat, $msg); + } + } + break; + } + + return $channel->toJson(); + } + + public function getGuessingGame(Channel $channel, $name) { + $this->authorize('editRestream', $channel); + + $cutoff = $channel->guessing_start; + if (is_null($cutoff)) { + $last = $channel->winners()->latest()->first(); + $cutoff = $last->pod; + } + $guesses = $channel->guesses()->where('created_at', '>=', $cutoff)->orderBy('created_at')->get(); + $winners = $channel->winners()->where('created_at', '>=', $cutoff)->orderBy('created_at')->get(); + return [ + 'guesses' => $guesses->toArray(), + 'winners' => $winners->toArray(), + ]; + } + public function saveGuessingGame(Request $request, Channel $channel, $name) { $this->authorize('editRestream', $channel); @@ -150,7 +226,7 @@ class ChannelController extends Controller { 'points_exact_first' => 'numeric|min:1|max:5', 'points_exact_other' => 'numeric|min:0|max:5', 'points_close_first' => 'numeric|min:0|max:5', - 'points_close_min' => 'integer|min:0', + 'points_close_max' => 'integer|min:0', 'points_close_other' => 'numeric|min:0|max:5', 'start_message' => 'string', 'stop_message' => 'string', diff --git a/app/Models/Channel.php b/app/Models/Channel.php index edc2ba8..d7d307e 100644 --- a/app/Models/Channel.php +++ b/app/Models/Channel.php @@ -2,13 +2,24 @@ namespace App\Models; +use Illuminate\Broadcasting\PrivateChannel; +use Illuminate\Database\Eloquent\BroadcastsEvents; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; -class Channel extends Model -{ +class Channel extends Model { + + use BroadcastsEvents; use HasFactory; + public function broadcastOn($event) { + $channels = [ + new PrivateChannel('Channel.'.$this->id), + ]; + return $channels; + } + public function getCurrentEpisode() { return $this->episodes() ->where('start', '<', now()->subMinutes(10)) @@ -135,6 +146,14 @@ class Channel extends Model return false; } + public function listAnd($entries) { + $lang = empty($this->languages) ? 'en' : $this->languages[0]; + if ($lang == 'de') { + return Arr::join($entries, ', ', ' und '); + } + return Arr::join($entries, ', ', ' and '); + } + public function crews() { return $this->hasMany(ChannelCrew::class); } diff --git a/app/Models/GuessingGuess.php b/app/Models/GuessingGuess.php index 12cff87..b8e413b 100644 --- a/app/Models/GuessingGuess.php +++ b/app/Models/GuessingGuess.php @@ -2,15 +2,25 @@ namespace App\Models; +use Illuminate\Broadcasting\PrivateChannel; +use Illuminate\Database\Eloquent\BroadcastsEvents; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class GuessingGuess extends Model { + use BroadcastsEvents; use HasFactory; public function channel() { return $this->belongsTo(Channel::class); } + public function broadcastOn($event) { + $channels = [ + new PrivateChannel('Channel.'.$this->channel_id), + ]; + return $channels; + } + } diff --git a/app/Models/GuessingWinner.php b/app/Models/GuessingWinner.php index 0bbdf00..e6945bf 100644 --- a/app/Models/GuessingWinner.php +++ b/app/Models/GuessingWinner.php @@ -2,15 +2,25 @@ namespace App\Models; +use Illuminate\Broadcasting\PrivateChannel; +use Illuminate\Database\Eloquent\BroadcastsEvents; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class GuessingWinner extends Model { + use BroadcastsEvents; use HasFactory; public function channel() { return $this->belongsTo(Channel::class); } + public function broadcastOn($event) { + $channels = [ + new PrivateChannel('Channel.'.$this->channel_id), + ]; + return $channels; + } + } diff --git a/app/TwitchBot/ChatCommand.php b/app/TwitchBot/ChatCommand.php index b00f0cf..f2e11b6 100644 --- a/app/TwitchBot/ChatCommand.php +++ b/app/TwitchBot/ChatCommand.php @@ -3,7 +3,6 @@ namespace App\TwitchBot; use App\Models\Channel; -use Illuminate\Support\Arr; abstract class ChatCommand { @@ -62,11 +61,7 @@ abstract class ChatCommand { } protected function listAnd($entries) { - $lang = empty($this->channels->languages) ? 'en' : $this->channel->languages[0]; - if ($lang == 'de') { - return Arr::join($entries, ', ', ' und '); - } - return Arr::join($entries, ', ', ' and '); + return $this->channel->listAnd($entries); } protected function messageChannel($str) { diff --git a/app/TwitchBot/GuessingCancelCommand.php b/app/TwitchBot/GuessingCancelCommand.php index 19f9953..bb6f361 100644 --- a/app/TwitchBot/GuessingCancelCommand.php +++ b/app/TwitchBot/GuessingCancelCommand.php @@ -5,7 +5,7 @@ namespace App\TwitchBot; class GuessingCancelCommand extends ChatCommand { public function execute($args) { - if ($this->chanel->hasActiveGuessing()) { + if ($this->channel->hasActiveGuessing()) { $this->channel->clearGuessing(); } $msg = $this->channel->getGuessingSetting('cancel_message'); diff --git a/resources/js/app/Routes.js b/resources/js/app/Routes.js index e5da45f..3f73834 100644 --- a/resources/js/app/Routes.js +++ b/resources/js/app/Routes.js @@ -127,6 +127,15 @@ const router = createBrowserRouter( '../pages/DoorsTracker' )} /> + + import( + /* webpackChunkName: "guessing" */ + '../pages/GuessingGameControls' + )} + /> + ) ); diff --git a/resources/js/components/common/ChannelSelect.js b/resources/js/components/common/ChannelSelect.js index 701f196..2fbb2c1 100644 --- a/resources/js/components/common/ChannelSelect.js +++ b/resources/js/components/common/ChannelSelect.js @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import Icon from './Icon'; import debounce from '../../helpers/debounce'; -const ChannelSelect = ({ joinable, manageable, onChange, value }) => { +const ChannelSelect = ({ autoSelect, joinable, manageable, onChange, value }) => { const [resolved, setResolved] = useState(null); const [results, setResults] = useState([]); const [search, setSearch] = useState(''); @@ -47,11 +47,17 @@ const ChannelSelect = ({ joinable, manageable, onChange, value }) => { }); ctrl = null; setResults(response.data); + if (autoSelect && !phrase && response.data.length === 1) { + onChange({ + channel: response.data[0], + target: { value: response.data[0].id }, + }); + } } catch (e) { ctrl = null; console.error(e); } - }, 300), [manageable]); + }, 300), [autoSelect, joinable, manageable]); useEffect(() => { fetch(search); @@ -117,6 +123,7 @@ const ChannelSelect = ({ joinable, manageable, onChange, value }) => { }; ChannelSelect.propTypes = { + autoSelect: PropTypes.bool, joinable: PropTypes.bool, manageable: PropTypes.bool, onChange: PropTypes.func, diff --git a/resources/js/components/common/Icon.js b/resources/js/components/common/Icon.js index d27df43..8f03bae 100644 --- a/resources/js/components/common/Icon.js +++ b/resources/js/components/common/Icon.js @@ -81,6 +81,7 @@ Icon.MENU = makePreset('MenuIcon', 'bars'); Icon.MICROPHONE = makePreset('MicrophoneIcon', 'microphone'); Icon.MONITOR = makePreset('MonitorIcon', 'tv'); Icon.MOUSE = makePreset('MouseIcon', 'arrow-pointer'); +Icon.OPEN = makePreset('OpenIcon', 'arrow-up-right-from-square'); Icon.PAUSE = makePreset('PauseIcon', 'pause'); Icon.PENDING = makePreset('PendingIcon', 'clock'); Icon.PIN = makePreset('PinIcon', 'location-pin'); diff --git a/resources/js/components/twitch-bot/Controls.js b/resources/js/components/twitch-bot/Controls.js index 822c201..84742d8 100644 --- a/resources/js/components/twitch-bot/Controls.js +++ b/resources/js/components/twitch-bot/Controls.js @@ -9,6 +9,7 @@ import CommandDialog from './CommandDialog'; import Commands from './Commands'; import GuessingSettingsForm from './GuessingSettingsForm'; import ChannelSelect from '../common/ChannelSelect'; +import Icon from '../common/Icon'; import ToggleSwitch from '../common/ToggleSwitch'; const Controls = () => { @@ -121,6 +122,7 @@ const Controls = () => { {t('twitchBot.channel')} { setChannel(channel); }} @@ -228,7 +230,22 @@ const Controls = () => { -

{t('twitchBot.guessingGame.settings')}

+
+

{t('twitchBot.guessingGame.settings')}

+ +
{ + const { t } = useTranslation(); + + const solutions = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + ]; + + return
+
+ + + +
+ {hasActiveGuessing(channel) ? +
+ {solutions.map(solution => + + )} +
+ : null} +
; +}; + +GuessingGameControls.propTypes = { + channel: PropTypes.shape({ + }), + onCancel: PropTypes.func, + onSolve: PropTypes.func, + onStart: PropTypes.func, + onStop: PropTypes.func, +}; + +export default GuessingGameControls; diff --git a/resources/js/components/twitch-bot/GuessingGuess.js b/resources/js/components/twitch-bot/GuessingGuess.js new file mode 100644 index 0000000..61997d3 --- /dev/null +++ b/resources/js/components/twitch-bot/GuessingGuess.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const GuessingGuess = ({ guess }) => { + const { t } = useTranslation(); + + return
+ + +
{guess.uname}
+
+ {t('twitchBot.guessingGame.guessTimestamp', { + timestamp: new Date(guess.created_at), + })} +
+ + +
{guess.guess}
+ +
+
; +}; + +GuessingGuess.propTypes = { + guess: PropTypes.shape({ + created_at: PropTypes.string, + guess: PropTypes.string, + uname: PropTypes.string, + }), +}; + +export default GuessingGuess; diff --git a/resources/js/components/twitch-bot/GuessingWinner.js b/resources/js/components/twitch-bot/GuessingWinner.js new file mode 100644 index 0000000..f7956ba --- /dev/null +++ b/resources/js/components/twitch-bot/GuessingWinner.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const GuessingWinner = ({ winner }) => { + const { t } = useTranslation(); + + const classNames = ['guessing-game-winner', 'my-2', 'p-2', 'border', 'rounded']; + if (!winner.score) { + classNames.push('no-points'); + } + + return
+ + +
{winner.uname}
+
{t('twitchBot.guessingGame.winnerScore', { score: winner.score })}
+ + +
{winner.guess}
+ +
+
; +}; + +GuessingWinner.propTypes = { + winner: PropTypes.shape({ + guess: PropTypes.string, + score: PropTypes.number, + uname: PropTypes.string, + }), +}; + +export default GuessingWinner; diff --git a/resources/js/helpers/Channel.js b/resources/js/helpers/Channel.js new file mode 100644 index 0000000..3155428 --- /dev/null +++ b/resources/js/helpers/Channel.js @@ -0,0 +1,11 @@ +export const hasActiveGuessing = channel => + channel && channel.guessing_start; + +export const isAcceptingGuesses = channel => + channel && channel.guessing_start && !channel.guessing_end; + +export const patchGuess = (guesses, guess) => + [guess, ...(guesses || []).filter(g => g.uid !== guess.uid)]; + +export const patchWinner = (winners, winner) => + [winner, ...(winners || []).filter(w => w.uid !== winner.uid)]; diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 9b7a0f3..262bc0d 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -79,6 +79,7 @@ export default { send: 'Senden', settings: 'Einstellungen', signUp: 'Anmelden', + start: 'Start', stop: 'Stop', unconfirm: 'Zurückziehen', unset: 'Zurücksetzen', @@ -535,6 +536,8 @@ export default { defaultStartMessage: 'Haut jetzt eure Zahlen raus!', defaultStopMessage: 'Annahme geschlossen', defaultWinnersMessage: 'Glückwunsch {names}!', + guesses: 'Tipps', + guessTimestamp: '{{ timestamp, LT }}', invalidSolutionMessage: 'Nachricht bei ungültiger (oder fehlender) Lösung', noWinnersMessage: 'Nachricht, falls keine Gewinner', notActiveMessage: 'Nachricht, wenn kein Spiel läuft', @@ -543,9 +546,13 @@ export default { pointsCloseOther: 'Punkte für weitere nächste Treffer', pointsExactFirst: 'Punkte für den ersten exakten Treffer', pointsExactOther: 'Punkte für weitere exakte Treffer', + popoutControls: 'Steuerung öffnen', settings: 'Guessing Game Einstellungen', startMessage: 'Nachricht bei Start', stopMessage: 'Nachricht bei Einsendeschluss', + winners: 'Gewinner', + winnerScore: '{{ score }} Punkte', + winnerScore_one: '{{ score }} Punkt', winnersMessage: 'Ankündigung der Gewinner', winnersMessageHint: '{names} wird durch die Namen der Gewinner ersetzt', }, diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 2bcbe91..2a4f2cb 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -79,6 +79,7 @@ export default { send: 'Send', settings: 'Settings', signUp: 'Sign up', + start: 'Start', stop: 'Stop', unconfirm: 'Retract', unset: 'Unset', @@ -535,6 +536,8 @@ export default { defaultStartMessage: 'Get your guesses in', defaultStopMessage: 'Guessing closed', defaultWinnersMessage: 'Congrats {names}!', + guesses: 'Guesses', + guessTimestamp: '{{ timestamp, LT }}', invalidSolutionMessage: 'Message for invalid (or missing) solution', noWinnersMessage: 'Announcement for no winners', notActiveMessage: 'Message when no game is currently active', @@ -543,9 +546,13 @@ export default { pointsCloseOther: 'Points for further close matches', pointsExactFirst: 'Points for first exact match', pointsExactOther: 'Points for further exact matches', + popoutControls: 'Popout controls', settings: 'Guessing game settings', startMessage: 'Starting announcement', stopMessage: 'Closing announcement', + winners: 'Winners', + winnerScore: '{{ score }} points', + winnerScore_one: '{{ score }} point', winnersMessage: 'Winners announcement', winnersMessageHint: '{names} will be replaced with a list of winners\' names', }, diff --git a/resources/js/pages/GuessingGameControls.js b/resources/js/pages/GuessingGameControls.js new file mode 100644 index 0000000..1c84689 --- /dev/null +++ b/resources/js/pages/GuessingGameControls.js @@ -0,0 +1,157 @@ +import axios from 'axios'; +import React from 'react'; +import { Alert, Col, Container, Form, Navbar, Row } from 'react-bootstrap'; +import { Helmet } from 'react-helmet'; +import { useTranslation } from 'react-i18next'; +import toastr from 'toastr'; + +import User from '../app/User'; +import ChannelSelect from '../components/common/ChannelSelect'; +import GuessingGameControls from '../components/twitch-bot/GuessingGameControls'; +import GuessingGuess from '../components/twitch-bot/GuessingGuess'; +import GuessingWinner from '../components/twitch-bot/GuessingWinner'; +import { patchGuess, patchWinner } from '../helpers/Channel'; + +export const Component = () => { + const [channel, setChannel] = React.useState(null); + const [guesses, setGuesses] = React.useState([]); + const [winners, setWinners] = React.useState([]); + + const { t } = useTranslation(); + + React.useEffect(() => { + if (!channel) { + setGuesses([]); + setWinners([]); + return; + } + if (channel.guessing_type) { + axios.get(`/api/channels/${channel.id}/guessing-game/${channel.guessing_type}`) + .then(res => { + res.data.guesses.forEach(g => { + setGuesses(gs => patchGuess(gs, g)); + }); + res.data.winners.forEach(w => { + setWinners(ws => patchGuess(ws, w)); + }); + }); + } + window.Echo.private(`Channel.${channel.id}`) + .listen('.GuessingGuessCreated', (e) => { + setGuesses(gs => patchGuess(gs, e.model)); + }) + .listen('.GuessingWinnerCreated', (e) => { + setWinners(ws => patchWinner(ws, e.model)); + }) + .listen('.ChannelUpdated', (e) => { + setChannel(c => ({ ...c, ...e.model })); + }); + return () => { + window.Echo.leave(`Channel.${channel.id}`); + }; + }, [channel && channel.id]); + + React.useEffect(() => { + const cutoff = channel && channel.guessing_start; + if (cutoff) { + setGuesses(gs => gs.filter(g => g.created_at >= cutoff)); + setWinners(ws => ws.filter(w => w.created_at >= cutoff)); + } + }, [channel && channel.guessing_start]); + + const onCancel = React.useCallback(async () => { + try { + const rsp = await axios.post( + `/api/channels/${channel.id}/guessing-game/gtbk`, + { action: 'cancel' }, + ); + setChannel(rsp.data); + } catch (e) { + toastr.error(t('twitchBot.controlError')); + } + }, [channel]); + + const onSolve = React.useCallback(async (solution) => { + try { + const rsp = await axios.post( + `/api/channels/${channel.id}/guessing-game/gtbk`, + { action: 'solve', solution }, + ); + setChannel(rsp.data); + } catch (e) { + toastr.error(t('twitchBot.controlError')); + } + }, [channel]); + + const onStart = React.useCallback(async () => { + try { + const rsp = await axios.post( + `/api/channels/${channel.id}/guessing-game/gtbk`, + { action: 'start' }, + ); + setChannel(rsp.data); + } catch (e) { + toastr.error(t('twitchBot.controlError')); + } + }, [channel]); + + const onStop = React.useCallback(async () => { + try { + const rsp = await axios.post( + `/api/channels/${channel.id}/guessing-game/gtbk`, + { action: 'stop' }, + ); + setChannel(rsp.data); + } catch (e) { + toastr.error(t('twitchBot.controlError')); + } + }, [channel]); + + return <> + + Guessing Game Controls + + + + { setChannel(channel); }} + value={channel ? channel.id : ''} + /> + + + + + {channel ? + + + + +

{t('twitchBot.guessingGame.winners')}

+ {winners.map(winner => + + )} + + +

{t('twitchBot.guessingGame.guesses')}

+ {guesses.map(guess => + + )} + +
: + + {t('twitchBot.selectChannel')} + + } +
+ ; +}; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index e05a274..8413908 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -6,6 +6,7 @@ // Custom @import 'common'; +@import 'channels'; @import 'discord'; @import 'doors'; @import 'episodes'; diff --git a/resources/sass/channels.scss b/resources/sass/channels.scss new file mode 100644 index 0000000..cffa578 --- /dev/null +++ b/resources/sass/channels.scss @@ -0,0 +1,7 @@ +.bkgg-buttons { + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; +} +.guessing-game-winner.no-points { + opacity: .6; + background: repeating-linear-gradient(150deg, #444, #444 1ex, transparent 1ex, transparent 1em); +} diff --git a/routes/api.php b/routes/api.php index 276ff63..2135a2f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -32,6 +32,8 @@ Route::post('channels/{channel}/join', 'App\Http\Controllers\ChannelController@j Route::post('channels/{channel}/part', 'App\Http\Controllers\ChannelController@part'); Route::delete('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@deleteCommand'); Route::put('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@saveCommand'); +Route::get('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@getGuessingGame'); +Route::post('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@controlGuessingGame'); Route::put('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@saveGuessingGame'); Route::get('content', 'App\Http\Controllers\TechniqueController@search'); diff --git a/routes/channels.php b/routes/channels.php index e93d25e..b0a2552 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -1,5 +1,6 @@ can('editRestream', $channel); +}); + Broadcast::channel('Protocol.{id}', function ($user, $id) { $tournament = Tournament::findOrFail($id); return $user->can('viewProtocol', $tournament); -- 2.39.2