~ashkeel/strimertul

a1fab34a703b4744af531d0d588424a3a71b1f1a — Ash Keel 7 months ago 5bdc05c
feat: add marker in recent events to show which events are new
M frontend/src/locale/en/translation.json => frontend/src/locale/en/translation.json +1 -0
@@ 267,6 267,7 @@
        "header": "Recent events",
        "warning": "This section only contains events that happened while {{APPNAME}} was open, so only use it for recent stuff",
        "anonymous": "An anonymous viewer",
        "marker": "Events from previous sessions",
        "events": {
          "follow": "<n>{{name}}</n> followed you",
          "redemption": "<n>{{name}}</n> redeemed <r>{{reward}}</r>",

M frontend/src/locale/it/translation.json => frontend/src/locale/it/translation.json +1 -0
@@ 152,6 152,7 @@
        "anonymous": "Uno spettatore anonimo",
        "header": "Eventi recenti",
        "warning": "Questa sezione contiene solo gli eventi accaduti mentre {{APPNAME}} era aperto, quindi utilizzala solo per cose recenti",
        "marker": "Eventi delle sessioni precedenti",
        "events": {
          "channel-updated": "Informazioni stream modificate",
          "cheered": "<n>{{name}}</n> ti ha tifato con <b>{{bits}} bit</b>",

M frontend/src/ui/components/LogViewer.tsx => frontend/src/ui/components/LogViewer.tsx +5 -4
@@ 1,8 1,7 @@
import { ClipboardCopyIcon, Cross2Icon, SizeIcon } from '@radix-ui/react-icons';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { RootState } from 'src/store';
import { useAppSelector } from 'src/store';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { delay } from '~/lib/time';
import { ProcessedLogEntry } from '~/store/logging/reducer';


@@ 294,12 293,14 @@ export function LogItem({ data, expandDefault }: LogItemProps) {
  const details = Object.entries(data.data).filter(([key]) => key.length > 1);
  const [copied, setCopied] = useState(false);
  const [showDetails, setShowDetails] = useState(expandDefault ?? false);

  const copyToClipboard = async () => {
    await navigator.clipboard.writeText(JSON.stringify(data.data));
    setCopied(true);
    await delay(2000);
    setCopied(false);
  };

  return (
    <LogEntryContainer level={levelStyle}>
      <LogTime level={levelStyle}>{formatTime(data.time)}</LogTime>


@@ 355,7 356,7 @@ interface LogDialogProps {
}

function LogDialog({ initialFilter }: LogDialogProps) {
  const logEntries = useSelector((state: RootState) => state.logging.messages);
  const logEntries = useAppSelector((state) => state.logging.messages);
  const [filter, setFilter] = useState({
    ...emptyFilter,
    ...Object.fromEntries(initialFilter.map((f) => [f, true])),


@@ 440,7 441,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
}

function LogViewer() {
  const logEntries = useSelector((state: RootState) => state.logging.messages);
  const logEntries = useAppSelector((state) => state.logging.messages);
  const [activeDialog, setActiveDialog] = useState<LogLevel>(null);

  const count = logEntries.reduce(

M frontend/src/ui/pages/Dashboard.tsx => frontend/src/ui/pages/Dashboard.tsx +95 -14
@@ 16,7 16,11 @@ import { modules } from '~/store/api/reducer';
import * as HoverCard from '@radix-ui/react-hover-card';
import { useEffect, useState } from 'react';
import { main } from '@wailsapp/go/models';
import { GetProblems, GetTwitchAuthURL } from '@wailsapp/go/main/App';
import {
  GetLastLogs,
  GetProblems,
  GetTwitchAuthURL,
} from '@wailsapp/go/main/App';
import { BrowserOpenURL } from '@wailsapp/runtime/runtime';
import {
  PageContainer,


@@ 362,8 366,64 @@ function TwitchEvent({ data }: { data: EventSubNotification }) {
  );
}

function TwitchEventLog({ events }: { events: EventSubNotification[] }) {
const block = {
  content: '""',
  display: 'inline-block',
  width: '1rem',
  height: '0.1rem',
  margin: '0 0.5rem',
  backgroundColor: '$gray9',
};

const SessionMarker = styled('div', {
  textTransform: 'uppercase',
  fontWeight: '600',
  fontSize: '0.75rem',
  color: '$gray11',
  margin: '0.25rem',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  '&::before': block,
  '&::after': block,
});

interface TwitchEventLogProps {
  events: EventSubNotification[];
  dateMarker: number;
}

type EventItem =
  | { type: 'marker' }
  | { type: 'event'; event: EventSubNotification };

function TwitchEventLog({ events, dateMarker }: TwitchEventLogProps) {
  const { t } = useTranslation();

  // Filter only supported events and order them by date
  const orderedEvents: EventItem[] = events
    .filter((ev) => supportedMessages.includes(ev.subscription.type))
    .sort((a, b) =>
      a.date && b.date
        ? Date.parse(b.date) - Date.parse(a.date)
        : Date.parse(b.subscription.created_at) -
          Date.parse(a.subscription.created_at),
    )
    .map((ev) => ({ type: 'event', event: ev }));

  // Add marker
  if (dateMarker) {
    const index = orderedEvents.findIndex((ev) => {
      if (ev.type !== 'event') {
        return false;
      }
      return Date.parse(ev.event.date) < dateMarker;
    });
    if (index >= 0) {
      orderedEvents.splice(index, 0, { type: 'marker' });
    }
  }

  return (
    <>
      <HoverCard.Root>


@@ 383,17 443,23 @@ function TwitchEventLog({ events }: { events: EventSubNotification[] }) {
      </HoverCard.Root>
      <Scrollbar vertical={true} viewport={{ maxHeight: '250px' }}>
        <EventListContainer>
          {events
            .filter((ev) => supportedMessages.includes(ev.subscription.type))
            .sort((a, b) =>
              a.date && b.date
                ? Date.parse(b.date) - Date.parse(a.date)
                : Date.parse(b.subscription.created_at) -
                  Date.parse(a.subscription.created_at),
            )
            .map((ev) => (
              <TwitchEvent key={eventSubKeyFunction(ev)} data={ev} />
            ))}
          {orderedEvents.map((ev) => {
            switch (ev.type) {
              case 'marker':
                return (
                  <SessionMarker key="marker">
                    {t('pages.dashboard.twitch-events.marker')}
                  </SessionMarker>
                );
              default:
                return (
                  <TwitchEvent
                    key={eventSubKeyFunction(ev.event)}
                    data={ev.event}
                  />
                );
            }
          })}
        </EventListContainer>
      </Scrollbar>
    </>


@@ 439,6 505,19 @@ function TwitchSection() {
  const twitchInfo = useLiveKey<StreamInfo[]>('twitch/stream-info');
  const kv = useAppSelector((state) => state.api.client);
  const [twitchEvents, setTwitchEvents] = useState<EventSubNotification[]>([]);
  const [oldestLog, setOldestLog] = useState(Date.now());

  useEffect(() => {
    GetLastLogs().then((res) => {
      // Get oldest log entry
      const oldest = res.reduce<number>((acc, log) => {
        const parsedDate = Date.parse(log.time);
        return parsedDate < acc ? parsedDate : acc;
      }, Date.now());

      setOldestLog(oldest);
    });
  }, []);

  const keyfn = (ev: EventSubNotification) => JSON.stringify(ev);



@@ 493,7 572,9 @@ function TwitchSection() {
      ) : (
        <TextBlock>{t('pages.dashboard.not-live')}</TextBlock>
      )}
      {twitchEvents ? <TwitchEventLog events={twitchEvents} /> : null}
      {twitchEvents ? (
        <TwitchEventLog events={twitchEvents} dateMarker={oldestLog} />
      ) : null}
    </>
  );
}