]> git.localhorst.tv Git - alttp.git/commitdiff
show which channels the chatbot is in
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 6 Sep 2024 10:16:08 +0000 (12:16 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 6 Sep 2024 10:16:08 +0000 (12:16 +0200)
13 files changed:
app/Console/Commands/TwitchChannelInfo.php
app/Http/Controllers/ChannelController.php
app/Http/Controllers/SitemapXmlController.php
app/Models/Channel.php
database/migrations/2024_09_06_090755_more_twitch_props.php [new file with mode: 0644]
resources/js/components/channel/Item.js [new file with mode: 0644]
resources/js/components/channel/Link.js
resources/js/components/channel/List.js [new file with mode: 0644]
resources/js/helpers/Channel.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/HorstieLog.js
resources/sass/channels.scss

index c297c7cb4ab34868691434fc9e5d1c9a6055dc8a..dabc018680e3e275da714929b0e5d251fdba3379 100644 (file)
@@ -113,6 +113,8 @@ class TwitchChannelInfo extends Command {
                        } else {
                                $channel->twitch_live = true;
                                $channel->twitch_category = $data['game_id'];
+                               $channel->twitch_category_name = $data['game_name'];
+                               $channel->twitch_title = $data['title'];
                                $channel->twitch_viewers = $data['viewer_count'];
                        }
                        $channel->save();
index 121ae4e9ce9149fc2162001128dc2943d4a9a004..cb16abba32852efc8794029833ec254e57a7f3e4 100644 (file)
@@ -13,6 +13,7 @@ class ChannelController extends Controller {
 
        public function search(Request $request) {
                $validatedData = $request->validate([
+                       'chatting' => 'boolean|nullable',
                        'id' => 'array',
                        'id.*' => 'integer|numeric',
                        'joinable' => 'boolean|nullable',
@@ -24,6 +25,9 @@ class ChannelController extends Controller {
                if (!empty($validatedData['id'])) {
                        $channels = $channels->whereIn('id', $validatedData['id']);
                }
+               if (isset($validatedData['chatting'])) {
+                       $channels = $channels->where('chat', '=', !!$validatedData['chatting']);
+               }
                if (isset($validatedData['joinable']) && $validatedData['joinable']) {
                        $channels = $channels->where('twitch_chat', '!=', '');
                }
index f85e40ebb11cfb66e1923bb9db0e967c63915e92..1ee7bfd239fca35b080279763fc1726d0bc5b650 100644 (file)
@@ -79,7 +79,7 @@ class SitemapXmlController extends Controller
                $url = new SitemapUrl();
                $url->path = '/horstielog';
                $url->lastmod = ChatBotLog::latest()->first()->created_at;
-               $url->changefreq = 'daily';
+               $url->changefreq = 'hourly';
                $url->priority = 0.5;
                $urls[] = $url;
 
index f50c267eb8a3cae89bfbba84bada33f682a86862..72c3e5e3902a77c20b69d0079ea77064498a3bb4 100644 (file)
@@ -16,6 +16,7 @@ class Channel extends Model {
 
        public function broadcastOn($event) {
                $channels = [
+                       new PublicChannel('Channel'),
                        new PrivateChannel('Channel.'.$this->id),
                ];
                if (!empty($this->access_key)) {
@@ -289,11 +290,11 @@ class Channel extends Model {
                'guessing_start' => 'datetime',
                'languages' => 'array',
                'join' => 'boolean',
+               'twitch_live' => 'boolean',
        ];
 
        protected $hidden = [
                'access_key',
-               'chat',
                'chat_commands',
                'chat_settings',
                'created_at',
diff --git a/database/migrations/2024_09_06_090755_more_twitch_props.php b/database/migrations/2024_09_06_090755_more_twitch_props.php
new file mode 100644 (file)
index 0000000..25017c4
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->string('twitch_category_name')->default('');
+                       $table->string('twitch_title')->default('');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->dropColumn('twitch_category_name');
+                       $table->dropColumn('twitch_title');
+               });
+       }
+};
diff --git a/resources/js/components/channel/Item.js b/resources/js/components/channel/Item.js
new file mode 100644 (file)
index 0000000..a2b712f
--- /dev/null
@@ -0,0 +1,45 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Icon from '../common/Icon';
+
+const Item = ({ channel }) => {
+       const classNames = [
+               'channel-item',
+               channel.twitch_live ? 'is-live' : 'not-live',
+       ];
+       return <a
+               className={classNames.join(' ')}
+               href={channel.stream_link}
+               rel="noreferrer"
+               target="_blank"
+       >
+               <div className="d-flex justify-content-between">
+                       <div className="channel-title fs-5">{channel.title}</div>
+                       {channel.twitch_live ?
+                               <div className="channel-viewers">
+                                       <Icon.STREAM />
+                                       {' '}
+                                       {channel.twitch_viewers}
+                               </div>
+                       : null}
+               </div>
+               <div className="channel-stream-title">{channel.twitch_title}</div>
+               <div className="channel-category text-muted">
+                       <small>{channel.twitch_category_name}</small>
+               </div>
+       </a>;
+};
+
+Item.propTypes = {
+       channel: PropTypes.shape({
+               stream_link: PropTypes.string,
+               title: PropTypes.string,
+               twitch_category_name: PropTypes.string,
+               twitch_live: PropTypes.bool,
+               twitch_title: PropTypes.string,
+               twitch_viewers: PropTypes.number,
+       }),
+};
+
+export default Item;
index cb6cb79db66e2718bb6b062f5039d6d02fb2588b..3eeb7b99a03e0f99d168bd8dc3170f78a5ed4ae4 100644 (file)
@@ -7,7 +7,7 @@ import Icon from '../common/Icon';
 const Link = ({ channel }) => {
        return <Button
                href={channel.stream_link}
-               rel="noreferer"
+               rel="noreferrer"
                target="_blank"
                title={channel.title}
                variant="outline-twitch"
diff --git a/resources/js/components/channel/List.js b/resources/js/components/channel/List.js
new file mode 100644 (file)
index 0000000..c1c4d41
--- /dev/null
@@ -0,0 +1,19 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Item from './Item';
+
+const List = ({ channels = [] }) => {
+       return <div className="channel-list">
+               {channels.map(channel =>
+                       <Item channel={channel} key={channel.id} />
+               )}
+       </div>;
+};
+
+List.propTypes = {
+       channels: PropTypes.arrayOf(PropTypes.shape({
+       })),
+};
+
+export default List;
index 315542822d3710d72e2eb875c598e573635634c8..1a5398fdbf9ab1511676274c1f43b0381069f7d2 100644 (file)
@@ -9,3 +9,44 @@ export const patchGuess = (guesses, guess) =>
 
 export const patchWinner = (winners, winner) =>
        [winner, ...(winners || []).filter(w => w.uid !== winner.uid)];
+
+export const compareLive = (a, b) => {
+       const a_live = a && a.twitch_live;
+       const b_live = b && b.twitch_live;
+       if (a_live) {
+               if (b_live) {
+                       return 0;
+               }
+               return -1;
+       }
+       if (b_live) {
+               return 1;
+       }
+       return 0;
+};
+
+export const compareName = (a, b) => {
+       const a_name = (a && (a.short_name || a.title)) || '';
+       const b_name = (b && (b.short_name || b.title)) || '';
+       return a_name.localeCompare(b_name);
+};
+
+export const compareTitle = (a, b) => {
+       const a_title = (a && a.title) || '';
+       const b_title = (b && b.title) || '';
+       return a_title.localeCompare(b_title);
+};
+
+export const compareViewers = (a, b) => {
+       const a_viewers = (a && a.twitch_live && a.twitch_viewers) || 0;
+       const b_viewers = (b && b.twitch_live && b.twitch_viewers) || 0;
+       return b_viewers - a_viewers;
+};
+
+export const compareHorstieLog = (a, b) => {
+       const live = compareLive(a, b);
+       if (live) return live;
+       const viewers = compareViewers(a, b);
+       if (viewers) return viewers;
+       return compareName(a, b);
+};
index b9dfb769e9d5a8da99a6da0e2a372ab011ad1ed4..ce3faa67a0b0fc2cbe6d86b88073e8ce8456da74 100644 (file)
@@ -724,6 +724,7 @@ export default {
                                wtf: 'WTF',
                                yes: 'Ja',
                        },
+                       chatChannels: 'Aktive Channel',
                        chatError: 'Fehler beim Senden',
                        chatMinAge: 'Mindestalter (in Tagen)',
                        chatSettings: 'Chat Bot Einstellungen',
index 259fe4ad14b023fa27b592b2a527916655ff9a0f..afd1aa17af18d953491ba249646d09262c3e0e54 100644 (file)
@@ -724,6 +724,7 @@ export default {
                                wtf: 'WTF',
                                yes: 'Yes',
                        },
+                       chatChannels: 'Active Channels',
                        chatError: 'Error sending message',
                        chatMinAge: 'Min. age (in days)',
                        chatSettings: 'Chat Bot Settings',
index 115b9fe0e716f4b49e9eb4023912034e2a1fcc84..55d37847d9a99e5b9284a79560d11b14df49a8b4 100644 (file)
@@ -1,18 +1,24 @@
 import axios from 'axios';
 import React from 'react';
-import { Container } from 'react-bootstrap';
+import { Col, Container, Row } from 'react-bootstrap';
 import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
 
+import ChannelList from '../components/channel/List';
 import List from '../components/chat-bot-logs/List';
 import ErrorBoundary from '../components/common/ErrorBoundary';
 import ErrorMessage from '../components/common/ErrorMessage';
 import Loading from '../components/common/Loading';
+import { compareHorstieLog } from '../helpers/Channel';
 
 export const Component = () => {
+       const [channels, setChannels] = React.useState([]);
        const [error, setError] = React.useState(null);
        const [loading, setLoading] = React.useState(true);
        const [log, setLog] = React.useState([]);
 
+       const { t } = useTranslation();
+
        React.useEffect(() => {
                const ctrl = new AbortController();
                if (!log.length) {
@@ -44,6 +50,49 @@ export const Component = () => {
                };
        }, []);
 
+       React.useEffect(() => {
+               const ctrl = new AbortController();
+               axios
+                       .get(`/api/channels`, {
+                               signal: ctrl.signal,
+                               params: {
+                                       chatting: 1,
+                               },
+                       })
+                       .then(response => {
+                               setChannels(response.data.sort(compareHorstieLog));
+                       })
+                       .catch(error => {
+                               if (!axios.isCancel(error)) {
+                                       setChannels([]);
+                               }
+                       });
+                       window.Echo.channel(`Channel`)
+                               .listen('.ChannelCreated', (e) => {
+                                       if (e.model.chat) {
+                                               setChannels(cs => [e.model, ...cs].sort(compareHorstieLog));
+                                       }
+                               })
+                               .listen('.ChannelUpdated', (e) => {
+                                       if (e.model.chat) {
+                                               setChannels(cs => {
+                                                       if (cs.find(c => c.id === e.model.id)) {
+                                                               return cs
+                                                                       .map(c => c.id === e.model.id ? e.model : c)
+                                                                       .sort(compareHorstieLog);
+                                                       } else {
+                                                               return [e.model, ...cs].sort(compareHorstieLog);
+                                                       }
+                                               });
+                                       } else {
+                                               setChannels(cs => cs.filter(c => c.id !== e.model.id));
+                                       }
+                               });
+               return () => {
+                       ctrl.abort();
+               };
+       }, []);
+
        if (loading) {
                return <Loading />;
        }
@@ -58,7 +107,15 @@ export const Component = () => {
                        <title>Horstie Log</title>
                </Helmet>
                <ErrorBoundary>
-                       <List log={log} />
+                       <Row>
+                               <Col md={9}>
+                                       <List log={log} />
+                               </Col>
+                               <Col className="horstielog-channels">
+                                       <h2 className="fs-4">{t('twitchBot.chatChannels')}</h2>
+                                       <ChannelList channels={channels} />
+                               </Col>
+                       </Row>
                </ErrorBoundary>
        </Container>;
 };
index a72080525c36779e95271e42cea8fc2ff595263e..ba961800bfd916fc53c14d7259172764049d7851 100644 (file)
                background-color: $danger;
        }
 }
+
+.horstielog-channels {
+       position: relative;
+
+       .channel-list {
+               position: sticky;
+               top: 0;
+       }
+
+       .channel-item {
+               display: block;
+               margin: 1rem 0;
+               padding: 0.5ex 1ex;
+               background: rgba(0, 0, 0, 0.3);
+               border-radius: 0.5ex;
+               color: inherit;
+               text-decoration: none;
+
+               &:hover {
+                       .channel-title, .channel-viewers {
+                               color: $twitch;
+                       }
+               }
+
+               &.not-live {
+                       background: rgba(0, 0, 0, 0.1);
+               }
+       }
+}