Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7ce502d
initial commit
SunnyWang0 Oct 14, 2025
ea5d40f
new changes
SunnyWang0 Oct 14, 2025
79e7f74
fix
SunnyWang0 Oct 14, 2025
267997d
fix slow badge load
SunnyWang0 Oct 14, 2025
7aa94e9
clean
SunnyWang0 Oct 14, 2025
a5f9c8e
clean up code
SunnyWang0 Oct 14, 2025
741c5fe
clean speed
SunnyWang0 Oct 14, 2025
dba4b2e
changes
SunnyWang0 Oct 15, 2025
6a7a16f
updates
SunnyWang0 Oct 15, 2025
d5fdfd3
notes
SunnyWang0 Oct 15, 2025
0651af1
fix
SunnyWang0 Oct 15, 2025
b0caee8
clean up
SunnyWang0 Oct 15, 2025
7ecaa3f
remove transition
SunnyWang0 Oct 15, 2025
3de86b5
testing
SunnyWang0 Oct 15, 2025
3792962
fix
SunnyWang0 Oct 15, 2025
4a8097c
reset
SunnyWang0 Oct 15, 2025
7bdb74d
optimization
SunnyWang0 Oct 15, 2025
dc960ef
remove dev docs
SunnyWang0 Oct 15, 2025
08a24bc
fixing styling and activation bug
SunnyWang0 Dec 3, 2025
006f816
eloward updates, styling
SunnyWang0 Dec 4, 2025
f6331fc
clean up eloward integration
SunnyWang0 Dec 4, 2025
bc51e8a
defer to the official extension when both are installed
SunnyWang0 Dec 4, 2025
e09c478
polish for prod
SunnyWang0 Dec 5, 2025
9b62e15
Merge branch 'SevenTV:master' into master
SunnyWang0 Dec 5, 2025
9b176e0
lint errors
SunnyWang0 Dec 5, 2025
b97e26d
Merge branch 'master' into master
SunnyWang0 Sep 15, 2026
965f422
fix: remove duplicate Badge import left by master merge
SunnyWang0 Sep 15, 2026
1d10d03
fix(eloward): address review issues against current upstream
SunnyWang0 Sep 15, 2026
3f04b34
fix(eloward): don't apply a rank lookup that finished after the categ…
SunnyWang0 Sep 15, 2026
f911012
fix(eloward): restore the badge theme tweak, scoped to the badge
SunnyWang0 Sep 15, 2026
238da80
fix(eloward): keep the theme tweak on 7TV's own badge element
SunnyWang0 Sep 15, 2026
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.26.1000

- Added EloWard League of Legends rank badges module

## 3.1.25.1000

- Fixed Kick Emote Menu position
Expand Down
72 changes: 71 additions & 1 deletion src/app/chat/UserTag.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<!--Badge List -->
<span
v-if="
!hideBadges && ((twitchBadges.length && twitchBadgeSets?.count) || cosmetics.badges.size || sourceData)
!hideBadges &&
((twitchBadges.length && twitchBadgeSets?.count) || cosmetics.badges.size || sourceData || elowardBadge)
"
class="seventv-chat-user-badge-list"
>
Expand Down Expand Up @@ -36,6 +37,9 @@
type="app"
/>
</template>

<!-- EloWard Rank Badge - Rightmost position (closest to username) -->
<EloWardBadge v-if="elowardBadge" :badge="elowardBadge" :username="user.username" />
</span>

<!-- Message Author -->
Expand Down Expand Up @@ -74,6 +78,10 @@ import { useChannelContext } from "@/composable/channel/useChannelContext";
import { useChatProperties } from "@/composable/chat/useChatProperties";
import { useCosmetics } from "@/composable/useCosmetics";
import { useConfig } from "@/composable/useSettings";
import EloWardBadge from "@/site/twitch.tv/modules/eloward/components/EloWardBadge.vue";
import { useEloWardRanks } from "@/site/twitch.tv/modules/eloward/composables/useEloWardRanks";
import type { EloWardBadge as EloWardBadgeType } from "@/site/twitch.tv/modules/eloward/composables/useEloWardRanks";
import { useGameDetection } from "@/site/twitch.tv/modules/eloward/composables/useGameDetection";
import Badge, { TwitchChatBadgeWithData } from "./Badge.vue";
import UserCard from "./UserCard.vue";
import UiDraggable from "@/ui/UiDraggable.vue";
Expand Down Expand Up @@ -116,6 +124,11 @@ const twitchBadges = ref<TwitchChatBadgeWithData[]>([]);
const twitchBadgeSets = toRef(properties, "twitchBadgeSets");
const mentionStyle = useConfig<MentionStyle>("chat.colored_mentions");

// EloWard integration
const elowardRanks = useEloWardRanks();
const gameDetection = useGameDetection();
const elowardEnabled = useConfig<boolean>("eloward.enabled");

const tagRef = ref<HTMLDivElement>();
const showUserCard = ref(false);
const cardHandle = ref<HTMLDivElement>();
Expand Down Expand Up @@ -184,6 +197,63 @@ const stop = watch(
},
{ immediate: true },
);

// EloWard badge integration
const elowardBadge = ref<EloWardBadgeType | null>(null);

// Detect official EloWard Chrome extension
function detectOfficialExtension(): boolean {
const bodyAttr = document.body?.getAttribute("data-eloward-chrome-ext");
const htmlAttr = document.documentElement?.getAttribute("data-eloward-chrome-ext");

return bodyAttr === "active" || htmlAttr === "active";
}

const initializeEloWardBadge = () => {
// Skip if official EloWard Chrome extension is detected
if (detectOfficialExtension()) {
elowardBadge.value = null;
return;
}

// Badges are never shown for mentions or the user card header, so don't look them up
if (props.hideBadges || !elowardEnabled.value || !gameDetection.isLeagueStream.value) {
elowardBadge.value = null;
return;
}

const username = props.user.username;
if (!username) {
elowardBadge.value = null;
return;
}

const cachedData = elowardRanks.getCachedRankData(username);

if (cachedData !== undefined) {
elowardBadge.value = cachedData ? elowardRanks.getRankBadge(cachedData) : null;
return;
}

elowardRanks
.fetchRankData(username)
.then((rankData) => {
// The stream's category or the setting can change while the request is in flight
if (username !== props.user.username || !elowardEnabled.value || !gameDetection.isLeagueStream.value)
return;

elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null;
})
.catch(() => {
elowardBadge.value = null;
});
};

initializeEloWardBadge();

watch(() => props.user.username, initializeEloWardBadge);
watch(elowardEnabled, initializeEloWardBadge);
watch(() => gameDetection.isLeagueStream.value, initializeEloWardBadge);
</script>

<style scoped lang="scss">
Expand Down
68 changes: 68 additions & 0 deletions src/site/twitch.tv/modules/eloward/EloWardModule.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template></template>

<script setup lang="ts">
import { watch, onMounted, onUnmounted } from "vue";
import { declareModule } from "@/composable/useModule";
import { useChannelContext } from "@/composable/channel/useChannelContext";
import { declareConfig } from "@/composable/useSettings";
import { useEloWardRanks } from "./composables/useEloWardRanks";
import { useGameDetection } from "./composables/useGameDetection";

const { dependenciesMet, markAsReady } = declareModule("eloward", {
name: "EloWard",
depends_on: ["chat"],
});

const ctx = useChannelContext();
const elowardRanks = useEloWardRanks();
const gameDetection = useGameDetection();

onMounted(async () => {
if (dependenciesMet.value) {
markAsReady();
} else {
const unwatch = watch(dependenciesMet, (met) => {
if (met) {
markAsReady();
unwatch();
}
});
}
});

// Clear cache when leaving League streams
watch(
() => gameDetection.isLeagueStream.value,
(isLeague) => {
if (!isLeague) {
elowardRanks.clearCache();
}
},
);

// Clear cache when switching channels
watch(
() => ctx.id,
(newChannelId, oldChannelId) => {
if (newChannelId !== oldChannelId && newChannelId) {
elowardRanks.clearCache();
}
},
);

onUnmounted(() => {
elowardRanks.clearCache();
});
</script>

<script lang="ts">
// Configuration declarations
export const config = [
declareConfig("eloward.enabled", "TOGGLE", {
path: ["Appearance", "Vanity"],
label: "EloWard Rank Badges",
hint: "Show League of Legends rank badges next to usernames when watching League of Legends streams",
defaultValue: true,
}),
];
</script>
137 changes: 137 additions & 0 deletions src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<div
class="seventv-chat-badge eloward-rank-badge"
:class="badgeClasses"
@click="handleClick"
@mouseenter="show(imgRef)"
@mouseleave="hide()"
>
<img
ref="imgRef"
:src="badge.imageUrl"
:srcset="srcset"
:alt="`${badge.tier} rank badge`"
class="eloward-badge-img"
loading="eager"
decoding="async"
fetchpriority="high"
/>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { useTooltip } from "@/composable/useTooltip";
import EloWardTooltip from "./EloWardTooltip.vue";
import { useEloWardRanks } from "../composables/useEloWardRanks";
import type { EloWardBadge } from "../composables/useEloWardRanks";

const props = defineProps<{
badge: EloWardBadge;
username: string;
}>();

const elowardRanks = useEloWardRanks();
const imgRef = ref<HTMLElement>();

// Use 7TV's tooltip system
const { show, hide } = useTooltip(EloWardTooltip, {
badge: props.badge,
username: props.username,
});

const badgeClasses = computed(() => ({
[`eloward-${props.badge.tier.toLowerCase()}`]: true,
"eloward-animated": props.badge.animated,
}));

// Generate srcset for responsive loading
const srcset = computed(() => {
return `${props.badge.imageUrl} 1x, ${props.badge.imageUrl} 2x`;
});

// Click handler to open OP.GG
const handleClick = () => {
// Use cached data if available
const url = elowardRanks.getOpGGUrl({
tier: props.badge.tier,
division: props.badge.division,
leaguePoints: props.badge.leaguePoints,
summonerName: props.badge.summonerName,
region: props.badge.region,
});

if (url) {
window.open(url, "_blank", "noopener,noreferrer");
}
};
</script>

<style scoped lang="scss">
// Base badge container - exactly matches standard 7TV badges
.seventv-chat-badge.eloward-rank-badge {
display: inline-block;
vertical-align: baseline;
cursor: pointer;
}

// Image - matches standard badge pattern
.eloward-badge-img {
height: 18px;
width: auto;
vertical-align: middle;
}

// Rank-specific scaling - kept minimal
.eloward-iron .eloward-badge-img {
transform: scale(1.15);
}

.eloward-bronze .eloward-badge-img {
transform: scale(1.1);
}

.eloward-silver .eloward-badge-img {
transform: scale(1.1);
}

.eloward-gold .eloward-badge-img {
transform: scale(1.15);
}

.eloward-platinum .eloward-badge-img {
transform: scale(1.15);
}

.eloward-emerald .eloward-badge-img {
transform: scale(1.15);
}

.eloward-diamond .eloward-badge-img {
transform: scale(1.05);
}

.eloward-master .eloward-badge-img {
transform: scale(1.1);
}

.eloward-grandmaster .eloward-badge-img {
transform: scale(1.05);
}

.eloward-challenger .eloward-badge-img {
transform: scale(1.15);
}

// Theme adjustments. The whole selector goes inside :global(), otherwise only the theme class
// survives compilation and the filter lands on the entire Twitch page.
/* stylelint-disable-next-line selector-class-pattern */
:global(.tw-root--theme-dark .seventv-chat-badge.eloward-rank-badge) {
filter: brightness(0.95);
}

/* stylelint-disable-next-line selector-class-pattern */
:global(.tw-root--theme-light .seventv-chat-badge.eloward-rank-badge) {
filter: brightness(1.05) contrast(1.1);
}
</style>
Loading
Loading