mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: refine startup announcement cards
This commit is contained in:
@@ -495,6 +495,7 @@ function getDefaultValues(): SettingsState {
|
|||||||
adaptiveThemeColour: false,
|
adaptiveThemeColour: false,
|
||||||
adaptiveThemeGradient: false,
|
adaptiveThemeGradient: false,
|
||||||
adaptiveThemeColourTransition: true,
|
adaptiveThemeColourTransition: true,
|
||||||
|
themeOfTheMonthDisabled: false,
|
||||||
autoCloudSettingsSync: true,
|
autoCloudSettingsSync: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-49
@@ -3726,45 +3726,136 @@ div.day-empty {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.whatsnewHeader.themeOfTheMonthHeader {
|
.themeOfTheMonthCard {
|
||||||
height: auto;
|
position: fixed;
|
||||||
min-height: unset;
|
right: max(18px, env(safe-area-inset-right));
|
||||||
|
bottom: max(18px, env(safe-area-inset-bottom));
|
||||||
|
z-index: 48;
|
||||||
|
width: min(360px, calc(100vw - 36px));
|
||||||
|
overflow: visible;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35);
|
||||||
|
animation: themeOfTheMonthCardIn 0.24s ease-out;
|
||||||
}
|
}
|
||||||
.whatsnewHeader.themeOfTheMonthHeader h1 {
|
.themeOfTheMonthCard::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardClosing {
|
||||||
|
pointer-events: none;
|
||||||
|
animation: themeOfTheMonthCardOut 0.18s ease-in forwards;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardClose {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 4px !important;
|
||||||
|
right: 4px !important;
|
||||||
|
z-index: 2;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: 16px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardImage {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardBody {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardEyebrow {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, var(--better-pri, #6366f1) 82%, var(--text-primary) 18%);
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCard h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.themeOfTheMonthSubtitle {
|
.themeOfTheMonthCardDescription {
|
||||||
margin: 0.25rem 0 0;
|
display: -webkit-box;
|
||||||
font-size: 0.95rem;
|
margin: 8px 0 14px;
|
||||||
font-weight: 500;
|
overflow: hidden;
|
||||||
letter-spacing: 0.01em;
|
-webkit-box-orient: vertical;
|
||||||
text-transform: uppercase;
|
-webkit-line-clamp: 3;
|
||||||
color: color-mix(in srgb, var(--text-primary) 65%, transparent);
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: color-mix(in srgb, var(--text-primary) 78%, transparent);
|
||||||
}
|
}
|
||||||
.themeOfTheMonthFooter {
|
.themeOfTheMonthCardActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-wrap: wrap;
|
||||||
padding: 1rem 0;
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.themeOfTheMonthViewButton {
|
.themeOfTheMonthCardPrimary,
|
||||||
|
.themeOfTheMonthCardSecondary {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.65rem 1.25rem;
|
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: 1rem;
|
padding: 0.58rem 0.9rem;
|
||||||
font-weight: 600;
|
font-size: 0.86rem;
|
||||||
letter-spacing: 0.01em;
|
font-weight: 700;
|
||||||
|
transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease;
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardPrimary {
|
||||||
background: var(--better-pri, #6366f1);
|
background: var(--better-pri, #6366f1);
|
||||||
color: white;
|
color: white;
|
||||||
transition: transform 0.15s ease, filter 0.15s ease;
|
|
||||||
}
|
}
|
||||||
.themeOfTheMonthViewButton:hover {
|
.themeOfTheMonthCardSecondary {
|
||||||
filter: brightness(1.1);
|
background: color-mix(in srgb, var(--text-primary) 10%, transparent);
|
||||||
transform: scale(1.03);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.themeOfTheMonthViewButton:active {
|
.themeOfTheMonthCardPrimary:hover,
|
||||||
transform: scale(0.98);
|
.themeOfTheMonthCardSecondary:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.themeOfTheMonthCardPrimary:active,
|
||||||
|
.themeOfTheMonthCardSecondary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
@keyframes themeOfTheMonthCardIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(18px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes themeOfTheMonthCardOut {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px) scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.themeOfTheMonthCard {
|
||||||
|
z-index: 2147483645;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-theme-highlight {
|
.bsplus-theme-highlight {
|
||||||
@@ -4428,38 +4519,63 @@ h2.home-subtitle {
|
|||||||
|
|
||||||
.bsplus-toast {
|
.bsplus-toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
right: max(18px, env(safe-area-inset-right));
|
||||||
right: 24px;
|
bottom: max(18px, env(safe-area-inset-bottom));
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
display: flex;
|
width: min(360px, calc(100vw - 36px));
|
||||||
align-items: flex-start;
|
padding: 14px 16px 16px;
|
||||||
gap: 12px;
|
border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent);
|
||||||
max-width: 380px;
|
border-radius: 20px;
|
||||||
padding: 16px 18px;
|
background: var(--background-primary, #fff);
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--background-secondary, #fff);
|
|
||||||
color: var(--text-primary, #1a1a1a);
|
color: var(--text-primary, #1a1a1a);
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
.bsplus-toast-content p {
|
.bsplus-toast-eyebrow {
|
||||||
margin: 6px 0 0;
|
margin: 0 0 6px !important;
|
||||||
opacity: 0.8;
|
font-size: 0.72rem !important;
|
||||||
font-size: 0.85rem;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, #ea580c 82%, var(--text-primary) 18%);
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
.dark .bsplus-toast-eyebrow {
|
||||||
|
color: color-mix(in srgb, #fb923c 82%, var(--text-primary) 18%);
|
||||||
|
}
|
||||||
|
.bsplus-toast-content strong {
|
||||||
|
display: block;
|
||||||
|
padding-right: 34px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.bsplus-toast-content p:not(.bsplus-toast-eyebrow) {
|
||||||
|
display: -webkit-box;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
color: color-mix(in srgb, var(--text-primary) 78%, transparent);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
.bsplus-toast-close {
|
.bsplus-toast-close {
|
||||||
flex-shrink: 0;
|
position: absolute !important;
|
||||||
background: none;
|
top: 4px !important;
|
||||||
border: none;
|
right: 4px !important;
|
||||||
color: var(--text-primary, #1a1a1a);
|
z-index: 2;
|
||||||
font-size: 1.3rem;
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: 16px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 2px;
|
font-size: 1.35rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
opacity: 0.5;
|
transition: filter 0.15s ease;
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
}
|
||||||
.bsplus-toast-close:hover {
|
.bsplus-toast-close:hover {
|
||||||
opacity: 1;
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ export function showEngageParentsToast() {
|
|||||||
settingsState.engageParentsAnnouncementShown = true;
|
settingsState.engageParentsAnnouncementShown = true;
|
||||||
|
|
||||||
const toast = document.createElement("div");
|
const toast = document.createElement("div");
|
||||||
toast.className = "bsplus-toast";
|
toast.className = "bsplus-toast engageParentsToast";
|
||||||
toast.innerHTML = /* html */ `
|
toast.innerHTML = /* html */ `
|
||||||
|
<button class="bsplus-toast-close" aria-label="Dismiss">×</button>
|
||||||
<div class="bsplus-toast-content">
|
<div class="bsplus-toast-content">
|
||||||
|
<p class="bsplus-toast-eyebrow">SEQTA Engage support</p>
|
||||||
<strong>BetterSEQTA+ now supports <span class="seqtaEngageAccent">SEQTA Engage</span></strong>
|
<strong>BetterSEQTA+ now supports <span class="seqtaEngageAccent">SEQTA Engage</span></strong>
|
||||||
<p>Buy your mum a BetterSEQTA Plus! Parents now get themes, a cleaner home page, and all the Plus polish on SEQTA Engage.</p>
|
<p>Buy your mum a BetterSEQTA Plus! Parents now get themes, a cleaner home page, and all the Plus polish on SEQTA Engage.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="bsplus-toast-close" aria-label="Dismiss">×</button>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
toast.style.opacity = "0";
|
toast.style.opacity = "0";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import stringToHTML from "../stringToHTML";
|
import stringToHTML from "../stringToHTML";
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
import { closePopup, openPopup } from "./PopupManager";
|
import { closePopup } from "./PopupManager";
|
||||||
import { getApiBase } from "../DevApiBase";
|
import { getApiBase } from "../DevApiBase";
|
||||||
import { openThemeStoreWithHighlight } from "../openThemeStoreWithHighlight";
|
import { openThemeStoreWithHighlight } from "../openThemeStoreWithHighlight";
|
||||||
import { cloudAuth } from "../CloudAuth";
|
import { cloudAuth } from "../CloudAuth";
|
||||||
@@ -47,7 +47,7 @@ export async function fetchThemeOfTheMonth(): Promise<ThemeOfTheMonthEntry | nul
|
|||||||
|
|
||||||
/** True when we have a new monthly entry the user hasn't dismissed yet. */
|
/** True when we have a new monthly entry the user hasn't dismissed yet. */
|
||||||
export function shouldShowThemeOfTheMonth(entry: ThemeOfTheMonthEntry | null): boolean {
|
export function shouldShowThemeOfTheMonth(entry: ThemeOfTheMonthEntry | null): boolean {
|
||||||
if (!entry) return false;
|
if (!entry || settingsState.themeOfTheMonthDisabled) return false;
|
||||||
return settingsState.themeOfTheMonthLastSeenId !== entry.id;
|
return settingsState.themeOfTheMonthLastSeenId !== entry.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,82 +108,90 @@ async function resolvePopupHeroImageUrl(entry: ThemeOfTheMonthEntry): Promise<st
|
|||||||
return fallback || null;
|
return fallback || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHeroImageContainer(imageUrl: string, alt: string): HTMLElement {
|
function closeThemeOfTheMonthCard(
|
||||||
const container = document.createElement("div");
|
card: HTMLElement,
|
||||||
container.classList.add("whatsnewImgContainer");
|
onDismissed?: () => void,
|
||||||
|
markSeen = true,
|
||||||
|
) {
|
||||||
|
if (card.classList.contains("themeOfTheMonthCardClosing")) return;
|
||||||
|
|
||||||
const img = document.createElement("img");
|
if (markSeen) {
|
||||||
img.src = imageUrl;
|
const entryId = card.dataset.entryId;
|
||||||
img.alt = alt;
|
if (entryId) settingsState.themeOfTheMonthLastSeenId = entryId;
|
||||||
img.classList.add("whatsnewImg");
|
}
|
||||||
container.appendChild(img);
|
|
||||||
|
|
||||||
return container;
|
card.classList.add("themeOfTheMonthCardClosing");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
card.remove();
|
||||||
|
onDismissed?.();
|
||||||
|
}, 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the Theme of the Month announcement popup.
|
* Renders the Theme of the Month announcement card.
|
||||||
*/
|
*/
|
||||||
export async function OpenThemeOfTheMonthPopup(
|
export async function OpenThemeOfTheMonthPopup(
|
||||||
entry: ThemeOfTheMonthEntry,
|
entry: ThemeOfTheMonthEntry,
|
||||||
onDismissed?: () => void,
|
onDismissed?: () => void,
|
||||||
) {
|
) {
|
||||||
if (document.getElementById("whatsnewbk")) {
|
document.getElementById("theme-of-the-month-card")?.remove();
|
||||||
onDismissed?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthLabel = formatMonthLabel(entry.month);
|
const monthLabel = formatMonthLabel(entry.month);
|
||||||
|
|
||||||
const header = stringToHTML(
|
|
||||||
/* html */ `
|
|
||||||
<div class="whatsnewHeader themeOfTheMonthHeader">
|
|
||||||
<h1>${escapeHTML(entry.title)}</h1>
|
|
||||||
<p class="themeOfTheMonthSubtitle">Theme of the Month · ${escapeHTML(monthLabel)}</p>
|
|
||||||
</div>`,
|
|
||||||
).firstChild as HTMLElement;
|
|
||||||
|
|
||||||
const heroUrl = await resolvePopupHeroImageUrl(entry);
|
const heroUrl = await resolvePopupHeroImageUrl(entry);
|
||||||
const imageContainer = heroUrl ? createHeroImageContainer(heroUrl, entry.title) : null;
|
const description = escapeHTML(entry.description).replace(/\n/g, " ");
|
||||||
|
const linkedThemeId = entry.theme_id ?? entry.theme?.id;
|
||||||
|
|
||||||
const descriptionHTML = escapeHTML(entry.description).replace(/\n/g, "<br />");
|
const card = stringToHTML(/* html */ `
|
||||||
const text = stringToHTML(/* html */ `
|
<aside id="theme-of-the-month-card" class="themeOfTheMonthCard" role="dialog" aria-label="Theme of the Month">
|
||||||
<div class="whatsnewTextContainer themeOfTheMonthDescription" style="height: 50%; overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
|
<button type="button" class="themeOfTheMonthCardClose" aria-label="Close Theme of the Month">×</button>
|
||||||
<p>${descriptionHTML}</p>
|
${
|
||||||
</div>
|
heroUrl
|
||||||
|
? `<img class="themeOfTheMonthCardImage" src="${escapeHTML(heroUrl)}" alt="${escapeHTML(entry.title)}" />`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="themeOfTheMonthCardBody">
|
||||||
|
<p class="themeOfTheMonthCardEyebrow">Theme of the Month · ${escapeHTML(monthLabel)}</p>
|
||||||
|
<h2>${escapeHTML(entry.title)}</h2>
|
||||||
|
<p class="themeOfTheMonthCardDescription">${description}</p>
|
||||||
|
<div class="themeOfTheMonthCardActions">
|
||||||
|
${
|
||||||
|
linkedThemeId
|
||||||
|
? `<button type="button" class="themeOfTheMonthCardPrimary">Open Store</button>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<button type="button" class="themeOfTheMonthCardSecondary">Don't show again</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
`).firstChild as HTMLElement;
|
`).firstChild as HTMLElement;
|
||||||
|
|
||||||
let footer: HTMLElement | null = null;
|
card.dataset.entryId = entry.id;
|
||||||
const linkedThemeId = entry.theme_id ?? entry.theme?.id;
|
const autoCloseTimeout = window.setTimeout(() => {
|
||||||
const linkedThemeName = entry.theme?.name;
|
closeThemeOfTheMonthCard(card, onDismissed);
|
||||||
if (linkedThemeId && linkedThemeName) {
|
}, 12000);
|
||||||
footer = document.createElement("div");
|
|
||||||
footer.classList.add("whatsnewFooter", "themeOfTheMonthFooter");
|
|
||||||
|
|
||||||
const viewBtn = document.createElement("button");
|
const dismiss = (markSeen = true) => {
|
||||||
viewBtn.type = "button";
|
window.clearTimeout(autoCloseTimeout);
|
||||||
viewBtn.classList.add("themeOfTheMonthViewButton");
|
closeThemeOfTheMonthCard(card, onDismissed, markSeen);
|
||||||
viewBtn.textContent = `View "${linkedThemeName}" in the Theme Store`;
|
};
|
||||||
viewBtn.addEventListener("click", () => {
|
|
||||||
void closePopup();
|
|
||||||
openThemeStoreWithHighlight(linkedThemeId);
|
|
||||||
});
|
|
||||||
|
|
||||||
footer.appendChild(viewBtn);
|
card.addEventListener("mouseenter", () => window.clearTimeout(autoCloseTimeout), { once: true });
|
||||||
}
|
|
||||||
|
|
||||||
settingsState.themeOfTheMonthLastSeenId = entry.id;
|
card.querySelector(".themeOfTheMonthCardClose")?.addEventListener("click", () => {
|
||||||
|
dismiss();
|
||||||
const content: (Node | null)[] = [];
|
|
||||||
if (imageContainer) content.push(imageContainer);
|
|
||||||
content.push(text);
|
|
||||||
if (footer) content.push(footer);
|
|
||||||
|
|
||||||
openPopup({
|
|
||||||
header,
|
|
||||||
content,
|
|
||||||
afterClose: onDismissed,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
card.querySelector(".themeOfTheMonthCardPrimary")?.addEventListener("click", () => {
|
||||||
|
dismiss();
|
||||||
|
openThemeStoreWithHighlight(linkedThemeId!);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => {
|
||||||
|
settingsState.themeOfTheMonthDisabled = true;
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface SettingsState {
|
|||||||
bsCloudAutoSyncAnnouncementShown?: boolean;
|
bsCloudAutoSyncAnnouncementShown?: boolean;
|
||||||
/** ID of the last Theme of the Month entry shown to the user (shows once per new entry). */
|
/** ID of the last Theme of the Month entry shown to the user (shows once per new entry). */
|
||||||
themeOfTheMonthLastSeenId?: string;
|
themeOfTheMonthLastSeenId?: string;
|
||||||
|
/** Permanently disables Theme of the Month startup prompts. */
|
||||||
|
themeOfTheMonthDisabled?: boolean;
|
||||||
timeFormat?: string;
|
timeFormat?: string;
|
||||||
animations: boolean;
|
animations: boolean;
|
||||||
defaultPage: string;
|
defaultPage: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user