ESLint --fix: auto-fix brace-style, indent, semi, no-trailing-spaces

Run eslint --fix across src/ to clear ~1900 mechanical lint errors
surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler
upgrade in the React 19 modernization PR.

Issues fixed automatically:
- brace-style (Allman): try/catch one-liners reformatted to multi-line
- indent: tab-vs-space and depth corrections
- semi: missing trailing semicolons
- no-trailing-spaces

No semantic changes. Remaining 701 errors are real-code issues
(set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that
need manual per-file review.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This commit is contained in:
simoleo89
2026-05-11 16:31:50 +00:00
parent 1b1e0c18bf
commit 535fa71020
115 changed files with 2217 additions and 1524 deletions
+24 -8
View File
@@ -36,7 +36,8 @@ const preloadUrl = async (url: string): Promise<void> =>
const response = await fetch(url, { cache: 'force-cache' }); const response = await fetch(url, { cache: 'force-cache' });
await response.arrayBuffer(); await response.arrayBuffer();
} }
catch {} catch
{}
}; };
const preloadImage = (url: string): void => const preloadImage = (url: string): void =>
@@ -49,7 +50,8 @@ const preloadImage = (url: string): void =>
image.decoding = 'async'; image.decoding = 'async';
image.src = url; image.src = url;
} }
catch {} catch
{}
}; };
const asStringArray = (value: unknown): string[] => const asStringArray = (value: unknown): string[] =>
@@ -126,8 +128,12 @@ export const App: FC<{}> = props =>
}); });
let payload: Record<string, unknown> = {}; let payload: Record<string, unknown> = {};
try { payload = await response.json(); } try
catch {} {
payload = await response.json();
}
catch
{}
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : ''); const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
@@ -175,8 +181,12 @@ export const App: FC<{}> = props =>
}); });
let payload: Record<string, unknown> = {}; let payload: Record<string, unknown> = {};
try { payload = await response.json(); } try
catch {} {
payload = await response.json();
}
catch
{}
if(response.ok) if(response.ok)
{ {
@@ -320,8 +330,14 @@ export const App: FC<{}> = props =>
// Configuration is loaded lazily — fetch it up-front so the login // Configuration is loaded lazily — fetch it up-front so the login
// screen toggle and Turnstile keys are available before we decide. // screen toggle and Turnstile keys are available before we decide.
let configInitError: unknown = null; let configInitError: unknown = null;
try { await GetConfiguration().init(); } try
catch(e) { configInitError = e; } {
await GetConfiguration().init();
}
catch(e)
{
configInitError = e;
}
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false); const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1; const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
+14 -4
View File
@@ -17,13 +17,20 @@ export const setAccessToken = (token: string | null | undefined, expiresAt?: num
window.localStorage.removeItem(EXPIRES_KEY); window.localStorage.removeItem(EXPIRES_KEY);
} }
} }
catch {} catch
{}
}; };
export const getAccessToken = (): string => export const getAccessToken = (): string =>
{ {
try { return window.localStorage.getItem(STORAGE_KEY) ?? ''; } try
catch { return ''; } {
return window.localStorage.getItem(STORAGE_KEY) ?? '';
}
catch
{
return '';
}
}; };
export const getAccessTokenExpiresAt = (): number => export const getAccessTokenExpiresAt = (): number =>
@@ -35,7 +42,10 @@ export const getAccessTokenExpiresAt = (): number =>
const value = parseInt(raw, 10); const value = parseInt(raw, 10);
return Number.isFinite(value) ? value : 0; return Number.isFinite(value) ? value : 0;
} }
catch { return 0; } catch
{
return 0;
}
}; };
export const clearAccessToken = (): void => export const clearAccessToken = (): void =>
+26 -7
View File
@@ -31,8 +31,14 @@ export interface CustomBadgeError
const interpolate = (value: string): string => const interpolate = (value: string): string =>
{ {
try { return GetConfiguration().interpolate(value); } try
catch { return value; } {
return GetConfiguration().interpolate(value);
}
catch
{
return value;
}
}; };
const getConfigUrl = (key: string, fallback: string): string => const getConfigUrl = (key: string, fallback: string): string =>
@@ -61,8 +67,14 @@ const parseJson = async <T>(response: Response): Promise<T> =>
{ {
const text = await response.text(); const text = await response.text();
if(!text) return {} as T; if(!text) return {} as T;
try { return JSON.parse(text) as T; } try
catch { throw new Error('Invalid response from server.'); } {
return JSON.parse(text) as T;
}
catch
{
throw new Error('Invalid response from server.');
}
}; };
const throwOnError = async (response: Response): Promise<void> => const throwOnError = async (response: Response): Promise<void> =>
@@ -129,8 +141,14 @@ const injectTextsIntoLocalization = (texts: Record<string, string> | null | unde
{ {
if(!texts) return; if(!texts) return;
let manager: ReturnType<typeof GetLocalizationManager> | null = null; let manager: ReturnType<typeof GetLocalizationManager> | null = null;
try { manager = GetLocalizationManager(); } try
catch { return; } {
manager = GetLocalizationManager();
}
catch
{
return;
}
if(!manager || typeof manager.setValue !== 'function') return; if(!manager || typeof manager.setValue !== 'function') return;
for(const key of Object.keys(texts)) for(const key of Object.keys(texts))
{ {
@@ -152,7 +170,8 @@ export const ensureCustomBadgeTexts = (): Promise<void> =>
const payload = await parseJson<{ texts: Record<string, string> }>(response); const payload = await parseJson<{ texts: Record<string, string> }>(response);
injectTextsIntoLocalization(payload.texts); injectTextsIntoLocalization(payload.texts);
} }
catch {} catch
{}
})(); })();
return customBadgeTextsLoadPromise; return customBadgeTextsLoadPromise;
}; };
+1 -1
View File
@@ -15,7 +15,7 @@ export class FurnitureOffer implements IPurchasableOffer
constructor(furniData: IFurnitureData) constructor(furniData: IFurnitureData)
{ {
this._furniData = furniData; this._furniData = furniData;
this._product = (new Product(this._furniData.type, this._furniData.id, this._furniData.customParams, 1, GetProductDataForLocalization(this._furniData.className), this._furniData) as IProduct); this._product = (new Product(this._furniData.type, this._furniData.id, this._furniData.customParams, 1, GetProductDataForLocalization(this._furniData.className), this._furniData));
} }
public activate(): void public activate(): void
+1 -1
View File
@@ -9,5 +9,5 @@ export const GetGroupChatData = (extraData: string) =>
const figure = splitData[1]; const figure = splitData[1];
const userId = parseInt(splitData[2]); const userId = parseInt(splitData[2]);
return ({ username: username, figure: figure, userId: userId } as IGroupChatData); return ({ username: username, figure: figure, userId: userId });
}; };
@@ -9,9 +9,11 @@ export class chooserSelectionVisualizer
{ {
if (this.animationFrameId !== null) return; if (this.animationFrameId !== null) return;
const animate = (time: number) => { const animate = (time: number) =>
{
const elapsed = time / 1000; // Convert to seconds const elapsed = time / 1000; // Convert to seconds
this.activeFilters.forEach(filter => { this.activeFilters.forEach(filter =>
{
filter.time = elapsed; // Update time uniform filter.time = elapsed; // Update time uniform
}); });
this.animationFrameId = requestAnimationFrame(animate); this.animationFrameId = requestAnimationFrame(animate);
@@ -22,7 +24,8 @@ export class chooserSelectionVisualizer
private static stopAnimation(): void private static stopAnimation(): void
{ {
if (this.animationFrameId !== null) { if (this.animationFrameId !== null)
{
cancelAnimationFrame(this.animationFrameId); cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null; this.animationFrameId = null;
} }
@@ -69,7 +72,8 @@ export class chooserSelectionVisualizer
sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter)); sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter));
} }
if (this.activeFilters.size === 0) { if (this.activeFilters.size === 0)
{
this.stopAnimation(); this.stopAnimation();
} }
} }
+1 -1
View File
@@ -33,6 +33,6 @@ export class MannequinUtilities
figureContainer.removePart(part); figureContainer.removePart(part);
} }
figureContainer.updatePart((this.MANNEQUIN_FIGURE[0] as string), (this.MANNEQUIN_FIGURE[1] as number), (this.MANNEQUIN_FIGURE[2] as number[])); figureContainer.updatePart((this.MANNEQUIN_FIGURE[0]), (this.MANNEQUIN_FIGURE[1]), (this.MANNEQUIN_FIGURE[2]));
}; };
} }
+12 -6
View File
@@ -18,8 +18,10 @@ interface IUiSettingsContext
const UiSettingsContext = createContext<IUiSettingsContext>({ const UiSettingsContext = createContext<IUiSettingsContext>({
settings: DEFAULT_UI_SETTINGS, settings: DEFAULT_UI_SETTINGS,
isCustomActive: false, isCustomActive: false,
updateSettings: () => {}, updateSettings: () =>
resetSettings: () => {}, {},
resetSettings: () =>
{},
getHeaderStyle: () => ({}), getHeaderStyle: () => ({}),
getTabsStyle: () => ({}), getTabsStyle: () => ({}),
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
@@ -42,7 +44,8 @@ const loadSettings = (): IUiSettings =>
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) }; if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
} }
catch(e) {} catch(e)
{}
return { ...DEFAULT_UI_SETTINGS }; return { ...DEFAULT_UI_SETTINGS };
}; };
@@ -53,7 +56,8 @@ const saveSettings = (settings: IUiSettings): void =>
{ {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} }
catch(e) {} catch(e)
{}
}; };
const sendComposer = (composer: any): void => const sendComposer = (composer: any): void =>
@@ -62,7 +66,8 @@ const sendComposer = (composer: any): void =>
{ {
GetCommunication()?.connection?.send(composer); GetCommunication()?.connection?.send(composer);
} }
catch(e) {} catch(e)
{}
}; };
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) => export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
@@ -93,7 +98,8 @@ export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
saveSettings(serverSettings); saveSettings(serverSettings);
} }
} }
catch(e) {} catch(e)
{}
}; };
connection.addMessageEvent(new UiSettingsDataEvent(handler)); connection.addMessageEvent(new UiSettingsDataEvent(handler));
+8 -3
View File
@@ -53,8 +53,12 @@ export const SetRememberLogin = (data: RememberLoginData): void =>
{ {
if(!data?.token?.length && !data?.ssoTicket?.length) return; if(!data?.token?.length && !data?.ssoTicket?.length) return;
try { window.localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data)); } try
catch {} {
window.localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data));
}
catch
{}
}; };
export const ClearRememberLogin = (): void => export const ClearRememberLogin = (): void =>
@@ -64,7 +68,8 @@ export const ClearRememberLogin = (): void =>
window.localStorage.removeItem(REMEMBER_LOGIN_KEY); window.localStorage.removeItem(REMEMBER_LOGIN_KEY);
window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY); window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY);
} }
catch {} catch
{}
}; };
export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void => export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void =>
+4 -1
View File
@@ -1,4 +1,7 @@
let _youtubeEnabled = false; let _youtubeEnabled = false;
export const getYoutubeRoomEnabled = () => _youtubeEnabled; export const getYoutubeRoomEnabled = () => _youtubeEnabled;
export const setYoutubeRoomEnabled = (enabled: boolean) => { _youtubeEnabled = enabled; }; export const setYoutubeRoomEnabled = (enabled: boolean) =>
{
_youtubeEnabled = enabled;
};
@@ -1,4 +1,4 @@
const rawNickIcons = import.meta.glob('./*.gif', { eager: true, import: 'default' }) as Record<string, string>; const rawNickIcons = import.meta.glob('./*.gif', { eager: true, import: 'default' });
export const NICK_ICON_URLS: Record<string, string> = Object.entries(rawNickIcons).reduce((accumulator, [ path, url ]) => export const NICK_ICON_URLS: Record<string, string> = Object.entries(rawNickIcons).reduce((accumulator, [ path, url ]) =>
{ {
+8 -4
View File
@@ -26,7 +26,8 @@ const setBootDebug = (message: string) =>
if(secureNode) secureNode.textContent = `${ secureNode.textContent }\n${ message }`; if(secureNode) secureNode.textContent = `${ secureNode.textContent }\n${ message }`;
} }
catch {} catch
{}
}; };
setBootDebug('boot: secure fetch installed'); setBootDebug('boot: secure fetch installed');
@@ -38,14 +39,16 @@ const deployBaseUrl = (): string =>
const loaderBase = (window as any).__nitroLoaderBase; const loaderBase = (window as any).__nitroLoaderBase;
if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString(); if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString();
} }
catch {} catch
{}
try try
{ {
const moduleUrl = (import.meta as any).url; const moduleUrl = (import.meta as any).url;
if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString(); if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString();
} }
catch {} catch
{}
try try
{ {
@@ -56,7 +59,8 @@ const deployBaseUrl = (): string =>
return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`; return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`;
} }
} }
catch {} catch
{}
return `${ window.location.origin }/`; return `${ window.location.origin }/`;
}; };
+1 -1
View File
@@ -19,4 +19,4 @@ export const ButtonGroup: FC<ButtonGroupProps> = props =>
}, [ classNames ]); }, [ classNames ]);
return <Base classNames={ getClassNames } { ...rest } />; return <Base classNames={ getClassNames } { ...rest } />;
} };
+1 -1
View File
@@ -145,4 +145,4 @@ export const Slider: FC<SliderProps> = props =>
) } ) }
</Flex> </Flex>
); );
} };
+6 -3
View File
@@ -20,7 +20,8 @@ export interface TextProps extends BaseProps<HTMLDivElement> {
textBreak?: boolean; textBreak?: boolean;
} }
export const Text: FC<TextProps> = props => { export const Text: FC<TextProps> = props =>
{
const { const {
variant = 'black', variant = 'black',
fontWeight = null, fontWeight = null,
@@ -40,10 +41,12 @@ export const Text: FC<TextProps> = props => {
...rest ...rest
} = props; } = props;
const getClassNames = useMemo(() => { const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [truncate ? 'block' : 'inline']; const newClassNames: string[] = [truncate ? 'block' : 'inline'];
if (variant) { if (variant)
{
if (variant === 'primary') newClassNames.push('text-[#1e7295]'); if (variant === 'primary') newClassNames.push('text-[#1e7295]');
if (variant == 'secondary') newClassNames.push('text-[#185d79]'); if (variant == 'secondary') newClassNames.push('text-[#185d79]');
if (variant === 'black') newClassNames.push('text-[#000000]'); if (variant === 'black') newClassNames.push('text-[#000000]');
+57 -29
View File
@@ -21,7 +21,8 @@ export interface DraggableWindowProps {
children?: ReactNode; children?: ReactNode;
} }
export const DraggableWindow: FC<DraggableWindowProps> = props => { export const DraggableWindow: FC<DraggableWindowProps> = props =>
{
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props; const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props;
const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
@@ -31,49 +32,61 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
const [dragHandler, setDragHandler] = useState<HTMLElement>(null); const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const bringToTop = useCallback(() => { const bringToTop = useCallback(() =>
{
let zIndex = 400; let zIndex = 400;
for (const existingWindow of CURRENT_WINDOWS) { for (const existingWindow of CURRENT_WINDOWS)
{
zIndex += 1; zIndex += 1;
existingWindow.style.zIndex = zIndex.toString(); existingWindow.style.zIndex = zIndex.toString();
} }
}, []); }, []);
const moveCurrentWindow = useCallback(() => { const moveCurrentWindow = useCallback(() =>
{
const index = CURRENT_WINDOWS.indexOf(elementRef.current); const index = CURRENT_WINDOWS.indexOf(elementRef.current);
if (index === -1) { if (index === -1)
{
CURRENT_WINDOWS.push(elementRef.current); CURRENT_WINDOWS.push(elementRef.current);
} else if (index === (CURRENT_WINDOWS.length - 1)) return; }
else if (index >= 0) { else if (index === (CURRENT_WINDOWS.length - 1)) return;
else if (index >= 0)
{
CURRENT_WINDOWS.splice(index, 1); CURRENT_WINDOWS.splice(index, 1);
CURRENT_WINDOWS.push(elementRef.current); CURRENT_WINDOWS.push(elementRef.current);
} }
bringToTop(); bringToTop();
}, [bringToTop]); }, [bringToTop]);
const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) => { const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) =>
{
moveCurrentWindow(); moveCurrentWindow();
}, [moveCurrentWindow]); }, [moveCurrentWindow]);
const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) => { const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) =>
{
moveCurrentWindow(); moveCurrentWindow();
}, [moveCurrentWindow]); }, [moveCurrentWindow]);
const startDragging = useCallback((startX: number, startY: number) => { const startDragging = useCallback((startX: number, startY: number) =>
{
setStart({ x: startX, y: startY }); setStart({ x: startX, y: startY });
setIsDragging(true); setIsDragging(true);
}, []); }, []);
const onDragMouseDown = useCallback((event: MouseEvent) => { const onDragMouseDown = useCallback((event: MouseEvent) =>
{
startDragging(event.clientX, event.clientY); startDragging(event.clientX, event.clientY);
}, [startDragging]); }, [startDragging]);
const onTouchDown = useCallback((event: TouchEvent) => { const onTouchDown = useCallback((event: TouchEvent) =>
{
const touch = event.touches[0]; const touch = event.touches[0];
startDragging(touch.clientX, touch.clientY); startDragging(touch.clientX, touch.clientY);
}, [startDragging]); }, [startDragging]);
const clampPosition = useCallback((newX: number, newY: number) => { const clampPosition = useCallback((newX: number, newY: number) =>
{
if (!elementRef.current) return { x: newX, y: newY }; if (!elementRef.current) return { x: newX, y: newY };
const windowWidth = elementRef.current.offsetWidth; const windowWidth = elementRef.current.offsetWidth;
@@ -88,7 +101,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
return { x: clampedX, y: clampedY }; return { x: clampedX, y: clampedY };
}, []); }, []);
const onDragMouseMove = useCallback((event: MouseEvent) => { const onDragMouseMove = useCallback((event: MouseEvent) =>
{
if (!elementRef.current || !isDragging) return; if (!elementRef.current || !isDragging) return;
const newDeltaX = event.clientX - start.x; const newDeltaX = event.clientX - start.x;
@@ -100,7 +114,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y }); setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
}, [start, offset, clampPosition, isDragging]); }, [start, offset, clampPosition, isDragging]);
const onDragTouchMove = useCallback((event: TouchEvent) => { const onDragTouchMove = useCallback((event: TouchEvent) =>
{
if (!elementRef.current || !isDragging) return; if (!elementRef.current || !isDragging) return;
const touch = event.touches[0]; const touch = event.touches[0];
@@ -113,7 +128,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y }); setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
}, [start, offset, clampPosition, isDragging]); }, [start, offset, clampPosition, isDragging]);
const completeDrag = useCallback(() => { const completeDrag = useCallback(() =>
{
if (!elementRef.current || !dragHandler || !isDragging) return; if (!elementRef.current || !dragHandler || !isDragging) return;
const finalOffsetX = offset.x + delta.x; const finalOffsetX = offset.x + delta.x;
@@ -124,29 +140,34 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
setOffset({ x: clampedPos.x, y: clampedPos.y }); setOffset({ x: clampedPos.x, y: clampedPos.y });
setIsDragging(false); setIsDragging(false);
if (uniqueKey !== null) { if (uniqueKey !== null)
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) } as WindowSaveOptions; {
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) };
newStorage.offset = { x: clampedPos.x, y: clampedPos.y }; newStorage.offset = { x: clampedPos.x, y: clampedPos.y };
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage); SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage);
} }
}, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]); }, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]);
const onDragMouseUp = useCallback((event: MouseEvent) => { const onDragMouseUp = useCallback((event: MouseEvent) =>
{
completeDrag(); completeDrag();
}, [completeDrag]); }, [completeDrag]);
const onDragTouchUp = useCallback((event: TouchEvent) => { const onDragTouchUp = useCallback((event: TouchEvent) =>
{
completeDrag(); completeDrag();
}, [completeDrag]); }, [completeDrag]);
useLayoutEffect(() => { useLayoutEffect(() =>
{
const element = elementRef.current as HTMLElement; const element = elementRef.current as HTMLElement;
if (!element) return; if (!element) return;
CURRENT_WINDOWS.push(element); CURRENT_WINDOWS.push(element);
bringToTop(); bringToTop();
if (!disableDrag) { if (!disableDrag)
{
const handle = element.querySelector(handleSelector); const handle = element.querySelector(handleSelector);
if (handle) setDragHandler(handle as HTMLElement); if (handle) setDragHandler(handle as HTMLElement);
} }
@@ -156,7 +177,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
switch (windowPosition) { switch (windowPosition)
{
case DraggableWindowPosition.TOP_CENTER: case DraggableWindowPosition.TOP_CENTER:
offsetY = 50 + offsetTop; offsetY = 50 + offsetTop;
offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft; offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
@@ -176,25 +198,29 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
setDelta({ x: 0, y: 0 }); setDelta({ x: 0, y: 0 });
setIsPositioned(true); setIsPositioned(true);
return () => { return () =>
{
const index = CURRENT_WINDOWS.indexOf(element); const index = CURRENT_WINDOWS.indexOf(element);
if (index >= 0) CURRENT_WINDOWS.splice(index, 1); if (index >= 0) CURRENT_WINDOWS.splice(index, 1);
}; };
}, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]); }, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]);
useEffect(() => { useEffect(() =>
{
if (!dragHandler) return; if (!dragHandler) return;
dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown); dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown);
return () => { return () =>
{
dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown); dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown); dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown);
}; };
}, [dragHandler, onDragMouseDown, onTouchDown]); }, [dragHandler, onDragMouseDown, onTouchDown]);
useEffect(() => { useEffect(() =>
{
if (!isDragging) return; if (!isDragging) return;
document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
@@ -202,7 +228,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove); document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
return () => { return () =>
{
document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp); document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp); document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove); document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
@@ -210,7 +237,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
}; };
}, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]); }, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]);
useEffect(() => { useEffect(() =>
{
if (!uniqueKey) return; if (!uniqueKey) return;
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`); const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`);
+1 -1
View File
@@ -19,5 +19,5 @@ export * from './draggable-window';
export * from './layout'; export * from './layout';
export * from './layout/limited-edition'; export * from './layout/limited-edition';
export * from './types'; export * from './types';
export * from "./Slider"; export * from './Slider';
export * from './utils'; export * from './utils';
+4 -1
View File
@@ -22,7 +22,10 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
{ {
isMounted.current = true; isMounted.current = true;
return () => { isMounted.current = false; }; return () =>
{
isMounted.current = false;
};
}, []); }, []);
const updateImage = useCallback(async (texture: any) => const updateImage = useCallback(async (texture: any) =>
+6 -3
View File
@@ -9,11 +9,13 @@ interface LayoutMiniCameraViewProps {
onClose: () => void; onClose: () => void;
} }
export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => { export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props =>
{
const { roomId = -1, textureReceiver = null, onClose = null } = props; const { roomId = -1, textureReceiver = null, onClose = null } = props;
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const getCameraBounds = () => { const getCameraBounds = () =>
{
if (!elementRef || !elementRef.current) return null; if (!elementRef || !elementRef.current) return null;
const frameBounds = elementRef.current.getBoundingClientRect(); const frameBounds = elementRef.current.getBoundingClientRect();
@@ -26,7 +28,8 @@ export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => {
); );
}; };
const takePicture = () => { const takePicture = () =>
{
PlaySound(SoundNames.CAMERA_SHUTTER); PlaySound(SoundNames.CAMERA_SHUTTER);
textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds())); textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds()));
}; };
@@ -21,7 +21,10 @@ export const LayoutRoomObjectImageView: FC<LayoutRoomObjectImageViewProps> = pro
{ {
isMounted.current = true; isMounted.current = true;
return () => { isMounted.current = false; }; return () =>
{
isMounted.current = false;
};
}, []); }, []);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
+43 -20
View File
@@ -9,8 +9,10 @@ interface AdsenseConfig {
fullWidthResponsive?: boolean; fullWidthResponsive?: boolean;
} }
const parsePublisherIdFromAdsTxt = (text: string): string | null => { const parsePublisherIdFromAdsTxt = (text: string): string | null =>
for (const rawLine of text.split(/\r?\n/)) { {
for (const rawLine of text.split(/\r?\n/))
{
const line = rawLine.split('#')[0].trim(); const line = rawLine.split('#')[0].trim();
if (!line) continue; if (!line) continue;
const parts = line.split(',').map(part => part.trim()); const parts = line.split(',').map(part => part.trim());
@@ -22,7 +24,8 @@ const parsePublisherIdFromAdsTxt = (text: string): string | null => {
return null; return null;
}; };
export const GoogleAdsView: FC<{}> = () => { export const GoogleAdsView: FC<{}> = () =>
{
const adsEnabled = GetConfigurationValue<boolean>('show.google.ads', false); const adsEnabled = GetConfigurationValue<boolean>('show.google.ads', false);
const [ isOpen, setIsOpen ] = useState(false); const [ isOpen, setIsOpen ] = useState(false);
const [ publisherId, setPublisherId ] = useState<string | null>(null); const [ publisherId, setPublisherId ] = useState<string | null>(null);
@@ -32,7 +35,8 @@ export const GoogleAdsView: FC<{}> = () => {
const pushedRef = useRef(false); const pushedRef = useRef(false);
const autoOpenedRef = useRef(false); const autoOpenedRef = useRef(false);
useEffect(() => { useEffect(() =>
{
if (!adsEnabled) return; if (!adsEnabled) return;
const handler = () => setIsOpen(prev => !prev); const handler = () => setIsOpen(prev => !prev);
window.addEventListener('ads:toggle', handler); window.addEventListener('ads:toggle', handler);
@@ -42,7 +46,8 @@ export const GoogleAdsView: FC<{}> = () => {
// Auto-open once on initial mount (the login / landing stage). // Auto-open once on initial mount (the login / landing stage).
// Subsequent toggles are driven by the "ads:toggle" window event // Subsequent toggles are driven by the "ads:toggle" window event
// (e.g. the Show Ad button in NitroSystemAlertView). // (e.g. the Show Ad button in NitroSystemAlertView).
useEffect(() => { useEffect(() =>
{
if (!adsEnabled) return; if (!adsEnabled) return;
if (autoOpenedRef.current) return; if (autoOpenedRef.current) return;
autoOpenedRef.current = true; autoOpenedRef.current = true;
@@ -50,11 +55,14 @@ export const GoogleAdsView: FC<{}> = () => {
return () => clearTimeout(t); return () => clearTimeout(t);
}, [ adsEnabled ]); }, [ adsEnabled ]);
useEffect(() => { useEffect(() =>
{
let cancelled = false; let cancelled = false;
(async () => { (async () =>
try { {
try
{
const [ adsTxtRes, configRes ] = await Promise.all([ const [ adsTxtRes, configRes ] = await Promise.all([
fetch('/ads.txt', { cache: 'no-cache' }), fetch('/ads.txt', { cache: 'no-cache' }),
fetch(configFileUrl('adsense.json', true), { cache: 'no-cache' }) fetch(configFileUrl('adsense.json', true), { cache: 'no-cache' })
@@ -73,39 +81,54 @@ export const GoogleAdsView: FC<{}> = () => {
if (cancelled) return; if (cancelled) return;
setPublisherId(pubId); setPublisherId(pubId);
setConfig(cfg); setConfig(cfg);
} catch (err) { }
catch (err)
{
if (!cancelled) setLoadError((err as Error).message); if (!cancelled) setLoadError((err as Error).message);
} }
})(); })();
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, []); }, []);
useEffect(() => { useEffect(() =>
if (!isOpen) { {
if (!isOpen)
{
pushedRef.current = false; pushedRef.current = false;
return; return;
} }
if (!insRef.current || pushedRef.current) return; if (!insRef.current || pushedRef.current) return;
if (!publisherId || !config?.slot) return; if (!publisherId || !config?.slot) return;
const tryPush = () => { const tryPush = () =>
try { {
// eslint-disable-next-line @typescript-eslint/no-explicit-any try
{
const w = window as any; const w = window as any;
w.adsbygoogle = w.adsbygoogle || []; w.adsbygoogle = w.adsbygoogle || [];
w.adsbygoogle.push({}); w.adsbygoogle.push({});
pushedRef.current = true; pushedRef.current = true;
} catch { }
catch
{
// AdSense script may not be ready yet; retry once // AdSense script may not be ready yet; retry once
setTimeout(() => { setTimeout(() =>
try { {
// eslint-disable-next-line @typescript-eslint/no-explicit-any try
{
const w = window as any; const w = window as any;
w.adsbygoogle = w.adsbygoogle || []; w.adsbygoogle = w.adsbygoogle || [];
w.adsbygoogle.push({}); w.adsbygoogle.push({});
pushedRef.current = true; pushedRef.current = true;
} catch { /* give up */ } }
catch
{ /* give up */ }
}, 500); }, 500);
} }
}; };
@@ -23,7 +23,10 @@ const findNearestColor = (hex: string, colors: IPartColor[]): IPartColor | null
const cb = color.rgb & 0xFF; const cb = color.rgb & 0xFF;
const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2; const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
if(dist < minDist) { minDist = dist; nearest = color; } if(dist < minDist)
{
minDist = dist; nearest = color;
}
} }
return nearest; return nearest;
@@ -40,7 +43,10 @@ export const AvatarEditorAdvancedColorView: FC<{
useEffect(() => useEffect(() =>
{ {
return () => { if(debounceRef.current) clearTimeout(debounceRef.current); }; return () =>
{
if(debounceRef.current) clearTimeout(debounceRef.current);
};
}, []); }, []);
const selectedColor = useMemo(() => const selectedColor = useMemo(() =>
@@ -83,7 +83,10 @@ export const AvatarEffectsView: FC<{}> = () =>
} }
})(); })();
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, [ isVisible, effects.length, loadError ]); }, [ isVisible, effects.length, loadError ]);
const session = GetSessionDataManager(); const session = GetSessionDataManager();
@@ -49,18 +49,21 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
setSelectedOverlay, setSelectedOverlay,
selectedCardBackground, selectedCardBackground,
setSelectedCardBackground setSelectedCardBackground
}) => { }) =>
{
const [activeTab, setActiveTab] = useState<TabType>('backgrounds'); const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
const remoteData = use(fetchBackgroundsData()); const remoteData = use(fetchBackgroundsData());
const { roomSession } = useRoom(); const { roomSession } = useRoom();
const processData = useCallback((configData: any[], idField: string): ItemData[] => { const processData = useCallback((configData: any[], idField: string): ItemData[] =>
{
if (!configData?.length) return []; if (!configData?.length) return [];
return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] })); return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] }));
}, []); }, []);
const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data'): any[] => { const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data'): any[] =>
{
const fromRemote = remoteData?.[key]; const fromRemote = remoteData?.[key];
if(Array.isArray(fromRemote)) return fromRemote; if(Array.isArray(fromRemote)) return fromRemote;
return GetOptionalConfigurationValue<any[]>(key, []) || []; return GetOptionalConfigurationValue<any[]>(key, []) || [];
@@ -73,7 +76,8 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId') cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId')
}), [processData, readData]); }), [processData, readData]);
const handleSelection = useCallback((id: number) => { const handleSelection = useCallback((id: number) =>
{
if (!roomSession) return; if (!roomSession) return;
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground }; const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground };
@@ -12,7 +12,8 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri
const value = LocalizeText(key, params ?? null, replacements ?? null); const value = LocalizeText(key, params ?? null, replacements ?? null);
if(value && value !== key) return value; if(value && value !== key) return value;
} }
catch {} catch
{}
if(!params || !replacements) return fallback; if(!params || !replacements) return fallback;
let out = fallback; let out = fallback;
@@ -58,7 +59,7 @@ const floodFill = (grid: Uint32Array, w: number, h: number, startX: number, star
const stack: number[] = [ startIdx ]; const stack: number[] = [ startIdx ];
while(stack.length) while(stack.length)
{ {
const idx = stack.pop() as number; const idx = stack.pop();
if(next[idx] !== target) continue; if(next[idx] !== target) continue;
next[idx] = replacement; next[idx] = replacement;
const x = idx % w; const x = idx % w;
@@ -157,12 +158,18 @@ const loadGridFromUrl = (url: string): Promise<Uint32Array> =>
{ {
const o = i * 4; const o = i * 4;
const a = data[o + 3]; const a = data[o + 3];
if(a === 0) { grid[i] = 0; continue; } if(a === 0)
{
grid[i] = 0; continue;
}
grid[i] = ((a & 0xff) << 24) | ((data[o] & 0xff) << 16) | ((data[o + 1] & 0xff) << 8) | (data[o + 2] & 0xff); grid[i] = ((a & 0xff) << 24) | ((data[o] & 0xff) << 16) | ((data[o + 1] & 0xff) << 8) | (data[o + 2] & 0xff);
} }
resolve(grid); resolve(grid);
} }
catch(err) { reject(err); } catch(err)
{
reject(err);
}
}; };
image.onerror = () => reject(new Error('Could not load badge image (CORS?).')); image.onerror = () => reject(new Error('Could not load badge image (CORS?).'));
image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(); image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
@@ -232,7 +239,13 @@ export const BadgeCreatorView: FC<{}> = () =>
return () => RemoveLinkEventTracker(tracker); return () => RemoveLinkEventTracker(tracker);
}, []); }, []);
useEffect(() => { if(isVisible) { refresh(); ensureCustomBadgeTexts(); } }, [ isVisible, refresh ]); useEffect(() =>
{
if(isVisible)
{
refresh(); ensureCustomBadgeTexts();
}
}, [ isVisible, refresh ]);
const resetEditor = useCallback(() => const resetEditor = useCallback(() =>
{ {
@@ -365,7 +378,10 @@ export const BadgeCreatorView: FC<{}> = () =>
useEffect(() => useEffect(() =>
{ {
const stopDrag = () => { isDraggingRef.current = false; }; const stopDrag = () =>
{
isDraggingRef.current = false;
};
window.addEventListener('mouseup', stopDrag); window.addEventListener('mouseup', stopDrag);
return () => window.removeEventListener('mouseup', stopDrag); return () => window.removeEventListener('mouseup', stopDrag);
}, []); }, []);
@@ -385,7 +401,10 @@ export const BadgeCreatorView: FC<{}> = () =>
const handleSave = useCallback(async () => const handleSave = useCallback(async () =>
{ {
if(submitting) return; if(submitting) return;
if(isEmpty) { setError(t('badgecreator.error.empty', 'Draw something first.')); return; } if(isEmpty)
{
setError(t('badgecreator.error.empty', 'Draw something first.')); return;
}
if(!editingBadgeId && !canCreateMore) if(!editingBadgeId && !canCreateMore)
{ {
setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ])); setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ]));
@@ -506,7 +525,10 @@ export const BadgeCreatorView: FC<{}> = () =>
<button <button
key={ idx } key={ idx }
type="button" type="button"
onClick={ () => { setSelectedColor(color); setTool('paint'); } } onClick={ () =>
{
setSelectedColor(color); setTool('paint');
} }
title={ isTransparent ? 'Transparent' : argbToCss(color) } title={ isTransparent ? 'Transparent' : argbToCss(color) }
style={ { style={ {
width: 22, width: 22,
@@ -10,29 +10,35 @@ export interface CameraWidgetShowPhotoViewProps {
onClick?: () => void; onClick?: () => void;
} }
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => { export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props =>
{
const { currentIndex = -1, currentPhotos = null, onClick = null } = props; const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
const [imageIndex, setImageIndex] = useState(0); const [imageIndex, setImageIndex] = useState(0);
const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null; const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
const next = () => { const next = () =>
setImageIndex(prevValue => { {
setImageIndex(prevValue =>
{
let newIndex = prevValue + 1; let newIndex = prevValue + 1;
if (newIndex >= currentPhotos.length) newIndex = 0; if (newIndex >= currentPhotos.length) newIndex = 0;
return newIndex; return newIndex;
}); });
}; };
const previous = () => { const previous = () =>
setImageIndex(prevValue => { {
setImageIndex(prevValue =>
{
let newIndex = prevValue - 1; let newIndex = prevValue - 1;
if (newIndex < 0) newIndex = currentPhotos.length - 1; if (newIndex < 0) newIndex = currentPhotos.length - 1;
return newIndex; return newIndex;
}); });
}; };
useEffect(() => { useEffect(() =>
{
setImageIndex(currentIndex); setImageIndex(currentIndex);
}, [currentIndex]); }, [currentIndex]);
@@ -43,7 +49,7 @@ export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = pro
const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL); const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
if (!roomObject) return; if (!roomObject) return;
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID); return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
} };
return ( return (
<Grid style={{ display: 'flex', flexDirection: 'column' }}> <Grid style={{ display: 'flex', flexDirection: 'column' }}>
@@ -16,7 +16,8 @@ export interface CameraWidgetEditorViewProps {
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ]; const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props => { export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
{
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props; const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
const [ currentTab, setCurrentTab ] = useState(TABS[0]); const [ currentTab, setCurrentTab ] = useState(TABS[0]);
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null); const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
@@ -35,37 +36,45 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
img.src = picture.imageUrl; img.src = picture.imageUrl;
}, [ picture ]); }, [ picture ]);
const getColorMatrixEffects = useMemo(() => { const getColorMatrixEffects = useMemo(() =>
{
return availableEffects.filter(effect => effect.colorMatrix); return availableEffects.filter(effect => effect.colorMatrix);
}, [ availableEffects ]); }, [ availableEffects ]);
const getCompositeEffects = useMemo(() => { const getCompositeEffects = useMemo(() =>
{
return availableEffects.filter(effect => effect.texture); return availableEffects.filter(effect => effect.texture);
}, [ availableEffects ]); }, [ availableEffects ]);
const getEffectList = useCallback(() => { const getEffectList = useCallback(() =>
{
return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects; return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]); }, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
const getSelectedEffectIndex = useCallback((name: string) => { const getSelectedEffectIndex = useCallback((name: string) =>
{
if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1; if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
return selectedEffects.findIndex(effect => effect.effect.name === name); return selectedEffects.findIndex(effect => effect.effect.name === name);
}, [ selectedEffects ]); }, [ selectedEffects ]);
const getCurrentEffectIndex = useMemo(() => { const getCurrentEffectIndex = useMemo(() =>
{
return getSelectedEffectIndex(selectedEffectName); return getSelectedEffectIndex(selectedEffectName);
}, [ selectedEffectName, getSelectedEffectIndex ]); }, [ selectedEffectName, getSelectedEffectIndex ]);
const getCurrentEffect = useMemo(() => { const getCurrentEffect = useMemo(() =>
{
if (!selectedEffectName) return null; if (!selectedEffectName) return null;
return selectedEffects[getCurrentEffectIndex] || null; return selectedEffects[getCurrentEffectIndex] || null;
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]); }, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
const setSelectedEffectAlpha = useCallback((alpha: number) => { const setSelectedEffectAlpha = useCallback((alpha: number) =>
{
const index = getCurrentEffectIndex; const index = getCurrentEffectIndex;
if (index === -1) return; if (index === -1) return;
setSelectedEffects(prevValue => { setSelectedEffects(prevValue =>
{
const clone = [ ...prevValue ]; const clone = [ ...prevValue ];
const currentEffect = clone[index]; const currentEffect = clone[index];
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha); clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
@@ -73,8 +82,10 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
}); });
}, [ getCurrentEffectIndex ]); }, [ getCurrentEffectIndex ]);
const processAction = useCallback((type: string, effectName: string = null) => { const processAction = useCallback((type: string, effectName: string = null) =>
switch (type) { {
switch (type)
{
case 'close': case 'close':
onClose(); onClose();
return; return;
@@ -102,7 +113,8 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
const existingIndex = getSelectedEffectIndex(effectName); const existingIndex = getSelectedEffectIndex(effectName);
if (existingIndex === -1) return; if (existingIndex === -1) return;
setSelectedEffects(prevValue => { setSelectedEffects(prevValue =>
{
const clone = [ ...prevValue ]; const clone = [ ...prevValue ];
clone.splice(existingIndex, 1); clone.splice(existingIndex, 1);
return clone; return clone;
@@ -141,10 +153,12 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
} }
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]); }, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
useEffect(() => { useEffect(() =>
{
if(!stableTexture) return; if(!stableTexture) return;
const processThumbnails = async () => { const processThumbnails = async () =>
{
const renderedEffects = await Promise.all( const renderedEffects = await Promise.all(
availableEffects.map(effect => availableEffects.map(effect =>
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false) GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
@@ -155,24 +169,28 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
processThumbnails(); processThumbnails();
}, [ stableTexture, availableEffects ]); }, [ stableTexture, availableEffects ]);
useEffect(() => { useEffect(() =>
{
if(!stableTexture) return; if(!stableTexture) return;
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(() => { debounceTimerRef.current = setTimeout(() =>
{
const id = ++requestIdRef.current; const id = ++requestIdRef.current;
GetRoomCameraWidgetManager() GetRoomCameraWidgetManager()
.applyEffects(stableTexture, selectedEffects, false) .applyEffects(stableTexture, selectedEffects, false)
.then(imageElement => { .then(imageElement =>
{
if (id !== requestIdRef.current) return; if (id !== requestIdRef.current) return;
setCurrentPictureUrl(imageElement.src); setCurrentPictureUrl(imageElement.src);
}) })
.catch(error => NitroLogger.error('Failed to apply effects to picture', error)); .catch(error => NitroLogger.error('Failed to apply effects to picture', error));
}, 50); }, 50);
return () => { return () =>
{
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
}; };
}, [ stableTexture, selectedEffects ]); }, [ stableTexture, selectedEffects ]);
@@ -26,7 +26,7 @@ export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = p
// return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />; // return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />;
return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } /> return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } />;
}) } }) }
</Grid> </Grid>
); );
@@ -97,7 +97,10 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
{ {
if(e.key === 'Escape') if(e.key === 'Escape')
{ {
if(editingOffer) { setEditingOffer(null); e.preventDefault(); return; } if(editingOffer)
{
setEditingOffer(null); e.preventDefault(); return;
}
if(editingPageData || editingRootPage || editingPageNode) if(editingPageData || editingRootPage || editingPageNode)
{ {
setEditingPageData(false); setEditingPageData(false);
+16 -5
View File
@@ -19,9 +19,11 @@ const CatalogClassicViewInner: FC<{}> = () =>
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog(); const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
const catalogAdmin = useCatalogAdmin(); const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false; const adminMode = catalogAdmin?.adminMode ?? false;
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {}); const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
{});
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {}); const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
{});
const loading = catalogAdmin?.loading ?? false; const loading = catalogAdmin?.loading ?? false;
const isMod = GetSessionDataManager().isModerator; const isMod = GetSessionDataManager().isModerator;
@@ -148,13 +150,19 @@ const CatalogClassicViewInner: FC<{}> = () =>
{ adminMode && { adminMode &&
<div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }> <div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }>
<FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') } <FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') }
onClick={ () => { catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } /> onClick={ () =>
{
catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
} } />
<span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') } <span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') }
onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }> onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }>
{ isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> } { isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> }
</span> </span>
<FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') } <FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') }
onClick={ () => { if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId); } } /> onClick={ () =>
{
if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId);
} } />
</div> } </div> }
</div> </div>
</NitroCardTabsItemView> </NitroCardTabsItemView>
@@ -180,7 +188,10 @@ const CatalogClassicViewInner: FC<{}> = () =>
</button> </button>
<button <button
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors" className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } } onClick={ () =>
{
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
} }
> >
<FaEdit className="text-[8px]" /> <FaEdit className="text-[8px]" />
<span>{ LocalizeText('catalog.admin.root') }</span> <span>{ LocalizeText('catalog.admin.root') }</span>
+8 -3
View File
@@ -21,9 +21,11 @@ const CatalogModernViewInner: FC<{}> = () =>
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog(); const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
const catalogAdmin = useCatalogAdmin(); const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false; const adminMode = catalogAdmin?.adminMode ?? false;
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {}); const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
{});
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {}); const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
{});
const loading = catalogAdmin?.loading ?? false; const loading = catalogAdmin?.loading ?? false;
const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites(); const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites();
const [ showFavorites, setShowFavorites ] = useState(false); const [ showFavorites, setShowFavorites ] = useState(false);
@@ -170,7 +172,10 @@ const CatalogModernViewInner: FC<{}> = () =>
<button <button
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors" className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
title={ LocalizeText('catalog.admin.edit.root') } title={ LocalizeText('catalog.admin.edit.root') }
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } } onClick={ () =>
{
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
} }
> >
<FaEdit className="text-[8px]" /> <FaEdit className="text-[8px]" />
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.root') }</span> <span className="whitespace-nowrap">{ LocalizeText('catalog.admin.root') }</span>
@@ -93,13 +93,19 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
<div <div
key={ page.pageId } key={ page.pageId }
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100" className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
onClick={ () => { activateNode(page.node); onClose(); } } onClick={ () =>
{
activateNode(page.node); onClose();
} }
> >
<CatalogIconView icon={ page.iconId } /> <CatalogIconView icon={ page.iconId } />
<span className="text-[11px] flex-1 truncate font-medium">{ page.name }</span> <span className="text-[11px] flex-1 truncate font-medium">{ page.name }</span>
<FaTimes <FaTimes
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer" className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
onClick={ e => { e.stopPropagation(); toggleFavoritePage(page.pageId); } } onClick={ e =>
{
e.stopPropagation(); toggleFavoritePage(page.pageId);
} }
/> />
</div> </div>
)) } )) }
@@ -118,7 +124,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
<div <div
key={ fav.offerId } key={ fav.offerId }
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100" className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
onClick={ () => { openPageByOfferId(fav.offerId); onClose(); } } onClick={ () =>
{
openPageByOfferId(fav.offerId); onClose();
} }
> >
{ /* Furni icon */ } { /* Furni icon */ }
<div className="w-7 h-7 flex items-center justify-center shrink-0 bg-white rounded border border-card-grid-item-border overflow-hidden"> <div className="w-7 h-7 flex items-center justify-center shrink-0 bg-white rounded border border-card-grid-item-border overflow-hidden">
@@ -132,7 +141,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
<span className="text-[11px] flex-1 truncate font-medium">{ fav.displayName }</span> <span className="text-[11px] flex-1 truncate font-medium">{ fav.displayName }</span>
<FaTimes <FaTimes
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer" className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
onClick={ e => { e.stopPropagation(); toggleFavoriteOffer(fav.offerId); } } onClick={ e =>
{
e.stopPropagation(); toggleFavoriteOffer(fav.offerId);
} }
/> />
</div> </div>
)) } )) }
@@ -133,7 +133,10 @@ export const CatalogGiftView: FC<{}> = props =>
if(isBuyingGift) return; if(isBuyingGift) return;
isBuyingGift = true; isBuyingGift = true;
setTimeout(() => { isBuyingGift = false; }, 10000); setTimeout(() =>
{
isBuyingGift = false;
}, 10000);
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace)); SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
return; return;
@@ -126,7 +126,10 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
{ !adminMode && node.pageId > 0 && { !adminMode && node.pageId > 0 &&
<FaStar <FaStar
className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` } className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
onClick={ e => { e.stopPropagation(); toggleFavoritePage(node.pageId); } } onClick={ e =>
{
e.stopPropagation(); toggleFavoritePage(node.pageId);
} }
/> } /> }
{ node.isBranch && { node.isBranch &&
<span className="text-[9px] text-muted shrink-0"> <span className="text-[9px] text-muted shrink-0">
@@ -77,7 +77,10 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> } <LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
<div <div
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` } className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
onClick={ e => { e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl); } } onClick={ e =>
{
e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl);
} }
onMouseDown={ e => e.stopPropagation() } onMouseDown={ e => e.stopPropagation() }
> >
<FaHeart className={ `text-[10px] drop-shadow transition-colors duration-100 ${ isFav ? 'text-danger' : 'text-muted hover:text-danger' }` } /> <FaHeart className={ `text-[10px] drop-shadow transition-colors duration-100 ${ isFav ? 'text-danger' : 'text-muted hover:text-danger' }` } />
@@ -81,7 +81,7 @@ export const CatalogSearchView: FC<{}> = () =>
FilterCatalogNode(search, foundFurniLines, rootNode, nodes); FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible)))); setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage)); setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1)));
}, 300); }, 300);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
@@ -149,7 +149,8 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
const meta = getOfferMeta(offer); const meta = getOfferMeta(offer);
return ( return (
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () => { <LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () =>
{
setPurchaseState(CatalogPurchaseState.NONE); setPurchaseState(CatalogPurchaseState.NONE);
setPendingOffer(offer); setPendingOffer(offer);
} }> } }>
@@ -117,7 +117,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
if(!prefixText.length) return; if(!prefixText.length) return;
const newColors: Record<number, string> = {}; const newColors: Record<number, string> = {};
[ ...prefixText ].forEach((_, i) => { newColors[i] = customColorInput; }); [ ...prefixText ].forEach((_, i) =>
{
newColors[i] = customColorInput;
});
setLetterColors(newColors); setLetterColors(newColors);
}; };
@@ -222,7 +225,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<Picker <Picker
data={ data } data={ data }
locale="it" locale="it"
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } } onEmojiSelect={ (emoji: { native: string }) =>
{
setSelectedIcon(emoji.native); setShowIconPicker(false);
} }
theme="dark" theme="dark"
previewPosition="none" previewPosition="none"
skinTonePosition="search" skinTonePosition="search"
@@ -268,7 +274,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
borderRight: '1px solid rgba(0,0,0,0.1)', borderRight: '1px solid rgba(0,0,0,0.1)',
opacity: colorMode === 'single' ? 1 : 0.6 opacity: colorMode === 'single' ? 1 : 0.6
} } } }
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }> onClick={ () =>
{
setColorMode('single'); setSelectedLetterIndex(null);
} }>
{ LocalizeText('catalog.prefix.color.single') } { LocalizeText('catalog.prefix.color.single') }
</button> </button>
<button <button
@@ -277,7 +286,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)', background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
opacity: colorMode === 'perLetter' ? 1 : 0.6 opacity: colorMode === 'perLetter' ? 1 : 0.6
} } } }
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }> onClick={ () =>
{
setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0);
} }>
{ LocalizeText('catalog.prefix.color.per.letter') } { LocalizeText('catalog.prefix.color.per.letter') }
</button> </button>
</div> </div>
@@ -328,7 +340,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
zIndex: isSelected ? 10 : 1, zIndex: isSelected ? 10 : 1,
boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none' boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none'
} } } }
onClick={ () => { setSelectedLetterIndex(i); setCustomColorInput(charColor); } }> onClick={ () =>
{
setSelectedLetterIndex(i); setCustomColorInput(charColor);
} }>
<span className="text-sm font-black" style={ { color: charColor } }> <span className="text-sm font-black" style={ { color: charColor } }>
{ char } { char }
</span> </span>
@@ -28,7 +28,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer" className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } onClick={ () =>
{
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
} }
> >
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') } <FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button> </button>
@@ -42,7 +42,10 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer" className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } onClick={ () =>
{
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
} }
> >
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') } <FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button> </button>
@@ -202,7 +202,10 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer" className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } onClick={ () =>
{
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
} }
> >
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') } <FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button> </button>
@@ -89,7 +89,10 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
isPurchasingCatalogItem = true; isPurchasingCatalogItem = true;
setPurchaseState(CatalogPurchaseState.PURCHASE); setPurchaseState(CatalogPurchaseState.PURCHASE);
setTimeout(() => { isPurchasingCatalogItem = false; }, 10000); setTimeout(() =>
{
isPurchasingCatalogItem = false;
}, 10000);
if(purchaseCallback) if(purchaseCallback)
{ {
@@ -5,7 +5,8 @@ import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text }
import { useChatHistory, useOnClickChat } from '../../hooks'; import { useChatHistory, useOnClickChat } from '../../hooks';
import { NitroInput } from '../../layout'; import { NitroInput } from '../../layout';
export const ChatHistoryView: FC<{}> = props => { export const ChatHistoryView: FC<{}> = props =>
{
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const { chatHistory = [] } = useChatHistory(); const { chatHistory = [] } = useChatHistory();
@@ -14,10 +15,12 @@ export const ChatHistoryView: FC<{}> = props => {
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const prevChatLength = useRef<number>(0); const prevChatLength = useRef<number>(0);
const filteredChatHistory = useMemo(() => { const filteredChatHistory = useMemo(() =>
{
let result = chatHistory; let result = chatHistory;
if (searchText.length > 0) { if (searchText.length > 0)
{
const text = searchText.toLowerCase(); const text = searchText.toLowerCase();
result = chatHistory.filter(entry => result = chatHistory.filter(entry =>
(entry.message && entry.message.toLowerCase().includes(text)) || (entry.message && entry.message.toLowerCase().includes(text)) ||
@@ -28,31 +31,38 @@ export const ChatHistoryView: FC<{}> = props => {
return [...result]; return [...result];
}, [chatHistory, searchText]); }, [chatHistory, searchText]);
useEffect(() => { useEffect(() =>
{
if (!elementRef.current || !isVisible) return; if (!elementRef.current || !isVisible) return;
const element = elementRef.current; const element = elementRef.current;
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight); const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50; const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50;
if (isFirstRender.current) { if (isFirstRender.current)
{
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' }); element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
isFirstRender.current = false; isFirstRender.current = false;
} else if (filteredChatHistory.length > prevChatLength.current) { }
else if (filteredChatHistory.length > prevChatLength.current)
{
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' }); element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
} }
prevChatLength.current = filteredChatHistory.length; prevChatLength.current = filteredChatHistory.length;
}, [filteredChatHistory, isVisible]); }, [filteredChatHistory, isVisible]);
useEffect(() => { useEffect(() =>
{
const linkTracker: ILinkEventTracker = { const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => { linkReceived: (url: string) =>
{
const parts = url.split('/'); const parts = url.split('/');
if (parts.length < 2) return; if (parts.length < 2) return;
switch (parts[1]) { switch (parts[1])
{
case 'show': case 'show':
setIsVisible(true); setIsVisible(true);
return; return;
@@ -572,7 +572,10 @@ export const CustomizeNickIconView: FC<{}> = () =>
<Picker <Picker
data={ data } data={ data }
locale="en" locale="en"
onEmojiSelect={ (emoji: { native: string }) => { setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false); } } onEmojiSelect={ (emoji: { native: string }) =>
{
setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false);
} }
previewPosition="none" previewPosition="none"
set="native" set="native"
theme="dark" /> theme="dark" />
@@ -28,7 +28,7 @@ export const FloorplanImportExportView: FC<FloorplanImportExportViewProps> = pro
convertNumbersForSaving(originalFloorplanSettings.thicknessFloor), convertNumbersForSaving(originalFloorplanSettings.thicknessFloor),
originalFloorplanSettings.wallHeight - 1 originalFloorplanSettings.wallHeight - 1
)); ));
} };
useEffect(() => useEffect(() =>
{ {
@@ -52,4 +52,4 @@ export const FloorplanImportExportView: FC<FloorplanImportExportViewProps> = pro
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );
} };
+2 -1
View File
@@ -7,7 +7,8 @@ import { FriendsMessengerView } from './views/messenger/FriendsMessengerView';
const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ]; const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ];
export const FriendsView: FC<{}> = props => { export const FriendsView: FC<{}> = props =>
{
const { settings = null, onlineFriends = [], requests = [] } = useFriends(); const { settings = null, onlineFriends = [], requests = [] } = useFriends();
const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null); const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null);
@@ -5,17 +5,21 @@ import { LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common'
import { useFriends } from '../../../../hooks'; import { useFriends } from '../../../../hooks';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => { export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
{
const { friend = null } = props; const { friend = null } = props;
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { followFriend = null } = useFriends(); const { followFriend = null } = useFriends();
const elementRef = useRef<HTMLDivElement>(null); const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() =>
const onClick = (event: MouseEvent) => { {
const onClick = (event: MouseEvent) =>
{
const element = elementRef.current; const element = elementRef.current;
if (!element) return; if (!element) return;
if ((event.target !== element) && !element.contains((event.target as Node))) { if ((event.target !== element) && !element.contains((event.target as Node)))
{
setVisible(false); setVisible(false);
} }
}; };
@@ -23,7 +27,8 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick); return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
}, []); }, []);
if (!friend) { if (!friend)
{
return ( return (
<div ref={elementRef} className="relative"> <div ref={elementRef} className="relative">
<motion.button <motion.button
@@ -42,14 +47,17 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
initial={{ opacity: 0, y: 10, scale: 0.95 }} initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }} exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]" className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]"
> >
<div className="text-white text-[13px] drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div> <div className="text-white text-[13px] drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div>
<div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div> <div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div>
<button <button
className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] transition-colors cursor-pointer mt-1" className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] transition-colors cursor-pointer mt-1"
onClick={event => { event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false); }} onClick={event =>
{
event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false);
}}
> >
{LocalizeText('friend.bar.find.button')} {LocalizeText('friend.bar.find.button')}
</button> </button>
@@ -92,15 +100,24 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
initial={{ opacity: 0, y: 10, scale: 0.95 }} initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }} exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]" className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]"
> >
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div> <div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
<div className="flex justify-center gap-3 px-2"> <div className="flex justify-center gap-3 px-2">
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false); }} /> <div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event =>
{
event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false);
}} />
{friend.online && {friend.online &&
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); followFriend(friend); setVisible(false); }} />} <div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event =>
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); GetUserProfile(friend.id); setVisible(false); }} /> {
event.stopPropagation(); followFriend(friend); setVisible(false);
}} />}
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event =>
{
event.stopPropagation(); GetUserProfile(friend.id); setVisible(false);
}} />
</div> </div>
</motion.div> </motion.div>
)} )}
@@ -19,7 +19,8 @@ const itemVariants = {
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } }, exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } },
}; };
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props => { export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props =>
{
const { onlineFriends = [], requestsCount = 0 } = props; const { onlineFriends = [], requestsCount = 0 } = props;
const [ indexOffset, setIndexOffset ] = useState(0); const [ indexOffset, setIndexOffset ] = useState(0);
const elementRef = useRef<HTMLDivElement>(null); const elementRef = useRef<HTMLDivElement>(null);
@@ -44,7 +45,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<div <div
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` } className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
onClick={ () => { if(indexOffset > 0) setIndexOffset(indexOffset - 1); } } onClick={ () =>
{
if(indexOffset > 0) setIndexOffset(indexOffset - 1);
} }
> >
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" /> <FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
</div> </div>
@@ -91,7 +95,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<div <div
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` } className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
onClick={ () => { if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1); } } onClick={ () =>
{
if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1);
} }
> >
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" /> <FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
</div> </div>
@@ -148,7 +148,10 @@ export const FriendsListView: FC<{}> = props =>
<NitroCardAccordionView fullHeight overflow="hidden"> <NitroCardAccordionView fullHeight overflow="hidden">
<NitroCardAccordionSetView className="friends-list-section" headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }> <NitroCardAccordionSetView className="friends-list-section" headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }>
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end"> <Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(onlineFriends.map(friend => friend.id)); } }> <span className="friends-list-toolbar-link" onClick={ event =>
{
event.stopPropagation(); toggleSelectFriends(onlineFriends.map(friend => friend.id));
} }>
{ onlineFriends.length && onlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0)) { onlineFriends.length && onlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
? LocalizeText('friendlist.unselect_all') ? LocalizeText('friendlist.unselect_all')
: LocalizeText('friendlist.select_all') } : LocalizeText('friendlist.select_all') }
@@ -158,7 +161,10 @@ export const FriendsListView: FC<{}> = props =>
</NitroCardAccordionSetView> </NitroCardAccordionSetView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }> <NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end"> <Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(offlineFriends.map(friend => friend.id)); } }> <span className="friends-list-toolbar-link" onClick={ event =>
{
event.stopPropagation(); toggleSelectFriends(offlineFriends.map(friend => friend.id));
} }>
{ offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0)) { offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
? LocalizeText('friendlist.unselect_all') ? LocalizeText('friendlist.unselect_all')
: LocalizeText('friendlist.select_all') } : LocalizeText('friendlist.select_all') }
@@ -93,9 +93,9 @@ export const FurniEditorView: FC<{}> = () =>
loadBySpriteId(spriteId); loadBySpriteId(spriteId);
}; };
window.addEventListener('furni-editor:open', handler as EventListener); window.addEventListener('furni-editor:open', handler);
return () => window.removeEventListener('furni-editor:open', handler as EventListener); return () => window.removeEventListener('furni-editor:open', handler);
}, [ isMod, loadBySpriteId ]); }, [ isMod, loadBySpriteId ]);
const handleSelect = useCallback((id: number) => const handleSelect = useCallback((id: number) =>
@@ -58,7 +58,10 @@ export const GroupForumListView: FC<GroupForumListViewProps> = props =>
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
value={ listMode } value={ listMode }
onChange={ e => { setListMode(parseInt(e.target.value)); setStartIndex(0); } }> onChange={ e =>
{
setListMode(parseInt(e.target.value)); setStartIndex(0);
} }>
<option value={ 0 }>{ LocalizeText('groupforum.list.tab.most_active') }</option> <option value={ 0 }>{ LocalizeText('groupforum.list.tab.most_active') }</option>
<option value={ 2 }>{ LocalizeText('groupforum.list.tab.my_forums') }</option> <option value={ 2 }>{ LocalizeText('groupforum.list.tab.my_forums') }</option>
</select> </select>
@@ -89,7 +92,10 @@ export const GroupForumListView: FC<GroupForumListViewProps> = props =>
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }> <Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
{ (forum.lastMessageAuthorId > 0) && <> { (forum.lastMessageAuthorId > 0) && <>
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text> <Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(forum.lastMessageAuthorId); } }> <Text small pointer underline onClick={ e =>
{
e.stopPropagation(); GetUserProfile(forum.lastMessageAuthorId);
} }>
{ forum.lastMessageAuthorName } { forum.lastMessageAuthorName }
</Text> </Text>
<Text small variant="muted">{ formatTimeAgo(forum.lastMessageTimeAsSecondsAgo) }</Text> <Text small variant="muted">{ formatTimeAgo(forum.lastMessageTimeAsSecondsAgo) }</Text>
@@ -165,7 +165,10 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
</Flex> </Flex>
<Flex gap={ 1 }> <Flex gap={ 1 }>
<Text small variant="muted">{ LocalizeText('messageboard.started.by') }</Text> <Text small variant="muted">{ LocalizeText('messageboard.started.by') }</Text>
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.authorId); } }> <Text small pointer underline onClick={ e =>
{
e.stopPropagation(); GetUserProfile(thread.authorId);
} }>
{ thread.authorName } { thread.authorName }
</Text> </Text>
<Text small variant="muted">- { formatTimeAgo(thread.creationTimeAsSecondsAgo) }</Text> <Text small variant="muted">- { formatTimeAgo(thread.creationTimeAsSecondsAgo) }</Text>
@@ -182,7 +185,10 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
</Column> } </Column> }
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }> <Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text> <Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.lastUserId); } }> <Text small pointer underline onClick={ e =>
{
e.stopPropagation(); GetUserProfile(thread.lastUserId);
} }>
{ thread.lastUserName } { thread.lastUserName }
</Text> </Text>
<Text small variant="muted">{ formatTimeAgo(thread.lastCommentTime) }</Text> <Text small variant="muted">{ formatTimeAgo(thread.lastCommentTime) }</Text>
+2 -1
View File
@@ -5,7 +5,8 @@ import { Base } from '../../common';
export interface RoomWidgetViewProps {} export interface RoomWidgetViewProps {}
export const RoomWidgetView: FC<RoomWidgetViewProps> = props => { export const RoomWidgetView: FC<RoomWidgetViewProps> = props =>
{
const poolId = GetConfigurationValue<string>('hotelview')['room.pool']; const poolId = GetConfigurationValue<string>('hotelview')['room.pool'];
const picnicId = GetConfigurationValue<string>('hotelview')['room.picnic']; const picnicId = GetConfigurationValue<string>('hotelview')['room.picnic'];
const rooftopId = GetConfigurationValue<string>('hotelview')['room.rooftop']; const rooftopId = GetConfigurationValue<string>('hotelview')['room.rooftop'];
@@ -126,7 +126,8 @@ export const InterfaceColorTabView: FC<{}> = () =>
setImportValue(''); setImportValue('');
setShowImport(false); setShowImport(false);
} }
catch(e) {} catch(e)
{}
}, [ importValue, updateSettings ]); }, [ importValue, updateSettings ]);
return ( return (
@@ -107,8 +107,14 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
} }
}, []); }, []);
useEffect(() => { refreshOwnCustomBadges(); }, [ refreshOwnCustomBadges ]); useEffect(() =>
useEffect(() => { ensureCustomBadgeTexts(); }, []); {
refreshOwnCustomBadges();
}, [ refreshOwnCustomBadges ]);
useEffect(() =>
{
ensureCustomBadgeTexts();
}, []);
const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]); const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]);
@@ -138,7 +144,8 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
await refreshOwnCustomBadges(); await refreshOwnCustomBadges();
refreshCustomBadgeTexts(); refreshCustomBadgeTexts();
} }
catch { /* error already surfaced server-side */ } catch
{ /* error already surfaced server-side */ }
}, },
null, null, null, null, null, null,
LocalizeText('inventory.delete.confirm_delete.title') LocalizeText('inventory.delete.confirm_delete.title')
+2 -1
View File
@@ -8,7 +8,8 @@ interface LoadingViewProps {
homeUrl?: string; homeUrl?: string;
} }
export const LoadingView: FC<LoadingViewProps> = props => { export const LoadingView: FC<LoadingViewProps> = props =>
{
const { isError = false, message = '', homeUrl = '' } = props; const { isError = false, message = '', homeUrl = '' } = props;
return ( return (
+107 -32
View File
@@ -27,8 +27,12 @@ const interpolate = (value: string | null | undefined): string =>
let output = value; let output = value;
try { output = GetConfiguration().interpolate(value) || value; } try
catch {} {
output = GetConfiguration().interpolate(value) || value;
}
catch
{}
return output.replace(/\$\{([^}]+)\}/g, (_, key: string) => return output.replace(/\$\{([^}]+)\}/g, (_, key: string) =>
{ {
@@ -45,7 +49,8 @@ const interpolate = (value: string | null | undefined): string =>
if(configValue) return configValue; if(configValue) return configValue;
} }
catch {} catch
{}
try try
{ {
@@ -53,7 +58,8 @@ const interpolate = (value: string | null | undefined): string =>
if(configValue) return configValue; if(configValue) return configValue;
} }
catch {} catch
{}
return ''; return '';
}); });
@@ -100,13 +106,20 @@ const readLock = (): AttemptState =>
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 }; if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
return JSON.parse(raw); return JSON.parse(raw);
} }
catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; } catch
{
return { attempts: 0, firstAt: 0, lockedUntil: 0 };
}
}; };
const writeLock = (state: AttemptState) => const writeLock = (state: AttemptState) =>
{ {
try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); } try
catch { } {
sessionStorage.setItem(LOCK_KEY, JSON.stringify(state));
}
catch
{ }
}; };
const normalizeLanguageCode = (value: string): string => const normalizeLanguageCode = (value: string): string =>
@@ -150,7 +163,8 @@ const readCachedLocale = (): LoginLocale =>
if(typeof settings.uiTextLanguage === 'string' && settings.uiTextLanguage.length) return resolveLoginLocale(settings.uiTextLanguage); if(typeof settings.uiTextLanguage === 'string' && settings.uiTextLanguage.length) return resolveLoginLocale(settings.uiTextLanguage);
} }
catch {} catch
{}
return getBrowserLocale(); return getBrowserLocale();
}; };
@@ -170,7 +184,8 @@ const applyLocaleSelection = (locale: LoginLocale): void =>
localStorage.setItem(CHAT_TRANSLATION_SETTINGS_KEY, JSON.stringify(nextSettings)); localStorage.setItem(CHAT_TRANSLATION_SETTINGS_KEY, JSON.stringify(nextSettings));
} }
catch {} catch
{}
}; };
const LoginSubmitButton: FC<{ isEntering: boolean; isLocked: boolean; loginPingingServer: boolean }> = ({ isEntering, isLocked, loginPingingServer }) => const LoginSubmitButton: FC<{ isEntering: boolean; isLocked: boolean; loginPingingServer: boolean }> = ({ isEntering, isLocked, loginPingingServer }) =>
@@ -411,8 +426,12 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
}); });
let payload: Record<string, unknown> = {}; let payload: Record<string, unknown> = {};
try { payload = await response.json(); } try
catch { } {
payload = await response.json();
}
catch
{ }
return { ok: response.ok, status: response.status, payload }; return { ok: response.ok, status: response.status, payload };
}, []); }, []);
@@ -705,7 +724,10 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
<button <button
type="button" type="button"
className="login-widget-button" className="login-widget-button"
onClick={ () => { if(btnLink) window.location.href = btnLink; } } onClick={ () =>
{
if(btnLink) window.location.href = btnLink;
} }
> >
{ btnText } { btnText }
</button> } </button> }
@@ -957,7 +979,12 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
if(resolved) return; if(resolved) return;
resolved = true; resolved = true;
if(timer !== null) window.clearTimeout(timer); if(timer !== null) window.clearTimeout(timer);
try { avatarImage?.dispose(); } catch {} try
{
avatarImage?.dispose();
}
catch
{}
avatarImage = null; avatarImage = null;
if(url) if(url)
{ {
@@ -976,17 +1003,26 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
const attempt = () => const attempt = () =>
{ {
if(resolved) return; if(resolved) return;
if(attempts >= AVATAR_PREVIEW_MAX_ATTEMPTS) { finish(''); return; } if(attempts >= AVATAR_PREVIEW_MAX_ATTEMPTS)
{
finish(''); return;
}
attempts++; attempts++;
try { avatarImage?.dispose(); } catch {} try
{
avatarImage?.dispose();
}
catch
{}
avatarImage = null; avatarImage = null;
try try
{ {
avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, { avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
resetFigure: () => attempt(), resetFigure: () => attempt(),
dispose: () => {}, dispose: () =>
{},
disposed: false disposed: false
}); });
} }
@@ -996,7 +1032,10 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
return; return;
} }
if(!avatarImage) { finish(''); return; } if(!avatarImage)
{
finish(''); return;
}
if(avatarImage.isPlaceholder()) return; if(avatarImage.isPlaceholder()) return;
@@ -1036,7 +1075,10 @@ const useAvatarPreview = (figure: string, gender: GenderKey, setType: string): s
{ {
if(!cancelled) setUrl(result); if(!cancelled) setUrl(result);
}); });
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, [ figure, gender, setType ]); }, [ figure, gender, setType ]);
return url; return url;
@@ -1061,7 +1103,10 @@ const AvatarPartRow: FC<AvatarPartRowProps> = ({ setType, selection, gender, onP
<div className="avatar-part-row"> <div className="avatar-part-row">
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } onClick={ onPrev }>&lsaquo;</button> <button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } onClick={ onPrev }>&lsaquo;</button>
<div className={ `part-preview part-preview-${ setType }` }> <div className={ `part-preview part-preview-${ setType }` }>
{ url && <img src={ url } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> } { url && <img src={ url } alt={ `${ setType } preview` } onError={ e =>
{
(e.currentTarget).style.visibility = 'hidden';
} } /> }
</div> </div>
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } onClick={ onNext }>&rsaquo;</button> <button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } onClick={ onNext }>&rsaquo;</button>
</div> </div>
@@ -1108,7 +1153,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
const ok = await onCheckServer(); const ok = await onCheckServer();
if(!cancelled) setServerReachable(ok); if(!cancelled) setServerReachable(ok);
})(); })();
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, [ onCheckServer ]); }, [ onCheckServer ]);
const resetWidget = useCallback(() => const resetWidget = useCallback(() =>
@@ -1117,15 +1165,24 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
setResetSignal(prev => prev + 1); setResetSignal(prev => prev + 1);
}, []); }, []);
useEffect(() => { setLocalError(null); }, [ step ]); useEffect(() =>
{
setLocalError(null);
}, [ step ]);
const [ figureData, setFigureData ] = useState<FigureData | null>(null); const [ figureData, setFigureData ] = useState<FigureData | null>(null);
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', ''); const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
const figureDataUrl = useMemo(() => const figureDataUrl = useMemo(() =>
{ {
if(!figureDataUrlRaw) return ''; if(!figureDataUrlRaw) return '';
try { return GetConfiguration().interpolate(figureDataUrlRaw); } try
catch { return figureDataUrlRaw; } {
return GetConfiguration().interpolate(figureDataUrlRaw);
}
catch
{
return figureDataUrlRaw;
}
}, [ figureDataUrlRaw ]); }, [ figureDataUrlRaw ]);
useEffect(() => useEffect(() =>
@@ -1134,9 +1191,16 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
let cancelled = false; let cancelled = false;
fetch(figureDataUrl, { credentials: 'omit' }) fetch(figureDataUrl, { credentials: 'omit' })
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.then(json => { if(!cancelled && json) setFigureData(json as FigureData); }) .then(json =>
.catch(() => { }); {
return () => { cancelled = true; }; if(!cancelled && json) setFigureData(json as FigureData);
})
.catch(() =>
{ });
return () =>
{
cancelled = true;
};
}, [ step, figureData, figureDataUrl ]); }, [ step, figureData, figureDataUrl ]);
const partOptions = useMemo(() => const partOptions = useMemo(() =>
@@ -1162,7 +1226,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
{ {
if(!PART_ROWS.includes(st.type)) continue; if(!PART_ROWS.includes(st.type)) continue;
const palette = figureData.palettes.find(p => p.id === st.paletteId); const palette = figureData.palettes.find(p => p.id === st.paletteId);
if(!palette) { result[st.type] = []; continue; } if(!palette)
{
result[st.type] = []; continue;
}
result[st.type] = palette.colors result[st.type] = palette.colors
.filter(c => c.selectable && c.club === 0) .filter(c => c.selectable && c.club === 0)
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() })); .map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
@@ -1199,12 +1266,16 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : ''; const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
const figure = typeof entry._figure === 'string' ? entry._figure : ''; const figure = typeof entry._figure === 'string' ? entry._figure : '';
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue; if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
parsed.push({ gender: rawGender as GenderKey, figure }); parsed.push({ gender: rawGender, figure });
} }
if(parsed.length) setHotLooks(parsed); if(parsed.length) setHotLooks(parsed);
}) })
.catch(() => { }); .catch(() =>
return () => { cancelled = true; }; { });
return () =>
{
cancelled = true;
};
}, [ step, hotLooks.length ]); }, [ step, hotLooks.length ]);
const applyLook = useCallback((figure: string, lookGender: GenderKey) => const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
@@ -1498,11 +1569,15 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
</div> </div>
<div className="avatar-preview"> <div className="avatar-preview">
{ previewSrc && <img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> } { previewSrc && <img src={ previewSrc } alt="Habbo preview" onError={ e =>
{
(e.currentTarget).style.visibility = 'hidden';
} } /> }
</div> </div>
<div className="avatar-color-col"> <div className="avatar-color-col">
{ PART_ROWS.map(setType => { { PART_ROWS.map(setType =>
{
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0; const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor; const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
const swatchHex = hexFor(setType, currentColor); const swatchHex = hexFor(setType, currentColor);
+12 -2
View File
@@ -67,7 +67,12 @@ export const TurnstileWidget: FC<TurnstileWidgetProps> = props =>
{ {
if(widgetIdRef.current && window.turnstile) if(widgetIdRef.current && window.turnstile)
{ {
try { window.turnstile.remove(widgetIdRef.current); } catch { } try
{
window.turnstile.remove(widgetIdRef.current);
}
catch
{ }
widgetIdRef.current = null; widgetIdRef.current = null;
} }
}; };
@@ -78,7 +83,12 @@ export const TurnstileWidget: FC<TurnstileWidgetProps> = props =>
if(resetSignal <= 0) return; if(resetSignal <= 0) return;
if(widgetIdRef.current && window.turnstile) if(widgetIdRef.current && window.turnstile)
{ {
try { window.turnstile.reset(widgetIdRef.current); } catch { } try
{
window.turnstile.reset(widgetIdRef.current);
}
catch
{ }
} }
}, [ resetSignal ]); }, [ resetSignal ]);
+20 -5
View File
@@ -45,7 +45,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
useEffect(() => useEffect(() =>
{ {
if(!newsUrl) { setFailed(true); return; } if(!newsUrl)
{
setFailed(true); return;
}
let cancelled = false; let cancelled = false;
const controller = new AbortController(); const controller = new AbortController();
@@ -63,7 +66,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
: Array.isArray(json) ? (json as RawNewsItem[]) : []; : Array.isArray(json) ? (json as RawNewsItem[]) : [];
setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1))); setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1)));
}) })
.catch(() => { if(!cancelled) setFailed(true); }); .catch(() =>
{
if(!cancelled) setFailed(true);
});
return () => return () =>
{ {
cancelled = true; cancelled = true;
@@ -87,8 +93,14 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
const current = items[Math.min(index, items.length - 1)]; const current = items[Math.min(index, items.length - 1)];
const hasMany = items.length > 1; const hasMany = items.length > 1;
const bumpAuto = () => setAutoTick(t => t + 1); const bumpAuto = () => setAutoTick(t => t + 1);
const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); }; const prev = () =>
const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); }; {
setIndex(i => (i - 1 + items.length) % items.length); bumpAuto();
};
const next = () =>
{
setIndex(i => (i + 1) % items.length); bumpAuto();
};
const safeLinkUrl = resolveNewsLink(current.linkUrl); const safeLinkUrl = resolveNewsLink(current.linkUrl);
const safeImageSrc = resolveNewsImage(current.image); const safeImageSrc = resolveNewsImage(current.image);
@@ -119,7 +131,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
<img <img
src={ safeImageSrc } src={ safeImageSrc }
alt={ current.title || 'news' } alt={ current.title || 'news' }
onError={ e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; } } onError={ e =>
{
(e.currentTarget).style.display = 'none';
} }
/> />
</div> </div>
} }
@@ -72,7 +72,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
const ok = await onCheckServer(); const ok = await onCheckServer();
if(!cancelled) setServerReachable(ok); if(!cancelled) setServerReachable(ok);
})(); })();
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, [ onCheckServer ]); }, [ onCheckServer ]);
const resetWidget = useCallback(() => const resetWidget = useCallback(() =>
@@ -81,7 +84,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
setResetSignal(prev => prev + 1); setResetSignal(prev => prev + 1);
}, []); }, []);
useEffect(() => { setLocalError(null); }, [ step ]); useEffect(() =>
{
setLocalError(null);
}, [ step ]);
const [ roomTemplates, setRoomTemplates ] = useState<RoomTemplate[] | null>(null); const [ roomTemplates, setRoomTemplates ] = useState<RoomTemplate[] | null>(null);
const [ roomTemplatesError, setRoomTemplatesError ] = useState<string | null>(null); const [ roomTemplatesError, setRoomTemplatesError ] = useState<string | null>(null);
@@ -92,8 +98,14 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
const figureDataUrl = useMemo(() => const figureDataUrl = useMemo(() =>
{ {
if(!figureDataUrlRaw) return ''; if(!figureDataUrlRaw) return '';
try { return GetConfiguration().interpolate(figureDataUrlRaw); } try
catch { return figureDataUrlRaw; } {
return GetConfiguration().interpolate(figureDataUrlRaw);
}
catch
{
return figureDataUrlRaw;
}
}, [ figureDataUrlRaw ]); }, [ figureDataUrlRaw ]);
useEffect(() => useEffect(() =>
@@ -102,9 +114,16 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
let cancelled = false; let cancelled = false;
fetch(figureDataUrl, { credentials: 'omit' }) fetch(figureDataUrl, { credentials: 'omit' })
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.then(json => { if(!cancelled && json) setFigureData(json as FigureData); }) .then(json =>
.catch(() => { }); {
return () => { cancelled = true; }; if(!cancelled && json) setFigureData(json as FigureData);
})
.catch(() =>
{ });
return () =>
{
cancelled = true;
};
}, [ step, figureData, figureDataUrl ]); }, [ step, figureData, figureDataUrl ]);
useEffect(() => useEffect(() =>
@@ -113,23 +132,29 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
let cancelled = false; let cancelled = false;
setRoomTemplatesError(null); setRoomTemplatesError(null);
fetch(roomTemplatesUrl, { credentials: 'include' }) fetch(roomTemplatesUrl, { credentials: 'include' })
.then(async r => { .then(async r =>
{
if(!r.ok) throw new Error(`status ${ r.status }`); if(!r.ok) throw new Error(`status ${ r.status }`);
return r.json(); return r.json();
}) })
.then(json => { .then(json =>
{
if(cancelled) return; if(cancelled) return;
const list = Array.isArray((json as { templates?: unknown })?.templates) const list = Array.isArray((json as { templates?: unknown })?.templates)
? (json as { templates: RoomTemplate[] }).templates ? (json as { templates: RoomTemplate[] }).templates
: []; : [];
setRoomTemplates(list); setRoomTemplates(list);
}) })
.catch(() => { .catch(() =>
{
if(cancelled) return; if(cancelled) return;
setRoomTemplates([]); setRoomTemplates([]);
setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.')); setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.'));
}); });
return () => { cancelled = true; }; return () =>
{
cancelled = true;
};
}, [ step, roomTemplates, roomTemplatesUrl ]); }, [ step, roomTemplates, roomTemplatesUrl ]);
const partOptions = useMemo(() => const partOptions = useMemo(() =>
@@ -155,7 +180,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
{ {
if(!PART_ROWS.includes(st.type)) continue; if(!PART_ROWS.includes(st.type)) continue;
const palette = figureData.palettes.find(p => p.id === st.paletteId); const palette = figureData.palettes.find(p => p.id === st.paletteId);
if(!palette) { result[st.type] = []; continue; } if(!palette)
{
result[st.type] = []; continue;
}
result[st.type] = palette.colors result[st.type] = palette.colors
.filter(c => c.selectable && c.club === 0) .filter(c => c.selectable && c.club === 0)
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() })); .map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
@@ -192,12 +220,16 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : ''; const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
const figure = typeof entry._figure === 'string' ? entry._figure : ''; const figure = typeof entry._figure === 'string' ? entry._figure : '';
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue; if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
parsed.push({ gender: rawGender as GenderKey, figure }); parsed.push({ gender: rawGender, figure });
} }
if(parsed.length) setHotLooks(parsed); if(parsed.length) setHotLooks(parsed);
}) })
.catch(() => { }); .catch(() =>
return () => { cancelled = true; }; { });
return () =>
{
cancelled = true;
};
}, [ step, hotLooks.length ]); }, [ step, hotLooks.length ]);
const applyLook = useCallback((figure: string, lookGender: GenderKey) => const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
@@ -499,14 +531,18 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
<div className="avatar-builder"> <div className="avatar-builder">
<div className="avatar-part-col"> <div className="avatar-part-col">
{ PART_ROWS.map(setType => { { PART_ROWS.map(setType =>
{
const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender); const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender);
return ( return (
<div className="avatar-part-row" key={ `part-${ setType }` }> <div className="avatar-part-row" key={ `part-${ setType }` }>
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } <button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` }
onClick={ () => cyclePart(setType, -1) }>&lsaquo;</button> onClick={ () => cyclePart(setType, -1) }>&lsaquo;</button>
<div className={ `part-preview part-preview-${ setType }` }> <div className={ `part-preview part-preview-${ setType }` }>
<img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> <img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e =>
{
(e.currentTarget).style.visibility = 'hidden';
} } />
</div> </div>
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } <button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` }
onClick={ () => cyclePart(setType, 1) }>&rsaquo;</button> onClick={ () => cyclePart(setType, 1) }>&rsaquo;</button>
@@ -516,11 +552,15 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
</div> </div>
<div className="avatar-preview"> <div className="avatar-preview">
<img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> <img src={ previewSrc } alt="Habbo preview" onError={ e =>
{
(e.currentTarget).style.visibility = 'hidden';
} } />
</div> </div>
<div className="avatar-color-col"> <div className="avatar-color-col">
{ PART_ROWS.map(setType => { { PART_ROWS.map(setType =>
{
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0; const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor; const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
const swatchHex = hexFor(setType, currentColor); const swatchHex = hexFor(setType, currentColor);
@@ -603,7 +643,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
onChange={ () => setSelectedTemplateId(template.templateId) } /> onChange={ () => setSelectedTemplateId(template.templateId) } />
{ template.thumbnail && { template.thumbnail &&
<img className="room-template-thumb" src={ template.thumbnail } alt={ template.title } <img className="room-template-thumb" src={ template.thumbnail } alt={ template.title }
onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> } onError={ e =>
{
(e.currentTarget).style.visibility = 'hidden';
} } /> }
<div className="room-template-body"> <div className="room-template-body">
<div className="room-template-title">{ template.title }</div> <div className="room-template-title">{ template.title }</div>
{ template.description && { template.description &&
+10 -3
View File
@@ -8,7 +8,8 @@ export const t = (key: string, fallback: string, params?: string[], replacements
const value = LocalizeText(key, params ?? null, replacements ?? null); const value = LocalizeText(key, params ?? null, replacements ?? null);
if(value && value !== key) return value; if(value && value !== key) return value;
} }
catch {} catch
{}
if(!params || !replacements) return fallback; if(!params || !replacements) return fallback;
let out = fallback; let out = fallback;
@@ -22,6 +23,12 @@ export const t = (key: string, fallback: string, params?: string[], replacements
export const interpolate = (value: string | null | undefined): string => export const interpolate = (value: string | null | undefined): string =>
{ {
if(!value) return ''; if(!value) return '';
try { return GetConfiguration().interpolate(value); } try
catch { return value; } {
return GetConfiguration().interpolate(value);
}
catch
{
return value;
}
}; };
+10 -3
View File
@@ -13,11 +13,18 @@ export const readLock = (): AttemptState =>
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 }; if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
return JSON.parse(raw); return JSON.parse(raw);
} }
catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; } catch
{
return { attempts: 0, firstAt: 0, lockedUntil: 0 };
}
}; };
export const writeLock = (state: AttemptState) => export const writeLock = (state: AttemptState) =>
{ {
try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); } try
catch { } {
sessionStorage.setItem(LOCK_KEY, JSON.stringify(state));
}
catch
{ }
}; };
+12 -3
View File
@@ -12,8 +12,14 @@ export const resolveNewsImage = (raw: string | null | undefined): string =>
if(value.startsWith('//')) return window.location.protocol + value; if(value.startsWith('//')) return window.location.protocol + value;
if(value.startsWith('/')) if(value.startsWith('/'))
{ {
try { return new URL(value, window.location.origin).href; } try
catch { return window.location.origin + value; } {
return new URL(value, window.location.origin).href;
}
catch
{
return window.location.origin + value;
}
} }
if(value.startsWith('data:')) if(value.startsWith('data:'))
{ {
@@ -46,5 +52,8 @@ export const resolveNewsLink = (raw: string | null | undefined): string =>
if(proto !== 'http:' && proto !== 'https:') return ''; if(proto !== 'http:' && proto !== 'https:') return '';
return url.href; return url.href;
} }
catch { return ''; } catch
{
return '';
}
}; };
@@ -34,7 +34,7 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
return ( return (
<NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Room Chatlog` } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ 'Room Chatlog' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" overflow="auto"> <NitroCardContentView className="text-black" overflow="auto">
{ roomChatlog && { roomChatlog &&
<ChatlogView records={ [ roomChatlog ] } /> } <ChatlogView records={ [ roomChatlog ] } /> }
@@ -21,7 +21,7 @@ export const NavigatorRoomSettingsAccessTabView: FC<NavigatorRoomSettingsTabView
if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return; if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return;
handleChange('password', password); handleChange('password', password);
} };
useEffect(() => useEffect(() =>
{ {
@@ -59,21 +59,21 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
CreateLinkEvent('navigator/search/myworld_view'); CreateLinkEvent('navigator/search/myworld_view');
}, },
null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title')); null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title'));
} };
const saveRoomName = () => const saveRoomName = () =>
{ {
if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return; if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return;
handleChange('name', roomName); handleChange('name', roomName);
} };
const saveRoomDescription = () => const saveRoomDescription = () =>
{ {
if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return; if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return;
handleChange('description', roomDescription); handleChange('description', roomDescription);
} };
const saveTags = (index: number) => const saveTags = (index: number) =>
{ {
@@ -86,7 +86,7 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
setTypeError(''); setTypeError('');
setTagIndex(index); setTagIndex(index);
handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]); handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]);
} };
useEffect(() => useEffect(() =>
{ {
@@ -27,12 +27,12 @@ export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewPro
if(index >= 0) newValue.splice(index, 1); if(index >= 0) newValue.splice(index, 1);
return newValue; return newValue;
}) });
SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId)); SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId));
setSelectedUserId(-1); setSelectedUserId(-1);
} };
useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event => useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event =>
{ {
@@ -115,4 +115,4 @@ export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewPro
</Column> </Column>
</Grid> </Grid>
); );
} };
@@ -189,7 +189,10 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
return ( return (
<NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings"> <NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } isInfoToHabboPages={ currentTab === TABS[3] } onClickInfoHabboPages={ () => { if(currentTab === TABS[3]) CreateLinkEvent('habbopages/chat/options'); } } onCloseClick={ onClose } /> <NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } isInfoToHabboPages={ currentTab === TABS[3] } onClickInfoHabboPages={ () =>
{
if(currentTab === TABS[3]) CreateLinkEvent('habbopages/chat/options');
} } onCloseClick={ onClose } />
<NitroCardTabsView> <NitroCardTabsView>
{ TABS.map(tab => { TABS.map(tab =>
{ {
@@ -100,8 +100,14 @@ export const NavigatorSearchResultItemInfoView: FC<NavigatorSearchResultItemInfo
ref={ elementRef } ref={ elementRef }
className="cursor-pointer nitro-icon icon-navigator-info" className="cursor-pointer nitro-icon icon-navigator-info"
onClick={ handleIconClick } onClick={ handleIconClick }
onMouseOver={ () => { if(!isControlled) setInternalVisible(true); } } onMouseOver={ () =>
onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } } {
if(!isControlled) setInternalVisible(true);
} }
onMouseLeave={ () =>
{
if(!isControlled) setInternalVisible(false);
} }
/> />
</Popover.Trigger> </Popover.Trigger>
<Popover.Portal> <Popover.Portal>
@@ -54,6 +54,6 @@ export const NitrobubbleHiddenView: FC<{}> = props =>
}, []); }, []);
if(!isVisible) return null; if(!isVisible) return null;
var stylecssnew = "<style>.newbubblehe { visibility: hidden !important; }</style>"; var stylecssnew = '<style>.newbubblehe { visibility: hidden !important; }</style>';
return ( <div dangerouslySetInnerHTML={ { __html: stylecssnew }} />); return ( <div dangerouslySetInnerHTML={ { __html: stylecssnew }} />);
} };
+9 -6
View File
@@ -157,7 +157,7 @@ const pluginApi: INitroPluginApi = {
// Create overlay container // Create overlay container
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.id = `nitro-plugin-window-${id}`; overlay.id = `nitro-plugin-window-${id}`;
overlay.style.cssText = `position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)`; overlay.style.cssText = 'position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)';
// Card wrapper // Card wrapper
const card = document.createElement('div'); const card = document.createElement('div');
@@ -165,14 +165,14 @@ const pluginApi: INitroPluginApi = {
// Header (draggable) // Header (draggable)
const header = document.createElement('div'); const header = document.createElement('div');
header.style.cssText = `display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none`; header.style.cssText = 'display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none';
const titleEl = document.createElement('span'); const titleEl = document.createElement('span');
titleEl.textContent = title; titleEl.textContent = title;
titleEl.style.cssText = `color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)`; titleEl.style.cssText = 'color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)';
const closeBtn = document.createElement('div'); const closeBtn = document.createElement('div');
closeBtn.style.cssText = `position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)`; closeBtn.style.cssText = 'position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)';
closeBtn.innerHTML = '✕'; closeBtn.innerHTML = '✕';
closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id)); closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id));
@@ -201,7 +201,10 @@ const pluginApi: INitroPluginApi = {
overlay.style.top = (e.clientY - offsetY) + 'px'; overlay.style.top = (e.clientY - offsetY) + 'px';
}; };
const onMouseUp = () => { isDragging = false; }; const onMouseUp = () =>
{
isDragging = false;
};
header.addEventListener('mousedown', onMouseDown); header.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove); document.addEventListener('mousemove', onMouseMove);
@@ -209,7 +212,7 @@ const pluginApi: INitroPluginApi = {
// Content area // Content area
const content = document.createElement('div'); const content = document.createElement('div');
content.style.cssText = `padding:16px`; content.style.cssText = 'padding:16px';
card.appendChild(header); card.appendChild(header);
card.appendChild(content); card.appendChild(content);
+26 -9
View File
@@ -8,7 +8,8 @@ import purseIcon from '../../assets/images/rightside/purse.gif';
import { CurrencyView } from './views/CurrencyView'; import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView'; import { SeasonalView } from './views/SeasonalView';
export const PurseView: FC<{}> = props => { export const PurseView: FC<{}> = props =>
{
const { purse = null, hcDisabled = false } = usePurse(); const { purse = null, hcDisabled = false } = usePurse();
const [ isOpen, setIsOpen ] = useState(true); const [ isOpen, setIsOpen ] = useState(true);
const [ isCompact, setIsCompact ] = useState(false); const [ isCompact, setIsCompact ] = useState(false);
@@ -16,7 +17,8 @@ export const PurseView: FC<{}> = props => {
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []); const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []); const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const getClubText = (() => { const getClubText = (() =>
{
if (!purse) return null; if (!purse) return null;
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays); const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
@@ -27,11 +29,13 @@ export const PurseView: FC<{}> = props => {
else return FriendlyTime.shortFormat(totalDays * 86400); else return FriendlyTime.shortFormat(totalDays * 86400);
})(); })();
const currencyTypes = useMemo(() => { const currencyTypes = useMemo(() =>
{
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return []; if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0)); const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
types.sort((a, b) => { types.sort((a, b) =>
{
if (a === 0) return -1; if (a === 0) return -1;
if (b === 0) return 1; if (b === 0) return 1;
if (a === 5) return -1; if (a === 5) return -1;
@@ -80,7 +84,8 @@ export const PurseView: FC<{}> = props => {
body: JSON.stringify({ ssoTicket, rememberToken }) body: JSON.stringify({ ssoTicket, rememberToken })
}); });
} }
catch { /* best-effort — proceed with local logout regardless */ } catch
{ /* best-effort — proceed with local logout regardless */ }
ClearRememberLogin(); ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
@@ -110,7 +115,10 @@ export const PurseView: FC<{}> = props => {
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) } { primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
</div> </div>
{ !hcDisabled && { !hcDisabled &&
<div className="nitro-purse-subscription" onClick={ event => { event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter'); } }> <div className="nitro-purse-subscription" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter');
} }>
<div className="nitro-purse-subscription__icon"> <div className="nitro-purse-subscription__icon">
<LayoutCurrencyIcon type="hc" /> <LayoutCurrencyIcon type="hc" />
</div> </div>
@@ -120,13 +128,22 @@ export const PurseView: FC<{}> = props => {
</div> </div>
</div> } </div> }
<div className="nitro-purse__actions"> <div className="nitro-purse__actions">
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event => { event.stopPropagation(); CreateLinkEvent('translation-settings/toggle'); } } title="Google Translate"> <button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('translation-settings/toggle');
} } title="Google Translate">
<FaLanguage /> <FaLanguage />
</button> </button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event => { event.stopPropagation(); CreateLinkEvent('help/show'); } } title={ LocalizeText('help.button.name') }> <button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('help/show');
} } title={ LocalizeText('help.button.name') }>
<FaQuestionCircle /> <FaQuestionCircle />
</button> </button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event => { event.stopPropagation(); CreateLinkEvent('user-settings/toggle'); } } title={ LocalizeText('widget.memenu.settings.title') }> <button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('user-settings/toggle');
} } title={ LocalizeText('widget.memenu.settings.title') }>
<i className="nitro-icon icon-cog" /> <i className="nitro-icon icon-cog" />
</button> </button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out"> <button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out">
+1 -1
View File
@@ -34,4 +34,4 @@ export const CurrencyView: FC<CurrencyViewProps> = props =>
</div> </div>
</div> </div>
); );
} };
+2 -1
View File
@@ -7,7 +7,8 @@ interface SeasonalViewProps {
amount: number; amount: number;
} }
export const SeasonalView: FC<SeasonalViewProps> = props => { export const SeasonalView: FC<SeasonalViewProps> = props =>
{
const { type = -1, amount = -1 } = props; const { type = -1, amount = -1 } = props;
const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue'); const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue');
const formattedAmount = LocalizeFormattedNumber(amount); const formattedAmount = LocalizeFormattedNumber(amount);
@@ -577,7 +577,10 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" /> <path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
</svg> </svg>
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text> <Text small wrap variant="white">Sprite: { (() =>
{
const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?';
})() }</Text>
</div> </div>
</div> } </div> }
{ (!avatarInfo.isWallItem && canMove) && { (!avatarInfo.isWallItem && canMove) &&
@@ -203,21 +203,25 @@ const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
</> </>
); );
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) => { export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) =>
{
const [remainingGrowTime, setRemainingGrowTime] = useState(0); const [remainingGrowTime, setRemainingGrowTime] = useState(0);
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0); const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
const { roomSession = null } = useRoom(); const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo(); const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
useEffect(() => { useEffect(() =>
{
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0); setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0); setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
}, [avatarInfo]); }, [avatarInfo]);
useEffect(() => { useEffect(() =>
{
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return; if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
const interval = setInterval(() => { const interval = setInterval(() =>
{
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1)); setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1)); setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
}, 1000); }, 1000);
@@ -226,12 +230,15 @@ export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatar
}, [avatarInfo]); }, [avatarInfo]);
const processButtonAction = useCallback( const processButtonAction = useCallback(
async (action: string) => { async (action: string) =>
try { {
try
{
let hideMenu = true; let hideMenu = true;
if (!action) return; if (!action) return;
switch (action) { switch (action)
{
case 'respect': case 'respect':
await respectPet(avatarInfo.id); await respectPet(avatarInfo.id);
if (petRespectRemaining - 1 >= 1) hideMenu = false; if (petRespectRemaining - 1 >= 1) hideMenu = false;
@@ -254,7 +261,9 @@ export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatar
} }
if (hideMenu) onClose(); if (hideMenu) onClose();
} catch (error) { }
catch (error)
{
console.error(`Failed to process action ${action}:`, error); console.error(`Failed to process action ${action}:`, error);
} }
}, },
@@ -16,7 +16,8 @@ interface InfoStandWidgetUserViewProps {
onClose: () => void; onClose: () => void;
} }
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => { export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =>
{
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props; const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
const [motto, setMotto] = useState<string>(null); const [motto, setMotto] = useState<string>(null);
const [isEditingMotto, setIsEditingMotto] = useState(false); const [isEditingMotto, setIsEditingMotto] = useState(false);
@@ -32,11 +33,18 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const infostandStandClass = `stand-${standId ?? 'default'}`; const infostandStandClass = `stand-${standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`; const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : ''; const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); const handleProfileClick = useCallback(() =>
{
GetUserProfile(avatarInfo.webID);
}, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []); const handleEditClick = useCallback((event: React.MouseEvent) =>
{
event.stopPropagation(); setIsVisible(prev => !prev);
}, []);
const saveMotto = (motto: string) => { const saveMotto = (motto: string) =>
{
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return; if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
roomSession.sendMottoMessage(motto); roomSession.sendMottoMessage(motto);
@@ -45,22 +53,26 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value); const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => { const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
{
event.stopPropagation(); event.stopPropagation();
switch (event.key) { switch (event.key)
{
case 'Enter': case 'Enter':
saveMotto((event.target as HTMLInputElement).value); saveMotto((event.target as HTMLInputElement).value);
return; return;
} }
}; };
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => { useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event =>
{
if (!avatarInfo || avatarInfo.webID !== event.userId) return; if (!avatarInfo || avatarInfo.webID !== event.userId) return;
// Deduplicate badges from server // Deduplicate badges from server
const seen = new Set<string>(); const seen = new Set<string>();
const dedupedBadges = event.badges.map(code => { const dedupedBadges = event.badges.map(code =>
{
if (!code || seen.has(code)) return ''; if (!code || seen.has(code)) return '';
seen.add(code); seen.add(code);
return code; return code;
@@ -70,7 +82,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
if (oldBadges === dedupedBadges.join('')) return; if (oldBadges === dedupedBadges.join('')) return;
setAvatarInfo(prevValue => { setAvatarInfo(prevValue =>
{
if (!prevValue) return prevValue; if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue); const newValue = CloneObject(prevValue);
@@ -79,10 +92,12 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
}); });
}); });
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => { useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event =>
{
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return; if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => { setAvatarInfo(prevValue =>
{
if (!prevValue) return prevValue; if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue); const newValue = CloneObject(prevValue);
@@ -103,10 +118,12 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
}); });
}); });
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => { useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event =>
{
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return; if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => { setAvatarInfo(prevValue =>
{
if (!prevValue) return prevValue; if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue); const newValue = CloneObject(prevValue);
@@ -119,7 +136,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
}); });
}); });
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => { useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event =>
{
const parser = event.getParser(); const parser = event.getParser();
if (!avatarInfo || avatarInfo.webID !== parser.userId) return; if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
@@ -127,7 +145,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
setRelationships(parser); setRelationships(parser);
}); });
useEffect(() => { useEffect(() =>
{
setIsEditingMotto(false); setIsEditingMotto(false);
setMotto(avatarInfo.motto); setMotto(avatarInfo.motto);
setBackgroundId(avatarInfo.backgroundId); setBackgroundId(avatarInfo.backgroundId);
@@ -137,7 +156,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
return () => { return () =>
{
setRelationships(null); setRelationships(null);
}; };
}, [avatarInfo]); }, [avatarInfo]);
@@ -188,7 +208,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
/> />
)} )}
<Column grow alignItems="center" gap={0}> <Column grow alignItems="center" gap={0}>
{ (() => { { (() =>
{
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5); const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER; const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
const showGroup = maxSlots <= 5; const showGroup = maxSlots <= 5;
@@ -196,23 +217,28 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const items: React.ReactNode[] = []; const items: React.ReactNode[] = [];
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
if(showGroup) { if(showGroup)
{
items.push( items.push(
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> <Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />} {avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
</Flex> </Flex>
); );
} else { }
else
{
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
} }
const startIdx = showGroup ? 1 : 2; const startIdx = showGroup ? 1 : 2;
for(let i = startIdx; i < maxSlots; i++) { for(let i = startIdx; i < maxSlots; i++)
{
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
} }
const rows: React.ReactNode[][] = []; const rows: React.ReactNode[][] = [];
for(let i = 0; i < items.length; i += 2) { for(let i = 0; i < items.length; i += 2)
{
rows.push(items.slice(i, i + 2)); rows.push(items.slice(i, i + 2));
} }
@@ -237,7 +263,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
<Flex grow alignItems="center" className="min-h-[18px]"> <Flex grow alignItems="center" className="min-h-[18px]">
{!isEditingMotto && ( {!isEditingMotto && (
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}> <Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
{motto}  {motto}
</Text> </Text>
)} )}
{isEditingMotto && ( {isEditingMotto && (
@@ -284,7 +284,10 @@ export const ChatInputView: FC<{}> = props =>
<ChatInputCommandSelectorView <ChatInputCommandSelectorView
commands={ filteredCommands } commands={ filteredCommands }
selectedIndex={ selectedIndex } selectedIndex={ selectedIndex }
onSelect={ (cmd) => { setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } onSelect={ (cmd) =>
{
setChatValue(':' + cmd.key + ' '); inputRef.current?.focus();
} }
onHover={ setSelectedIndex } onHover={ setSelectedIndex }
/> } /> }
<div className="flex-1 items-center input-sizer"> <div className="flex-1 items-center input-sizer">
@@ -69,7 +69,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
chooserSelectionVisualizer.clearAll(); chooserSelectionVisualizer.clearAll();
} }
} }
} };
const isChecked = (id: number) => checkedIds.includes(id); const isChecked = (id: number) => checkedIds.includes(id);
@@ -80,7 +80,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
setCheckAll(false); setCheckAll(false);
chooserSelectionVisualizer.clearAll(); chooserSelectionVisualizer.clearAll();
setSelectedItems([]); setSelectedItems([]);
} };
const filteredItems = useMemo(() => const filteredItems = useMemo(() =>
{ {
@@ -179,7 +179,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
alignItems="center" alignItems="center"
className={ classNames('rounded p-1', selectedItems.some(item => item.id === row.id) && 'bg-muted') } className={ classNames('rounded p-1', selectedItems.some(item => item.id === row.id) && 'bg-muted') }
pointer pointer
onClick={ () => { toggleItemSelection(row); if(pickallFurni) checkedId(row.id); } } onClick={ () =>
{
toggleItemSelection(row); if(pickallFurni) checkedId(row.id);
} }
> >
{ pickallFurni && ( { pickallFurni && (
<input <input
@@ -187,7 +190,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
type="checkbox" type="checkbox"
checked={ isChecked(row.id) } checked={ isChecked(row.id) }
onChange={ () => checkedId(row.id) } onChange={ () => checkedId(row.id) }
onClick={ e => { e.stopPropagation(); toggleItemSelection(row); } } onClick={ e =>
{
e.stopPropagation(); toggleItemSelection(row);
} }
/> />
)} )}
<Text truncate> <Text truncate>
@@ -30,7 +30,8 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
children = null, children = null,
collapsable = false, collapsable = false,
...rest ...rest
}) => { }) =>
{
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null }); const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
const [opacity, setOpacity] = useState(1); const [opacity, setOpacity] = useState(1);
const [isFading, setIsFading] = useState(false); const [isFading, setIsFading] = useState(false);
@@ -40,19 +41,24 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
const maxStackRef = useRef(-1000000); const maxStackRef = useRef(-1000000);
const updatePosition = useCallback( const updatePosition = useCallback(
(bounds: NitroRectangle, location: { x: number; y: number }) => { (bounds: NitroRectangle, location: { x: number; y: number }) =>
{
if (!bounds || !location || !elementRef.current) return; if (!bounds || !location || !elementRef.current) return;
let offset = -elementRef.current.offsetHeight; let offset = -elementRef.current.offsetHeight;
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType)) { if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType))
{
offset += bounds.height > 50 ? 15 : 0; offset += bounds.height > 50 ? 15 : 0;
} else { }
else
{
offset -= 14; offset -= 14;
} }
stackRef.current.addValue(location.y - bounds.top); stackRef.current.addValue(location.y - bounds.top);
let maxStack = stackRef.current.getMax(); let maxStack = stackRef.current.getMax();
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED) { if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED)
{
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED; maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
} }
maxStackRef.current = maxStack; maxStackRef.current = maxStack;
@@ -73,7 +79,8 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
[userType] [userType]
); );
const getClassNames = useMemo(() => { const getClassNames = useMemo(() =>
{
const classes = [ const classes = [
'nitro-context-menu', 'nitro-context-menu',
'p-[2px]!', 'p-[2px]!',
@@ -104,14 +111,17 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
[pos, opacity, isFading, style] [pos, opacity, isFading, style]
); );
useEffect(() => { useEffect(() =>
{
if (!elementRef.current) return; if (!elementRef.current) return;
const update = () => { const update = () =>
{
if (!elementRef.current) return; if (!elementRef.current) return;
const roomSession = GetRoomSession(); const roomSession = GetRoomSession();
if (!roomSession) { if (!roomSession)
{
onClose(); onClose();
return; return;
} }
@@ -127,10 +137,12 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
return () => ticker.remove(update); return () => ticker.remove(update);
}, [objectId, category, updatePosition, onClose]); }, [objectId, category, updatePosition, onClose]);
useEffect(() => { useEffect(() =>
{
if (!fades) return; if (!fades) return;
const timeout = setTimeout(() => { const timeout = setTimeout(() =>
{
setIsFading(true); setIsFading(true);
setTimeout(onClose, FADE_LENGTH); setTimeout(onClose, FADE_LENGTH);
}, FADE_DELAY); }, FADE_DELAY);
@@ -138,7 +150,8 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [fades, onClose]); }, [fades, onClose]);
useEffect(() => { useEffect(() =>
{
if (!isFading) return; if (!isFading) return;
setOpacity(0); setOpacity(0);
}, [isFading]); }, [isFading]);
@@ -4,16 +4,19 @@ import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks'; import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView'; import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
export const FurnitureExternalImageView: FC<{}> = props => { export const FurnitureExternalImageView: FC<{}> = props =>
{
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget(); const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
const { report = null } = useHelp(); const { report = null } = useHelp();
if (objectId === -1 || currentPhotoIndex === -1) return null; if (objectId === -1 || currentPhotoIndex === -1) return null;
const handleOpenFullPhoto = () => { const handleOpenFullPhoto = () =>
{
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png'); const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
if (photoUrl) { if (photoUrl)
console.log("Opened photo URL:", photoUrl); {
console.log('Opened photo URL:', photoUrl);
window.open(photoUrl, '_blank'); window.open(photoUrl, '_blank');
} }
}; };
@@ -7,7 +7,8 @@ import { Text } from '../../../../common';
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks'; import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi'; import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
export const RoomToolsWidgetView: FC<{}> = props => { export const RoomToolsWidgetView: FC<{}> = props =>
{
const [areBubblesMuted, setAreBubblesMuted] = useState(false); const [areBubblesMuted, setAreBubblesMuted] = useState(false);
const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false); const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false);
const [roomName, setRoomName] = useState<string>(null); const [roomName, setRoomName] = useState<string>(null);
@@ -27,19 +28,25 @@ export const RoomToolsWidgetView: FC<{}> = props => {
return subscribePlugins(() => setPlugins(getRegisteredPlugins())); return subscribePlugins(() => setPlugins(getRegisteredPlugins()));
}, []); }, []);
const handleToolClick = (action: string, value?: string) => { const handleToolClick = (action: string, value?: string) =>
{
if (!roomSession) return; if (!roomSession) return;
switch (action) { switch (action)
{
case 'settings': case 'settings':
CreateLinkEvent('navigator/toggle-room-info'); CreateLinkEvent('navigator/toggle-room-info');
return; return;
case 'zoom': case 'zoom':
setIsZoomedIn(prevValue => { setIsZoomedIn(prevValue =>
if (GetConfigurationValue('room.zoom.enabled', true)) { {
if (GetConfigurationValue('room.zoom.enabled', true))
{
const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1); const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1);
GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1); GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1);
} else { }
else
{
const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1); const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1);
if (geometry) geometry.performZoom(); if (geometry) geometry.performZoom();
} }
@@ -77,7 +84,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
} }
}; };
const onChangeRoomHistory = (roomId: number, roomName: string) => { const onChangeRoomHistory = (roomId: number, roomName: string) =>
{
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'); let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return; if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
@@ -88,7 +96,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
SetLocalStorage('nitro.room.history', newStorage); SetLocalStorage('nitro.room.history', newStorage);
}; };
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event => { useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event =>
{
const parser = event.getParser(); const parser = event.getParser();
if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return; if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
@@ -98,18 +107,22 @@ export const RoomToolsWidgetView: FC<{}> = props => {
onChangeRoomHistory(parser.data.roomId, parser.data.roomName); onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
}); });
useEffect(() => { useEffect(() =>
{
setIsOpen(true); setIsOpen(true);
const timeout = setTimeout(() => setIsOpen(false), 5000); const timeout = setTimeout(() => setIsOpen(false), 5000);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [roomName, roomOwner, roomTags]); }, [roomName, roomOwner, roomTags]);
useEffect(() => { useEffect(() =>
{
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]')); setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
}, []); }, []);
useEffect(() => { useEffect(() =>
const handleTabClose = () => { {
const handleTabClose = () =>
{
window.localStorage.removeItem('nitro.room.history'); window.localStorage.removeItem('nitro.room.history');
}; };
window.addEventListener('beforeunload', handleTabClose); window.addEventListener('beforeunload', handleTabClose);
+8 -2
View File
@@ -203,7 +203,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } /> <ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> } </motion.div> }
</AnimatePresence> </AnimatePresence>
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }> <motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event =>
{
setMeExpanded(value => !value); event.stopPropagation();
} }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } /> <LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
</motion.div> </motion.div>
{ (getTotalUnseen > 0) && { (getTotalUnseen > 0) &&
@@ -298,7 +301,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } /> <ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> } </motion.div> }
</AnimatePresence> </AnimatePresence>
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }> <motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event =>
{
setMeExpanded(value => !value); event.stopPropagation();
} }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } /> <LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
</motion.div> </motion.div>
{ (getTotalUnseen > 0) && { (getTotalUnseen > 0) &&
+200 -116
View File
@@ -1,31 +1,34 @@
import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer"; import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from "react"; import { FC, useEffect, useRef, useState } from 'react';
import ReactPlayer from "react-player/youtube"; import ReactPlayer from 'react-player/youtube';
import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api"; import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common"; import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from '../../common';
import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks"; import { useFurnitureYoutubeWidget, useMessageEvent } from '../../hooks';
const CONTROL_COMMAND_PREVIOUS_VIDEO = 0; const CONTROL_COMMAND_PREVIOUS_VIDEO = 0;
const CONTROL_COMMAND_NEXT_VIDEO = 1; const CONTROL_COMMAND_NEXT_VIDEO = 1;
const CONTROL_COMMAND_PAUSE_VIDEO = 2; const CONTROL_COMMAND_PAUSE_VIDEO = 2;
const CONTROL_COMMAND_CONTINUE_VIDEO = 3; const CONTROL_COMMAND_CONTINUE_VIDEO = 3;
const extractVideoId = (input: string): string => { const extractVideoId = (input: string): string =>
{
const patterns = [ const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/, /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
/^([a-zA-Z0-9_-]{11})$/, /^([a-zA-Z0-9_-]{11})$/,
]; ];
for (const pattern of patterns) { for (const pattern of patterns)
{
const match = input.match(pattern); const match = input.match(pattern);
if (match) return match[1]; if (match) return match[1];
} }
return input; return input;
}; };
export const YouTubePlayerView: FC<{}> = () => { export const YouTubePlayerView: FC<{}> = () =>
{
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [tab, setTab] = useState< | "player" | "playlist" | "spectators" | "settings" | "history" | "share" >("player"); const [tab, setTab] = useState< | 'player' | 'playlist' | 'spectators' | 'settings' | 'history' | 'share' >('player');
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState('');
const [isRoomMode, setIsRoomMode] = useState(false); const [isRoomMode, setIsRoomMode] = useState(false);
const [volume, setVolume] = useState(100); const [volume, setVolume] = useState(100);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
@@ -38,121 +41,174 @@ export const YouTubePlayerView: FC<{}> = () => {
const playerRef = useRef<ReactPlayer | null>(null); const playerRef = useRef<ReactPlayer | null>(null);
const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget(); const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget();
const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]); const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]);
const [broadcastVideo, setBroadcastVideo] = useState(""); const [broadcastVideo, setBroadcastVideo] = useState('');
const [broadcastSender, setBroadcastSender] = useState(""); const [broadcastSender, setBroadcastSender] = useState('');
const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]); const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]);
const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set()); const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set());
const [youtubeEnabled, setYoutubeEnabled] = useState(getYoutubeRoomEnabled()); const [youtubeEnabled, setYoutubeEnabled] = useState(getYoutubeRoomEnabled());
useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event => { useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event =>
{
setYoutubeEnabled(event.getParser().youtubeEnabled); setYoutubeEnabled(event.getParser().youtubeEnabled);
}); });
useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event => { useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event =>
{
const parser = event.getParser(); const parser = event.getParser();
setBroadcastVideo(parser.videoId); setBroadcastVideo(parser.videoId);
setBroadcastSender(parser.senderName); setBroadcastSender(parser.senderName);
setBroadcastPlaylist(parser.playlist); setBroadcastPlaylist(parser.playlist);
if (parser.videoId) { if (parser.videoId)
{
setInputValue(parser.videoId); setInputValue(parser.videoId);
setIsOpen(true); setIsOpen(true);
setTab("player"); setTab('player');
} else { }
setInputValue(""); else
setBroadcastVideo(""); {
setBroadcastSender(""); setInputValue('');
setBroadcastVideo('');
setBroadcastSender('');
setBroadcastPlaylist([]); setBroadcastPlaylist([]);
} }
}); });
useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event => { setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers(); }); useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event =>
{
setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers();
});
const sentWatchingRef = useRef(false); const sentWatchingRef = useRef(false);
const hasVideo = !!(inputValue && extractVideoId(inputValue)); const hasVideo = !!(inputValue && extractVideoId(inputValue));
useEffect(() => { useEffect(() =>
if (isOpen && hasVideo && !sentWatchingRef.current) { {
try { SendMessageComposer(new YouTubeRoomWatchingComposer(true)); } catch(e) {} if (isOpen && hasVideo && !sentWatchingRef.current)
{
try
{
SendMessageComposer(new YouTubeRoomWatchingComposer(true));
}
catch(e)
{}
sentWatchingRef.current = true; sentWatchingRef.current = true;
} else if ((!isOpen || !hasVideo) && sentWatchingRef.current) { }
try { SendMessageComposer(new YouTubeRoomWatchingComposer(false)); } catch(e) {} else if ((!isOpen || !hasVideo) && sentWatchingRef.current)
{
try
{
SendMessageComposer(new YouTubeRoomWatchingComposer(false));
}
catch(e)
{}
sentWatchingRef.current = false; sentWatchingRef.current = false;
} }
}, [isOpen, hasVideo]); }, [isOpen, hasVideo]);
const loadRoomUsers = () => { const loadRoomUsers = () =>
try { {
try
{
const roomSession = GetRoomSession(); const roomSession = GetRoomSession();
if (!roomSession) { setSpectators([]); return; } if (!roomSession)
{
setSpectators([]); return;
}
const users: { id: number; name: string; look: string }[] = []; const users: { id: number; name: string; look: string }[] = [];
const seen = new Set<number>(); const seen = new Set<number>();
for (let i = 0; i < 500; i++) { for (let i = 0; i < 500; i++)
{
const userData = roomSession.userDataManager.getUserDataByIndex(i); const userData = roomSession.userDataManager.getUserDataByIndex(i);
if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId)) { if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId))
{
seen.add(userData.userId); seen.add(userData.userId);
users.push({ id: userData.userId, name: userData.name, look: userData.figure }); users.push({ id: userData.userId, name: userData.name, look: userData.figure });
} }
} }
setSpectators(users); setSpectators(users);
} catch (e) { }
catch (e)
{
setSpectators([]); setSpectators([]);
} }
}; };
useEffect(() => { useEffect(() =>
{
if (isOpen) loadRoomUsers(); if (isOpen) loadRoomUsers();
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() =>
if (youtubeObjectId && youtubeObjectId !== -1) { {
if (youtubeObjectId && youtubeObjectId !== -1)
{
setIsRoomMode(true); setIsRoomMode(true);
if (roomVideoId) { if (roomVideoId)
{
setInputValue(roomVideoId); setInputValue(roomVideoId);
} }
} else { }
else
{
setIsRoomMode(false); setIsRoomMode(false);
} }
}, [youtubeObjectId, roomVideoId]); }, [youtubeObjectId, roomVideoId]);
useEffect(() => { useEffect(() =>
{
const handler = () => setIsOpen((p) => !p); const handler = () => setIsOpen((p) => !p);
window.addEventListener("youtube:toggle", handler); window.addEventListener('youtube:toggle', handler);
return () => window.removeEventListener("youtube:toggle", handler); return () => window.removeEventListener('youtube:toggle', handler);
}, []); }, []);
useEffect(() => { useEffect(() =>
const savedHistory = localStorage.getItem("youtube_history"); {
if (savedHistory) { const savedHistory = localStorage.getItem('youtube_history');
try { if (savedHistory)
{
try
{
const parsed = JSON.parse(savedHistory); const parsed = JSON.parse(savedHistory);
if (Array.isArray(parsed)) { if (Array.isArray(parsed))
setHistory(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean)); {
setHistory(parsed.map((entry: any) => typeof entry === 'string' ? entry : entry?.id).filter(Boolean));
} }
} catch (e) {}
} }
const savedPlaylist = localStorage.getItem("youtube_playlist"); catch (e)
if (savedPlaylist) { {}
try { }
const savedPlaylist = localStorage.getItem('youtube_playlist');
if (savedPlaylist)
{
try
{
const parsed = JSON.parse(savedPlaylist); const parsed = JSON.parse(savedPlaylist);
if (Array.isArray(parsed)) { if (Array.isArray(parsed))
setPlaylist(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean)); {
setPlaylist(parsed.map((entry: any) => typeof entry === 'string' ? entry : entry?.id).filter(Boolean));
} }
} catch (e) {} }
catch (e)
{}
} }
}, []); }, []);
useEffect(() => { useEffect(() =>
{
localStorage.setItem( localStorage.setItem(
"youtube_history", 'youtube_history',
JSON.stringify(history.slice(0, 50)), JSON.stringify(history.slice(0, 50)),
); );
}, [history]); }, [history]);
useEffect(() => { useEffect(() =>
localStorage.setItem("youtube_playlist", JSON.stringify(playlist)); {
localStorage.setItem('youtube_playlist', JSON.stringify(playlist));
}, [playlist]); }, [playlist]);
const addToHistory = (id: string) => { const addToHistory = (id: string) =>
{
if (!id) return; if (!id) return;
setHistory((prev) => { setHistory((prev) =>
{
const filtered = prev.filter((v) => v !== id); const filtered = prev.filter((v) => v !== id);
return [id, ...filtered].slice(0, 50); return [id, ...filtered].slice(0, 50);
}); });
@@ -199,9 +255,11 @@ export const YouTubePlayerView: FC<{}> = () => {
), ),
); );
const addToPlaylist = () => { const addToPlaylist = () =>
{
const id = extractVideoId(inputValue); const id = extractVideoId(inputValue);
if (id && !playlist.includes(id)) { if (id && !playlist.includes(id))
{
setPlaylist((p) => [...p, id]); setPlaylist((p) => [...p, id]);
} }
}; };
@@ -222,11 +280,12 @@ export const YouTubePlayerView: FC<{}> = () => {
label: string; label: string;
}) => ( }) => (
<button <button
onClick={() => { onClick={() =>
{
setVolume(value); setVolume(value);
setVolumePreset(value); setVolumePreset(value);
}} }}
className={`px-2 py-1 rounded text-xs ${volumePreset === value ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-2 py-1 rounded text-xs ${volumePreset === value ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
{label} {label}
</button> </button>
@@ -234,55 +293,58 @@ export const YouTubePlayerView: FC<{}> = () => {
return ( return (
<NitroCardView <NitroCardView
className={`youtube-player-modal ${isFullscreen ? "!fixed inset-0 w-full h-full z-[9999] rounded-none" : "w-[550px]"}`} className={`youtube-player-modal ${isFullscreen ? '!fixed inset-0 w-full h-full z-[9999] rounded-none' : 'w-[550px]'}`}
> >
<NitroCardHeaderView <NitroCardHeaderView
headerText={isRoomMode ? "📺 YouTube TV" : "▶ YouTube"} headerText={isRoomMode ? '📺 YouTube TV' : '▶ YouTube'}
onCloseClick={() => setIsOpen(false)} onCloseClick={() => setIsOpen(false)}
/> />
<NitroCardContentView> <NitroCardContentView>
<div className="flex gap-1 mb-3 border-b border-gray-700 pb-2 flex-wrap"> <div className="flex gap-1 mb-3 border-b border-gray-700 pb-2 flex-wrap">
<button <button
onClick={() => setTab("player")} onClick={() => setTab('player')}
className={`px-3 py-1 rounded text-sm ${tab === "player" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-3 py-1 rounded text-sm ${tab === 'player' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
</button> </button>
<button <button
onClick={() => setTab("playlist")} onClick={() => setTab('playlist')}
className={`px-3 py-1 rounded text-sm ${tab === "playlist" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-3 py-1 rounded text-sm ${tab === 'playlist' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
📋 {playlist.length} 📋 {playlist.length}
</button> </button>
<button <button
onClick={() => setTab("history")} onClick={() => setTab('history')}
className={`px-3 py-1 rounded text-sm ${tab === "history" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-3 py-1 rounded text-sm ${tab === 'history' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
🕐 {history.length} 🕐 {history.length}
</button> </button>
<button <button
onClick={() => setTab("share")} onClick={() => setTab('share')}
className={`px-3 py-1 rounded text-sm ${tab === "share" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-3 py-1 rounded text-sm ${tab === 'share' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
📤 📤
</button> </button>
{watcherIds.size > 0 && ( {watcherIds.size > 0 && (
<button <button
onClick={() => { setTab("spectators"); loadRoomUsers(); }} onClick={() =>
className={`px-3 py-1 rounded text-sm ${tab === "spectators" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} {
setTab('spectators'); loadRoomUsers();
}}
className={`px-3 py-1 rounded text-sm ${tab === 'spectators' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
📺 {watcherIds.size} 📺 {watcherIds.size}
</button> </button>
)} )}
<button <button
onClick={() => setTab("settings")} onClick={() => setTab('settings')}
className={`px-3 py-1 rounded text-sm ${tab === "settings" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`} className={`px-3 py-1 rounded text-sm ${tab === 'settings' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
> >
</button> </button>
</div> </div>
{tab === "player" && ( {tab === 'player' && (
<> <>
{isRoomMode && ( {isRoomMode && (
<div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm"> <div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm">
@@ -311,10 +373,13 @@ export const YouTubePlayerView: FC<{}> = () => {
{videoId ? ( {videoId ? (
<ReactPlayer <ReactPlayer
ref={ref => { playerRef.current = ref; }} ref={ref =>
{
playerRef.current = ref;
}}
url={`https://www.youtube.com/watch?v=${videoId}`} url={`https://www.youtube.com/watch?v=${videoId}`}
width="100%" width="100%"
height={isFullscreen ? "100%" : 280} height={isFullscreen ? '100%' : 280}
playing playing
muted={isMuted} muted={isMuted}
loop={isLooping} loop={isLooping}
@@ -347,7 +412,7 @@ export const YouTubePlayerView: FC<{}> = () => {
} }
className="px-4 py-1 bg-amber-600 rounded text-white font-bold text-sm" className="px-4 py-1 bg-amber-600 rounded text-white font-bold text-sm"
> >
{isPlaying ? "⏸" : "▶"} {isPlaying ? '⏸' : '▶'}
</button> </button>
<button <button
onClick={handleNext} onClick={handleNext}
@@ -363,12 +428,16 @@ export const YouTubePlayerView: FC<{}> = () => {
<span className="text-purple-300">📡 {broadcastSender} broadcasting</span> <span className="text-purple-300">📡 {broadcastSender} broadcasting</span>
{isMyRoom && ( {isMyRoom && (
<button <button
onClick={() => { onClick={() =>
try { {
SendMessageComposer(new YouTubeRoomPlayComposer("", [])); try
} catch(e) {} {
setBroadcastVideo(""); SendMessageComposer(new YouTubeRoomPlayComposer('', []));
setBroadcastSender(""); }
catch(e)
{}
setBroadcastVideo('');
setBroadcastSender('');
setBroadcastPlaylist([]); setBroadcastPlaylist([]);
}} }}
className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-white text-xs" className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-white text-xs"
@@ -385,15 +454,19 @@ export const YouTubePlayerView: FC<{}> = () => {
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
disabled={!!broadcastVideo && !isMyRoom} disabled={!!broadcastVideo && !isMyRoom}
className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? "bg-gray-800" : "bg-gray-700"}`} className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? 'bg-gray-800' : 'bg-gray-700'}`}
placeholder="YouTube URL / video ID" placeholder="YouTube URL / video ID"
/> />
{isMyRoom && youtubeEnabled && videoId && ( {isMyRoom && youtubeEnabled && videoId && (
<button <button
onClick={() => { onClick={() =>
try { {
try
{
SendMessageComposer(new YouTubeRoomPlayComposer(videoId, playlist)); SendMessageComposer(new YouTubeRoomPlayComposer(videoId, playlist));
} catch(e) {} }
catch(e)
{}
}} }}
className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap" className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap"
title="Speel deze video voor iedereen in de kamer" title="Speel deze video voor iedereen in de kamer"
@@ -405,7 +478,7 @@ export const YouTubePlayerView: FC<{}> = () => {
</> </>
)} )}
{tab === "playlist" && ( {tab === 'playlist' && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -415,7 +488,7 @@ export const YouTubePlayerView: FC<{}> = () => {
placeholder="Add video URL..." placeholder="Add video URL..."
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm" className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
onKeyDown={(e) => onKeyDown={(e) =>
e.key === "Enter" && addToPlaylist() e.key === 'Enter' && addToPlaylist()
} }
/> />
<button <button
@@ -427,7 +500,7 @@ export const YouTubePlayerView: FC<{}> = () => {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setInputValue("")} onClick={() => setInputValue('')}
className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm" className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm"
> >
🔄 New video 🔄 New video
@@ -449,9 +522,10 @@ export const YouTubePlayerView: FC<{}> = () => {
<div <div
key={i} key={i}
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer" className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
onClick={() => { onClick={() =>
{
setInputValue(id); setInputValue(id);
setTab("player"); setTab('player');
}} }}
> >
<span className="text-amber-500 text-sm w-6"> <span className="text-amber-500 text-sm w-6">
@@ -461,7 +535,8 @@ export const YouTubePlayerView: FC<{}> = () => {
{id} {id}
</div> </div>
<button <button
onClick={(e) => { onClick={(e) =>
{
e.stopPropagation(); e.stopPropagation();
setPlaylist((p) => setPlaylist((p) =>
p.filter((x) => x !== id), p.filter((x) => x !== id),
@@ -478,7 +553,7 @@ export const YouTubePlayerView: FC<{}> = () => {
</div> </div>
)} )}
{tab === "history" && ( {tab === 'history' && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="text-gray-400 text-sm"> <div className="text-gray-400 text-sm">
@@ -501,9 +576,10 @@ export const YouTubePlayerView: FC<{}> = () => {
<div <div
key={i} key={i}
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer" className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
onClick={() => { onClick={() =>
{
setInputValue(id); setInputValue(id);
setTab("player"); setTab('player');
}} }}
> >
<div className="flex-1 min-w-0 text-white text-sm truncate font-mono"> <div className="flex-1 min-w-0 text-white text-sm truncate font-mono">
@@ -516,7 +592,7 @@ export const YouTubePlayerView: FC<{}> = () => {
</div> </div>
)} )}
{tab === "share" && ( {tab === 'share' && (
<div className="space-y-3"> <div className="space-y-3">
<div className="p-3 bg-gray-800 rounded"> <div className="p-3 bg-gray-800 rounded">
<div className="text-gray-400 text-sm mb-2"> <div className="text-gray-400 text-sm mb-2">
@@ -532,7 +608,8 @@ export const YouTubePlayerView: FC<{}> = () => {
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm" className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
/> />
<button <button
onClick={() => { onClick={() =>
{
navigator.clipboard.writeText( navigator.clipboard.writeText(
`https://youtube.com/watch?v=${videoId}`, `https://youtube.com/watch?v=${videoId}`,
); );
@@ -555,12 +632,14 @@ export const YouTubePlayerView: FC<{}> = () => {
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<button <button
onClick={() => { onClick={() =>
if (videoId) { {
if (videoId)
{
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent( const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
'Now watching: https://youtube.com/watch?v=${videoId}', 'Now watching: https://youtube.com/watch?v=${videoId}',
)}`; )}`;
window.open(url, "_blank"); window.open(url, '_blank');
} }
}} }}
disabled={!videoId} disabled={!videoId}
@@ -573,13 +652,17 @@ export const YouTubePlayerView: FC<{}> = () => {
</div> </div>
)} )}
{tab === "spectators" && (() => { {tab === 'spectators' && (() =>
{
const watchers: { id: number; name: string; look: string }[] = []; const watchers: { id: number; name: string; look: string }[] = [];
const rs = GetRoomSession(); const rs = GetRoomSession();
if (rs) { if (rs)
for (const uid of watcherIds) { {
for (const uid of watcherIds)
{
const ud = rs.userDataManager.getUserData(uid); const ud = rs.userDataManager.getUserData(uid);
if (ud && ud.name) { if (ud && ud.name)
{
watchers.push({ id: ud.userId, name: ud.name, look: ud.figure }); watchers.push({ id: ud.userId, name: ud.name, look: ud.figure });
} }
} }
@@ -623,7 +706,7 @@ export const YouTubePlayerView: FC<{}> = () => {
); );
})()} })()}
{tab === "settings" && ( {tab === 'settings' && (
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
@@ -636,7 +719,7 @@ export const YouTubePlayerView: FC<{}> = () => {
} }
className="text-gray-400 text-xs" className="text-gray-400 text-xs"
> >
{showVolumeSlider ? "▼" : "▲"} {showVolumeSlider ? '▼' : '▲'}
</button> </button>
</div> </div>
{showVolumeSlider && ( {showVolumeSlider && (
@@ -645,7 +728,8 @@ export const YouTubePlayerView: FC<{}> = () => {
min="0" min="0"
max="100" max="100"
value={volume} value={volume}
onChange={(e) => { onChange={(e) =>
{
setVolume(parseInt(e.target.value)); setVolume(parseInt(e.target.value));
setVolumePreset( setVolumePreset(
parseInt(e.target.value), parseInt(e.target.value),
@@ -702,19 +786,19 @@ export const YouTubePlayerView: FC<{}> = () => {
<div className="p-2 bg-gray-800 rounded text-xs text-gray-400"> <div className="p-2 bg-gray-800 rounded text-xs text-gray-400">
<div className="font-bold mb-1"> Info</div> <div className="font-bold mb-1"> Info</div>
<div> <div>
📡 Broadcast:{" "} 📡 Broadcast:{' '}
{broadcastVideo {broadcastVideo
? <span className="text-green-400"> Active ({broadcastSender} playing)</span> ? <span className="text-green-400"> Active ({broadcastSender} playing)</span>
: <span className="text-gray-500"> No video</span>} : <span className="text-gray-500"> No video</span>}
</div> </div>
<div> <div>
🎮 Controle:{" "} 🎮 Controle:{' '}
{isMyRoom {isMyRoom
? <span className="text-green-400"> You are the owner</span> ? <span className="text-green-400"> You are the owner</span>
: <span className="text-gray-500"> Viewing only</span>} : <span className="text-gray-500"> Viewing only</span>}
</div> </div>
<div> <div>
👁 Viewers:{" "} 👁 Viewers:{' '}
<span className="text-amber-400">{watcherIds.size}</span> <span className="text-amber-400">{watcherIds.size}</span>
</div> </div>
</div> </div>
@@ -194,7 +194,8 @@ export const WiredActionSendSignalView: FC<{}> = () =>
selectionCount={ antennaIds.length } selectionCount={ antennaIds.length }
selectionLimit={ selectionLimit } selectionLimit={ selectionLimit }
selectionEnabledValues={ [ SOURCE_SELECTED ] } selectionEnabledValues={ [ SOURCE_SELECTED ] }
onChange={ () => {} } onChange={ () =>
{} }
onSelectionActivate={ () => switchSelection('antenna') } /> onSelectionActivate={ () => switchSelection('antenna') } />
<WiredFurniSelectionSourceRow <WiredFurniSelectionSourceRow
title="Furni da mandare avanti:" title="Furni da mandare avanti:"
@@ -127,7 +127,7 @@ export const WiredExtraFilterByVariableView: FC<WiredExtraFilterByVariableViewPr
if(!variableToken) return mainEntries; if(!variableToken) return mainEntries;
if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries; if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries;
const fallbackEntry = createFallbackVariableEntry(target as WiredVariablePickerTarget, variableToken); const fallbackEntry = createFallbackVariableEntry(target, variableToken);
return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries; return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries;
}, [ mainEntries, target, variableToken ]); }, [ mainEntries, target, variableToken ]);
@@ -79,7 +79,7 @@ export const WiredExtraTextInputVariableView: FC<{}> = () =>
const itemId = getCustomVariableItemId(variableToken); const itemId = getCustomVariableItemId(variableToken);
return (targetDefinitions.find(definition => (definition.itemId === itemId)) ?? null) as IVariableDefinition | null; return (targetDefinitions.find(definition => (definition.itemId === itemId)) ?? null);
}, [ targetDefinitions, variableToken ]); }, [ targetDefinitions, variableToken ]);
const canUseTextDisplay = !!selectedVariableDefinition?.isTextConnected; const canUseTextDisplay = !!selectedVariableDefinition?.isTextConnected;
@@ -137,7 +137,7 @@ export const WiredSelectorWithVariableView: FC<WiredSelectorWithVariableViewProp
if(!variableToken) return mainEntries; if(!variableToken) return mainEntries;
if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries; if(flattenWiredVariablePickerEntries(mainEntries).some(entry => (entry.token === variableToken))) return mainEntries;
const fallbackEntry = createFallbackVariableEntry(selectorTarget as WiredVariablePickerTarget, variableToken); const fallbackEntry = createFallbackVariableEntry(selectorTarget, variableToken);
return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries; return fallbackEntry ? [ fallbackEntry, ...mainEntries ] : mainEntries;
}, [ mainEntries, selectorTarget, variableToken ]); }, [ mainEntries, selectorTarget, variableToken ]);
const selectedMainEntry = useMemo(() => flattenWiredVariablePickerEntries(resolvedMainEntries).find(entry => (entry.token === variableToken)) || null, [ resolvedMainEntries, variableToken ]); const selectedMainEntry = useMemo(() => flattenWiredVariablePickerEntries(resolvedMainEntries).find(entry => (entry.token === variableToken)) || null, [ resolvedMainEntries, variableToken ]);
+2 -1
View File
@@ -146,7 +146,8 @@ export const useFurniEditor = () =>
furniData = JSON.parse(parser.furniDataJson); furniData = JSON.parse(parser.furniDataJson);
} }
} }
catch(e) {} catch(e)
{}
setFurniDataEntry(furniData); setFurniDataEntry(furniData);
}); });
+2 -2
View File
@@ -59,7 +59,7 @@ const useInventoryBotsState = () =>
for(const botData of addedDatas) for(const botData of addedDatas)
{ {
const botItem = { botData } as IBotItem; const botItem = { botData };
const unseen = isUnseen(UnseenItemCategory.BOT, botData.id); const unseen = isUnseen(UnseenItemCategory.BOT, botData.id);
if(unseen) newValue.unshift(botItem); if(unseen) newValue.unshift(botItem);
@@ -82,7 +82,7 @@ const useInventoryBotsState = () =>
if(index >= 0) return prevValue; if(index >= 0) return prevValue;
const botItem = { botData: parser.item } as IBotItem; const botItem = { botData: parser.item };
const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id); const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id);
if(unseen) newValue.unshift(botItem); if(unseen) newValue.unshift(botItem);
@@ -13,7 +13,7 @@ const resolveUserType = (userType: number): string =>
case 3: return 'Bot'; case 3: return 'Bot';
default: return '-'; default: return '-';
} }
} };
const useUserChooserWidgetState = () => const useUserChooserWidgetState = () =>
{ {
+3 -3
View File
@@ -12,7 +12,7 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
{ {
try try
{ {
const item = typeof window !== 'undefined' ? GetLocalStorage<T>(key) as T : undefined; const item = typeof window !== 'undefined' ? GetLocalStorage<T>(key) : undefined;
return item ?? initialValue; return item ?? initialValue;
} }
@@ -37,9 +37,9 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
{ {
NitroLogger.error(error); NitroLogger.error(error);
} }
} };
return [ storedValue, setValue ]; return [ storedValue, setValue ];
} };
export const useLocalStorage = useLocalStorageState; export const useLocalStorage = useLocalStorageState;
+6 -6
View File
@@ -67,9 +67,9 @@ const useWiredState = () =>
{ {
if(!furniData) return null; if(!furniData) return null;
const rawValue = (furniData as any).interactionType const rawValue = (furniData).interactionType
?? (furniData as any).interactionTypeName ?? (furniData).interactionTypeName
?? (furniData as any).interactionTypeId; ?? (furniData).interactionTypeId;
if(rawValue === undefined || rawValue === null) return null; if(rawValue === undefined || rawValue === null) return null;
if(typeof rawValue !== 'string') return null; if(typeof rawValue !== 'string') return null;
@@ -83,9 +83,9 @@ const useWiredState = () =>
const values = [ const values = [
getInteractionTypeName(furniData), getInteractionTypeName(furniData),
(typeof (furniData as any).className === 'string') ? (furniData as any).className.toLowerCase() : null, (typeof (furniData).className === 'string') ? (furniData).className.toLowerCase() : null,
(typeof (furniData as any).fullName === 'string') ? (furniData as any).fullName.toLowerCase() : null, (typeof (furniData).fullName === 'string') ? (furniData).fullName.toLowerCase() : null,
(typeof (furniData as any).name === 'string') ? (furniData as any).name.toLowerCase() : null (typeof (furniData).name === 'string') ? (furniData).name.toLowerCase() : null
]; ];
return values.filter((value, index, array): value is string => !!value && (array.indexOf(value) === index)); return values.filter((value, index, array): value is string => !!value && (array.indexOf(value) === index));
+12 -6
View File
@@ -26,14 +26,16 @@ const getDeployBaseUrl = (): string =>
const loaderBase = (window as any).__nitroLoaderBase; const loaderBase = (window as any).__nitroLoaderBase;
if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString(); if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString();
} }
catch {} catch
{}
try try
{ {
const moduleUrl = (import.meta as any).url; const moduleUrl = (import.meta as any).url;
if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString(); if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString();
} }
catch {} catch
{}
try try
{ {
@@ -44,7 +46,8 @@ const getDeployBaseUrl = (): string =>
return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`; return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`;
} }
} }
catch {} catch
{}
return `${ window.location.origin }/`; return `${ window.location.origin }/`;
}; };
@@ -101,7 +104,8 @@ const setDebugState = (message: string): void =>
node.textContent = (window as any).__nitroSecureDebugLog.slice(-8).join('\n'); node.textContent = (window as any).__nitroSecureDebugLog.slice(-8).join('\n');
document.body.appendChild(node); document.body.appendChild(node);
} }
catch {} catch
{}
}; };
const textEncoder = new TextEncoder(); const textEncoder = new TextEncoder();
@@ -137,7 +141,8 @@ export const getClientMode = (): NitroClientMode =>
}; };
} }
} }
catch {} catch
{}
return { ...CLIENT_MODE_DEFAULTS }; return { ...CLIENT_MODE_DEFAULTS };
}; };
@@ -574,7 +579,8 @@ export const installSecureFetch = (): void =>
scheduleSecureRekey(); scheduleSecureRekey();
} }
} }
catch {} catch
{}
return decrypted; return decrypted;
} }