Skip to content

Commit

Permalink
Merge branch 'dev' into feature/shared-chat
Browse files Browse the repository at this point in the history
  • Loading branch information
Excellify authored Sep 30, 2024
2 parents 48d7bb7 + 62e3a1f commit 076007a
Show file tree
Hide file tree
Showing 35 changed files with 1,407 additions and 733 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
### 3.1.1 3000
### 3.1.2.1000

- Implement support for the new Kick site
- Added option to settings to hide Stories from the sidebar
- Fixed settings to hide recommended channels and viewers also watch channels
- Fixed an issue where history navigation is accidentally triggered during IME composition

### 3.1.1.3000

- Added support for animated FFZ emotes
- Added option to hide whispers
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Improve your viewing experience on Twitch & YouTube with new features, emotes, vanity and performance.",
"private": true,
"version": "3.1.1",
"dev_version": "3.0",
"dev_version": "4.0",
"scripts": {
"start": "NODE_ENV=dev yarn build:dev && NODE_ENV=dev vite --mode dev",
"build:section:app": "vite build --config vite.config.mts",
Expand Down
1 change: 1 addition & 0 deletions src/app/emote-menu/EmoteMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ onKeyStroke("e", (ev) => {
// Up/Down Arrow iterates providers
useEventListener("keydown", (ev) => {
if (ev.isComposing) return;
if (!["ArrowUp", "ArrowDown"].includes(ev.key)) return;
const cur = Object.keys(visibleProviders).indexOf(activeProvider.value ?? "7TV");
Expand Down
2 changes: 1 addition & 1 deletion src/common/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function imageHostToSrcset(
multiplier /= targetSize;

if (srcset) srcset += ", ";
srcset += `https:${host.url}/${size.name} ${multiplier}x`;
srcset += `${host.url}/${size.name} ${multiplier}x`;
}

if (targetSize === 1) known[host.url] = srcset;
Expand Down
26 changes: 26 additions & 0 deletions src/common/Input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function getSearchRange(text: string, position: number): [number, number] {
let start = 0;
let end = 0;

for (let i = position; ; i--) {
if (i < 1 || (text.charAt(i - 1) === " " && i !== position)) {
start = i;
break;
}
}

for (let i = position + 1; ; i++) {
if (i > text.length || text.charAt(i - 1) === " ") {
end = i - 1;
break;
}
}

return [start, end];
}

export interface TabToken {
token: string;
priority: number;
item?: SevenTV.ActiveEmote;
}
76 changes: 76 additions & 0 deletions src/common/ReactHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export function getRootVNode(): ReactExtended.ReactVNode | undefined {
return undefined;
}

export function getReactProps<T>(element: Element): T | undefined {
for (const k in element) {
if (k.startsWith("__reactProps")) {
const props = Reflect.get(element, k);
return props;
}
}
return undefined;
}
/**
* Searches the React VDOM tree for a component, starting at the defined node, searching upwards.
* @param node React VDOM node to start at
Expand Down Expand Up @@ -427,6 +436,73 @@ export function useComponentHook<C extends ReactExtended.WritableComponent>(
return hook;
}

export function getElementFiberStatic<P>(
elem: Element,
offset: number,
): ReactExtended.ReactFunctionalFiber<P> | undefined {
const node = getVNodeFromDOM(elem);
if (!node) return;
let current = node;
for (let i = 0; i < offset; i++) {
if (!current.return) return;
current = current.return;
}
if (!current.elementType) return;

return current as unknown as ReactExtended.ReactFunctionalFiber<P>;
}

export async function useStaticRenderFunctionHook<P extends object>(
selector: string,
offset: number,
func: HookedElementFunction<P>,
) {
let elem = document.querySelector(selector);

if (!elem || !getElementFiberStatic(elem, offset)) {
elem = await new Promise<Element>((resolve) => {
const observer = new MutationObserver((records) => {
for (const record of records) {
if (!record.addedNodes) continue;
record.addedNodes.forEach((node) => {
if (node instanceof Element && node.querySelector(selector)) {
if (getElementFiberStatic(node, offset)) {
resolve(node);
observer.disconnect();
return;
}
}
});
}
});
observer.observe(document, { childList: true, subtree: true });
});
}

if (!elem) return;

const fiber = getElementFiberStatic<P>(elem, offset)!;

defineFunctionHook(fiber.elementType, "render", func);
func.call(fiber.elementType, null, fiber.pendingProps);

onUnmounted(() => {
unsetPropertyHook(fiber.elementType, "render");
});
}

type RenderFunction<P extends object> = (props: P, ref?: React.RefObject<Element>) => ReactExtended.ReactRuntimeElement;

type HookedElementFunction<
P extends object,
E extends ReactExtended.ReactFunctionalFiber<P> = ReactExtended.ReactFunctionalFiber<P>,
> = (
this: E["elementType"],
old: RenderFunction<P> | null,
props: P,
ref?: React.RefObject<Element>,
) => ReactExtended.ReactRuntimeElement | null;

interface ComponentCriteria {
parentSelector?: string;
childSelector?: string;
Expand Down
34 changes: 31 additions & 3 deletions src/common/Tokenize.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import type { AnyToken, ChatUser, EmoteToken, LinkToken, VoidToken } from "@/common/chat/ChatMessage";
import type { AnyToken, ChatUser, EmoteToken, LinkToken, TextToken, VoidToken } from "@/common/chat/ChatMessage";
import { convertExternalKickEmote } from "./Transform";
import { parse as tldParse } from "tldts";

const URL_PROTOCOL_REGEXP = /^https?:\/\//i;
const KICK_EMOTE_REGEXP = /^\[emote:(?<id>\d+):(?<name>.+)\]$/;
const backwardModifierBlacklist = new Set(["w!", "h!", "v!", "z!"]);

const delimiter = /( )|(\[emote:\d{1,10}:.{1,30}\])/;

export function tokenize(opt: TokenizeOptions) {
const tokens = [] as AnyToken[];

const textParts = opt.body.split(" ");
const textParts = opt.body.split(delimiter).filter((part) => !!part);

const getEmote = (name: string) => {
if (opt.isKick) {
const match = KICK_EMOTE_REGEXP.exec(name);
if (match && match.groups) {
const { name, id } = match.groups;
return {
name: name,
id: id,
data: convertExternalKickEmote(id, name),
provider: "PLATFORM",
} as SevenTV.ActiveEmote;
}
}
if (opt.localEmoteMap?.[name] && Object.hasOwn(opt.localEmoteMap, name)) {
return opt.localEmoteMap[name];
}
Expand Down Expand Up @@ -79,10 +96,20 @@ export function tokenize(opt: TokenizeOptions) {
url: parsedUrl.toString(),
},
} as LinkToken);
} else if (tokens.at(-1)?.kind === "TEXT") {
const prev = tokens.at(-1) as TextToken;
prev.content += part;
prev.range = [prev.range[0], next - 1];
} else if (part !== " " || !nextEmote || !prevEmote) {
tokens.push({
kind: "TEXT",
range: [cursor + 1, next - 1],
content: part,
} as TextToken);
}

cursor = next;
if (!maybeEmote && !!part) lastEmoteToken = undefined;
if (!maybeEmote && !!part && part !== " ") lastEmoteToken = undefined;
}

tokens.sort((a, b) => a.range[0] - b.range[0]);
Expand Down Expand Up @@ -113,4 +140,5 @@ export interface TokenizeOptions {
filteredWords?: string[];
actorUsername?: string;
showModifiers?: boolean;
isKick?: boolean;
}
85 changes: 85 additions & 0 deletions src/common/Transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,39 @@ export function convertPlatformEmoteSet(data: Twitch.TwitchEmoteSet): SevenTV.Em
};
}

export function convertKickEmoteSet(data: Kick.KickEmoteSet): SevenTV.EmoteSet {
const isGlobalSet = !("user" in data);

return {
id: "PLATFORM#" + data.id,
name: isGlobalSet ? data.name : data.user.username,
immutable: true,
privileged: true,
tags: [],
flags: 0,
provider: "PLATFORM",
scope: isGlobalSet ? "GLOBAL" : "CHANNEL",
owner: !isGlobalSet
? {
id: data.user_id.toString(),
username: data.user.username,
display_name: data.user.username,
avatar_url: data.user.profile_pic,
}
: undefined,
emotes: data.emotes.map((e) => {
const d = convertKickEmote(e, !isGlobalSet ? data.user : undefined);
return {
id: e.id.toString(),
name: e.name,
flags: 0,
provider: "PLATFORM",
data: d,
};
}),
};
}

export function convertTwitchEmote(
data: Partial<Twitch.TwitchEmote>,
owner?: Twitch.TwitchEmoteSet["owner"],
Expand Down Expand Up @@ -80,6 +113,36 @@ export function convertTwitchEmote(
return emote;
}

export function convertKickEmote(data: Kick.KickEmote, owner?: Kick.KickUserEmoteSet["user"]): SevenTV.Emote {
return {
id: data.id?.toString(),
name: data.name,
flags: undefined,
tags: [],
state: [],
lifecycle: 3,
listed: true,
owner: owner
? {
id: owner.id,
username: owner.username,
display_name: owner.username,
avatar_url: owner.profile_pic,
}
: null,
host: {
url: "//files.kick.com/emotes/" + data.id.toString(),
files: [
{
name: "fullsize",
format: "PNG",
},
],
srcset: `https://files.kick.com/emotes/${data.id}/fullsize 2.2x`,
},
};
}

export function convertCheerEmote(data: Twitch.ChatMessage.EmotePart["content"]): SevenTV.Emote {
return {
id: data.emoteID ?? "",
Expand Down Expand Up @@ -347,3 +410,25 @@ export function semanticVersionToNumber(ver: string): number {

return result;
}

export function convertExternalKickEmote(id: string, name: string): SevenTV.Emote {
return {
id: id,
name: name,
tags: [],
state: [],
lifecycle: 3,
listed: true,
owner: null,
host: {
url: "//files.kick.com/emotes/" + id,
files: [
{
name: "fullsize",
format: "PNG",
},
],
srcset: `https://files.kick.com/emotes/${id}/fullsize 2.2x`,
},
};
}
4 changes: 3 additions & 1 deletion src/composable/useCosmetics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { until, useTimeout } from "@vueuse/core";
import { DecimalToStringRGBA } from "@/common/Color";
import { log } from "@/common/Logger";
import { db } from "@/db/idb";
import { useConfig } from "@/composable/useSettings";
import { useLiveQuery } from "./useLiveQuery";
import { useWorker } from "./useWorker";

Expand All @@ -23,6 +24,7 @@ const data = reactive({

staticallyAssigned: {} as Record<string, Record<string, never> | undefined>,
});
const dropShadowRender = useConfig("vanity.paints_drop_shadows");

class CosmeticMap<T extends SevenTV.CosmeticKind> extends Map<string, SevenTV.Cosmetic<T>> {
private providers = new Set<SevenTV.Provider>();
Expand Down Expand Up @@ -384,7 +386,7 @@ export function updatePaintStyle(paint: SevenTV.Cosmetic<"PAINT">, remove = fals

const gradients = paint.data.gradients.map((g) => createGradientFromPaint(g));
const filter = (() => {
if (!paint.data.shadows) {
if (!paint.data.shadows || dropShadowRender.value === false) {
return "";
}

Expand Down
27 changes: 17 additions & 10 deletions src/composable/useLiveQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ export function useLiveQuery<T>(
) {
const value = ref<T>();

if (opt.reactives) {
opt.reactives.forEach((r) => watch(r, async () => handleResult(await queryFn()), { deep: true }));
watch(queryFn, async () => handleResult(await queryFn()));
}

const handleResult = (result: T | undefined) => {
if (!result) return;
if (typeof opt.count === "number" && opt.count-- <= 0) {
Expand All @@ -24,19 +19,31 @@ export function useLiveQuery<T>(
onResult?.(result);
};

const observable = liveQuery(queryFn);
const sub = observable.subscribe({
let observable = liveQuery(queryFn);
let sub = observable.subscribe({
next(x) {
handleResult(x);
},
});

tryOnUnmounted(() => sub.unsubscribe());
const reset = () => {
sub.unsubscribe();
observable = liveQuery(queryFn);
sub = observable.subscribe({
next(x) {
handleResult(x);
},
});
};

if (opt.until) {
opt.until.then(() => sub.unsubscribe());
if (opt.reactives) {
watch(opt.reactives, reset);
}

tryOnUnmounted(sub.unsubscribe);

opt.until?.then(sub.unsubscribe);

return value;
}

Expand Down
Loading

0 comments on commit 076007a

Please sign in to comment.