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

- Added custom moderation action reasons
- Added an option to select the default timeout duration
- Added an option to hide the pin confirmation prompt

### 3.1.15.1000

- Added required Firefox built-in consent metadata to the manifest
Expand Down
14 changes: 12 additions & 2 deletions src/app/chat/UserMessageButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
ref="pinButtonRef"
v-tooltip="'Pin'"
class="seventv-button"
@click="pinPrompt = true"
@click="onPinPrompt"
>
<PinIcon />
</div>
<div v-tooltip="'Reply'" class="seventv-button" @click="openReplyTray">
<div v-if="!msg.moderation.deleted" v-tooltip="'Reply'" class="seventv-button" @click="openReplyTray">
<component :is="msg.parent ? TwChatReply : ReplyIcon" />
</div>
</div>
Expand Down Expand Up @@ -95,7 +95,9 @@ const tray = useTray("Reply", () => ({
: {}),
}));

const showPinPrompt = useConfig<boolean>("chat.pin_prompt_toggle");
const showCopyIcon = useConfig<boolean>("chat.copy_icon_toggle");

const copyToastOpen = ref(false);
const copyButtonRef = ref<HTMLElement>();
const copyToastContainer = useFloatScreen(copyButtonRef, {
Expand Down Expand Up @@ -125,6 +127,14 @@ function openReplyTray(): void {
tray.open();
}

function onPinPrompt(): void {
if (showPinPrompt.value) {
pinPrompt.value = true;
} else {
emit("pin");
}
}

function onPinAnswer(answer: string): void {
if (answer !== "yes") return;

Expand Down
186 changes: 186 additions & 0 deletions src/app/settings/SettingsConfigActionReasons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<template>
<main class="seventv-settings-action-reasons">
<div class="create-new">
<FormInput label="New Reason..." :onkeydown="onNewReason" />
</div>

<UiScrollable>
<div v-for="(reason, index) in reasons" :key="index" class="seventv-settings-action-reason-item">
<div class="controls">
<div class="control" @click="onReasonMove(index, 'up')">
<ArrowIcon for="exit-icon" direction="up" />
</div>
<div class="control" @click="onReasonMove(index, 'down')">
<ArrowIcon for="exit-icon" direction="down" />
</div>
</div>
<div class="content">
<div class="use-virtual-input" tabindex="0" @click="onInputFocus(index)">
<span>{{ reason }}</span>
<FormInput
:ref="(n) => virtualInputs.set(index, n as InstanceType<typeof FormInput>)"
:model-value="reason"
@blur="onInputBlur(index)"
/>
</div>
<div v-tooltip="'Remove'" class="control" @click="onReasonRemove(index)">
<CloseIcon tabindex="0" />
</div>
</div>
</div>
</UiScrollable>
</main>
</template>

<script setup lang="ts">
import { clamp } from "@vueuse/core";
import { useConfig } from "@/composable/useSettings";
import FormInput from "@/site/global/components/FormInput.vue";
import ArrowIcon from "@/assets/svg/icons/ArrowIcon.vue";
import CloseIcon from "@/assets/svg/icons/CloseIcon.vue";
import UiScrollable from "@/ui/UiScrollable.vue";

const reasons = useConfig<string[]>("chat.mod_action_reasons.list");

const virtualInputs = new Map<number, InstanceType<typeof FormInput>>();

function onNewReason(event: KeyboardEvent) {
if (event.target instanceof HTMLInputElement === false) return;
if (event.target.value.length === 0) return;

if (event.key !== "Enter") return;

reasons.value = [...reasons.value, event.target.value];
event.target.value = "";
}

function onReasonRemove(index: number) {
reasons.value.splice(index, 1);
reasons.value = [...reasons.value];
}

function onReasonMove(index: number, direction: "up" | "down") {
const newIndex = clamp(direction === "up" ? index - 1 : index + 1, 0, reasons.value.length);

const [reason] = reasons.value.splice(index, 1);
reasons.value.splice(newIndex, 0, reason);
reasons.value = [...reasons.value];
}

function onInputFocus(index: number) {
const input = virtualInputs.get(index);

input?.focus();
}

function onInputBlur(index: number) {
const input = virtualInputs.get(index);

if (!input) return;

const value = input.value();

// if the input is now empty, we shall delete the reason
if (!value || value.length === 0) {
reasons.value.splice(index, 1);
} else {
reasons.value.splice(index, 1, value);
}

reasons.value = [...reasons.value];
}
</script>

<style scoped lang="scss">
.seventv-settings-action-reasons {
display: grid;
grid-template-rows: min-content 1fr;
max-height: 35vh;
gap: 1rem;

.create-new {
display: flex;
flex-direction: row;

input {
width: 100%;
}
}
}

.control {
display: flex;
justify-content: center;
align-items: center;
width: 3rem;
height: 3rem;

&:hover {
background: hsla(0deg, 0%, 30%, 32%);
border-radius: 0.25rem;
cursor: pointer;
}
}

.seventv-settings-action-reason-item {
display: flex;
flex-direction: row;
padding: 0.5rem;
width: 100%;
align-items: center;
gap: 1rem;

&:hover,
&:focus-within {
background-color: #3333;
}

&:nth-child(odd) {
background-color: var(--seventv-background-shade-2);
}

.controls {
display: flex;
justify-content: center;
color: var(--seventv-input-border);
}

.content {
display: grid;
grid-template-columns: 1fr min-content;
align-items: center;
width: 100%;
gap: 1rem;
height: 3.5rem;
}

.use-virtual-input {
cursor: text;
padding: 0.5rem;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

input {
width: 0;
height: 0;
opacity: 0;
}

&:focus-within {
padding: 0;

span {
display: none;
}

input {
opacity: 1;
width: 100%;
height: initial;
}
}
}
}
</style>
1 change: 1 addition & 0 deletions src/common/Constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const UNICODE_TAG_0 = "\u{34f}";
export const UNICODE_TAG_0_REGEX = new RegExp(UNICODE_TAG_0, "g");

export const TWITCH_PROFILE_IMAGE_REGEX = /(\d+x\d+)(?=\.\w{3,4}$)/;
export const TWITCH_TIMEOUT_REGEX = new RegExp("^[0-9]+[dhms]$", "i");

export const HOSTNAME_SUPPORTED_REGEXP = /([a-z0-9]+[.])*(youtube|kick)[.]com/;
export const SEVENTV_EMOTE_LINK = new RegExp(
Expand Down
2 changes: 2 additions & 0 deletions src/composable/chat/useChatProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface ChatProperties {
imageFormat: SevenTV.ImageFormat;
twitchBadgeSets: Twitch.BadgeSets | null;
blockedUsers: Set<string>;
chatRules: string[];
}

type ChatPauseReason = "MOUSEOVER" | "SCROLL" | "ALTKEY";
Expand All @@ -36,6 +37,7 @@ export function useChatProperties(ctx: ChannelContext) {
twitchBadgeSets: {} as Twitch.BadgeSets | null,
blockedUsers: new Set<string>(),
fontAprilFools: "unset",
chatRules: [],
});

m.set(ctx, data);
Expand Down
4 changes: 2 additions & 2 deletions src/composable/useCosmetics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,12 @@ db.ready().then(async () => {
break;
}

log.debug("<Cosmetics>", "Assigned", ents.length.toString(), "stored entitlements");

if (assigned) {
data.staticallyAssigned[ent.user_id] = {};
}
}

log.debug("<Cosmetics>", "Assigned", ents.length.toString(), "stored entitlements");
})
.then(flush);
});
Expand Down
1 change: 1 addition & 0 deletions src/site/global/components/FormInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const inputEl = ref<HTMLInputElement | null>(null);

defineExpose({
focus: () => inputEl.value?.focus(),
value: () => inputEl.value?.value,
});

onMounted(() => {
Expand Down
1 change: 1 addition & 0 deletions src/site/twitch.tv/modules/chat/ChatController.vue
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ definePropertyHook(controller.value.component, "props", {

// Keep track of chat props
properties.isDarkTheme = v.theme;
properties.chatRules = v.chatRules ?? [];

// Send presence upon message sent
messages.sendMessage = v.chatConnectionAPI.sendMessage;
Expand Down
63 changes: 63 additions & 0 deletions src/site/twitch.tv/modules/chat/ChatModule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ defineExpose({
</script>

<script lang="ts">
import { TWITCH_TIMEOUT_REGEX } from "@/common/Constant";
import { HighlightDef } from "@/composable/chat/useChatHighlights";
import { declareConfig, useConfig } from "@/composable/useSettings";
import SettingsConfigActionReasons from "@/app/settings/SettingsConfigActionReasons.vue";
import SettingsConfigHighlights from "@/app/settings/SettingsConfigHighlights.vue";

export type TimestampFormatKey = "infer" | "12" | "24";
Expand Down Expand Up @@ -167,6 +169,61 @@ export const config = [
hint: "If enabled, messages will be kept intact when a moderator clears the chat",
defaultValue: true,
}),
declareConfig("chat.mod_action_reasons", "TOGGLE", {
path: ["Chat", "Moderation", 100],
label: "Show Moderation Action Reasons",
hint: "Show a list of reasons when clicking on an action button",
defaultValue: true,
}),
declareConfig("chat.mod_action_reasons.list", "CUSTOM", {
path: ["Chat", "Moderation", 101],
custom: {
component: markRaw(SettingsConfigActionReasons),
gridMode: "new-row",
},
label: "Custom Action Reasons",
hint: "A list of custom action reasons",
defaultValue: [],
disabledIf: () => !useConfig("chat.mod_action_reasons").value,
}),
declareConfig("chat.mod_action_reasons.include_rules", "TOGGLE", {
path: ["Chat", "Moderation", 102],
label: "Include Chat Rules",
hint: "Add current channel's chat rules to the list of reasons",
defaultValue: true,
disabledIf: () => !useConfig("chat.mod_action_reasons").value,
}),
declareConfig("chat.mod_action_reasons.ban", "DROPDOWN", {
path: ["Chat", "Moderation", 104],
label: "Moderation Action for Ban",
hint: "Which button is used to show mod action reasons for ban",
options: [
["Off", 0],
["Left-Click", 1],
["Right-Click", 2],
],
defaultValue: 2,
disabledIf: () => !useConfig("chat.mod_action_reasons").value,
}),
declareConfig("chat.mod_action_reasons.timeout", "DROPDOWN", {
path: ["Chat", "Moderation", 105],
label: "Moderation Action for Timeout",
hint: "Which button is used to show mod action reasons for timeout",
options: [
["Off", 0],
["Left-Click", 1],
["Right-Click", 2],
],
defaultValue: 2,
disabledIf: () => !useConfig("chat.mod_action_reasons").value,
}),
declareConfig("chat.mod_action.timeout_duration", "INPUT", {
path: ["Chat", "Moderation", 106],
label: "Moderation Action Timeout Duration",
hint: "The default amount of time someone is timed out for (min. 1s, max. 14d)",
defaultValue: "600s",
predicate: (v) => TWITCH_TIMEOUT_REGEX.test(v),
}),
declareConfig("chat.slash_me_style", "DROPDOWN", {
path: ["Chat", "Style"],
label: "/me Style",
Expand Down Expand Up @@ -354,6 +411,12 @@ export const config = [
hint: "Show a 'Copy' icon when hovering over a chat message to copy the message",
defaultValue: true,
}),
declareConfig("chat.pin_prompt_toggle", "TOGGLE", {
path: ["Chat", "Tools"],
label: "Pin Prompt",
hint: "Show a pin confirmation prompt prior to pinning a message",
defaultValue: true,
}),
declareConfig<boolean>("highlights.basic.mention", "TOGGLE", {
path: ["Highlights", "Built-In"],
label: "Show Mention Highlights",
Expand Down
Loading