mirror of
https://github.com/DustinBrett/daedalOS.git
synced 2026-01-15 12:15:02 +00:00
Bundle optimizations
This commit is contained in:
@@ -65,8 +65,6 @@ export const bookmarks: Bookmark[] = [
|
||||
|
||||
export const HOME_PAGE = "https://www.google.com/webhp?igu=1";
|
||||
|
||||
export const LOCAL_HOST = new Set(["127.0.0.1", "localhost"]);
|
||||
|
||||
export const NOT_FOUND =
|
||||
'<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>404 Not Found</title><style>h1{display:inline;}</style></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>';
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import StyledBrowser from "components/apps/Browser/StyledBrowser";
|
||||
import {
|
||||
DINO_GAME,
|
||||
HOME_PAGE,
|
||||
LOCAL_HOST,
|
||||
NOT_FOUND,
|
||||
PROXIES,
|
||||
bookmarks,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
} from "utils/constants";
|
||||
import {
|
||||
GOOGLE_SEARCH_QUERY,
|
||||
LOCAL_HOST,
|
||||
getExtension,
|
||||
getUrlOrSearch,
|
||||
haltEvent,
|
||||
|
||||
@@ -16,7 +16,11 @@ import { useProcesses } from "contexts/process";
|
||||
import { useViewport } from "contexts/viewport";
|
||||
import useDoubleClick from "hooks/useDoubleClick";
|
||||
import Button from "styles/common/Button";
|
||||
import { HIGH_PRIORITY_ELEMENT, IMAGE_FILE_EXTENSIONS } from "utils/constants";
|
||||
import {
|
||||
HIGH_PRIORITY_ELEMENT,
|
||||
IMAGE_FILE_EXTENSIONS,
|
||||
NATIVE_IMAGE_FORMATS,
|
||||
} from "utils/constants";
|
||||
import {
|
||||
bufferToUrl,
|
||||
getExtension,
|
||||
@@ -24,7 +28,6 @@ import {
|
||||
haltEvent,
|
||||
label,
|
||||
} from "utils/functions";
|
||||
import { decodeImageToBuffer } from "utils/imageDecoder";
|
||||
|
||||
const { maxScale, minScale } = panZoomConfig;
|
||||
|
||||
@@ -45,9 +48,15 @@ const Photos: FC<ComponentProcessProps> = ({ id }) => {
|
||||
);
|
||||
const { fullscreenElement, toggleFullscreen } = useViewport();
|
||||
const loadPhoto = useCallback(async (): Promise<void> => {
|
||||
const fileContents = await readFile(url);
|
||||
let fileContents = await readFile(url);
|
||||
const ext = getExtension(url);
|
||||
const imageBuffer = await decodeImageToBuffer(ext, fileContents);
|
||||
|
||||
if (!NATIVE_IMAGE_FORMATS.has(ext)) {
|
||||
const { decodeImageToBuffer } = await import("utils/imageDecoder");
|
||||
const decodedData = await decodeImageToBuffer(ext, fileContents);
|
||||
|
||||
if (decodedData) fileContents = decodedData;
|
||||
}
|
||||
|
||||
setSrc((currentSrc) => {
|
||||
const [currentUrl] = Object.keys(currentSrc);
|
||||
@@ -58,7 +67,7 @@ const Photos: FC<ComponentProcessProps> = ({ id }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
[url]: bufferToUrl(imageBuffer || fileContents, getMimeType(url)),
|
||||
[url]: bufferToUrl(fileContents, getMimeType(url)),
|
||||
};
|
||||
});
|
||||
prependFileToTitle(basename(url));
|
||||
|
||||
@@ -14,7 +14,7 @@ declare global {
|
||||
export type WallpaperConfig =
|
||||
| Partial<StableDiffusionConfig>
|
||||
| Partial<typeof MatrixConfig>
|
||||
| VantaWavesConfig;
|
||||
| Partial<VantaWavesConfig>;
|
||||
|
||||
export type WallpaperFunc = (
|
||||
el: HTMLElement | null,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type WallpaperMessage,
|
||||
type WallpaperConfig,
|
||||
} from "components/system/Desktop/Wallpapers/types";
|
||||
import { config as vantaConfig } from "components/system/Desktop/Wallpapers/vantaWaves/config";
|
||||
import { useFileSystem } from "contexts/fileSystem";
|
||||
import { useSession } from "contexts/session";
|
||||
import useWorker from "hooks/useWorker";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
IMAGE_FILE_EXTENSIONS,
|
||||
MILLISECONDS_IN_DAY,
|
||||
MILLISECONDS_IN_MINUTE,
|
||||
NATIVE_IMAGE_FORMATS,
|
||||
PICTURES_FOLDER,
|
||||
PROMPT_FILE,
|
||||
SLIDESHOW_FILE,
|
||||
@@ -63,7 +63,7 @@ const useWallpaper = (
|
||||
);
|
||||
const vantaWireframe = wallpaperImage === "VANTA WIREFRAME";
|
||||
const wallpaperWorker = useWorker<void>(
|
||||
WALLPAPER_WORKERS[wallpaperName],
|
||||
sessionLoaded ? WALLPAPER_WORKERS[wallpaperName] : undefined,
|
||||
undefined,
|
||||
vantaWireframe ? "Wireframe" : ""
|
||||
);
|
||||
@@ -107,12 +107,13 @@ const useWallpaper = (
|
||||
|
||||
if (wallpaperName === "VANTA") {
|
||||
config = {
|
||||
...vantaConfig,
|
||||
waveSpeed:
|
||||
vantaConfig.waveSpeed *
|
||||
(prefersReducedMotion ? REDUCED_MOTION_PERCENT : 1),
|
||||
material: {
|
||||
options: {
|
||||
wireframe: vantaWireframe || !isTopWindow,
|
||||
},
|
||||
},
|
||||
waveSpeed: prefersReducedMotion ? REDUCED_MOTION_PERCENT : 1,
|
||||
};
|
||||
vantaConfig.material.options.wireframe = vantaWireframe || !isTopWindow;
|
||||
} else if (wallpaperImage.startsWith("MATRIX")) {
|
||||
config = {
|
||||
animationSpeed: prefersReducedMotion ? REDUCED_MOTION_PERCENT : 1,
|
||||
@@ -374,14 +375,17 @@ const useWallpaper = (
|
||||
}
|
||||
}
|
||||
} else if (await exists(wallpaperImage)) {
|
||||
const { decodeImageToBuffer } = await import("utils/imageDecoder");
|
||||
const fileData = await readFile(wallpaperImage);
|
||||
const imageBuffer = await decodeImageToBuffer(
|
||||
getExtension(wallpaperImage),
|
||||
fileData
|
||||
);
|
||||
let fileData = await readFile(wallpaperImage);
|
||||
const imgExt = getExtension(wallpaperImage);
|
||||
|
||||
wallpaperUrl = bufferToUrl(imageBuffer || fileData);
|
||||
if (!NATIVE_IMAGE_FORMATS.has(imgExt)) {
|
||||
const { decodeImageToBuffer } = await import("utils/imageDecoder");
|
||||
const decodedData = await decodeImageToBuffer(imgExt, fileData);
|
||||
|
||||
if (decodedData) fileData = decodedData;
|
||||
}
|
||||
|
||||
wallpaperUrl = bufferToUrl(fileData);
|
||||
}
|
||||
|
||||
if (wallpaperUrl) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { type WallpaperConfig } from "components/system/Desktop/Wallpapers/types";
|
||||
import { disableControls } from "components/system/Desktop/Wallpapers/vantaWaves/config";
|
||||
import {
|
||||
config as vantaConfig,
|
||||
disableControls,
|
||||
} from "components/system/Desktop/Wallpapers/vantaWaves/config";
|
||||
import { type VantaWavesConfig } from "components/system/Desktop/Wallpapers/vantaWaves/types";
|
||||
import { loadFiles } from "utils/functions";
|
||||
|
||||
@@ -28,10 +31,18 @@ const vantaWaves = (
|
||||
|
||||
if (WAVES) {
|
||||
try {
|
||||
const { material, waveSpeed } = config as VantaWavesConfig;
|
||||
const wavesConfig = {
|
||||
...vantaConfig,
|
||||
waveSpeed: vantaConfig.waveSpeed * waveSpeed,
|
||||
};
|
||||
|
||||
wavesConfig.material.options.wireframe = material.options.wireframe;
|
||||
|
||||
WAVES({
|
||||
el,
|
||||
...disableControls,
|
||||
...(config as VantaWavesConfig),
|
||||
...wavesConfig,
|
||||
});
|
||||
} catch {
|
||||
fallback?.();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type OffscreenRenderProps } from "components/system/Desktop/Wallpapers/types";
|
||||
import { libs } from "components/system/Desktop/Wallpapers/vantaWaves";
|
||||
import {
|
||||
config,
|
||||
config as vantaConfig,
|
||||
disableControls,
|
||||
} from "components/system/Desktop/Wallpapers/vantaWaves/config";
|
||||
import {
|
||||
@@ -30,11 +30,7 @@ globalThis.addEventListener(
|
||||
waveEffect?.renderer.setSize(width, height);
|
||||
waveEffect?.resize();
|
||||
} else {
|
||||
const {
|
||||
canvas,
|
||||
config: offscreenConfig,
|
||||
devicePixelRatio,
|
||||
} = data as OffscreenRenderProps;
|
||||
const { canvas, config, devicePixelRatio } = data as OffscreenRenderProps;
|
||||
const { VANTA: { current: currentEffect = waveEffect, WAVES } = {} } =
|
||||
globalThis;
|
||||
|
||||
@@ -42,8 +38,16 @@ globalThis.addEventListener(
|
||||
if (currentEffect) currentEffect.destroy();
|
||||
|
||||
try {
|
||||
const { material, waveSpeed } = config as VantaWavesConfig;
|
||||
const wavesConfig = {
|
||||
...vantaConfig,
|
||||
waveSpeed: vantaConfig.waveSpeed * waveSpeed,
|
||||
};
|
||||
|
||||
wavesConfig.material.options.wireframe = material.options.wireframe;
|
||||
|
||||
waveEffect = WAVES({
|
||||
...((offscreenConfig || config) as VantaWavesConfig),
|
||||
...wavesConfig,
|
||||
...disableControls,
|
||||
canvas,
|
||||
devicePixelRatio,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
MAX_ICON_SIZE,
|
||||
MAX_THUMBNAIL_FILE_SIZE,
|
||||
MOUNTED_FOLDER_ICON,
|
||||
NATIVE_IMAGE_FORMATS,
|
||||
NEW_FOLDER_ICON,
|
||||
ONE_TIME_PASSIVE_EVENT,
|
||||
PHOTO_ICON,
|
||||
@@ -365,14 +366,23 @@ export const getInfoWithExtension = (
|
||||
getInfoByFileExtension(PHOTO_ICON, (signal) =>
|
||||
fs.readFile(path, async (error, contents = Buffer.from("")) => {
|
||||
if (!error && contents.length > 0 && !signal.aborted) {
|
||||
const { decodeImageToBuffer } = await import("utils/imageDecoder");
|
||||
let image = contents;
|
||||
|
||||
if (!NATIVE_IMAGE_FORMATS.has(extension)) {
|
||||
const { decodeImageToBuffer } = await import("utils/imageDecoder");
|
||||
|
||||
if (!signal.aborted) {
|
||||
const decodedImage = await decodeImageToBuffer(
|
||||
extension,
|
||||
contents
|
||||
);
|
||||
|
||||
if (decodedImage) image = decodedImage;
|
||||
}
|
||||
}
|
||||
|
||||
if (!signal.aborted) {
|
||||
const image = await decodeImageToBuffer(extension, contents);
|
||||
|
||||
if (image && !signal.aborted) {
|
||||
getInfoByFileExtension(bufferToUrl(image, getMimeType(path)));
|
||||
}
|
||||
getInfoByFileExtension(bufferToUrl(image, getMimeType(path)));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, useRef } from "react";
|
||||
import { useTheme } from "styled-components";
|
||||
import dynamic from "next/dynamic";
|
||||
import { sortFiles } from "components/system/Files/FileManager/functions";
|
||||
import { type SortBy } from "components/system/Files/FileManager/useSortBy";
|
||||
import StyledColumns from "components/system/Files/FileManager/Columns/StyledColumns";
|
||||
@@ -11,7 +12,10 @@ import {
|
||||
} from "components/system/Files/FileManager/Columns/constants";
|
||||
import { useSession } from "contexts/session";
|
||||
import { type Files } from "components/system/Files/FileManager/useFolder";
|
||||
import { Down } from "components/apps/FileExplorer/NavigationIcons";
|
||||
|
||||
const Down = dynamic(() =>
|
||||
import("components/apps/FileExplorer/NavigationIcons").then((mod) => mod.Down)
|
||||
);
|
||||
|
||||
type ColumnsProps = {
|
||||
columns: ColumnsObject;
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
import { memo } from "react";
|
||||
|
||||
export const Search = memo(() => (
|
||||
<svg
|
||||
style={{
|
||||
border: "1px solid transparent",
|
||||
borderRight: 0,
|
||||
borderTop: 0,
|
||||
height: "17px",
|
||||
marginLeft: "-1px",
|
||||
}}
|
||||
viewBox="3 -1 30 30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 0q1.516 0 2.922.391T26.547 1.5t2.227 1.727 1.727 2.227 1.109 2.625.391 2.922-.391 2.922-1.109 2.625-1.727 2.227-2.227 1.727-2.625 1.109-2.922.391q-1.953 0-3.742-.656t-3.289-1.891L1.703 31.705q-.297.297-.703.297t-.703-.297T0 31.002t.297-.703l12.25-12.266q-1.234-1.5-1.891-3.289T10 11.002q0-1.516.391-2.922T11.5 5.455t1.727-2.227 2.227-1.727T18.079.392t2.922-.391zm0 20q1.859 0 3.5-.711t2.859-1.93 1.93-2.859T30 11t-.711-3.5-1.93-2.859-2.859-1.93T21 2t-3.5.711-2.859 1.93-1.93 2.859T12 11t.711 3.5 1.93 2.859 2.859 1.93T21 20z"
|
||||
stroke="#FFF"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
|
||||
export const RightArrow = memo(() => (
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m257.5 977.5 465-465.5-465-465.5 45-45 511 510.5-511 510.5z" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTheme } from "styled-components";
|
||||
import { Search as SearchIcon } from "components/system/Taskbar/Search/Icons";
|
||||
import { memo } from "react";
|
||||
import StyledTaskbarButton from "components/system/Taskbar/StyledTaskbarButton";
|
||||
import {
|
||||
importSearch,
|
||||
@@ -15,6 +15,26 @@ type StartButtonProps = {
|
||||
toggleSearch: (showMenu?: boolean) => void;
|
||||
};
|
||||
|
||||
const SearchIcon = memo(() => (
|
||||
<svg
|
||||
style={{
|
||||
border: "1px solid transparent",
|
||||
borderRight: 0,
|
||||
borderTop: 0,
|
||||
height: "17px",
|
||||
marginLeft: "-1px",
|
||||
}}
|
||||
viewBox="3 -1 30 30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 0q1.516 0 2.922.391T26.547 1.5t2.227 1.727 1.727 2.227 1.109 2.625.391 2.922-.391 2.922-1.109 2.625-1.727 2.227-2.227 1.727-2.625 1.109-2.922.391q-1.953 0-3.742-.656t-3.289-1.891L1.703 31.705q-.297.297-.703.297t-.703-.297T0 31.002t.297-.703l12.25-12.266q-1.234-1.5-1.891-3.289T10 11.002q0-1.516.391-2.922T11.5 5.455t1.727-2.227 2.227-1.727T18.079.392t2.922-.391zm0 20q1.859 0 3.5-.711t2.859-1.93 1.93-2.859T30 11t-.711-3.5-1.93-2.859-2.859-1.93T21 2t-3.5.711-2.859 1.93-1.93 2.859T12 11t.711 3.5 1.93 2.859 2.859 1.93T21 20z"
|
||||
stroke="#FFF"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
|
||||
const SearchButton: FC<StartButtonProps> = ({
|
||||
searchVisible,
|
||||
toggleSearch,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { extname, join } from "path";
|
||||
import { openDB } from "idb";
|
||||
import { type openDB } from "idb";
|
||||
import {
|
||||
type Mount,
|
||||
type ExtendedEmscriptenFileSystem,
|
||||
@@ -34,7 +34,7 @@ export const UNKNOWN_SIZE = -1;
|
||||
export const UNKNOWN_STATE_CODES = new Set(["EIO", "ENOENT"]);
|
||||
export const KEYVAL_STORE_NAME = "keyval";
|
||||
|
||||
const KEYVAL_DB = `${KEYVAL_STORE_NAME}-store`;
|
||||
export const KEYVAL_DB = `${KEYVAL_STORE_NAME}-store`;
|
||||
|
||||
const IDX_SIZE = 1;
|
||||
const IDX_MTIME = 2;
|
||||
@@ -158,8 +158,28 @@ export const supportsIndexedDB = (): Promise<boolean> =>
|
||||
}
|
||||
});
|
||||
|
||||
export const getKeyValStore = (): ReturnType<typeof openDB> =>
|
||||
openDB(KEYVAL_DB, 1, {
|
||||
export const hasIndexedDB = async (name: string): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
try {
|
||||
const db = window.indexedDB.open(name);
|
||||
|
||||
db.addEventListener("upgradeneeded", () => {
|
||||
db.transaction?.abort();
|
||||
resolve(false);
|
||||
});
|
||||
db.addEventListener("success", () => {
|
||||
db.result.close();
|
||||
resolve(true);
|
||||
});
|
||||
db.addEventListener("error", () => resolve(false));
|
||||
db.addEventListener("blocked", () => resolve(false));
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
export const getKeyValStore = async (): ReturnType<typeof openDB> =>
|
||||
(await import("idb")).openDB(KEYVAL_DB, 1, {
|
||||
upgrade: (db) => db.createObjectStore(KEYVAL_STORE_NAME),
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
import { type NewPath } from "components/system/Files/FileManager/useFolder";
|
||||
import {
|
||||
getFileSystemHandles,
|
||||
hasIndexedDB,
|
||||
isMountedFolder,
|
||||
KEYVAL_DB,
|
||||
} from "contexts/fileSystem/core";
|
||||
import useAsyncFs, {
|
||||
type AsyncFS,
|
||||
@@ -617,25 +619,27 @@ const useFileSystemContextState = (): FileSystemContextState => {
|
||||
|
||||
let mappedOntoDesktop = false;
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(await getFileSystemHandles()).map(
|
||||
async ([handleDirectory, handle]) => {
|
||||
if (!(await exists(handleDirectory))) {
|
||||
try {
|
||||
const mapDirectory = SYSTEM_DIRECTORIES.has(handleDirectory)
|
||||
? handleDirectory
|
||||
: dirname(handleDirectory);
|
||||
if (await hasIndexedDB(KEYVAL_DB)) {
|
||||
await Promise.all(
|
||||
Object.entries(await getFileSystemHandles()).map(
|
||||
async ([handleDirectory, handle]) => {
|
||||
if (!(await exists(handleDirectory))) {
|
||||
try {
|
||||
const mapDirectory = SYSTEM_DIRECTORIES.has(handleDirectory)
|
||||
? handleDirectory
|
||||
: dirname(handleDirectory);
|
||||
|
||||
await mapFs(mapDirectory, handle);
|
||||
await mapFs(mapDirectory, handle);
|
||||
|
||||
if (mapDirectory === DESKTOP_PATH) mappedOntoDesktop = true;
|
||||
} catch {
|
||||
// Ignore failure
|
||||
if (mapDirectory === DESKTOP_PATH) mappedOntoDesktop = true;
|
||||
} catch {
|
||||
// Ignore failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (mappedOntoDesktop) updateFolder(DESKTOP_PATH);
|
||||
};
|
||||
|
||||
@@ -96,14 +96,10 @@ export const TIFF_IMAGE_FORMATS = new Set([
|
||||
|
||||
export const CLIPBOARD_FILE_EXTENSIONS = new Set([".jpeg", ".jpg", ".png"]);
|
||||
|
||||
export const IMAGE_FILE_EXTENSIONS = new Set([
|
||||
...HEIF_IMAGE_FORMATS,
|
||||
...TIFF_IMAGE_FORMATS,
|
||||
".ani",
|
||||
export const NATIVE_IMAGE_FORMATS = new Set([
|
||||
".apng",
|
||||
".avif",
|
||||
".bmp",
|
||||
".cur",
|
||||
".gif",
|
||||
".ico",
|
||||
".jfif",
|
||||
@@ -111,16 +107,24 @@ export const IMAGE_FILE_EXTENSIONS = new Set([
|
||||
".jpe",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".jxl",
|
||||
".pjp",
|
||||
".pjpeg",
|
||||
".png",
|
||||
".svg",
|
||||
".qoi",
|
||||
".webp",
|
||||
".xbm",
|
||||
]);
|
||||
|
||||
export const IMAGE_FILE_EXTENSIONS = new Set([
|
||||
...NATIVE_IMAGE_FORMATS,
|
||||
...HEIF_IMAGE_FORMATS,
|
||||
...TIFF_IMAGE_FORMATS,
|
||||
".ani",
|
||||
".cur",
|
||||
".jxl",
|
||||
".qoi",
|
||||
]);
|
||||
|
||||
export const UNSUPPORTED_SLIDESHOW_EXTENSIONS = new Set([
|
||||
...HEIF_IMAGE_FORMATS,
|
||||
...TIFF_IMAGE_FORMATS,
|
||||
|
||||
@@ -26,9 +26,6 @@ import {
|
||||
TIMESTAMP_DATE_FORMAT,
|
||||
USER_ICON_PATH,
|
||||
} from "utils/constants";
|
||||
import { LOCAL_HOST } from "components/apps/Browser/config";
|
||||
|
||||
export const GOOGLE_SEARCH_QUERY = "https://www.google.com/search?igu=1&q=";
|
||||
|
||||
export const bufferToBlob = (buffer: Buffer, type?: string): Blob =>
|
||||
new Blob([buffer], type ? { type } : undefined);
|
||||
@@ -805,6 +802,9 @@ export const getTZOffsetISOString = (): string => {
|
||||
).toISOString();
|
||||
};
|
||||
|
||||
export const LOCAL_HOST = new Set(["127.0.0.1", "localhost"]);
|
||||
export const GOOGLE_SEARCH_QUERY = "https://www.google.com/search?igu=1&q=";
|
||||
|
||||
export const getUrlOrSearch = async (input: string): Promise<URL> => {
|
||||
const isIpfs = input.startsWith("ipfs://");
|
||||
const hasHttpSchema =
|
||||
|
||||
Reference in New Issue
Block a user