mirror of
https://github.com/Equicord/Equicord.git
synced 2025-03-04 00:10:05 -05:00
Merge remote-tracking branch 'upstream/dev'
This commit is contained in:
commit
a199cd25f6
11 changed files with 111 additions and 48 deletions
|
@ -36,7 +36,7 @@ export interface ProfileBadge {
|
||||||
image?: string;
|
image?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(): void;
|
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
@ -89,9 +89,7 @@ export function _getBadges(args: BadgeUserArgs) {
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
export interface BadgeUserArgs {
|
||||||
user: User;
|
user: User;
|
||||||
profile: Profile;
|
guildId: string;
|
||||||
premiumSince: Date;
|
|
||||||
premiumGuildSince?: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectedAccount {
|
interface ConnectedAccount {
|
||||||
|
|
|
@ -10,9 +10,11 @@ import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { DevsById, EquicordDevsById } from "@utils/constants";
|
import { DevsById, EquicordDevsById } from "@utils/constants";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||||
|
import { pluralise } from "@utils/misc";
|
||||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
@ -72,6 +74,8 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
}, [user.id, user.username]);
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
|
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
|
@ -84,30 +88,48 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
|
|
||||||
<div className={cl("links")}>
|
<div className={cl("links")}>
|
||||||
{website && (
|
{website && (
|
||||||
<MaskedLink
|
<Tooltip text={website}>
|
||||||
href={"https://" + website}
|
{props => (
|
||||||
>
|
<MaskedLink {...props} href={"https://" + website}>
|
||||||
<WebsiteIcon />
|
<WebsiteIcon />
|
||||||
</MaskedLink>
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{githubName && (
|
{githubName && (
|
||||||
<MaskedLink href={`https://github.com/${githubName}`}>
|
<Tooltip text={githubName}>
|
||||||
<GithubIcon />
|
{props => (
|
||||||
</MaskedLink>
|
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
||||||
|
<GithubIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cl("plugins")}>
|
{plugins.length ? (
|
||||||
{plugins.map(p =>
|
<Forms.FormText>
|
||||||
<PluginCard
|
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||||
key={p.name}
|
</Forms.FormText>
|
||||||
plugin={p}
|
) : (
|
||||||
disabled={p.required ?? false}
|
<Forms.FormText>
|
||||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||||
/>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
{!!plugins.length && (
|
||||||
|
<div className={cl("plugins")}>
|
||||||
|
{plugins.map(p =>
|
||||||
|
<PluginCard
|
||||||
|
key={p.name}
|
||||||
|
plugin={p}
|
||||||
|
disabled={p.required ?? false}
|
||||||
|
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,13 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 16px;
|
width: 32px;
|
||||||
background: var(--background-tertiary);
|
background: var(--background-tertiary);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
left: -16px;
|
left: -32px;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
border-top-left-radius: 9999px;
|
||||||
|
border-bottom-left-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-author-modal-avatar {
|
.vc-author-modal-avatar {
|
||||||
|
@ -55,4 +57,5 @@
|
||||||
.vc-author-modal-plugins {
|
.vc-author-modal-plugins {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,14 +35,13 @@ const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
description: "Vencord Contributor",
|
||||||
image: CONTRIBUTOR_BADGE,
|
image: CONTRIBUTOR_BADGE,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
props: {
|
|
||||||
style: {
|
|
||||||
borderRadius: "50%",
|
|
||||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldShow: ({ user }) => isPluginDev(user.id),
|
shouldShow: ({ user }) => isPluginDev(user.id),
|
||||||
link: "https://github.com/Vendicated/Vencord"
|
onClick(_, { user }) {
|
||||||
|
// circular import shenanigans
|
||||||
|
const { openContributorModal } = require("@components/PluginSettings/ContributorModal") as typeof import("@components/PluginSettings/ContributorModal");
|
||||||
|
// setImmediate is needed to run on later tick to workaround limitation in proxyLazy
|
||||||
|
setImmediate(() => openContributorModal(user));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const EquicordContributorBadge: ProfileBadge = {
|
const EquicordContributorBadge: ProfileBadge = {
|
||||||
|
@ -106,7 +105,7 @@ export default definePlugin({
|
||||||
// conditionally override their onClick with badge.onClick if it exists
|
// conditionally override their onClick with badge.onClick if it exists
|
||||||
{
|
{
|
||||||
match: /href:(\i)\.link/,
|
match: /href:(\i)\.link/,
|
||||||
replace: "...($1.onClick && { onClick: $1.onClick }),$&"
|
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,31 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
disableAnalytics: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable Discord's tracking (analytics/'science')",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoTrack",
|
name: "NoTrack",
|
||||||
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
description: "Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting",
|
||||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "AnalyticsActionHandlers.handle",
|
find: "AnalyticsActionHandlers.handle",
|
||||||
|
predicate: () => settings.store.disableAnalytics,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /^.+$/,
|
match: /^.+$/,
|
||||||
replace: "()=>{}",
|
replace: "()=>{}",
|
||||||
|
@ -44,11 +58,11 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /this\._intervalId=/,
|
match: /this\._intervalId=/,
|
||||||
replace: "this._intervalId=undefined&&"
|
replace: "this._intervalId=void 0&&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(increment\(\i\){)/,
|
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
|
||||||
replace: "$1return;"
|
replace: "$&return;"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,9 +9,8 @@ import { Devs } from "@utils/constants";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { React, RelationshipStore } from "@webpack/common";
|
import { Heading, React, RelationshipStore, Text } from "@webpack/common";
|
||||||
|
|
||||||
const { Heading, Text } = findByPropsLazy("Heading", "Text");
|
|
||||||
const container = findByPropsLazy("memberSinceWrapper");
|
const container = findByPropsLazy("memberSinceWrapper");
|
||||||
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
|
||||||
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
|
||||||
|
|
|
@ -91,7 +91,7 @@ const settings = definePluginSettings({
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ResurrectHome",
|
name: "ResurrectHome",
|
||||||
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
|
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking a server.",
|
||||||
authors: [Devs.Dolfies, Devs.Nuckyz],
|
authors: [Devs.Dolfies, Devs.Nuckyz],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ export default definePlugin({
|
||||||
find: "487e85_1",
|
find: "487e85_1",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
|
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
|
||||||
replace: "badge:$self.ViewServerHomeButton({serverGuide:$1}),"
|
replace: "trailing:$self.ViewServerHomeButton({serverGuide:$1}),"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Disable view Server Home override when the Server Home is unmouted
|
// Disable view Server Home override when the Server Home is unmouted
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default definePlugin({
|
||||||
find: "renderPrioritySpeaker",
|
find: "renderPrioritySpeaker",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /renderName\(\).{0,100}speaking:.+?\.clanTag.+?"div",{/,
|
match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
|
||||||
replace: "$&...$self.getVoiceProps(this.props),"
|
replace: "$&...$self.getVoiceProps(this.props),"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -115,3 +115,7 @@ export const isMobile = navigator.userAgent.includes("Mobi");
|
||||||
|
|
||||||
export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
|
export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
|
||||||
export const isEquicordPluginDev = (id: string) => Object.hasOwn(EquicordDevsById, id);
|
export const isEquicordPluginDev = (id: string) => Object.hasOwn(EquicordDevsById, id);
|
||||||
|
|
||||||
|
export function pluralise(amount: number, singular: string, plural = singular + "s") {
|
||||||
|
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ export let Tooltip: t.Tooltip;
|
||||||
export let TextInput: t.TextInput;
|
export let TextInput: t.TextInput;
|
||||||
export let TextArea: t.TextArea;
|
export let TextArea: t.TextArea;
|
||||||
export let Text: t.Text;
|
export let Text: t.Text;
|
||||||
|
export let Heading: t.HeadingTag;
|
||||||
export let Select: t.Select;
|
export let Select: t.Select;
|
||||||
export let SearchableSelect: t.SearchableSelect;
|
export let SearchableSelect: t.SearchableSelect;
|
||||||
export let Slider: t.Slider;
|
export let Slider: t.Slider;
|
||||||
|
@ -58,6 +59,28 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
|
||||||
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
|
||||||
|
|
||||||
waitFor(["FormItem", "Button"], m => {
|
waitFor(["FormItem", "Button"], m => {
|
||||||
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m);
|
({
|
||||||
|
useToken,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
FormSwitch: Switch,
|
||||||
|
Tooltip,
|
||||||
|
TextInput,
|
||||||
|
TextArea,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
SearchableSelect,
|
||||||
|
Slider,
|
||||||
|
ButtonLooks,
|
||||||
|
TabBar,
|
||||||
|
Popout,
|
||||||
|
Dialog,
|
||||||
|
Paginator,
|
||||||
|
ScrollerThin,
|
||||||
|
Clickable,
|
||||||
|
Avatar,
|
||||||
|
FocusLock,
|
||||||
|
Heading
|
||||||
|
} = m);
|
||||||
Forms = m;
|
Forms = m;
|
||||||
});
|
});
|
||||||
|
|
9
src/webpack/common/types/components.d.ts
vendored
9
src/webpack/common/types/components.d.ts
vendored
|
@ -21,23 +21,24 @@ import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttribute
|
||||||
|
|
||||||
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
|
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
|
||||||
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
|
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
|
||||||
export type Heading = `h${1 | 2 | 3 | 4 | 5 | 6}`;
|
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
|
||||||
|
|
||||||
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
|
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
|
||||||
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
|
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
|
||||||
|
|
||||||
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
|
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
|
||||||
variant?: TextVariant;
|
variant?: TextVariant;
|
||||||
tag?: "div" | "span" | "p" | "strong" | Heading;
|
tag?: "div" | "span" | "p" | "strong" | HeadingTag;
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
lineClamp?: number;
|
lineClamp?: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Text = ComponentType<TextProps>;
|
export type Text = ComponentType<TextProps>;
|
||||||
|
export type Heading = ComponentType<TextProps>;
|
||||||
|
|
||||||
export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{
|
export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{
|
||||||
/** default is h5 */
|
/** default is h5 */
|
||||||
tag?: Heading;
|
tag?: HeadingTag;
|
||||||
faded?: boolean;
|
faded?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
@ -46,7 +47,7 @@ export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChi
|
||||||
|
|
||||||
export type FormSection = ComponentType<PropsWithChildren<{
|
export type FormSection = ComponentType<PropsWithChildren<{
|
||||||
/** default is h5 */
|
/** default is h5 */
|
||||||
tag?: Heading;
|
tag?: HeadingTag;
|
||||||
className?: string;
|
className?: string;
|
||||||
titleClassName?: string;
|
titleClassName?: string;
|
||||||
titleId?: string;
|
titleId?: string;
|
||||||
|
|
Loading…
Add table
Reference in a new issue