diff --git a/plugins/typing-avatars/src/components/AvatarStack.tsx b/plugins/typing-avatars/src/components/AvatarStack.tsx
new file mode 100644
index 0000000..443fe82
--- /dev/null
+++ b/plugins/typing-avatars/src/components/AvatarStack.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { View, Image, StyleSheet } from "react-native";
+import { getTypingAvatarURL } from "../lib/avatarUrl";
+
+const SIZE = 20;
+const OVERLAP = 8;
+
+// No text label and no tap handler by design - purely decorative avatars replacing the
+// "X is typing..." text entirely. If a lot of people are typing, this wraps to a second
+// overlapping row rather than clipping or capping with a "+N" badge.
+export default function AvatarStack({ typingUserIds, guildId }: { typingUserIds: string[]; guildId: string | undefined }) {
+ if (!typingUserIds?.length) return null;
+
+ return (
+
+ {typingUserIds.map((userId, i) => {
+ const uri = getTypingAvatarURL(guildId, userId, SIZE);
+ if (!uri) return null;
+ return (
+ 0 ? st.overlap : null]}
+ />
+ );
+ })}
+
+ );
+}
+
+const st = StyleSheet.create({
+ row: { flexDirection: "row", flexWrap: "wrap", alignItems: "center" },
+ avatar: { width: SIZE, height: SIZE, borderRadius: SIZE / 2 },
+ overlap: { marginLeft: -OVERLAP },
+});
diff --git a/plugins/typing-avatars/src/index.ts b/plugins/typing-avatars/src/index.ts
index f2d33c2..67ed0e0 100644
--- a/plugins/typing-avatars/src/index.ts
+++ b/plugins/typing-avatars/src/index.ts
@@ -1,11 +1,14 @@
import { logger } from "@vendetta";
import { applyPatches } from "@shared/lib/patcher";
+import patchTypingIndicator from "./patches/typingIndicator";
let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
- unpatchAll = applyPatches("TypingAvatars", logger, {});
+ unpatchAll = applyPatches("TypingAvatars", logger, {
+ "typing indicator avatars": patchTypingIndicator
+ });
},
onUnload: () => unpatchAll()
};
diff --git a/plugins/typing-avatars/src/patches/typingIndicator.tsx b/plugins/typing-avatars/src/patches/typingIndicator.tsx
new file mode 100644
index 0000000..799eb9f
--- /dev/null
+++ b/plugins/typing-avatars/src/patches/typingIndicator.tsx
@@ -0,0 +1,51 @@
+import { React } from "@vendetta/metro/common";
+import { findInReactTree } from "@vendetta/utils";
+import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
+import AvatarStack from "../components/AvatarStack";
+
+// Mirrors role-color-everywhere/src/patches/typingWrapper.ts for the detection/wrapping part
+// (already proven live in this repo), but replaces the label's content with AvatarStack instead
+// of recoloring the existing per-user text nodes. This only needs props.typingUserIds directly,
+// so unlike the color-matching version it isn't affected by the "several people are typing..."
+// collapsed-string case.
+export default function patchTypingIndicator(): () => void {
+ const cleanups: (() => void)[] = [];
+ patchCreateElement(cleanups);
+
+ registerTypeDetector(
+ "typing-avatars-indicator",
+ (type) => typeof type === "function" ? type.name === "TypingIndicatorInner" : type?.type?.name === "TypingIndicatorInner",
+ (TypingIndicatorInner: any) => {
+ const inner = typeof TypingIndicatorInner === "function" ? TypingIndicatorInner : TypingIndicatorInner.type;
+
+ const PatchedTypingIndicatorInner = (props: any) => {
+ const ret = inner(props);
+
+ try {
+ const label = findInReactTree(
+ ret,
+ (n: any) =>
+ Array.isArray(n?.props?.children) &&
+ n.props.children.some((c: any) => typeof c === "string" && c.includes("typing..."))
+ );
+ if (!label?.props) return ret;
+
+ const typingUserIds: string[] = props?.typingUserIds ?? [];
+ if (!typingUserIds.length) return ret;
+
+ label.props.children = (
+
+ );
+ } catch {
+ // Leave the default "X is typing..." text alone.
+ }
+
+ return ret;
+ };
+
+ registerIntercept(TypingIndicatorInner, PatchedTypingIndicatorInner);
+ }
+ );
+
+ return () => cleanups.forEach((fn) => fn());
+}