typing-avatars: add AvatarStack component and typing-indicator patch

This commit is contained in:
Ally Founder 2026-08-10 14:10:03 -04:00
parent 99ce783b96
commit 7b182ff950
3 changed files with 90 additions and 1 deletions

View file

@ -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 (
<View style={st.row}>
{typingUserIds.map((userId, i) => {
const uri = getTypingAvatarURL(guildId, userId, SIZE);
if (!uri) return null;
return (
<Image
key={userId}
source={{ uri }}
style={[st.avatar, i > 0 ? st.overlap : null]}
/>
);
})}
</View>
);
}
const st = StyleSheet.create({
row: { flexDirection: "row", flexWrap: "wrap", alignItems: "center" },
avatar: { width: SIZE, height: SIZE, borderRadius: SIZE / 2 },
overlap: { marginLeft: -OVERLAP },
});

View file

@ -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()
};

View file

@ -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 = (
<AvatarStack typingUserIds={typingUserIds} guildId={props?.channel?.guild_id} />
);
} catch {
// Leave the default "X is typing..." text alone.
}
return ret;
};
registerIntercept(TypingIndicatorInner, PatchedTypingIndicatorInner);
}
);
return () => cleanups.forEach((fn) => fn());
}