Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 3.1.22.1000

- Fixed an issue that caused chat input to break after a Twitch update

### 3.1.21.1000

- Added new extension notice
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 3.1.22

- Fixed an issue that caused chat input to break after a Twitch update

### 3.1.21

- Added new extension notice
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "7TV",
"description": "Improve your viewing experience on Twitch & Kick with new features, emotes, vanity and performance.",
"private": true,
"version": "3.1.21",
"version": "3.1.22",
"dev_version": "1.0",
"scripts": {
"start": "cross-env NODE_ENV=dev yarn build:dev && cross-env NODE_ENV=dev vite --mode dev",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
<template>
<template v-for="({ parent, component, props, condition }, i) of tButtons.values()" :key="i">
<Teleport v-if="parent.current && (typeof condition !== 'function' || condition())" :to="parent.current">
<Teleport v-if="parent.current && isButtonActive(condition)" :to="parent.current">
<Component :is="component" v-bind="props" />
</Teleport>
</template>
</template>

<script setup lang="ts">
import { markRaw, reactive, ref } from "vue";
import { REACT_TYPEOF_TOKEN } from "@/common/Constant";
import { REACT_ELEMENT_SYMBOL, useComponentHook } from "@/common/ReactHooks";
import { markRaw, onMounted, onUnmounted, onUpdated, reactive, ref } from "vue";
import { declareModule } from "@/composable/useModule";

const { markAsReady } = declareModule("chat-input-controller", {
Expand All @@ -19,43 +17,36 @@ const { markAsReady } = declareModule("chat-input-controller", {

// Button renderer
const tButtons = reactive(new Map<string, InsertedButton<ComponentFactory>>());
useComponentHook<Twitch.ChatInputControllerComponent>(
{
parentSelector: ".chat-input",
predicate: (n) => {
return n.handleGlobalMousedown && n.props && n.props.children && n.props.onClickOut;
},
},
{
hooks: {
render(inst, cur) {
if (!inst.component.container || !inst.component.container.parentElement) return cur;
if (!inst.component.container.parentElement.classList.contains("chat-input")) return cur;

const props = (cur as ReactExtended.ReactRuntimeElement).props ?? {};
const child = (props.children as ReactExtended.ReactRuntimeElement[]).find(
(c) => c.props.className === "chat-input__buttons-container",
);
if (!child) return cur;

const buttons = child.props.children.at(-1);
if (!buttons) return cur;

for (const btn of tButtons.values()) {
buttons.props.children.splice(buttons.props.children.length - btn.offset, 0, {
[REACT_TYPEOF_TOKEN]: REACT_ELEMENT_SYMBOL,
key: null,
ref: btn.parent,
type: "seventv-chat-input-button-container",
props: {},
});
}

return cur;
},
},
},
);
let mountFrame = 0;
let observer: MutationObserver | undefined;

onMounted(() => {
observer = new MutationObserver(() => scheduleRemount());
observer.observe(document, {
childList: true,
subtree: true,
});

scheduleRemount();
});

onUpdated(() => {
scheduleRemount();
});

onUnmounted(() => {
observer?.disconnect();
observer = undefined;

if (mountFrame) {
cancelAnimationFrame(mountFrame);
mountFrame = 0;
}

for (const btn of tButtons.values()) {
btn.parent.current?.remove();
}
});

/**
* Add a button under the chat input, with a given offset
Expand All @@ -69,21 +60,67 @@ function addButton<T extends ComponentFactory>(
offset: number,
condition?: () => boolean,
) {
const track = ref({ current: null as HTMLElement | null });

if (tButtons)
tButtons.set(key, {
key,
offset,
parent: track.value,
component: markRaw(com),
props,
condition: condition ?? (() => true),
});
const container = document.createElement("seventv-chat-input-button-container");
const track = ref({ current: container as HTMLElement | null });

tButtons.set(key, {
key,
offset,
parent: track.value,
component: markRaw(com),
props,
condition: condition ?? (() => true),
});

scheduleRemount();

return track;
}

function scheduleRemount() {
if (mountFrame) return;

mountFrame = requestAnimationFrame(() => {
mountFrame = 0;
remountButtons();
});
}

function remountButtons() {
const row = document.querySelector<HTMLElement>("div[data-test-selector='chat-input-buttons-container']");
if (!row) return;

const anchor =
row.querySelector<HTMLElement>("button[data-a-target='chat-settings']") ??
row.querySelector<HTMLElement>("button[data-a-target='chat-send-button']");
const wrapper = anchor?.parentElement;
const container = wrapper?.parentElement ?? row;

const buttons = [...tButtons.values()].sort((a, b) => b.offset - a.offset);

let reference: Element | null = wrapper ?? anchor;
for (let i = buttons.length - 1; i >= 0; i--) {
const btn = buttons[i];
const el = btn.parent.current;
if (!el) continue;

if (!isButtonActive(btn.condition)) {
el.remove();
continue;
}

if (el.parentElement !== container || el.nextSibling !== reference) {
container.insertBefore(el, reference);
}

reference = el;
}
}

function isButtonActive(condition?: () => boolean): boolean {
return typeof condition !== "function" || condition();
}

interface InsertedButton<T extends ComponentFactory> {
key: string;
offset: number;
Expand All @@ -99,3 +136,18 @@ defineExpose({

markAsReady();
</script>

<style lang="scss">
div[data-test-selector="chat-input-buttons-container"] seventv-chat-input-button-container {
display: flex;
align-items: center;
}

div[data-test-selector="chat-input-buttons-container"] > div:has(button[data-a-target="chat-settings"]) {
display: contents !important;

> :not(:has(button[data-a-target="chat-settings"])) {
display: none !important;
}
}
</style>
Loading