Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
c79d6e110b
33 changed files with 207 additions and 296 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -20,4 +20,8 @@
|
|||
}
|
||||
],
|
||||
"equicord-companion.showSidebar": true,
|
||||
"css.format.spaceAroundSelectorSeparator": true,
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
}
|
||||
}
|
||||
57
packages/discord-types/src/components.d.ts
vendored
57
packages/discord-types/src/components.d.ts
vendored
|
|
@ -476,19 +476,66 @@ export type MaskedLink = ComponentType<PropsWithChildren<{
|
|||
channelId?: string;
|
||||
}>>;
|
||||
|
||||
export type ScrollerThin = ComponentType<PropsWithChildren<{
|
||||
export interface ScrollerBaseProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
|
||||
dir?: "ltr";
|
||||
orientation?: "horizontal" | "vertical" | "auto";
|
||||
paddingFix?: boolean;
|
||||
fade?: boolean;
|
||||
|
||||
onClose?(): void;
|
||||
onScroll?(): void;
|
||||
}
|
||||
|
||||
export type ScrollerThin = ComponentType<PropsWithChildren<ScrollerBaseProps & {
|
||||
orientation?: "horizontal" | "vertical" | "auto";
|
||||
fade?: boolean;
|
||||
}>>;
|
||||
|
||||
interface BaseListItem {
|
||||
anchorId: any;
|
||||
listIndex: number;
|
||||
offsetTop: number;
|
||||
section: number;
|
||||
}
|
||||
interface ListSection extends BaseListItem {
|
||||
type: "section";
|
||||
}
|
||||
interface ListRow extends BaseListItem {
|
||||
type: "row";
|
||||
row: number;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
export type ListScrollerThin = ComponentType<ScrollerBaseProps & {
|
||||
sections: number[];
|
||||
renderSection?: (item: ListSection) => React.ReactNode;
|
||||
renderRow: (item: ListRow) => React.ReactNode;
|
||||
renderFooter?: (item: any) => React.ReactNode;
|
||||
renderSidebar?: (listVisible: boolean, sidebarVisible: boolean) => React.ReactNode;
|
||||
wrapSection?: (section: number, children: React.ReactNode) => React.ReactNode;
|
||||
|
||||
sectionHeight: number;
|
||||
rowHeight: number;
|
||||
footerHeight?: number;
|
||||
sidebarHeight?: number;
|
||||
|
||||
chunkSize?: number;
|
||||
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
fade?: boolean;
|
||||
onResize?: Function;
|
||||
getAnchorId?: any;
|
||||
|
||||
innerTag?: string;
|
||||
innerId?: string;
|
||||
innerClassName?: string;
|
||||
innerRole?: string;
|
||||
innerAriaLabel?: string;
|
||||
// Yes, Discord uses this casing
|
||||
innerAriaMultiselectable?: boolean;
|
||||
innerAriaOrientation?: "vertical" | "horizontal";
|
||||
}>;
|
||||
|
||||
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
|
||||
tag?: T;
|
||||
}) => ReactNode;
|
||||
|
|
|
|||
|
|
@ -104,9 +104,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||
</div>
|
||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import { Settings } from "@api/Settings";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DispatchWithoutAction } from "react";
|
||||
|
||||
|
|
@ -103,21 +103,9 @@ export function useLogs() {
|
|||
|
||||
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
||||
const [removing, setRemoving] = useState(false);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const div = ref.current!;
|
||||
|
||||
const setHeight = () => {
|
||||
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
|
||||
div.style.height = `${div.clientHeight}px`;
|
||||
};
|
||||
|
||||
setHeight();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cl("wrapper", { removing })} ref={ref}>
|
||||
<div className={cl("wrapper", { removing })}>
|
||||
<NotificationComponent
|
||||
{...data}
|
||||
permanent={true}
|
||||
|
|
@ -129,13 +117,13 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
||||
}}
|
||||
richBody={
|
||||
<div className={cl("body")}>
|
||||
{data.body}
|
||||
<div className={cl("body-wrapper")}>
|
||||
<div className={cl("body")}>{data.body}</div>
|
||||
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,9 +139,14 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={cl("container")}>
|
||||
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
|
||||
</div>
|
||||
<ListScrollerThin
|
||||
className={cl("container")}
|
||||
sections={[log.length]}
|
||||
sectionHeight={0}
|
||||
rowHeight={120}
|
||||
renderSection={() => null}
|
||||
renderRow={item => <NotificationEntry data={log[item.row]} key={log[item.row].id} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -161,15 +154,15 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
|
|||
const [log, pending] = useLogs();
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
||||
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<div style={{ width: "100%" }}>
|
||||
<NotificationLog log={log} pending={pending} />
|
||||
</ModalContent>
|
||||
</div>
|
||||
|
||||
<ModalFooter>
|
||||
<Flex>
|
||||
|
|
|
|||
|
|
@ -4,17 +4,13 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-default);
|
||||
background-color: var(--background-base-lower-alt);
|
||||
background-color: var(--background-base-low);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-notification-root {
|
||||
background-color: var(--background-base-low);
|
||||
}
|
||||
|
||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
|
|
@ -32,6 +28,7 @@
|
|||
|
||||
.vc-notification-content {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vc-notification-header {
|
||||
|
|
@ -81,6 +78,11 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-notification-log-modal {
|
||||
max-width: 962px;
|
||||
width: clamp(var(--modal-width-large, 800px), 962px, 85vw);
|
||||
}
|
||||
|
||||
.vc-notification-log-empty {
|
||||
height: 218px;
|
||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
||||
|
|
@ -88,19 +90,23 @@
|
|||
}
|
||||
|
||||
.vc-notification-log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
overflow: hidden;
|
||||
max-height: min(750px, 75vh);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-notification-log-wrapper {
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
transition: 200ms ease;
|
||||
transition-property: height, opacity;
|
||||
}
|
||||
|
||||
.vc-notification-log-wrapper:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.vc-notification-root {
|
||||
height: 104px;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-notification-log-removing {
|
||||
|
|
@ -109,9 +115,18 @@
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-notification-log-body {
|
||||
.vc-notification-log-body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vc-notification-log-body {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.vc-notification-log-timestamp {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
import { React, TextInput } from "@webpack/common";
|
||||
|
||||
// TODO: Refactor settings to use this as well
|
||||
interface TextInputProps {
|
||||
/**
|
||||
* WARNING: Changing this between renders will have no effect!
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
.vc-addon-card {
|
||||
background-color: var(--background-base-lower-alt);
|
||||
background-color: var(--card-primary-bg);
|
||||
color: var(--interactive-active);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
|
@ -11,26 +12,15 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-addon-card {
|
||||
background-color: var(--card-primary-bg);
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.vc-addon-card-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-addon-card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.visual-refresh .vc-addon-card:hover {
|
||||
/* same as non-hover, here to overwrite the non-refresh hover background */
|
||||
background-color: var(--card-primary-bg);
|
||||
}
|
||||
|
||||
.vc-addon-header {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
.vc-settings-quickActions-pill {
|
||||
all: unset;
|
||||
background: var(--background-base-lower);
|
||||
background: var(--button-secondary-background);
|
||||
color: var(--header-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
|
||||
.vc-settings-quickActions-pill:hover {
|
||||
background: var(--background-base-lower-alt);
|
||||
background: var(--button-secondary-background-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
|
@ -36,14 +36,6 @@
|
|||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-settings-quickActions-pill {
|
||||
background: var(--button-secondary-background);
|
||||
}
|
||||
|
||||
.visual-refresh .vc-settings-quickActions-pill:hover {
|
||||
background: var(--button-secondary-background-hover);
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
.vc-author-modal-name {
|
||||
text-transform: none;
|
||||
flex-grow: 0;
|
||||
background: var(--background-tertiary);
|
||||
background: var(--background-base-lowest);
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
padding: 6px 0.8em 6px 0.5em;
|
||||
font-size: 20px;
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
position: absolute;
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
background: var(--background-tertiary);
|
||||
background: var(--background-base-lowest);
|
||||
z-index: -1;
|
||||
left: -32px;
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--background-tertiary);
|
||||
border: 4px solid var(--background-base-lowest);
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
}
|
||||
|
||||
.vc-settings-theme-links:focus {
|
||||
background-color: var(--background-tertiary);
|
||||
background-color: var(--background-base-lowest);
|
||||
}
|
||||
|
||||
.vc-cloud-settings-sync-grid {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,8 @@ export default definePlugin({
|
|||
{
|
||||
find: '"sticker")',
|
||||
replacement: {
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(.+?(\i)\.push)/,
|
||||
replace: (m, not, children) => not
|
||||
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
|
||||
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
|
||||
match: /return\(\i\.\i\|\|(?=\(.+?(\i)\.push)/,
|
||||
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),false)||"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -60,8 +60,7 @@ export default definePlugin({
|
|||
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||
},
|
||||
{
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
|
||||
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?\(\)=>)\2/,
|
||||
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
}
|
||||
|
||||
/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */
|
||||
.visual-refresh .vc-betterFolders-sidebar-grid {
|
||||
.vc-betterFolders-sidebar-grid {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end];
|
||||
grid-template-areas:
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ function createColorsOverrides(styles: string) {
|
|||
const darkThemeBaseLightness = visualRefreshColorsLightness["--neutral-69-hsl"];
|
||||
|
||||
createOrUpdateStyle(OVERRIDES_STYLE_ID, [
|
||||
`.visual-refresh.theme-light {\n ${generateNewColorVars(visualRefreshColorsLightness, lightThemeBaseLightness)} \n}`,
|
||||
`.visual-refresh.theme-dark {\n ${generateNewColorVars(visualRefreshColorsLightness, darkThemeBaseLightness)} \n}`,
|
||||
`.theme-light {\n ${generateNewColorVars(visualRefreshColorsLightness, lightThemeBaseLightness)} \n}`,
|
||||
`.theme-dark {\n ${generateNewColorVars(visualRefreshColorsLightness, darkThemeBaseLightness)} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ export default definePlugin({
|
|||
{
|
||||
find: ".selectPreviousCommandOption(",
|
||||
replacement: {
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
|
||||
replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
|
||||
match: /(?<=(\i)\.which!==\i\.\i.ENTER\|\|).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=\|\|\(\i\.preventDefault)/,
|
||||
replace: "!$self.shouldSubmit($1,$2)"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -257,11 +257,8 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Disallow the emoji for premium locked if the intention doesn't allow it
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
|
||||
replace: (m, not) => not
|
||||
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
|
||||
match: /!(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
|
||||
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
|
||||
},
|
||||
{
|
||||
// Allow animated emojis to be used if the intention allows it
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(?<=roleName:\i,)(colorString:)/,
|
||||
replace: "$1$self.calculateNameColorForListContext(arguments[0]),originalColor:"
|
||||
match: /(?<=roleName:\i,)colorString:/,
|
||||
replace: "colorString:$self.calculateNameColorForListContext(arguments[0]),originalColor:"
|
||||
},
|
||||
predicate: () => settings.store.memberListColors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
|
@ -90,14 +89,6 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
async start() {
|
||||
// TODO(OptionType.CUSTOM Related): Remove DataStore tags migration once enough time has passed
|
||||
const oldTags = await DataStore.get<Tag[]>(DATA_KEY);
|
||||
if (oldTags != null) {
|
||||
// @ts-expect-error
|
||||
settings.store.tagsList = Object.fromEntries(oldTags.map(oldTag => (delete oldTag.enabled, [oldTag.name, oldTag])));
|
||||
await DataStore.del(DATA_KEY);
|
||||
}
|
||||
|
||||
const tags = getTags();
|
||||
for (const tagName in tags) {
|
||||
createTagCommand(tags[tagName]);
|
||||
|
|
|
|||
|
|
@ -100,9 +100,8 @@ export default definePlugin({
|
|||
replace: "true"
|
||||
},
|
||||
{
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(!)?\(0,\i\.isDesktop\)\(\)/,
|
||||
replace: (_, not) => not ? "false" : "true"
|
||||
match: /\(0,\i\.isDesktop\)\(\)/,
|
||||
replace: "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -46,9 +46,8 @@ export default definePlugin({
|
|||
find: "#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}",
|
||||
replacement: [
|
||||
{
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /{(?:\i:(?:function\(\){return |\(\)=>)\i}?,?){2}}/,
|
||||
replace: m => m.replaceAll(canonicalizeMatch(/(function\(\){return |\(\)=>)\i/g), "$1()=>Promise.resolve(true)")
|
||||
match: /{(?:\i:\(\)=>\i,?){2}}/,
|
||||
replace: m => m.replaceAll(canonicalizeMatch(/\(\)=>\i/g), "()=>Promise.resolve(true)")
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.onboarding
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermission
|
|||
export const cl = classNameFactory("vc-permviewer-");
|
||||
|
||||
export function getSortedRolesForMember({ id: guildId }: Guild, member: GuildMember) {
|
||||
// the guild id is the @everyone role
|
||||
// The guild id is the @everyone role
|
||||
return GuildRoleStore
|
||||
.getSortedRoles(guildId)
|
||||
.filter(role => role.id === guildId || member.roles.includes(role.id));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
|
|
@ -28,8 +26,6 @@ let forceUpdateDms: (() => void) | undefined = undefined;
|
|||
export let currentUserCategories: Category[] = [];
|
||||
|
||||
export async function init() {
|
||||
await migrateData();
|
||||
|
||||
const userId = UserStore.getCurrentUser()?.id;
|
||||
if (userId == null) return;
|
||||
|
||||
|
|
@ -154,28 +150,3 @@ export function moveChannel(channelId: string, direction: -1 | 1) {
|
|||
|
||||
swapElementsInArray(category.channels, a, b);
|
||||
}
|
||||
|
||||
// TODO(OptionType.CUSTOM Related): Remove DataStore PinnedDms migration once enough time has passed
|
||||
async function migrateData() {
|
||||
if (Settings.plugins.PinDMs.dmSectioncollapsed != null) {
|
||||
settings.store.dmSectionCollapsed = Settings.plugins.PinDMs.dmSectioncollapsed;
|
||||
delete Settings.plugins.PinDMs.dmSectioncollapsed;
|
||||
}
|
||||
|
||||
const dataStoreKeys = await DataStore.keys();
|
||||
const pinDmsKeys = dataStoreKeys.map(key => String(key)).filter(key => key.startsWith(CATEGORY_BASE_KEY));
|
||||
|
||||
if (pinDmsKeys.length === 0) return;
|
||||
|
||||
for (const pinDmsKey of pinDmsKeys) {
|
||||
const categories = await DataStore.get<Category[]>(pinDmsKey);
|
||||
if (categories == null) continue;
|
||||
|
||||
const userId = pinDmsKey.replace(CATEGORY_BASE_KEY, "");
|
||||
settings.store.userBasedCategoryList[userId] = categories;
|
||||
|
||||
await DataStore.del(pinDmsKey);
|
||||
}
|
||||
|
||||
await Promise.all([DataStore.del(CATEGORY_MIGRATED_PINDMS_KEY), DataStore.del(CATEGORY_MIGRATED_KEY), DataStore.del(OLD_CATEGORY_KEY)]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,11 +126,8 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Prevent Discord from trying to connect to hidden voice channels
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(?=(\|\||&&)\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
|
||||
replace: (_, condition, channel) => condition === "||"
|
||||
? `||$self.isHiddenChannel(${channel})`
|
||||
: `&&!$self.isHiddenChannel(${channel})`
|
||||
match: /(?=\|\|\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
|
||||
replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
|
||||
},
|
||||
{
|
||||
// Make Discord show inside the channel if clicking on a hidden or locked channel
|
||||
|
|
@ -143,11 +140,8 @@ export default definePlugin({
|
|||
{
|
||||
find: ".AUDIENCE),{isSubscriptionGated",
|
||||
replacement: {
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(!)?(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
|
||||
replace: (m, not, channel) => not
|
||||
? `${m}&&!$self.isHiddenChannel(${channel})`
|
||||
: `${m}||$self.isHiddenChannel(${channel})`
|
||||
match: /(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
|
||||
replace: (m, channel) => `${m}||$self.isHiddenChannel(${channel})`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -210,18 +204,8 @@ export default definePlugin({
|
|||
},
|
||||
// Make voice channels also appear as muted if they are muted
|
||||
{
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)(if\()?(\i)(?:\)return |\?)(\i\.MUTED)/,
|
||||
replace: (_, otherClasses, isIf, isMuted, mutedClassExpression) => isIf
|
||||
? `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
|
||||
: `${isMuted}?${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
|
||||
},
|
||||
{
|
||||
// Make muted channels also appear as unread if hide unreads is false and the channel is hidden
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
predicate: () => settings.store.channelStyle === ChannelStyle.MutedUnread || settings.store.channelStyle === ChannelStyle.Unread,
|
||||
match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
|
||||
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
|
||||
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)if\((\i)(?:\)return |\?)(\i\.MUTED)/,
|
||||
replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -230,8 +214,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.channelStyle !== ChannelStyle.Unread && settings.store.channelStyle !== ChannelStyle.MutedUnread,
|
||||
replacement: [
|
||||
{
|
||||
// FIXME(Bundler change related): Remove old compatiblity once enough time has passed
|
||||
match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
|
||||
match: /(?<=\.LOCKED;if\()(?<={channel:(\i).+?)/,
|
||||
replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
|
||||
},
|
||||
{
|
||||
|
|
@ -324,8 +307,8 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
// Change the role permission check to CONNECT if the channel is locked
|
||||
match: /\i\.\i\(\i\.\i\.ADMINISTRATOR,\i\.\i\.VIEW_CHANNEL\)(?<=context:(\i)}.+?)/,
|
||||
replace: (m, channel) => `$self.fixPermCheck(${m},${channel})`
|
||||
match: /(forceRoles:.+?)(\i\.\i\(\i\.\i\.ADMINISTRATOR,\i\.\i\.VIEW_CHANNEL\))(?<=context:(\i)}.+?)/,
|
||||
replace: (_, rest, mergedPermissions, channel) => `${rest}$self.swapViewChannelWithConnectPermission(${mergedPermissions},${channel})`
|
||||
},
|
||||
{
|
||||
// Change the permissionOverwrite check to CONNECT if the channel is locked
|
||||
|
|
@ -335,7 +318,7 @@ export default definePlugin({
|
|||
{
|
||||
// Include the @everyone role in the allowed roles list for Hidden Channels
|
||||
match: /getSortedRoles.+?\.filter\(\i=>(?=!)/,
|
||||
replace: m => `${m}$self.isHiddenChannel(arguments[0].channel)?true:`
|
||||
replace: m => `${m}$self.isHiddenChannel(arguments[0]?.channel)?true:`
|
||||
},
|
||||
{
|
||||
// If the @everyone role has the required permissions, make the array only contain it
|
||||
|
|
@ -519,13 +502,13 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
|
||||
fixPermCheck(originalPerms: bigint, channel: Channel) {
|
||||
swapViewChannelWithConnectPermission(mergedPermissions: bigint, channel: Channel) {
|
||||
if (!PermissionStore.can(PermissionsBits.CONNECT, channel)) {
|
||||
originalPerms &= ~PermissionsBits.VIEW_CHANNEL;
|
||||
originalPerms |= PermissionsBits.CONNECT;
|
||||
mergedPermissions &= ~PermissionsBits.VIEW_CHANNEL;
|
||||
mergedPermissions |= PermissionsBits.CONNECT;
|
||||
}
|
||||
|
||||
return originalPerms;
|
||||
return mergedPermissions;
|
||||
},
|
||||
|
||||
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ export default definePlugin({
|
|||
find: "#{intl::GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL}),allowOverflow:",
|
||||
predicate: () => settings.store.showModView,
|
||||
replacement: {
|
||||
match: /(role:)\i(?<=\i\.roles,\i\.highestRoleId,(\i)\].+)/,
|
||||
replace: "$1$self.findHighestRole(arguments[0],$2)",
|
||||
match: /(?<=\.highestRole\),)role:\i(?<=\[\i\.roles,\i\.highestRoleId,(\i)\].+)/,
|
||||
replace: "role:$self.getHighestRole(arguments[0],$2)",
|
||||
}
|
||||
},
|
||||
// allows you to open mod view on yourself
|
||||
|
|
@ -87,7 +87,7 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
findHighestRole({ member }: { member: GuildMember; }, roles: Role[]): Role | undefined {
|
||||
getHighestRole({ member }: { member: GuildMember; }, roles: Role[]): Role | undefined {
|
||||
try {
|
||||
return roles.find(role => role.id === member.highestRoleId);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import "./spotifyStyles.css";
|
||||
import "./visualRefreshSpotifyStyles.css"; // TODO: merge with spotifyStyles.css and remove when old UI is discontinued
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
#vc-spotify-player {
|
||||
padding: 0.375rem 0.5rem;
|
||||
padding: 12px;
|
||||
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt)));
|
||||
margin: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
|
||||
--vc-spotify-green: var(--spotify, #1db954);
|
||||
|
||||
/* so custom themes can easily change it */
|
||||
--vc-spotify-green: var(--spotify, #1db954);
|
||||
--vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%);
|
||||
--vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%);
|
||||
}
|
||||
|
|
@ -18,12 +21,13 @@
|
|||
}
|
||||
|
||||
.vc-spotify-button {
|
||||
margin: 0 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--interactive-normal);
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -49,13 +53,16 @@
|
|||
filter: brightness(1.3);
|
||||
} */
|
||||
|
||||
.vc-spotify-shuffle-on,
|
||||
.vc-spotify-repeat-context,
|
||||
.vc-spotify-repeat-track,
|
||||
.vc-spotify-shuffle-on:hover,
|
||||
.vc-spotify-shuffle-on {
|
||||
background-color: var(--vc-spotify-green-90);
|
||||
}
|
||||
|
||||
.vc-spotify-repeat-context:hover,
|
||||
.vc-spotify-repeat-track:hover {
|
||||
color: var(--vc-spotify-green);
|
||||
.vc-spotify-repeat-track:hover,
|
||||
.vc-spotify-shuffle-on:hover {
|
||||
background-color: var(--vc-spotify-green-80);
|
||||
}
|
||||
|
||||
.vc-spotify-tooltip-text {
|
||||
|
|
@ -76,6 +83,15 @@
|
|||
|
||||
.vc-spotify-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.vc-spotify-secondary-song-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vc-spotify-song-info-prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#vc-spotify-info-wrapper {
|
||||
|
|
@ -129,7 +145,7 @@
|
|||
.vc-spotify-album {
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--header-secondary);
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.vc-spotify-comma {
|
||||
|
|
@ -157,16 +173,26 @@
|
|||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] [class^="bar-"] {
|
||||
height: 4px !important;
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="bar"] {
|
||||
height: 3px !important;
|
||||
top: calc(12px - 4px / 2 + var(--bar-offset));
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] {
|
||||
background-color: var(--interactive-active);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] {
|
||||
background-color: var(--vc-spotify-green);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] [class^="grabber"] {
|
||||
/* these importants are necessary, it applies a width and height through inline styles */
|
||||
height: 10px !important;
|
||||
width: 10px !important;
|
||||
margin-top: 4px;
|
||||
background-color: var(--interactive-normal);
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
margin-top: calc(17px/-2 + var(--bar-offset)/2);
|
||||
margin-left: -0.5px;
|
||||
background-color: var(--interactive-active);
|
||||
border-color: var(--interactive-normal);
|
||||
color: var(--interactive-normal);
|
||||
opacity: 0;
|
||||
|
|
@ -185,6 +211,8 @@
|
|||
font-size: 12px;
|
||||
top: 10px;
|
||||
position: absolute;
|
||||
margin-top: 8px;
|
||||
font-family: var(--font-code);
|
||||
}
|
||||
|
||||
.vc-spotify-time-left {
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
/* TODO: merge with spotifyStyles.css and remove when old UI is discontinued */
|
||||
.visual-refresh {
|
||||
#vc-spotify-player {
|
||||
padding: 12px;
|
||||
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt)));
|
||||
margin: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.vc-spotify-song-info-prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-spotify-artist,
|
||||
.vc-spotify-album {
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.vc-spotify-secondary-song-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar {
|
||||
position: relative;
|
||||
color: var(--text-default);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] [class^="bar"] {
|
||||
height: 3px !important;
|
||||
top: calc(12px - 4px / 2 + var(--bar-offset));
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] [class^="barFill"] {
|
||||
background-color: var(--interactive-active);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"]:hover [class^="barFill"] {
|
||||
background-color: var(--vc-spotify-green);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar>[class^="slider"] [class^="grabber"] {
|
||||
background-color: var(--interactive-active);
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
margin-top: calc(17px/-2 + var(--bar-offset)/2);
|
||||
margin-left: -0.5px;
|
||||
}
|
||||
|
||||
.vc-spotify-progress-time {
|
||||
margin-top: 8px;
|
||||
font-family: var(--font-code);
|
||||
}
|
||||
|
||||
.vc-spotify-button-row {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.vc-spotify-button {
|
||||
margin: 0 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.vc-spotify-repeat-context,
|
||||
.vc-spotify-repeat-track,
|
||||
.vc-spotify-shuffle-on {
|
||||
background-color: var(--vc-spotify-green-90);
|
||||
}
|
||||
|
||||
.vc-spotify-repeat-context:hover,
|
||||
.vc-spotify-repeat-track:hover,
|
||||
.vc-spotify-shuffle-on:hover {
|
||||
background-color: var(--vc-spotify-green-80);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ export default definePlugin({
|
|||
name: "StartupTimings",
|
||||
description: "Adds Startup Timings to the Settings menu",
|
||||
authors: [Devs.Megu],
|
||||
|
||||
patches: [{
|
||||
find: "#{intl::ACTIVITY_SETTINGS}",
|
||||
replacement: [
|
||||
|
|
@ -36,5 +37,6 @@ export default definePlugin({
|
|||
},
|
||||
]
|
||||
}],
|
||||
|
||||
StartupTimingPage
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
|
|
@ -243,20 +242,5 @@ export default definePlugin({
|
|||
// Channel used for sharing rules, applying rules here would be messy
|
||||
if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
|
||||
msg.content = applyRules(msg.content);
|
||||
},
|
||||
|
||||
async start() {
|
||||
// TODO(OptionType.CUSTOM Related): Remove DataStore rules migrations once enough time has passed
|
||||
const oldStringRules = await DataStore.get<Rule[]>(STRING_RULES_KEY);
|
||||
if (oldStringRules != null) {
|
||||
settings.store.stringRules = oldStringRules;
|
||||
await DataStore.del(STRING_RULES_KEY);
|
||||
}
|
||||
|
||||
const oldRegexRules = await DataStore.get<Rule[]>(REGEX_RULES_KEY);
|
||||
if (oldRegexRules != null) {
|
||||
settings.store.regexRules = oldRegexRules;
|
||||
await DataStore.del(REGEX_RULES_KEY);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Mai",
|
||||
id: 722647978577363026n
|
||||
},
|
||||
echo: {
|
||||
name: "ECHO",
|
||||
id: 712639419785412668n
|
||||
amy: {
|
||||
name: "Amy",
|
||||
id: 603229858612510720n
|
||||
},
|
||||
katlyn: {
|
||||
name: "katlyn",
|
||||
|
|
|
|||
|
|
@ -70,14 +70,23 @@ export const ColorPicker = waitForComponent<t.ColorPicker>("ColorPicker", filter
|
|||
export const UserSummaryItem = waitForComponent("UserSummaryItem", filters.componentByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
|
||||
export let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin;
|
||||
export let createListScroller: (scrollBarClassName: string, fadeClassName: string, someOtherClassIdkMan: string, resizeObserverClass: typeof ResizeObserver) => t.ListScrollerThin;
|
||||
export let scrollerClasses: Record<string, string>;
|
||||
export let listScrollerClasses: Record<string, string>;
|
||||
|
||||
waitFor(filters.byCode('="ltr",orientation:', "customTheme:", "forwardRef"), m => createScroller = m);
|
||||
waitFor(filters.byCode("getScrollerNode:", "resizeObserver:", "sectionHeight:"), m => createListScroller = m);
|
||||
waitFor(["thin", "auto", "customTheme"], m => scrollerClasses = m);
|
||||
waitFor(m => m.thin && m.auto && !m.customTheme, m => listScrollerClasses = m);
|
||||
|
||||
export const ScrollerNone = LazyComponent(() => createScroller(scrollerClasses.none, scrollerClasses.fade, scrollerClasses.customTheme));
|
||||
export const ScrollerThin = LazyComponent(() => createScroller(scrollerClasses.thin, scrollerClasses.fade, scrollerClasses.customTheme));
|
||||
export const ScrollerAuto = LazyComponent(() => createScroller(scrollerClasses.auto, scrollerClasses.fade, scrollerClasses.customTheme));
|
||||
|
||||
export const ListScrollerNone = LazyComponent(() => createListScroller(listScrollerClasses.none, listScrollerClasses.fade, "", ResizeObserver));
|
||||
export const ListScrollerThin = LazyComponent(() => createListScroller(listScrollerClasses.thin, listScrollerClasses.fade, "", ResizeObserver));
|
||||
export const ListScrollerAuto = LazyComponent(() => createListScroller(listScrollerClasses.auto, listScrollerClasses.fade, "", ResizeObserver));
|
||||
|
||||
const { FocusLock_ } = mapMangledModuleLazy('document.getElementById("app-mount"))', {
|
||||
FocusLock_: filters.componentByCode(".containerRef")
|
||||
}) as {
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ export let MessageStore: Omit<t.MessageStore, "getMessages"> & GenericStore & {
|
|||
getMessages(chanId: string): any;
|
||||
};
|
||||
|
||||
// TODO: The correct name for this is ChannelActionCreators and it has already been exported again from utils. Remove this export once enough time has passed
|
||||
export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel");
|
||||
export let PermissionStore: GenericStore;
|
||||
export let GuildChannelStore: GenericStore;
|
||||
export let ReadStateStore: GenericStore;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue