YouBar+: merge BetterInbox's inbox feature in as a toggle, credit shin, fix reaction-ownership bug
This commit is contained in:
parent
31157788c5
commit
ea1b986090
7 changed files with 457 additions and 3 deletions
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"name": "YouBar+",
|
||||
"description": "Adds Settings and DM buttons to the YouBar. (May require a restart after enabling.)",
|
||||
"description": "Adds Settings, DM, and categorized Inbox buttons to the YouBar. (May require a restart after enabling.)",
|
||||
"authors": [
|
||||
{ "name": "Rosie" },
|
||||
{ "name": "Purple_Ξye™" }
|
||||
{ "name": "Purple_Ξye™" },
|
||||
{ "name": "shin" }
|
||||
],
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import patchYouBarButtons from "./patches/youBarButtons";
|
||||
import { startTrackingNotifications } from "./lib/notifications";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let unpatchButtons: () => void = () => {};
|
||||
let stopTrackingNotifications: () => void = () => {};
|
||||
let patched = false;
|
||||
let retryHandle: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
|
|
@ -34,6 +36,14 @@ export default {
|
|||
onLoad: () => {
|
||||
storage.showDMButton ??= false;
|
||||
storage.showSettingsButton ??= true;
|
||||
storage.showInboxButton ??= false;
|
||||
storage.notifications ??= [];
|
||||
|
||||
// Same "may require a restart" contract as the other buttons - only tracks in the
|
||||
// background when the feature is actually on, not for every YouBar+ user by default.
|
||||
if (storage.showInboxButton) {
|
||||
stopTrackingNotifications = startTrackingNotifications();
|
||||
}
|
||||
|
||||
attempt();
|
||||
if (!patched) {
|
||||
|
|
@ -47,6 +57,7 @@ export default {
|
|||
onUnload: () => {
|
||||
stopRetrying();
|
||||
unpatchButtons();
|
||||
stopTrackingNotifications();
|
||||
},
|
||||
settings: Settings
|
||||
};
|
||||
|
|
|
|||
178
plugins/you-bar-plus/src/lib/notifications.ts
Normal file
178
plugins/you-bar-plus/src/lib/notifications.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { findByStoreName } from "@vendetta/metro";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { fluxSubscribe } from "@shared/lib/flux";
|
||||
import type { MentionSubCategory, NotificationItem } from "./types";
|
||||
|
||||
// Ported from fshinz/Revenge-Plugins' BetterInbox (credit: shin) - merged in here so only one
|
||||
// plugin ever patches YouBarNotificationsButton, since two independent instead() patches on the
|
||||
// same component is what caused the hook-order crash this merge exists to fix.
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
const ChannelStore = findByStoreName("ChannelStore");
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const MessageStore = findByStoreName("MessageStore");
|
||||
const GuildMemberStore = findByStoreName("GuildMemberStore");
|
||||
|
||||
let memoryNotifications: NotificationItem[] = [];
|
||||
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function syncStorageDebounced() {
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(() => {
|
||||
storage.notifications = memoryNotifications.slice(0, 100);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function pushNotification(item: NotificationItem) {
|
||||
if (memoryNotifications.some((n) => n.id === item.id)) return;
|
||||
memoryNotifications = [item, ...memoryNotifications];
|
||||
syncStorageDebounced();
|
||||
}
|
||||
|
||||
export function getNotifications(): NotificationItem[] {
|
||||
return memoryNotifications;
|
||||
}
|
||||
|
||||
function processMentionMessage(channelId: string, messageId: string, rawMsg?: any) {
|
||||
try {
|
||||
const currentUser = UserStore?.getCurrentUser?.();
|
||||
if (!currentUser) return;
|
||||
|
||||
const msg = MessageStore?.getMessage?.(channelId, messageId) || rawMsg;
|
||||
const channel = ChannelStore?.getChannel?.(channelId);
|
||||
if (!msg || !channel) return;
|
||||
|
||||
const author = msg.author || UserStore?.getUser?.(msg.author?.id);
|
||||
if (!author || author.id === currentUser.id) return;
|
||||
|
||||
const guild = channel.guild_id ? GuildStore?.getGuild?.(channel.guild_id) : undefined;
|
||||
const guildName = guild?.name || (channel.isGroupDM?.() ? "Group DM" : "Direct Message");
|
||||
const channelName = channel.name ? `#${channel.name}` : "DM";
|
||||
const timestamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
|
||||
const isReply = msg.type === 19 || msg.referenced_message?.author?.id === currentUser.id;
|
||||
|
||||
const category: "mentions" | "replies" = isReply ? "replies" : "mentions";
|
||||
let subCategory: MentionSubCategory = "people";
|
||||
|
||||
if (author.bot) {
|
||||
subCategory = "bot";
|
||||
} else if (msg.mention_roles?.length > 0 || msg.mentionRoles?.length > 0) {
|
||||
subCategory = "role";
|
||||
}
|
||||
|
||||
pushNotification({
|
||||
id: msg.id || `${Date.now()}`,
|
||||
category,
|
||||
subCategory,
|
||||
title: isReply
|
||||
? `${author.globalName || author.username || "Someone"} replied to you`
|
||||
: subCategory === "role"
|
||||
? `${author.globalName || author.username || "Someone"} mentioned a role you have`
|
||||
: `${author.globalName || author.username || "Someone"} mentioned you`,
|
||||
content: msg.content || "",
|
||||
guildName,
|
||||
channelName,
|
||||
guildId: guild?.id,
|
||||
channelId,
|
||||
messageId: msg.id,
|
||||
timestamp,
|
||||
author,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[YouBar+] Inbox mention process error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleIncomingMessage(payload: any) {
|
||||
try {
|
||||
const currentUser = UserStore?.getCurrentUser?.();
|
||||
if (!currentUser) return;
|
||||
|
||||
const msg = payload?.message || payload;
|
||||
if (!msg || !msg.channel_id) return;
|
||||
if (msg.author?.id === currentUser.id) return;
|
||||
|
||||
const isDirectMention = msg.mentions?.some((u: any) => u.id === currentUser.id);
|
||||
const isReplyToMe = msg.referenced_message?.author?.id === currentUser.id;
|
||||
|
||||
let isRoleMention = false;
|
||||
const msgRoles = msg.mention_roles || msg.mentionRoles || [];
|
||||
if (msgRoles.length > 0 && msg.guild_id) {
|
||||
const myMember = GuildMemberStore?.getMember?.(msg.guild_id, currentUser.id);
|
||||
const myRoles: string[] = myMember?.roles || [];
|
||||
isRoleMention = msgRoles.some((roleId: string) => myRoles.includes(roleId));
|
||||
}
|
||||
|
||||
if (!isDirectMention && !isReplyToMe && !isRoleMention) return;
|
||||
|
||||
processMentionMessage(msg.channel_id, msg.id, msg);
|
||||
} catch (err) {
|
||||
console.error("[YouBar+] Inbox incoming message error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReactionAdd(payload: any) {
|
||||
try {
|
||||
const currentUser = UserStore?.getCurrentUser?.();
|
||||
if (!currentUser) return;
|
||||
|
||||
const channelId = payload.channel_id || payload.channelId;
|
||||
const targetMessageId = payload.message_id || payload.messageId;
|
||||
const reactorId = payload.user_id || payload.userId;
|
||||
if (reactorId === currentUser.id) return;
|
||||
|
||||
// Must positively confirm the reacted-to message is one of ours - if it isn't cached
|
||||
// locally (common; reaction events fire for any message you're viewing, not just your
|
||||
// own), this used to fall through and record the reaction anyway instead of skipping it.
|
||||
const targetMessage = MessageStore?.getMessage?.(channelId, targetMessageId);
|
||||
if (!targetMessage || targetMessage.author?.id !== currentUser.id) return;
|
||||
|
||||
const channel = ChannelStore?.getChannel?.(channelId);
|
||||
const guild = channel?.guild_id ? GuildStore?.getGuild?.(channel.guild_id) : undefined;
|
||||
const guildName = guild?.name || "Direct Message";
|
||||
const channelName = channel?.name ? `#${channel.name}` : "DM";
|
||||
const timestamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
|
||||
const reactorUser = payload.member?.user || payload.user || UserStore?.getUser?.(reactorId);
|
||||
const finalAuthor = reactorUser || {
|
||||
id: reactorId,
|
||||
username: payload.member?.nick || "Someone",
|
||||
globalName: payload.member?.nick || "Someone",
|
||||
avatar: null,
|
||||
};
|
||||
|
||||
const reactorName = finalAuthor.globalName || finalAuthor.username || "Someone";
|
||||
const emojiName = payload.emoji?.name || "an emoji";
|
||||
|
||||
pushNotification({
|
||||
id: `react-${targetMessageId}-${reactorId}`,
|
||||
category: "reactions",
|
||||
title: `${reactorName} reacted ${emojiName}`,
|
||||
content: targetMessage?.content ? `"${targetMessage.content}"` : `Reacted to your message in ${channelName}`,
|
||||
guildName,
|
||||
channelName,
|
||||
guildId: guild?.id,
|
||||
channelId,
|
||||
messageId: targetMessageId,
|
||||
timestamp,
|
||||
author: finalAuthor,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[YouBar+] Inbox reaction error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export function startTrackingNotifications(): () => void {
|
||||
memoryNotifications = Array.isArray(storage.notifications) ? [...storage.notifications] : [];
|
||||
|
||||
const unsubMessage = fluxSubscribe("MESSAGE_CREATE", handleIncomingMessage);
|
||||
const unsubReaction = fluxSubscribe("MESSAGE_REACTION_ADD", handleReactionAdd);
|
||||
|
||||
return () => {
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
storage.notifications = memoryNotifications.slice(0, 100);
|
||||
unsubMessage();
|
||||
unsubReaction();
|
||||
};
|
||||
}
|
||||
25
plugins/you-bar-plus/src/lib/types.ts
Normal file
25
plugins/you-bar-plus/src/lib/types.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export type NotificationCategory = "mentions" | "replies" | "reactions" | "other";
|
||||
export type MentionSubCategory = "people" | "bot" | "role";
|
||||
|
||||
export interface NotificationAuthor {
|
||||
id: string;
|
||||
username: string;
|
||||
globalName?: string;
|
||||
avatar?: string | null;
|
||||
bot?: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationItem {
|
||||
id: string;
|
||||
category: NotificationCategory;
|
||||
subCategory?: MentionSubCategory;
|
||||
title: string;
|
||||
content: string;
|
||||
guildName: string;
|
||||
channelName: string;
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
messageId?: string;
|
||||
timestamp: string;
|
||||
author?: NotificationAuthor;
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { findByProps, findByTypeName } from "@vendetta/metro";
|
||||
import { findByProps, findByTypeName, findByName } from "@vendetta/metro";
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { instead } from "@vendetta/patcher";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import NotificationCenter from "../ui/NotificationCenter";
|
||||
|
||||
// Retry logic lives one level up in index.ts, calling this repeatedly until it succeeds - keep
|
||||
// this function itself simple.
|
||||
|
|
@ -14,11 +15,38 @@ export default function patchYouBarButtons(): (() => void) | null {
|
|||
const userSettingsAction = findByProps("openUserSettings");
|
||||
const transitionModule = findByProps("transitionToGuild");
|
||||
|
||||
const Navigation = findByProps("push", "pushLazy", "pop");
|
||||
const Navigator = findByName("Navigator") ?? findByProps("Navigator")?.Navigator;
|
||||
const modalCloseButton =
|
||||
findByProps("getRenderCloseButton")?.getRenderCloseButton ??
|
||||
findByProps("getHeaderCloseButton")?.getHeaderCloseButton;
|
||||
|
||||
const SettingsIcon = getAssetIDByName("SettingsIcon");
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
const BellIcon = getAssetIDByName("BellIcon") ?? getAssetIDByName("NotificationBellIcon");
|
||||
|
||||
if (!YouBarNotificationsButton) return null;
|
||||
|
||||
const openInbox = () => {
|
||||
if (!Navigator || !Navigation?.push) {
|
||||
console.error("[YouBar+] Inbox: Navigator not available");
|
||||
return;
|
||||
}
|
||||
Navigation.push(() => (
|
||||
<Navigator
|
||||
initialRouteName="YouBarInbox"
|
||||
goBackOnBackPress
|
||||
screens={{
|
||||
YouBarInbox: {
|
||||
title: "Inbox",
|
||||
headerLeft: modalCloseButton?.(() => Navigation.pop()),
|
||||
render: () => <NotificationCenter />,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
|
||||
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
||||
|
||||
|
|
@ -44,6 +72,15 @@ export default function patchYouBarButtons(): (() => void) | null {
|
|||
/>
|
||||
)}
|
||||
|
||||
{storage.showInboxButton && BellIcon && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={BellIcon}
|
||||
onPress={openInbox}
|
||||
/>
|
||||
)}
|
||||
|
||||
{storage.showSettingsButton && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
|
|
|
|||
191
plugins/you-bar-plus/src/ui/NotificationCenter.tsx
Normal file
191
plugins/you-bar-plus/src/ui/NotificationCenter.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { React, ReactNative, NavigationNative } from "@vendetta/metro/common";
|
||||
import { findByProps, findByDisplayName } from "@vendetta/metro";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { TableRow, TableRowGroup } from "@shared/ui/table";
|
||||
import { getNotifications } from "../lib/notifications";
|
||||
import type { MentionSubCategory, NotificationCategory, NotificationItem } from "../lib/types";
|
||||
|
||||
// Ported from fshinz/Revenge-Plugins' BetterInbox (credit: shin), merged in here as an optional
|
||||
// YouBar+ feature - see lib/notifications.ts for why.
|
||||
|
||||
const { View, Text, TouchableOpacity, ScrollView, Image, StyleSheet } = ReactNative;
|
||||
const { useState, useMemo, useCallback, memo } = React;
|
||||
|
||||
const Router = findByProps("transitionToGuild", "transitionTo");
|
||||
const NativeTabs = findByDisplayName("Tabs");
|
||||
const useTabsState = findByProps("useTabsState")?.useTabsState;
|
||||
const NativeSegmentedControl = findByDisplayName("SegmentedControl");
|
||||
|
||||
function getAvatarUrl(author: any): string {
|
||||
if (!author) return "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
const { id, avatar, discriminator } = author;
|
||||
|
||||
if (avatar) {
|
||||
const ext = typeof avatar === "string" && avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${id}/${avatar}.${ext}?size=128`;
|
||||
}
|
||||
|
||||
try {
|
||||
const defaultIndex = discriminator && discriminator !== "0"
|
||||
? parseInt(discriminator, 10) % 5
|
||||
: Number((BigInt(id || "0") >> 22n) % 6n);
|
||||
return `https://cdn.discordapp.com/embed/avatars/${defaultIndex}.png`;
|
||||
} catch {
|
||||
return "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationRow = memo(({ item, onPress }: { item: NotificationItem; onPress: () => void }) => {
|
||||
const subLabelText = `${item.guildName} · ${item.channelName}\n${item.content || ""}`.trim();
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
label={item.title}
|
||||
subLabel={subLabelText}
|
||||
trailing={<Text style={styles.timestampText}>{item.timestamp}</Text>}
|
||||
icon={<Image source={{ uri: getAvatarUrl(item.author) }} style={styles.avatarImage} />}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default function NotificationCenter(): JSX.Element {
|
||||
const [activeTabIdx, setActiveTabIdx] = useState(0);
|
||||
const [mentionFilterIdx, setMentionFilterIdx] = useState(0);
|
||||
|
||||
const categories: NotificationCategory[] = ["mentions", "replies", "reactions", "other"];
|
||||
const subFilters: Array<"all" | MentionSubCategory> = ["all", "people", "role", "bot"];
|
||||
|
||||
const currentCategory = categories[activeTabIdx] ?? "mentions";
|
||||
const currentMentionFilter = subFilters[mentionFilterIdx] ?? "all";
|
||||
|
||||
const tabsState = useTabsState
|
||||
? useTabsState({
|
||||
items: categories.map((cat) => ({ id: cat, label: cat.charAt(0).toUpperCase() + cat.slice(1) })),
|
||||
initialIndex: 0,
|
||||
})
|
||||
: null;
|
||||
|
||||
const notifications = getNotifications();
|
||||
|
||||
const displayedNotifications = useMemo(() => {
|
||||
const filtered = notifications.filter((n) => {
|
||||
if (currentCategory === "mentions") {
|
||||
if (n.category !== "mentions") return false;
|
||||
if (currentMentionFilter === "all") return true;
|
||||
return n.subCategory === currentMentionFilter;
|
||||
}
|
||||
return n.category === currentCategory;
|
||||
});
|
||||
return filtered.slice(0, 30);
|
||||
}, [notifications, currentCategory, currentMentionFilter]);
|
||||
|
||||
const jumpToMessage = useCallback((guildId?: string, channelId?: string, messageId?: string) => {
|
||||
if (!channelId || !messageId) return;
|
||||
try {
|
||||
if (Router?.transitionToGuild) {
|
||||
Router.transitionToGuild(guildId || "@me", channelId, messageId);
|
||||
} else if (NavigationNative?.navigate) {
|
||||
NavigationNative.navigate("Channel", { guildId, channelId, messageId });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[YouBar+] Inbox navigation error:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{NativeTabs && tabsState ? (
|
||||
<NativeTabs
|
||||
state={{
|
||||
...tabsState,
|
||||
activeIndex: activeTabIdx,
|
||||
setActiveIndex: (idx: number) => {
|
||||
tabsState.setActiveIndex?.(idx);
|
||||
setActiveTabIdx(idx);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.tabBar}>
|
||||
{categories.map((tab, idx) => (
|
||||
<TouchableOpacity
|
||||
key={tab}
|
||||
style={[styles.tabButton, activeTabIdx === idx && styles.activeTabButton]}
|
||||
onPress={() => setActiveTabIdx(idx)}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTabIdx === idx && styles.activeTabText]}>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{currentCategory === "mentions" && (
|
||||
<View style={styles.subFilterWrapper}>
|
||||
{NativeSegmentedControl ? (
|
||||
<NativeSegmentedControl
|
||||
value={currentMentionFilter}
|
||||
options={subFilters.map((sub) => ({ value: sub, label: sub.toUpperCase() }))}
|
||||
onChange={(val: string) => {
|
||||
const idx = subFilters.indexOf(val as any);
|
||||
if (idx !== -1) setMentionFilterIdx(idx);
|
||||
}}
|
||||
onValueChange={(val: string) => {
|
||||
const idx = subFilters.indexOf(val as any);
|
||||
if (idx !== -1) setMentionFilterIdx(idx);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.subFilterBar}>
|
||||
{subFilters.map((sub, idx) => (
|
||||
<TouchableOpacity
|
||||
key={sub}
|
||||
style={[styles.subFilterButton, mentionFilterIdx === idx && styles.activeSubFilter]}
|
||||
onPress={() => setMentionFilterIdx(idx)}
|
||||
>
|
||||
<Text style={styles.subFilterText}>{sub.toUpperCase()}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView style={styles.feed} removeClippedSubviews>
|
||||
{displayedNotifications.length === 0 ? (
|
||||
<Text style={styles.emptyText}>No notifications found for this category.</Text>
|
||||
) : (
|
||||
<TableRowGroup title={`RECENT ${currentCategory.toUpperCase()}`}>
|
||||
{displayedNotifications.map((item) => (
|
||||
<NotificationRow
|
||||
key={item.id || `${item.channelId}-${item.messageId}`}
|
||||
item={item}
|
||||
onPress={() => jumpToMessage(item.guildId, item.channelId, item.messageId)}
|
||||
/>
|
||||
))}
|
||||
</TableRowGroup>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
subFilterWrapper: { paddingHorizontal: 12, paddingVertical: 8 },
|
||||
tabBar: { flexDirection: "row", paddingVertical: 4 },
|
||||
tabButton: { flex: 1, paddingVertical: 10, alignItems: "center" },
|
||||
activeTabButton: { borderBottomWidth: 2, borderBottomColor: "#5865F2" },
|
||||
tabText: { color: "#949ba4", fontWeight: "600", fontSize: 13 },
|
||||
activeTabText: { color: "#ffffff" },
|
||||
subFilterBar: { flexDirection: "row", justifyContent: "center" },
|
||||
subFilterButton: { marginHorizontal: 6, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
|
||||
activeSubFilter: { backgroundColor: "#404249" },
|
||||
subFilterText: { color: "#dbdee1", fontSize: 11, fontWeight: "bold" },
|
||||
feed: { flex: 1, paddingHorizontal: 8, paddingVertical: 12 },
|
||||
emptyText: { color: "#949ba4", textAlign: "center", marginTop: 40, fontSize: 14 },
|
||||
avatarImage: { width: 36, height: 36, borderRadius: 18, backgroundColor: "#4e5058" },
|
||||
timestampText: { color: "#949ba4", fontSize: 11, alignSelf: "center" },
|
||||
});
|
||||
|
|
@ -19,6 +19,8 @@ export default function Settings() {
|
|||
Fork of Purple-EyeZ's YouBar+, ported to Revenge's Vendetta-compat API. Like the
|
||||
original, this patches the YouBar the moment it loads - if you enable it while
|
||||
Discord's already running, you may need to restart once for the buttons to appear.
|
||||
Inbox categorization is merged in from shin's BetterInbox (fshinz/Revenge-Plugins),
|
||||
with permission, so both features share one patch instead of two conflicting ones.
|
||||
</NoteBox>
|
||||
<TableRowGroup title="Buttons">
|
||||
<TableSwitchRow
|
||||
|
|
@ -30,6 +32,15 @@ export default function Settings() {
|
|||
updateYouBar();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Inbox button"
|
||||
subLabel="Show a categorized mentions/replies/reactions inbox in the YouBar - requires a restart to take effect"
|
||||
value={!!storage.showInboxButton}
|
||||
onValueChange={(v: boolean) => {
|
||||
storage.showInboxButton = v;
|
||||
updateYouBar();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Settings button"
|
||||
subLabel="Show the Settings button in the YouBar"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue