Skip to content

Commit

Permalink
Feat: Mod Warn feature (#1063)
Browse files Browse the repository at this point in the history
Co-authored-by: Excellify <[email protected]>
  • Loading branch information
ChrisScr3ams and Excellify authored Sep 30, 2024
1 parent edeb5b5 commit 6fd369b
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added option to highlight messages of specific usernames
- Added option to highlight messages based on badges
- Added button to user card to toggle highlighting for a users messages
- Added mod icon for warning users to chat mod icons and user card

### 3.1.1.3000

Expand Down
1 change: 1 addition & 0 deletions locale/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ user_card:
unban_button: Unban
mod_button: Mod
unmod_button: Unmod
warn_button: Warn

# Emote Menu
# This interface allows the user to pick from available emotes
Expand Down
12 changes: 9 additions & 3 deletions src/app/chat/UserCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
v-if="ctx.actor.roles.has('MODERATOR')"
:active-tab="data.activeTab"
:message-count="data.count.messages"
:warning-count="data.count.warnings"
:ban-count="data.count.bans"
:timeout-count="data.count.timeouts"
:comment-count="data.count.comments"
Expand Down Expand Up @@ -215,12 +216,14 @@ const data = reactive({
messageCursors: new WeakMap<ChatMessage, string>(),
timelines: {
messages: {} as Record<string, ChatMessage[]>,
warnings: {} as Record<string, ChatMessage[]>,
bans: {} as Record<string, ChatMessage[]>,
timeouts: {} as Record<string, ChatMessage[]>,
comments: {} as Record<string, ChatMessage[]>,
} as Record<UserCardTabName, Record<string, ChatMessage[]>>,
count: {
messages: 0,
warnings: 0,
bans: 0,
timeouts: 0,
comments: 0,
Expand Down Expand Up @@ -277,6 +280,7 @@ async function fetchModeratorData(): Promise<void> {
if (!resp || resp.errors?.length || !resp.data.channelUser) return;
data.count.messages = resp.data.viewerCardModLogs.messages.count ?? 0;
data.count.warnings = resp.data.viewerCardModLogs.warnings.count ?? 0;
data.count.bans = resp.data.viewerCardModLogs.bans.count ?? 0;
data.count.timeouts = resp.data.viewerCardModLogs.timeouts.count ?? 0;
data.count.comments = resp.data.viewerCardModLogs.comments.edges.length ?? 0;
Expand All @@ -285,11 +289,13 @@ async function fetchModeratorData(): Promise<void> {
const timeouts = resp.data.viewerCardModLogs.timeouts.edges;
const bans = resp.data.viewerCardModLogs.bans.edges;
const warnings = resp.data.viewerCardModLogs.warnings.edges;
// Add timeouts and bans to the timeline
// Add warnings, timeouts and bans to the timeline
for (const [tabName, a] of [
["timeouts", timeouts] as [UserCardTabName, typeof timeouts],
["bans", bans] as [UserCardTabName, typeof bans],
["warnings", warnings] as [UserCardTabName, typeof warnings],
]) {
const result = [] as ChatMessage[];
Expand Down Expand Up @@ -542,7 +548,7 @@ onMounted(async () => {
</script>

<style scoped lang="scss">
$card-width: 32rem;
$card-width: 37rem;
$card-height: 48rem;
main.seventv-user-card-container {
Expand Down Expand Up @@ -719,7 +725,7 @@ main.seventv-user-card-container {
// grid: position each badge next to the other on the same row, wrapping if necessary
display: flex;
flex-wrap: wrap;
max-width: 18rem;
max-width: 22rem;
gap: 0.5rem;
align-self: start;
z-index: 1;
Expand Down
18 changes: 16 additions & 2 deletions src/app/chat/UserCardMod.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
/>
</div>

<div class="seventv-user-card-mod-side seventv-user-card-mod-warn">
<WarningIcon
v-tooltip="t('user_card.warn_button')"
@click="tools.openViewerWarnPopover(target.id, target.username, 0)"
/>
</div>

<div v-show="!ban" class="seventv-user-card-mod-timeout-options">
<button
v-for="opt of timeoutOptions"
Expand Down Expand Up @@ -38,9 +45,11 @@ import { useI18n } from "vue-i18n";
import type { ChatUser } from "@/common/chat/ChatMessage";
import { useChannelContext } from "@/composable/channel/useChannelContext";
import { useChatModeration } from "@/composable/chat/useChatModeration";
import { useChatTools } from "@/composable/chat/useChatTools";
import { TwTypeChatBanStatus } from "@/assets/gql/tw.gql";
import GavelIcon from "@/assets/svg/icons/GavelIcon.vue";
import ShieldIcon from "@/assets/svg/icons/ShieldIcon.vue";
import WarningIcon from "@/assets/svg/icons/WarningIcon.vue";
const props = defineProps<{
target: ChatUser;
Expand All @@ -59,6 +68,7 @@ const { t } = useI18n();
const ctx = useChannelContext();
const mod = useChatModeration(ctx, props.target.username);
const tools = useChatTools(ctx);
async function banUser(duration: string): Promise<void> {
const resp = await mod.banUserFromChat(duration).catch(() => void 0);
Expand Down Expand Up @@ -87,9 +97,9 @@ const timeoutOptions = ["1s", "30s", "1m", "10m", "30m", "1h", "4h", "12h", "1d"
<style scoped lang="scss">
.seventv-user-card-mod {
display: grid;
grid-template-columns: 3em 1fr 3em;
grid-template-columns: 3em 3em 1fr 3em;
grid-template-rows: 1fr;
grid-template-areas: ". . .";
grid-template-areas: ". . . .";
height: 3rem;
align-items: center;
border-top: 0.1rem solid hsla(0deg, 0%, 100%, 10%);
Expand Down Expand Up @@ -127,5 +137,9 @@ const timeoutOptions = ["1s", "30s", "1m", "10m", "30m", "1h", "4h", "12h", "1d"
.seventv-user-card-mod-ban[is-banned="1"]:hover {
color: var(--seventv-accent);
}
.seventv-user-card-mod-warn:hover {
color: #fd0;
}
}
</style>
8 changes: 5 additions & 3 deletions src/app/chat/UserCardTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
<script setup lang="ts">
import { computed } from "vue";
export type UserCardTabName = "messages" | "timeouts" | "bans" | "comments";
export type UserCardTabName = "messages" | "warnings" | "timeouts" | "bans" | "comments";
const props = defineProps<{
activeTab: UserCardTabName;
messageCount: number;
warningCount: number;
timeoutCount: number;
banCount: number;
commentCount: number;
Expand All @@ -33,6 +34,7 @@ const buttons = computed(
() =>
[
{ id: "messages", label: "Messages", count: props.messageCount, maxCount: 1000 },
{ id: "warnings", label: "Warnings", count: props.warningCount, maxCount: 99 },
{ id: "timeouts", label: "Timeouts", count: props.timeoutCount, maxCount: 99 },
{ id: "bans", label: "Bans", count: props.banCount, maxCount: 99 },
{ id: "comments", label: "Comments", count: props.commentCount, maxCount: 10 },
Expand All @@ -48,8 +50,8 @@ function formatCount(count: number, maxCount: number): string {
.seventv-user-card-tabs {
border-top: 0.1rem solid hsla(0deg, 0%, 100%, 10%);
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-areas: ". . . .";
grid-template-columns: repeat(5, 1fr);
grid-template-areas: ". . . . .";
height: 4rem;
box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 35%);
background-color: var(--seventv-background-transparent-1);
Expand Down
6 changes: 5 additions & 1 deletion src/assets/gql/tw.user-card.gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ export const twitchUserCardModLogsQuery = gql`
untimeouts: targetedActions(first: 99, type: UNTIMEOUT) {
...modLogsTargetedActionsResultFragment
}
warnings: targetedActions(first: 99, type: WARN) {
...modLogsTargetedActionsResultFragment
}
comments(first: 100) {
... on ModLogsCommentConnection {
edges {
Expand Down Expand Up @@ -291,7 +294,7 @@ export namespace twitchUserCardModLogsQuery {
}[];
};
timestamp: string;
type: "BAN" | "TIMEOUT" | "UNBAN" | "UNTIMEOUT";
type: "BAN" | "WARN" | "TIMEOUT" | "UNBAN" | "UNTIMEOUT";
}

interface ActionsConnection {
Expand Down Expand Up @@ -333,6 +336,7 @@ export namespace twitchUserCardModLogsQuery {
};
bans: ActionsConnection;
timeouts: ActionsConnection;
warnings: ActionsConnection;
};
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/assets/svg/twitch/TwChatModWarn.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor">
<g>
<path
fill-rule="evenodd"
d="M10.954 3.543c-.422-.724-1.486-.724-1.908 0l-6.9 11.844c-.418.719.11 1.613.955 1.613h13.798c.844 0 1.373-.894.955-1.613l-6.9-11.844zM11 15H9v-2h2v2zm0-3H9V7h2v5z"
clip-rule="evenodd"
></path>
</g>
</svg>
</template>
18 changes: 16 additions & 2 deletions src/composable/chat/useChatTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { ChannelContext } from "../channel/useChannelContext";
interface ChatTools {
TWITCH: {
onShowViewerCard: Twitch.ViewerCardComponent["onShowViewerCard"];
onShowViewerWarnPopover: (
userId: string,
userLogin: string,
placement: Twitch.WarnUserPopoverPlacement,
) => void;
};
YOUTUBE: Record<string, never>;
KICK: Record<string, never>;
Expand All @@ -18,6 +23,7 @@ export function useChatTools(ctx: ChannelContext) {
data = reactive<ChatTools>({
TWITCH: {
onShowViewerCard: () => void 0,
onShowViewerWarnPopover: () => void 0,
},
YOUTUBE: {},
KICK: {},
Expand All @@ -27,10 +33,10 @@ export function useChatTools(ctx: ChannelContext) {
m.set(ctx, data);
}

function update<P extends Platform>(platorm: P, key: keyof ChatTools[P], value: ChatTools[P][keyof ChatTools[P]]) {
function update<P extends Platform>(platform: P, key: keyof ChatTools[P], value: ChatTools[P][keyof ChatTools[P]]) {
if (!data) return;

data[platorm][key] = value;
data[platform][key] = value;
}

function openViewerCard(e: MouseEvent, username: string, msgID: string) {
Expand All @@ -41,8 +47,16 @@ export function useChatTools(ctx: ChannelContext) {
return true;
}

function openViewerWarnPopover(userId: string, userLogin: string, placement: Twitch.WarnUserPopoverPlacement) {
if (!data || !userId || !userLogin) return false;

data[ctx.platform].onShowViewerWarnPopover(userId, userLogin, placement);
return true;
}

return {
update,
openViewerCard,
openViewerWarnPopover,
};
}
13 changes: 13 additions & 0 deletions src/site/twitch.tv/modules/chat/ChatController.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,19 @@ a.then(
() => null,
);
tools.update(
"TWITCH",
"onShowViewerWarnPopover",
(userId: string, userLogin: string, placement: Twitch.WarnUserPopoverPlacement) => {
const props = controller.value.component.props;
props.setWarnUserTarget({
targetUserID: userId,
targetUserLogin: userLogin,
});
props.setWarnUserPopoverPlacement(placement);
},
);
if (a instanceof ObserverPromise) {
until(useTimeout(1e4))
.toBeTruthy()
Expand Down
21 changes: 21 additions & 0 deletions src/site/twitch.tv/modules/chat/components/mod/ModIcons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
<TwChatModTimeout />
</span>

<span
v-if="msg.author && !msg.author.isActor"
ref="warnRef"
@click="showWarnPopover()"
@mouseenter="warnTooltip.show(warnRef)"
@mouseleave="warnTooltip.hide()"
>
<TwChatModWarn />
</span>

<span
ref="deleteRef"
@click="deleteChatMessage(msg.id)"
Expand All @@ -36,24 +46,35 @@ import { ref } from "vue";
import type { ChatMessage } from "@/common/chat/ChatMessage";
import { useChannelContext } from "@/composable/channel/useChannelContext";
import { useChatModeration } from "@/composable/chat/useChatModeration";
import { useChatTools } from "@/composable/chat/useChatTools";
import { useTooltip } from "@/composable/useTooltip";
import TwChatModBan from "@/assets/svg/twitch/TwChatModBan.vue";
import TwChatModDelete from "@/assets/svg/twitch/TwChatModDelete.vue";
import TwChatModTimeout from "@/assets/svg/twitch/TwChatModTimeout.vue";
import TwChatModWarn from "@/assets/svg/twitch/TwChatModWarn.vue";
const props = defineProps<{
msg: ChatMessage;
}>();
const ctx = useChannelContext();
const tools = useChatTools(ctx);
const { banUserFromChat, deleteChatMessage } = useChatModeration(ctx, props.msg.author?.username ?? "");
function showWarnPopover() {
if (!props.msg.author) return;
tools.openViewerWarnPopover(props.msg.author.id, props.msg.author.username, 0);
}
const banRef = ref();
const banTooltip = useTooltip(`Ban ${props.msg.author?.username ?? "???"}`);
const timeoutRef = ref();
const timeoutTooltip = useTooltip(`Timeout ${props.msg.author?.username ?? "???"}`);
const warnRef = ref();
const warnTooltip = useTooltip(`Warn ${props.msg.author?.username ?? "???"}`);
const deleteRef = ref();
const deleteTooltip = useTooltip(`Delete message by ${props.msg.author?.username ?? "???"}`);
</script>
Expand Down
9 changes: 9 additions & 0 deletions src/types/twitch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ declare module Twitch {
};
};

export enum WarnUserPopoverPlacement {
CHAT = 0,
VIEWERS_LIST = 1,
}

export type ChatControllerComponent = ReactExtended.WritableComponent<{
authToken: string | undefined;
channelDisplayName: string;
Expand Down Expand Up @@ -161,6 +166,10 @@ declare module Twitch {
userDisplayName: string | undefined;
userID: string | undefined;
userLogin: string | undefined;
warnUserTargetLogin: string | undefined;
warnUserTargetID: string | undefined;
setWarnUserTarget: (target: { targetUserLogin: string; targetUserID: string }) => void;
setWarnUserPopoverPlacement: (placement: WarnUserPopoverPlacement | null) => void;
}> & {
pushMessage: (msg: Partial<ChatMessage>) => void;
sendMessage: (msg: string, n?: any) => void;
Expand Down

0 comments on commit 6fd369b

Please sign in to comment.