Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/platforms/kick/kick.platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ChatHighlightUserModule from "$kick/modules/chat-highlight-user/chat-high
import ChatMessageMenuModule from "$kick/modules/chat-message-menu/chat-message-menu.module.tsx";
import MessageMenuModule from "$kick/modules/chat-message-menu/message-menu.module.tsx";
import ChatMessagePopupModule from "$kick/modules/chat-message-popup/chat-message-popup.module.tsx";
import ChatMonitorButtonModule from "$kick/modules/chat-monitor-button/chat-monitor-button.module.tsx";
import ChatNicknameCustomizationModule from "$kick/modules/chat-nickname-customization/chat-nickname-customization.module.ts";
import ChatModule from "$kick/modules/chat/chat.module.ts";
import LocalWatchtimeCounterModule from "$kick/modules/local-watchtime-counter/local-watchtime-counter.module.tsx";
Expand Down Expand Up @@ -47,6 +48,7 @@ export default class KickPlatform extends Platform<KickModule, KickEvents, KickS
new ChatAttachmentsModule(...dependencies),
new ChatBadgesModule(...dependencies),
new SettingsButtonModule(...dependencies),
new ChatMonitorButtonModule(...dependencies),
new SettingsModule(...dependencies),
new ChatNicknameCustomizationModule(...dependencies),
new StreamLatencyModule(...dependencies),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ChatMonitorPingMenu } from "$shared/components/chat-monitor-ping/chat-monitor-ping-menu.component.tsx";
import { TooltipComponent } from "$shared/components/tooltip/tooltip.component.tsx";
import type { KickModuleConfig } from "$types/shared/module/module.types.ts";
import { render } from "preact";
import { useState } from "preact/hooks";
import styled from "styled-components";
import KickModule from "../../kick.module.ts";

export default class ChatMonitorButtonModule extends KickModule {
readonly config: KickModuleConfig = {
name: "chat-monitor-button",
appliers: [
{
type: "selector",
selectors: ["#header-primary-menu"],
callback: this.run.bind(this),
key: "chat-monitor-button-main",
once: true,
},
{
type: "event",
event: "extension:chat-monitor-open",
callback: this.openChatMonitor.bind(this),
key: "chat-monitor-open",
},
],
};

private menuContainer: HTMLDivElement | null = null;

private async run(elements: Element[], key: string) {
const wrappers = this.commonUtils().createEmptyElements(this.getId(), elements, "div");
const logo = await this.commonUtils().getAssetFile(this.workerService(), "enhancer/logo-gray.svg");

wrappers.forEach((element) => {
render(
<TooltipComponent content={<p>Open Chat Monitor</p>} position="bottom">
<ChatMonitorButtonComponent onClick={this.openChatMonitor.bind(this)} logoUrl={logo} />
</TooltipComponent>,
element,
);
});
}

private openChatMonitor() {
// Create menu container if it doesn't exist
if (!this.menuContainer) {
this.menuContainer = document.createElement("div");
this.menuContainer.id = `${this.getId()}-menu-container`;
document.body.appendChild(this.menuContainer);
}

// Render the menu
render(
<ChatMonitorPingMenu workerService={this.workerService()} onClose={this.closeChatMonitor.bind(this)} />,
this.menuContainer,
);
}

private closeChatMonitor() {
if (this.menuContainer) {
render(null, this.menuContainer);
}
}

async initialize() {
this.commonUtils().createGlobalStyle(`
#header-primary-menu .enhancer-chat-monitor-button {
display: flex;
align-items: center;
}
`);

// Listen for keyword match events from background via worker service
this.workerService().onBackgroundMessage((message) => {
if (message.action === "chatMonitorPing") {
// Show notification or update badge
this.showNotificationBadge();
}
});
}

private showNotificationBadge() {
// Add a visual indicator that there's a new match
const buttons = document.querySelectorAll(".enhancer-chat-monitor-button button");
for (const button of buttons) {
button.classList.add("has-notification");
}
}
}

const StyledChatMonitorButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
width: 36px;
height: 36px;
cursor: pointer;
position: relative;

border: none;
background: transparent;
padding: 0;
color: inherit;

&:hover {
background: rgba(255, 255, 255, 0.1);
}

&:focus-visible {
outline: 2px solid var(--color-focus, #007bff);
outline-offset: 2px;
}

&.has-notification::after {
content: "";
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
background: #53fc18;
border-radius: 50%;
border: 2px solid #000;
}

img {
width: 24px;
height: 24px;
filter: brightness(0) invert(0.5);
}

&:hover img {
filter: brightness(0) invert(0.7);
}
`;

interface ChatMonitorButtonComponentProps {
onClick: () => void;
logoUrl: string;
}

function ChatMonitorButtonComponent({ onClick, logoUrl }: ChatMonitorButtonComponentProps) {
const [hasNotification] = useState(false);

return (
<StyledChatMonitorButton onClick={onClick} className={hasNotification ? "has-notification" : ""}>
<img src={logoUrl} alt={"Chat Monitor"} />
</StyledChatMonitorButton>
);
}
12 changes: 12 additions & 0 deletions src/platforms/kick/modules/settings/settings.module.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { KICK_DEFAULT_SETTINGS } from "$kick/kick.constants.ts";
import KickModule from "$kick/kick.module.ts";
import { ChatMonitorSettingsComponent } from "$shared/components/chat-monitor-settings/chat-monitor-settings.component.tsx";
import { ExportImportComponent } from "$shared/components/export-import/export-import.component.tsx";
import { EnhancerAboutComponent } from "$shared/components/settings/about.component.tsx";
import Settings, { SettingsOverlay } from "$shared/components/settings/settings.component.tsx";
Expand Down Expand Up @@ -172,6 +173,17 @@ export default class SettingsModule extends KickModule {
},
hideInfo: true,
},
{
id: "chat-monitor",
title: "Chat Monitor",
description: "Monitor chat messages from specified channels for keywords",
type: "text",
tabIndex: tabIndexes.General,
content: () => {
return <ChatMonitorSettingsComponent workerService={workerService} />;
},
hideInfo: true,
},
{
id: "export-import",
title: "Export/Import Data",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ChatMonitorPingMenu } from "$shared/components/chat-monitor-ping/chat-monitor-ping-menu.component.tsx";
import { TooltipComponent } from "$shared/components/tooltip/tooltip.component.tsx";
import type { TwitchModuleConfig } from "$types/shared/module/module.types.ts";
import { render } from "preact";
import { useState } from "preact/hooks";
import styled from "styled-components";
import TwitchModule from "../../twitch.module.ts";

export default class ChatMonitorButtonModule extends TwitchModule {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move it to the settings-button module and rename this module to navbar-icons-module wdyt, we could also seperate logic in files in this module direcotry for settings and chat monitor

readonly config: TwitchModuleConfig = {
name: "chat-monitor-button",
appliers: [
{
type: "selector",
selectors: [".top-nav__menu"],
callback: this.run.bind(this),
key: "chat-monitor-button-main",
once: true,
},
{
type: "event",
event: "extension:chat-monitor-open",
callback: this.openChatMonitor.bind(this),
key: "chat-monitor-open",
},
],
};

private menuContainer: HTMLDivElement | null = null;

private async run(elements: Element[], key: string) {
const properElements = elements
.filter((element) => element.children.length > 0)
.map((element) => [...element.children].at(-1))
.filter((element) => element !== undefined) as Element[];
const wrappers = this.commonUtils().createEmptyElements(this.getId(), properElements, "span");
const logo = await this.commonUtils().getAssetFile(this.workerService(), "enhancer/logo-gray.svg");

wrappers.forEach((element) => {
render(
<TooltipComponent content={<p>Open Chat Monitor</p>} position="bottom">
<ChatMonitorButtonComponent onClick={this.openChatMonitor.bind(this)} logoUrl={logo} />
</TooltipComponent>,
element,
);
});
}

private openChatMonitor() {
// Create menu container if it doesn't exist
if (!this.menuContainer) {
this.menuContainer = document.createElement("div");
this.menuContainer.id = `${this.getId()}-menu-container`;
document.body.appendChild(this.menuContainer);
}

// Render the menu
render(
<ChatMonitorPingMenu workerService={this.workerService()} onClose={this.closeChatMonitor.bind(this)} />,
this.menuContainer,
);
}

private closeChatMonitor() {
if (this.menuContainer) {
render(null, this.menuContainer);
}
}

async initialize() {
this.commonUtils().createGlobalStyle(`
.top-nav__menu .enhancer-chat-monitor-button { order: -6 !important; }
`);

// Listen for keyword match events from background via worker service
this.workerService().onBackgroundMessage((message) => {
if (message.action === "chatMonitorPing") {
// Show notification or update badge
this.showNotificationBadge();
}
});
}

private showNotificationBadge() {
// Add a visual indicator that there's a new match
const buttons = document.querySelectorAll(".enhancer-chat-monitor-button button");
for (const button of buttons) {
button.classList.add("has-notification");
}
}
}

const StyledChatMonitorButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--border-radius-medium);
width: 30px;
height: 30px;
cursor: pointer;
position: relative;
margin-top: 4px;

border: none;
background: transparent;
padding: 0;
color: inherit;

&:hover {
background: var(--color-background-button-text-hover);
}

&:focus-visible {
outline: 2px solid var(--color-focus, #007bff);
outline-offset: 2px;
}

&.has-notification::after {
content: "";
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
background: #9147ff;
border-radius: 50%;
border: 2px solid #0d0d0d;
}

img {
filter: brightness(0) invert(0.5);
}

&:hover img {
filter: brightness(0) invert(0.7);
}
`;

interface ChatMonitorButtonComponentProps {
onClick: () => void;
logoUrl: string;
}

function ChatMonitorButtonComponent({ onClick, logoUrl }: ChatMonitorButtonComponentProps) {
const [hasNotification] = useState(false);

return (
<StyledChatMonitorButton onClick={onClick} className={hasNotification ? "has-notification" : ""}>
<img src={logoUrl} alt={"Chat Monitor"} />
</StyledChatMonitorButton>
);
}
12 changes: 12 additions & 0 deletions src/platforms/twitch/modules/settings/settings.module.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChatMonitorSettingsComponent } from "$shared/components/chat-monitor-settings/chat-monitor-settings.component.tsx";
import { ExportImportComponent } from "$shared/components/export-import/export-import.component.tsx";
import { EnhancerAboutComponent } from "$shared/components/settings/about.component.tsx";
import Settings, { SettingsOverlay } from "$shared/components/settings/settings.component.tsx";
Expand Down Expand Up @@ -232,6 +233,17 @@ export default class SettingsModule extends TwitchModule {
},
hideInfo: true,
},
{
id: "chat-monitor",
title: "Chat Monitor",
description: "Monitor chat messages from specified channels for keywords",
type: "text",
tabIndex: tabIndexes.General,
content: () => {
return <ChatMonitorSettingsComponent workerService={workerService} />;
},
hideInfo: true,
},
{
id: "export-import",
title: "Export/Import Data",
Expand Down
2 changes: 2 additions & 0 deletions src/platforms/twitch/twitch.platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ChatBadgesModule from "$twitch/modules/chat-badges/chat-badges.module.tsx
import ChatMentionSoundModule from "$twitch/modules/chat-mention-sound/chat-mention-sound.module.tsx";
import ChatMessageMenuModule from "$twitch/modules/chat-message-menu/chat-message-menu.module.tsx";
import MessageMenuModule from "$twitch/modules/chat-message-menu/message-menu.module.tsx";
import ChatMonitorButtonModule from "$twitch/modules/chat-monitor-button/chat-monitor-button.module.tsx";
import ChatNicknameCustomizationModule from "$twitch/modules/chat-nickname-customization/chat-nickname-customization.module.tsx";
import ChattersModule from "$twitch/modules/chatters/chatters.module.tsx";
import LocalWatchtimeCounterModule from "$twitch/modules/local-watchtime-counter/local-watchtime-counter.module.tsx";
Expand Down Expand Up @@ -65,6 +66,7 @@ export default class TwitchPlatform extends Platform<TwitchModule, TwitchEvents,
new PinStreamerModule(...dependencies),
new WatchTimeModule(...dependencies),
new SettingsButtonModule(...dependencies),
new ChatMonitorButtonModule(...dependencies),
new LocalWatchtimeCounterModule(...dependencies),
new SettingsModule(...dependencies),
new ChatNicknameCustomizationModule(...dependencies),
Expand Down
Loading
Loading