mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
🆙 Small updates for the HK
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"wired.action.mute.user.max.length": 100,
|
"wired.action.mute.user.max.length": 100,
|
||||||
"game.center.enabled": false,
|
"game.center.enabled": false,
|
||||||
"guides.enabled": true,
|
"guides.enabled": true,
|
||||||
|
"housekeeping.enabled": true,
|
||||||
"toolbar.hide.quests": true,
|
"toolbar.hide.quests": true,
|
||||||
"catalog.style.new": true,
|
"catalog.style.new": true,
|
||||||
"show.google.ads": false,
|
"show.google.ads": false,
|
||||||
|
|||||||
@@ -33,15 +33,8 @@ const passesFilter = (entry: IHousekeepingActionLogEntry, target: TargetFilter,
|
|||||||
export const HousekeepingAuditTab: FC = () =>
|
export const HousekeepingAuditTab: FC = () =>
|
||||||
{
|
{
|
||||||
const { actionLog, refreshAuditLog, metricsByAction, resetActionMetrics } = useHousekeepingStore();
|
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 telemetryEnabled = useMemo(() => GetConfigurationValue<boolean>('housekeeping.telemetry.enabled', false) === true, []);
|
||||||
const [ isTelemetryExpanded, setIsTelemetryExpanded ] = useState(false);
|
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 [ targetFilter, setTargetFilter ] = useLocalStorage<TargetFilter>('nitro.housekeeping.audit.target_filter', 'all');
|
||||||
const [ successFilter, setSuccessFilter ] = useLocalStorage<SuccessFilter>('nitro.housekeeping.audit.success_filter', 'all');
|
const [ successFilter, setSuccessFilter ] = useLocalStorage<SuccessFilter>('nitro.housekeeping.audit.success_filter', 'all');
|
||||||
const [ query, setQuery ] = useLocalStorage<string>('nitro.housekeeping.audit.query', '');
|
const [ query, setQuery ] = useLocalStorage<string>('nitro.housekeeping.audit.query', '');
|
||||||
@@ -73,7 +66,6 @@ export const HousekeepingAuditTab: FC = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{ /* Header w/ counts + refresh */ }
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<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">
|
<h3 className="text-xs uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaFilter size={ 10 } />
|
<FaFilter size={ 10 } />
|
||||||
@@ -87,7 +79,7 @@ export const HousekeepingAuditTab: FC = () =>
|
|||||||
</h3>
|
</h3>
|
||||||
<Button size="sm" variant="secondary" disabled={ isRefreshing } onClick={ refresh }>
|
<Button size="sm" variant="secondary" disabled={ isRefreshing } onClick={ refresh }>
|
||||||
<FaSync size={ 9 } className={ isRefreshing ? 'animate-spin' : '' } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -70,26 +70,17 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
const [ now, setNow ] = useState(() => Date.now());
|
const [ now, setNow ] = useState(() => Date.now());
|
||||||
const [ refreshedAt, setRefreshedAt ] = useState<number | null>(null);
|
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(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(dashboard) setRefreshedAt(Date.now());
|
if(dashboard) setRefreshedAt(Date.now());
|
||||||
}, [ dashboard ]);
|
}, [ 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(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
const id = setInterval(() => setNow(Date.now()), 1_000);
|
const id = setInterval(() => setNow(Date.now()), 1_000);
|
||||||
return () => clearInterval(id);
|
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);
|
const refreshRef = useRef(refreshDashboard);
|
||||||
refreshRef.current = refreshDashboard;
|
refreshRef.current = refreshDashboard;
|
||||||
|
|
||||||
@@ -161,7 +152,6 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{ /* Header row: title + live status badge + refresh */ }
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xs uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
<h3 className="text-xs uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaChartLine size={ 10 } />
|
<FaChartLine size={ 10 } />
|
||||||
@@ -176,7 +166,7 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
</span>
|
</span>
|
||||||
<Button size="sm" variant="secondary" disabled={ isDashboardLoading } onClick={ () => refreshDashboard() }>
|
<Button size="sm" variant="secondary" disabled={ isDashboardLoading } onClick={ () => refreshDashboard() }>
|
||||||
<FaSync size={ 9 } className={ isDashboardLoading ? 'animate-spin' : '' } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +186,6 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
|
|
||||||
{ dashboard &&
|
{ 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="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 justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -252,7 +241,6 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaBolt size={ 9 } className="text-amber-500" />
|
<FaBolt size={ 9 } className="text-amber-500" />
|
||||||
@@ -275,7 +263,6 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
</form>
|
</form>
|
||||||
</> }
|
</> }
|
||||||
|
|
||||||
{ /* Recent sanctions */ }
|
|
||||||
{ recentSanctions.length > 0 &&
|
{ recentSanctions.length > 0 &&
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
|
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
|
||||||
@@ -294,7 +281,6 @@ export const HousekeepingDashboardTab: FC = () =>
|
|||||||
</ul>
|
</ul>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
{ /* Recent lookups — clickable pills that re-select the target */ }
|
|
||||||
{ recentLookups.length > 0 &&
|
{ recentLookups.length > 0 &&
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
|
<h4 className="text-[10px] uppercase tracking-wider font-semibold opacity-60 pt-1">
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{ /* Target banner */ }
|
|
||||||
{ !selectedUser
|
{ !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">
|
<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 className="text-sm font-semibold tabular-nums">{ selectedUser.username } <span className="text-zinc-400 font-normal">#{ selectedUser.id }</span></div>
|
||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
{ /* Currency grants — tone-coded surfaces */ }
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<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">
|
<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' ] } />
|
<LayoutCurrencyIcon type={ -1 } classNames={ [ 'shrink-0' ] } />
|
||||||
<input
|
<input
|
||||||
@@ -58,11 +54,9 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
onChange={ event => setCreditsAmount(parseInt(event.target.value) || 0) } />
|
onChange={ event => setCreditsAmount(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveCredits(selectedUser.id, creditsAmount) }>
|
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveCredits(selectedUser.id, creditsAmount) }>
|
||||||
<FaPiggyBank size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</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">
|
<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' ] } />
|
<LayoutCurrencyIcon type={ 0 } classNames={ [ 'shrink-0' ] } />
|
||||||
<input
|
<input
|
||||||
@@ -73,11 +67,9 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
onChange={ event => setDucketsAmount(parseInt(event.target.value) || 0) } />
|
onChange={ event => setDucketsAmount(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDuckets(selectedUser.id, ducketsAmount) }>
|
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDuckets(selectedUser.id, ducketsAmount) }>
|
||||||
<FaPiggyBank size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</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">
|
<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' ] } />
|
<LayoutCurrencyIcon type={ 5 } classNames={ [ 'shrink-0' ] } />
|
||||||
<input
|
<input
|
||||||
@@ -88,12 +80,10 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
onChange={ event => setDiamondsAmount(parseInt(event.target.value) || 0) } />
|
onChange={ event => setDiamondsAmount(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDiamonds(selectedUser.id, diamondsAmount) }>
|
<Button variant="success" disabled={ disableUserActions } className="grow ml-auto" onClick={ () => giveDiamonds(selectedUser.id, diamondsAmount) }>
|
||||||
<FaPiggyBank size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaGift size={ 8 } className="text-violet-500" />
|
<FaGift size={ 8 } className="text-violet-500" />
|
||||||
@@ -116,12 +106,10 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
onChange={ event => setItemQuantity(parseInt(event.target.value) || 0) } />
|
onChange={ event => setItemQuantity(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="primary" disabled={ disableUserActions || !itemId } className="grow" onClick={ () => grantItem(selectedUser.id, itemId, itemQuantity) }>
|
<Button variant="primary" disabled={ disableUserActions || !itemId } className="grow" onClick={ () => grantItem(selectedUser.id, itemId, itemQuantity) }>
|
||||||
<FaGift size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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" />
|
<FaCrown size={ 13 } className="text-amber-600 shrink-0" />
|
||||||
<input
|
<input
|
||||||
@@ -136,8 +124,6 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
<span className="ml-1">{ LocalizeText('housekeeping.economy.set_hc_days') }</span>
|
<span className="ml-1">{ LocalizeText('housekeeping.economy.set_hc_days') }</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ /* Hotel-wide alert */ }
|
|
||||||
<div className="flex flex-col gap-1.5 rounded-md border border-rose-200 bg-rose-50/40 p-2">
|
<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">
|
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaBullhorn size={ 9 } className="text-rose-500" />
|
<FaBullhorn size={ 9 } className="text-rose-500" />
|
||||||
@@ -154,7 +140,6 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
{
|
{
|
||||||
const dispatch = () => sendHotelAlert(trimmedAlert);
|
const dispatch = () => sendHotelAlert(trimmedAlert);
|
||||||
|
|
||||||
// Lungo alert hotel-wide → conferma esplicita.
|
|
||||||
if(trimmedAlert.length >= HOTEL_ALERT_CONFIRM_THRESHOLD)
|
if(trimmedAlert.length >= HOTEL_ALERT_CONFIRM_THRESHOLD)
|
||||||
{
|
{
|
||||||
confirm(LocalizeText('housekeeping.hotel.alert.confirm', [ 'count' ], [ String(trimmedAlert.length) ]), dispatch);
|
confirm(LocalizeText('housekeeping.hotel.alert.confirm', [ 'count' ], [ String(trimmedAlert.length) ]), dispatch);
|
||||||
@@ -165,7 +150,7 @@ export const HousekeepingEconomyTab: FC = () =>
|
|||||||
dispatch();
|
dispatch();
|
||||||
} }>
|
} }>
|
||||||
<FaBullhorn size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,15 +16,8 @@ export const HousekeepingRoomsTab: FC = () =>
|
|||||||
const [ query, setQuery ] = useState('');
|
const [ query, setQuery ] = useState('');
|
||||||
const [ muteMinutes, setMuteMinutes ] = useState<number>(DEFAULT_MUTE_MINUTES);
|
const [ muteMinutes, setMuteMinutes ] = useState<number>(DEFAULT_MUTE_MINUTES);
|
||||||
const [ newOwnerId, setNewOwnerId ] = useState<number>(0);
|
const [ newOwnerId, setNewOwnerId ] = useState<number>(0);
|
||||||
|
|
||||||
const confirm = useHousekeepingConfirm();
|
const confirm = useHousekeepingConfirm();
|
||||||
|
|
||||||
const currentRoomId = roomSession && roomSession.roomId > 0 ? roomSession.roomId : 0;
|
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 submitLookup = () =>
|
||||||
{
|
{
|
||||||
const trimmed = query.trim();
|
const trimmed = query.trim();
|
||||||
@@ -53,7 +46,6 @@ export const HousekeepingRoomsTab: FC = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{ /* Lookup bar */ }
|
|
||||||
<div className="flex gap-1.5 items-center">
|
<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">
|
<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 } />
|
<FaSearch className="text-zinc-400 shrink-0" size={ 11 } />
|
||||||
@@ -83,8 +75,6 @@ export const HousekeepingRoomsTab: FC = () =>
|
|||||||
<span>{ LocalizeText('housekeeping.room.search.button') }</span>
|
<span>{ LocalizeText('housekeeping.room.search.button') }</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ /* Selected room hero card */ }
|
|
||||||
{ selectedRoom
|
{ 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">
|
<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') }
|
{ LocalizeText('housekeeping.room.none') }
|
||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
{ /* Open / Close + Mute */ }
|
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<Button variant="success" disabled={ disableActions || !selectedRoom?.isLocked } onClick={ () => openRoom(selectedRoom.id) }>
|
<Button variant="success" disabled={ disableActions || !selectedRoom?.isLocked } onClick={ () => openRoom(selectedRoom.id) }>
|
||||||
<FaDoorOpen size={ 10 } />
|
<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>
|
||||||
<Button variant="danger" disabled={ disableActions || selectedRoom?.isLocked } onClick={ () => closeRoom(selectedRoom.id) }>
|
<Button variant="danger" disabled={ disableActions || selectedRoom?.isLocked } onClick={ () => closeRoom(selectedRoom.id) }>
|
||||||
<FaLock size={ 10 } />
|
<FaLock size={ 10 } />
|
||||||
<span className="ml-1">{ LocalizeText('housekeeping.room.close') }</span>
|
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.close') }</span>
|
||||||
</Button>
|
</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">
|
<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" />
|
<FaVolumeMute size={ 11 } className="text-amber-600" />
|
||||||
@@ -169,11 +157,9 @@ export const HousekeepingRoomsTab: FC = () =>
|
|||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" disabled={ disableActions } onClick={ () => confirmAndRun('housekeeping.room.delete.confirm', () => deleteRoom(selectedRoom.id)) }>
|
<Button variant="danger" disabled={ disableActions } onClick={ () => confirmAndRun('housekeeping.room.delete.confirm', () => deleteRoom(selectedRoom.id)) }>
|
||||||
<FaTrash size={ 10 } />
|
<FaTrash size={ 10 } />
|
||||||
<span className="ml-1">{ LocalizeText('housekeeping.room.delete') }</span>
|
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.delete') }</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ /* Transfer ownership card */ }
|
|
||||||
<div className="flex flex-col gap-1.5 rounded-md border border-violet-200 bg-violet-50/40 p-2">
|
<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">
|
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 flex items-center gap-1">
|
||||||
<FaExchangeAlt size={ 8 } className="text-violet-500" />
|
<FaExchangeAlt size={ 8 } className="text-violet-500" />
|
||||||
@@ -189,7 +175,7 @@ export const HousekeepingRoomsTab: FC = () =>
|
|||||||
onChange={ event => setNewOwnerId(parseInt(event.target.value) || 0) } />
|
onChange={ event => setNewOwnerId(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="primary" disabled={ disableActions || !newOwnerId } className="grow" onClick={ () => transferRoomOwnership(selectedRoom.id, newOwnerId) }>
|
<Button variant="primary" disabled={ disableActions || !newOwnerId } className="grow" onClick={ () => transferRoomOwnership(selectedRoom.id, newOwnerId) }>
|
||||||
<FaExchangeAlt size={ 10 } />
|
<FaExchangeAlt size={ 10 } />
|
||||||
<span className="ml-1">{ LocalizeText('housekeeping.room.transfer') }</span>
|
<span className="ml-1 text-white">{ LocalizeText('housekeeping.room.transfer') }</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,9 +64,6 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
|
|
||||||
if(!template) return;
|
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);
|
setReason(template.defaultReason);
|
||||||
|
|
||||||
if(template.type === HousekeepingSanctionType.BAN) setBanHours(template.durationValue);
|
if(template.type === HousekeepingSanctionType.BAN) setBanHours(template.durationValue);
|
||||||
@@ -78,8 +75,6 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
{
|
{
|
||||||
if(selectedUserIds.length === 0) return;
|
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)
|
if(selectedUserIds.length >= BULK_CONFIRM_THRESHOLD)
|
||||||
{
|
{
|
||||||
confirm(
|
confirm(
|
||||||
@@ -102,7 +97,6 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Lookup with autocomplete */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex gap-1.5 items-center">
|
<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">
|
<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) }
|
onFocus={ () => setIsFocused(true) }
|
||||||
onBlur={ () =>
|
onBlur={ () =>
|
||||||
{
|
{
|
||||||
// Defer hide so onClick on a suggestion fires first
|
|
||||||
if(blurTimerRef.current) clearTimeout(blurTimerRef.current);
|
if(blurTimerRef.current) clearTimeout(blurTimerRef.current);
|
||||||
blurTimerRef.current = setTimeout(() => setIsFocused(false), 120);
|
blurTimerRef.current = setTimeout(() => setIsFocused(false), 120);
|
||||||
} }
|
} }
|
||||||
@@ -182,11 +175,6 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
</div> }
|
</div> }
|
||||||
</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 &&
|
{ selectedUserIds.length > 0 &&
|
||||||
<div className="flex items-center gap-1 flex-wrap rounded border border-sky-300 bg-sky-50 p-1.5">
|
<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">
|
<span className="text-[10px] uppercase tracking-wide font-semibold text-sky-800 mr-1">
|
||||||
@@ -212,35 +200,10 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
</button>
|
</button>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
{ /* Selected user hero card */ }
|
|
||||||
{ selectedUser
|
{ 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="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">
|
<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">
|
<div className="relative rounded-full bg-sky-100 ring-2 ring-sky-200 shrink-0 w-[50px] h-[50px] overflow-hidden">
|
||||||
{ selectedUser.figure
|
{ selectedUser.figure
|
||||||
? <LayoutAvatarImageView classNames={ [ '!absolute', '!-left-[20px]', '!-top-[20px]' ] } direction={ 2 } figure={ selectedUser.figure } headOnly={ true } />
|
? <LayoutAvatarImageView classNames={ [ '!absolute', '!-left-[20px]', '!-top-[20px]' ] } direction={ 2 } figure={ selectedUser.figure } headOnly={ true } />
|
||||||
@@ -267,12 +230,7 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-zinc-600 truncate mt-0.5 italic">{ selectedUser.motto || '—' }</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">
|
<div className="grid grid-cols-3 gap-1 text-[10px] mt-2">
|
||||||
{ /* LayoutCurrencyIcon resolves `currency.asset.icon.url`
|
<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') }>
|
||||||
(`${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 } />
|
<LayoutCurrencyIcon type={ -1 } />
|
||||||
<span className="tabular-nums font-semibold text-amber-800">{ selectedUser.creditsBalance.toLocaleString() }</span>
|
<span className="tabular-nums font-semibold text-amber-800">{ selectedUser.creditsBalance.toLocaleString() }</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,7 +265,6 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
{ /* Live in-room actions */ }
|
|
||||||
{ selectedUser && selectedUser.online &&
|
{ 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">
|
<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">
|
<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>
|
</Button>
|
||||||
</div> }
|
</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 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">
|
<div className="flex items-center gap-1.5">
|
||||||
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 shrink-0">Template</label>
|
<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) } />
|
onChange={ event => setReason(event.target.value) } />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ /* Sanctions */ }
|
|
||||||
<label className="text-[10px] uppercase tracking-wider font-semibold opacity-60 -mb-0.5">{ LocalizeText('housekeeping.field.duration') }</label>
|
<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="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">
|
<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>
|
<span className="text-[10px] text-rose-700">h</span>
|
||||||
<Button variant="danger" disabled={ disableActions } className="grow ml-auto" onClick={ () => banUser(selectedUser.id, reasonOrDefault, banHours) }>
|
<Button variant="danger" disabled={ disableActions } className="grow ml-auto" onClick={ () => banUser(selectedUser.id, reasonOrDefault, banHours) }>
|
||||||
<FaBan size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50/40 px-1.5 py-1">
|
<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>
|
<span className="text-[10px] text-fuchsia-700">h</span>
|
||||||
<Button variant="secondary" disabled={ disableActions } className="grow ml-auto" onClick={ () => tradeLockUser(selectedUser.id, tradeLockHours, reasonOrDefault) }>
|
<Button variant="secondary" disabled={ disableActions } className="grow ml-auto" onClick={ () => tradeLockUser(selectedUser.id, tradeLockHours, reasonOrDefault) }>
|
||||||
<FaLock size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="warning" disabled={ disableActions } onClick={ () => kickUser(selectedUser.id, reasonOrDefault) }>
|
<Button variant="warning" disabled={ disableActions } onClick={ () => kickUser(selectedUser.id, reasonOrDefault) }>
|
||||||
@@ -401,15 +356,14 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" disabled={ disableActions || !selectedUser?.isBanned } onClick={ () => unbanUser(selectedUser.id) }>
|
<Button variant="danger" disabled={ disableActions || !selectedUser?.isBanned } onClick={ () => unbanUser(selectedUser.id) }>
|
||||||
<FaExclamationTriangle size={ 10 } />
|
<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>
|
||||||
<Button variant="danger" disabled={ disableActions } onClick={ () => forceDisconnectUser(selectedUser.id, reasonOrDefault) }>
|
<Button variant="danger" disabled={ disableActions } onClick={ () => forceDisconnectUser(selectedUser.id, reasonOrDefault) }>
|
||||||
<FaPlug size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</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="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">
|
<div className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
@@ -421,12 +375,12 @@ export const HousekeepingUsersTab: FC = () =>
|
|||||||
onChange={ event => setRankDraft(parseInt(event.target.value) || 0) } />
|
onChange={ event => setRankDraft(parseInt(event.target.value) || 0) } />
|
||||||
<Button variant="primary" disabled={ disableActions } className="grow" onClick={ () => setUserRank(selectedUser.id, rankDraft) }>
|
<Button variant="primary" disabled={ disableActions } className="grow" onClick={ () => setUserRank(selectedUser.id, rankDraft) }>
|
||||||
<FaUserShield size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="dark" disabled={ disableActions } onClick={ () => resetUserPassword(selectedUser.id) }>
|
<Button variant="dark" disabled={ disableActions } onClick={ () => resetUserPassword(selectedUser.id) }>
|
||||||
<FaKey size={ 10 } />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer';
|
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 { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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 { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common';
|
||||||
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useWiredTools } from '../../hooks';
|
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useWiredTools } from '../../hooks';
|
||||||
import { ToolbarItemView } from './ToolbarItemView';
|
import { ToolbarItemView } from './ToolbarItemView';
|
||||||
@@ -43,6 +43,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
|||||||
const { iconState = MessengerIconState.HIDDEN } = useMessenger();
|
const { iconState = MessengerIconState.HIDDEN } = useMessenger();
|
||||||
const { openMonitor, showToolbarButton } = useWiredTools();
|
const { openMonitor, showToolbarButton } = useWiredTools();
|
||||||
const isMod = useHasPermission('acc_supporttool');
|
const isMod = useHasPermission('acc_supporttool');
|
||||||
|
const isHk = useHasPermission('acc_housekeeping');
|
||||||
|
const hkEnabled = useMemo(() => isHousekeepingEnabled(), []);
|
||||||
const { tickets = [] } = useModTools();
|
const { tickets = [] } = useModTools();
|
||||||
const openTicketsCount = useMemo(
|
const openTicketsCount = useMemo(
|
||||||
() => isMod ? tickets.filter(ticket => ticket && (ticket.state === 1)).length : 0,
|
() => 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 }>
|
<motion.div variants={ itemVariants }>
|
||||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||||
</motion.div> }
|
</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>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -378,6 +384,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
|||||||
<motion.div variants={ itemVariants }>
|
<motion.div variants={ itemVariants }>
|
||||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||||
</motion.div> }
|
</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">
|
<motion.div variants={ itemVariants } className="relative">
|
||||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||||
{ (requests.length > 0) &&
|
{ (requests.length > 0) &&
|
||||||
|
|||||||
@@ -77,6 +77,13 @@
|
|||||||
height: 34px;
|
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 {
|
.nitro-icon.icon-furnieditor {
|
||||||
background-image: url("@/assets/images/toolbar/icons/furnieditor.png");
|
background-image: url("@/assets/images/toolbar/icons/furnieditor.png");
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
|||||||
Reference in New Issue
Block a user