🆙 Small updates for the HK

This commit is contained in:
duckietm
2026-05-26 12:51:33 +02:00
parent b9bcf44192
commit 11702fa5e0
8 changed files with 37 additions and 116 deletions
+1
View File
@@ -25,6 +25,7 @@
"wired.action.mute.user.max.length": 100,
"game.center.enabled": false,
"guides.enabled": true,
"housekeeping.enabled": true,
"toolbar.hide.quests": true,
"catalog.style.new": true,
"show.google.ads": false,
@@ -33,15 +33,8 @@ const passesFilter = (entry: IHousekeepingActionLogEntry, target: TargetFilter,
export const HousekeepingAuditTab: FC = () =>
{
const { actionLog, refreshAuditLog, metricsByAction, resetActionMetrics } = useHousekeepingStore();
// Gated behind a UI-config flag (off by default) so non-debug
// operators don't see internal latency stats. The flag is read
// once at mount — flipping the config requires a reload, same
// as every other UI-config gate.
const telemetryEnabled = useMemo(() => GetConfigurationValue<boolean>('housekeeping.telemetry.enabled', false) === true, []);
const [ isTelemetryExpanded, setIsTelemetryExpanded ] = useState(false);
// Filter state is persisted per user so an HK who's iterating on
// "show me failures only, filtered to 'spam'" doesn't reset every
// time they switch tabs or close the panel.
const [ targetFilter, setTargetFilter ] = useLocalStorage<TargetFilter>('nitro.housekeeping.audit.target_filter', 'all');
const [ successFilter, setSuccessFilter ] = useLocalStorage<SuccessFilter>('nitro.housekeeping.audit.success_filter', 'all');
const [ query, setQuery ] = useLocalStorage<string>('nitro.housekeeping.audit.query', '');
@@ -73,7 +66,6 @@ export const HousekeepingAuditTab: FC = () =>
return (
<div className="flex flex-col gap-2">
{ /* Header w/ counts + refresh */ }
<div className="flex items-center justify-between gap-2">
<h3 className="text-xs uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaFilter size={ 10 } />
@@ -87,7 +79,7 @@ export const HousekeepingAuditTab: FC = () =>
</h3>
<Button size="sm" variant="secondary" disabled={ isRefreshing } onClick={ refresh }>
<FaSync size={ 9 } className={ isRefreshing ? 'animate-spin' : '' } />
<span className="ml-1">{ LocalizeText('housekeeping.audit.refresh') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.audit.refresh') }</span>
</Button>
</div>
@@ -70,26 +70,17 @@ export const HousekeepingDashboardTab: FC = () =>
const [ now, setNow ] = useState(() => Date.now());
const [ refreshedAt, setRefreshedAt ] = useState<number | null>(null);
// Tag every successful dashboard payload with a local timestamp.
// Triggered when the `dashboard` reference flips post-refresh.
useEffect(() =>
{
if(dashboard) setRefreshedAt(Date.now());
}, [ dashboard ]);
// Wall-clock tick — drives the "live X seconds ago" copy + the
// stale-banner trigger. Cheap (1 setState/s) and only runs while
// the tab is mounted.
useEffect(() =>
{
const id = setInterval(() => setNow(Date.now()), 1_000);
return () => clearInterval(id);
}, []);
// Auto-refresh: polls getDashboard every AUTO_REFRESH_MS while the
// tab is mounted. The HK panel's parent already kicks off an
// initial refresh on open, so this only handles the steady-state
// re-fetch; closing the tab unmounts the effect and stops the loop.
const refreshRef = useRef(refreshDashboard);
refreshRef.current = refreshDashboard;
@@ -161,7 +152,6 @@ export const HousekeepingDashboardTab: FC = () =>
return (
<div className="flex flex-col gap-2.5">
{ /* Header row: title + live status badge + refresh */ }
<div className="flex items-center justify-between">
<h3 className="text-xs uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaChartLine size={ 10 } />
@@ -176,7 +166,7 @@ export const HousekeepingDashboardTab: FC = () =>
</span>
<Button size="sm" variant="secondary" disabled={ isDashboardLoading } onClick={ () => refreshDashboard() }>
<FaSync size={ 9 } className={ isDashboardLoading ? 'animate-spin' : '' } />
<span className="ml-1">{ LocalizeText('housekeeping.dashboard.refresh') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.dashboard.refresh') }</span>
</Button>
</div>
</div>
@@ -196,7 +186,6 @@ export const HousekeepingDashboardTab: FC = () =>
{ dashboard &&
<>
{ /* Hero card: BIG online count + pulsing dot + peak today */ }
<div className="relative overflow-hidden rounded-lg border border-emerald-200 bg-gradient-to-br from-emerald-50 via-white to-sky-50 p-3 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -252,7 +241,6 @@ export const HousekeepingDashboardTab: FC = () =>
</div>
</div>
{ /* Quick hotel-alert inline */ }
<form onSubmit={ onSubmitAlert } className="flex flex-col gap-1.5 rounded-lg border border-amber-200 bg-amber-50/40 p-2.5">
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaBolt size={ 9 } className="text-amber-500" />
@@ -275,7 +263,6 @@ export const HousekeepingDashboardTab: FC = () =>
</form>
</> }
{ /* Recent sanctions */ }
{ recentSanctions.length > 0 &&
<div className="flex flex-col gap-1">
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
@@ -294,7 +281,6 @@ export const HousekeepingDashboardTab: FC = () =>
</ul>
</div> }
{ /* Recent lookups — clickable pills that re-select the target */ }
{ recentLookups.length > 0 &&
<div className="flex flex-col gap-1">
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
@@ -28,7 +28,6 @@ export const HousekeepingEconomyTab: FC = () =>
return (
<div className="flex flex-col gap-2.5">
{ /* Target banner */ }
{ !selectedUser
? (
<div className="flex items-center gap-2 rounded-lg border border-dashed border-amber-300 bg-amber-50/50 p-2.5 text-xs text-amber-700">
@@ -44,10 +43,7 @@ export const HousekeepingEconomyTab: FC = () =>
<div className="text-sm font-semibold tabular-nums">{ selectedUser.username } <span className="text-zinc-400 font-normal">#{ selectedUser.id }</span></div>
</div>
) }
{ /* Currency grants — tone-coded surfaces */ }
<div className="flex flex-col gap-1.5">
{ /* Credits — amber */ }
<div className="flex items-center gap-1.5 rounded-md border border-amber-200 bg-amber-50/40 px-2 py-1.5">
<LayoutCurrencyIcon type={ -1 } classNames={ [ 'shrink-0' ] } />
<input
@@ -58,11 +54,9 @@ export const HousekeepingEconomyTab: FC = () =>
onChange={ event => setCreditsAmount(parseInt(event.target.value) || 0) } />
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveCredits(selectedUser.id, creditsAmount) }>
<FaPiggyBank size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.economy.give_credits') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.economy.give_credits') }</span>
</Button>
</div>
{ /* Duckets — orange */ }
<div className="flex items-center gap-1.5 rounded-md border border-orange-200 bg-orange-50/40 px-2 py-1.5">
<LayoutCurrencyIcon type={ 0 } classNames={ [ 'shrink-0' ] } />
<input
@@ -73,11 +67,9 @@ export const HousekeepingEconomyTab: FC = () =>
onChange={ event => setDucketsAmount(parseInt(event.target.value) || 0) } />
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDuckets(selectedUser.id, ducketsAmount) }>
<FaPiggyBank size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.economy.give_duckets') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.economy.give_duckets') }</span>
</Button>
</div>
{ /* Diamonds — sky */ }
<div className="flex items-center gap-1.5 rounded-md border border-sky-200 bg-sky-50/40 px-2 py-1.5">
<LayoutCurrencyIcon type={ 5 } classNames={ [ 'shrink-0' ] } />
<input
@@ -88,12 +80,10 @@ export const HousekeepingEconomyTab: FC = () =>
onChange={ event => setDiamondsAmount(parseInt(event.target.value) || 0) } />
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDiamonds(selectedUser.id, diamondsAmount) }>
<FaPiggyBank size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.economy.give_diamonds') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.economy.give_diamonds') }</span>
</Button>
</div>
</div>
{ /* Grant item card */ }
<div className="flex flex-col gap-1.5 rounded-md border border-violet-200 bg-violet-50/40 p-2">
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaGift size={ 8 } className="text-violet-500" />
@@ -116,12 +106,10 @@ export const HousekeepingEconomyTab: FC = () =>
onChange={ event => setItemQuantity(parseInt(event.target.value) || 0) } />
<Button variant="primary" disabled={ disableUserActions || !itemId } className="grow" onClick={ () => grantItem(selectedUser.id, itemId, itemQuantity) }>
<FaGift size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.economy.grant_item') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.economy.grant_item') }</span>
</Button>
</div>
</div>
{ /* HC subscription */ }
<div className="flex items-center gap-1.5 rounded-md border border-amber-200 bg-gradient-to-r from-amber-50 to-yellow-50 px-2 py-1.5">
<FaCrown size={ 13 } className="text-amber-600 shrink-0" />
<input
@@ -136,8 +124,6 @@ export const HousekeepingEconomyTab: FC = () =>
<span className="ml-1">{ LocalizeText('housekeeping.economy.set_hc_days') }</span>
</Button>
</div>
{ /* Hotel-wide alert */ }
<div className="flex flex-col gap-1.5 rounded-md border border-rose-200 bg-rose-50/40 p-2">
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaBullhorn size={ 9 } className="text-rose-500" />
@@ -154,7 +140,6 @@ export const HousekeepingEconomyTab: FC = () =>
{
const dispatch = () => sendHotelAlert(trimmedAlert);
// Lungo alert hotel-wide → conferma esplicita.
if(trimmedAlert.length >= HOTEL_ALERT_CONFIRM_THRESHOLD)
{
confirm(LocalizeText('housekeeping.hotel.alert.confirm', [ 'count' ], [ String(trimmedAlert.length) ]), dispatch);
@@ -165,7 +150,7 @@ export const HousekeepingEconomyTab: FC = () =>
dispatch();
} }>
<FaBullhorn size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.hotel.alert.send') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.hotel.alert.send') }</span>
</Button>
</div>
</div>
@@ -16,15 +16,8 @@ export const HousekeepingRoomsTab: FC = () =>
const [ query, setQuery ] = useState('');
const [ muteMinutes, setMuteMinutes ] = useState<number>(DEFAULT_MUTE_MINUTES);
const [ newOwnerId, setNewOwnerId ] = useState<number>(0);
const confirm = useHousekeepingConfirm();
const currentRoomId = roomSession && roomSession.roomId > 0 ? roomSession.roomId : 0;
// Empty query + Cerca → fall back to the room the operator is
// currently standing in. Saves a copy-paste of the room id from
// navigator just to inspect "this room". Mirrors how /ban / /kick
// in chat default to the active room.
const submitLookup = () =>
{
const trimmed = query.trim();
@@ -53,7 +46,6 @@ export const HousekeepingRoomsTab: FC = () =>
return (
<div className="flex flex-col gap-2">
{ /* Lookup bar */ }
<div className="flex gap-1.5 items-center">
<div className="flex items-center gap-1 grow rounded-md border border-zinc-300 bg-white px-2 py-1 shadow-sm focus-within:ring-1 focus-within:ring-sky-300 focus-within:border-sky-400 transition-colors">
<FaSearch className="text-zinc-400 shrink-0" size={ 11 } />
@@ -83,8 +75,6 @@ export const HousekeepingRoomsTab: FC = () =>
<span>{ LocalizeText('housekeeping.room.search.button') }</span>
</Button>
</div>
{ /* Selected room hero card */ }
{ selectedRoom
? (
<div className="relative overflow-hidden rounded-lg border border-sky-200 bg-gradient-to-br from-sky-50 via-white to-violet-50 p-3 shadow-sm">
@@ -139,16 +129,14 @@ export const HousekeepingRoomsTab: FC = () =>
{ LocalizeText('housekeeping.room.none') }
</div>
) }
{ /* Open / Close + Mute */ }
<div className="grid grid-cols-2 gap-1.5">
<Button variant="success" disabled={ disableActions || !selectedRoom?.isLocked } onClick={ () => openRoom(selectedRoom.id) }>
<FaDoorOpen size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.room.open') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.open') }</span>
</Button>
<Button variant="danger" disabled={ disableActions || selectedRoom?.isLocked } onClick={ () => closeRoom(selectedRoom.id) }>
<FaLock size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.room.close') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.close') }</span>
</Button>
<div className="col-span-2 flex items-center gap-1.5 rounded-md border border-amber-200 bg-amber-50/40 px-2 py-1.5">
<FaVolumeMute size={ 11 } className="text-amber-600" />
@@ -169,11 +157,9 @@ export const HousekeepingRoomsTab: FC = () =>
</Button>
<Button variant="danger" disabled={ disableActions } onClick={ () => confirmAndRun('housekeeping.room.delete.confirm', () => deleteRoom(selectedRoom.id)) }>
<FaTrash size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.room.delete') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.delete') }</span>
</Button>
</div>
{ /* Transfer ownership card */ }
<div className="flex flex-col gap-1.5 rounded-md border border-violet-200 bg-violet-50/40 p-2">
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
<FaExchangeAlt size={ 8 } className="text-violet-500" />
@@ -189,7 +175,7 @@ export const HousekeepingRoomsTab: FC = () =>
onChange={ event => setNewOwnerId(parseInt(event.target.value) || 0) } />
<Button variant="primary" disabled={ disableActions || !newOwnerId } className="grow" onClick={ () => transferRoomOwnership(selectedRoom.id, newOwnerId) }>
<FaExchangeAlt size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.room.transfer') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.transfer') }</span>
</Button>
</div>
</div>
@@ -64,9 +64,6 @@ export const HousekeepingUsersTab: FC = () =>
if(!template) return;
// Pre-fill the duration inputs the template targets, and seed
// the reason textarea — operator can still tweak before
// hitting the per-action button.
setReason(template.defaultReason);
if(template.type === HousekeepingSanctionType.BAN) setBanHours(template.durationValue);
@@ -78,8 +75,6 @@ export const HousekeepingUsersTab: FC = () =>
{
if(selectedUserIds.length === 0) return;
// Big bulks need a confirm — a stray click can sanction
// dozens of users in one shot otherwise.
if(selectedUserIds.length >= BULK_CONFIRM_THRESHOLD)
{
confirm(
@@ -102,7 +97,6 @@ export const HousekeepingUsersTab: FC = () =>
return (
<div className="flex flex-col gap-2">
{/* Lookup with autocomplete */}
<div className="relative">
<div className="flex gap-1.5 items-center">
<div className="flex items-center gap-1 grow rounded border border-zinc-300 bg-white px-2 py-1">
@@ -115,7 +109,6 @@ export const HousekeepingUsersTab: FC = () =>
onFocus={ () => setIsFocused(true) }
onBlur={ () =>
{
// Defer hide so onClick on a suggestion fires first
if(blurTimerRef.current) clearTimeout(blurTimerRef.current);
blurTimerRef.current = setTimeout(() => setIsFocused(false), 120);
} }
@@ -182,11 +175,6 @@ export const HousekeepingUsersTab: FC = () =>
</div> }
</div>
{/* Bulk selection footer — appears whenever at least one user
is checked in the autocomplete dropdown. Applies the same
sanction (using the current reason + duration controls
below) to every selected user; ≥5 selected triggers a
themed confirm modal. */}
{ selectedUserIds.length > 0 &&
<div className="flex items-center gap-1 flex-wrap rounded border border-sky-300 bg-sky-50 p-1.5">
<span className="text-[10px] uppercase tracking-wide font-semibold text-sky-800 mr-1">
@@ -212,35 +200,10 @@ export const HousekeepingUsersTab: FC = () =>
</button>
</div> }
{ /* Selected user hero card */ }
{ selectedUser
? (
<div className="relative overflow-hidden rounded-lg border border-sky-200 bg-gradient-to-br from-sky-50 via-white to-emerald-50 p-3 shadow-sm">
<div className="flex items-start gap-3">
{ /* Live avatar head — renders the selected user's
in-game figure as a head crop. Falls back to
the modtools sprite when figure is empty (e.g.
a never-logged-in account).
Implementation note: `LayoutAvatarImageView`
is internally a fixed 90x130 background-image
box anchored at left:-2px (see
`src/common/layout/LayoutAvatarImageView.tsx`).
The earlier approach of forcing the box to
28x28 + transform:scale(0.42) was wrong on
two counts: clamping width/height clipped
the head BEFORE the transform ran, and the
transform-origin centered the residual on
a 28x28 layout that no longer contained
anything visible — the bubble rendered empty.
Correct pattern (same one `GroupMembersView`
uses for its 40x50 head bubbles): leave the
avatar element at its natural 90x130 size,
position it absolutely with negative offsets
so the head sits centered in the viewport,
and let `overflow-hidden` on the parent crop
the rest. No scale, no width override. */ }
<div className="relative rounded-full bg-sky-100 ring-2 ring-sky-200 shrink-0 w-[50px] h-[50px] overflow-hidden">
{ selectedUser.figure
? <LayoutAvatarImageView classNames={ [ '!absolute', '!-left-[20px]', '!-top-[20px]' ] } direction={ 2 } figure={ selectedUser.figure } headOnly={ true } />
@@ -267,11 +230,6 @@ export const HousekeepingUsersTab: FC = () =>
</div>
<div className="text-xs text-zinc-600 truncate mt-0.5 italic">{ selectedUser.motto || '—' }</div>
<div className="grid grid-cols-3 gap-1 text-[10px] mt-2">
{ /* LayoutCurrencyIcon resolves `currency.asset.icon.url`
(`${images.url}/wallet/%type%.png`) — type=-1 is the
credits coin, type=0 the ducket/pixel, type=5 the
diamond. Sizes default to 15x15 which fits the badge
row without extra overrides. */ }
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-50 border border-amber-200" title={ LocalizeText('housekeeping.user.credits') }>
<LayoutCurrencyIcon type={ -1 } />
<span className="tabular-nums font-semibold text-amber-800">{ selectedUser.creditsBalance.toLocaleString() }</span>
@@ -307,7 +265,6 @@ export const HousekeepingUsersTab: FC = () =>
</div>
) }
{ /* Live in-room actions */ }
{ selectedUser && selectedUser.online &&
<div className="rounded-md border border-amber-200 bg-gradient-to-r from-amber-50 to-orange-50 p-1.5 flex items-center gap-1 flex-wrap shadow-sm">
<span className="text-[10px] uppercase tracking-wider font-bold text-amber-800 mr-1 flex items-center gap-1">
@@ -331,7 +288,6 @@ export const HousekeepingUsersTab: FC = () =>
</Button>
</div> }
{ /* Sanction template + Reason — grouped surface */ }
<div className="flex flex-col gap-1.5 rounded-md border border-zinc-200 bg-zinc-50/50 p-2">
<div className="flex items-center gap-1.5">
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 shrink-0">Template</label>
@@ -353,7 +309,6 @@ export const HousekeepingUsersTab: FC = () =>
onChange={ event => setReason(event.target.value) } />
</div>
{ /* Sanctions */ }
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 -mb-0.5">{ LocalizeText('housekeeping.field.duration') }</label>
<div className="grid grid-cols-2 gap-1.5">
<div className="flex items-center gap-1 rounded-md border border-rose-200 bg-rose-50/40 px-1.5 py-1">
@@ -366,7 +321,7 @@ export const HousekeepingUsersTab: FC = () =>
<span className="text-[10px] text-rose-700">h</span>
<Button variant="danger" disabled={ disableActions } className="grow ml-auto" onClick={ () => banUser(selectedUser.id, reasonOrDefault, banHours) }>
<FaBan size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.ban_h', [ 'h' ], [ String(banHours) ]) }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.ban_h', [ 'h' ], [ String(banHours) ]) }</span>
</Button>
</div>
<div className="flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50/40 px-1.5 py-1">
@@ -392,7 +347,7 @@ export const HousekeepingUsersTab: FC = () =>
<span className="text-[10px] text-fuchsia-700">h</span>
<Button variant="secondary" disabled={ disableActions } className="grow ml-auto" onClick={ () => tradeLockUser(selectedUser.id, tradeLockHours, reasonOrDefault) }>
<FaLock size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.trade_lock_h', [ 'h' ], [ String(tradeLockHours) ]) }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.trade_lock_h', [ 'h' ], [ String(tradeLockHours) ]) }</span>
</Button>
</div>
<Button variant="warning" disabled={ disableActions } onClick={ () => kickUser(selectedUser.id, reasonOrDefault) }>
@@ -401,15 +356,14 @@ export const HousekeepingUsersTab: FC = () =>
</Button>
<Button variant="danger" disabled={ disableActions || !selectedUser?.isBanned } onClick={ () => unbanUser(selectedUser.id) }>
<FaExclamationTriangle size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.unban') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.unban') }</span>
</Button>
<Button variant="danger" disabled={ disableActions } onClick={ () => forceDisconnectUser(selectedUser.id, reasonOrDefault) }>
<FaPlug size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.force_disconnect') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.force_disconnect') }</span>
</Button>
</div>
{ /* Rank + password — privileged actions card */ }
<div className="grid grid-cols-2 gap-1.5 rounded-md border border-violet-200 bg-violet-50/40 p-2">
<div className="flex items-center gap-1">
<input
@@ -421,12 +375,12 @@ export const HousekeepingUsersTab: FC = () =>
onChange={ event => setRankDraft(parseInt(event.target.value) || 0) } />
<Button variant="primary" disabled={ disableActions } className="grow" onClick={ () => setUserRank(selectedUser.id, rankDraft) }>
<FaUserShield size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.set_rank') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.set_rank') }</span>
</Button>
</div>
<Button variant="dark" disabled={ disableActions } onClick={ () => resetUserPassword(selectedUser.id) }>
<FaKey size={ 10 } />
<span className="ml-1">{ LocalizeText('housekeeping.action.reset_password') }</span>
<span className="ml-1 text-white">{ LocalizeText('housekeeping.action.reset_password') }</span>
</Button>
</div>
+11 -1
View File
@@ -1,7 +1,7 @@
import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer';
import { AnimatePresence, motion, Variants } from 'framer-motion';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { GetConfigurationValue, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api';
import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api';
import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common';
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useWiredTools } from '../../hooks';
import { ToolbarItemView } from './ToolbarItemView';
@@ -43,6 +43,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
const { iconState = MessengerIconState.HIDDEN } = useMessenger();
const { openMonitor, showToolbarButton } = useWiredTools();
const isMod = useHasPermission('acc_supporttool');
const isHk = useHasPermission('acc_housekeeping');
const hkEnabled = useMemo(() => isHousekeepingEnabled(), []);
const { tickets = [] } = useModTools();
const openTicketsCount = useMemo(
() => isMod ? tickets.filter(ticket => ticket && (ticket.state === 1)).length : 0,
@@ -270,6 +272,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
{ (isHk && hkEnabled) &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
</motion.div> }
</motion.div>
</motion.div>
<motion.div
@@ -378,6 +384,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
{ (isHk && hkEnabled) &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
</motion.div> }
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
{ (requests.length > 0) &&
+7
View File
@@ -77,6 +77,13 @@
height: 34px;
}
.nitro-icon.icon-housekeeping {
background-image: url("@/assets/images/toolbar/icons/modtools.png");
width: 29px;
height: 34px;
filter: hue-rotate(140deg);
}
.nitro-icon.icon-furnieditor {
background-image: url("@/assets/images/toolbar/icons/furnieditor.png");
width: 30px;