mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
WIP preserve local changes before duckie merge
This commit is contained in:
@@ -8,6 +8,7 @@ import { CameraWidgetView } from './camera/CameraWidgetView';
|
||||
import { CampaignView } from './campaign/CampaignView';
|
||||
import { CatalogView } from './catalog/CatalogView';
|
||||
import { ChatHistoryView } from './chat-history/ChatHistoryView';
|
||||
import { CustomizeNickIconView } from './customize/CustomizeNickIconView';
|
||||
import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
|
||||
import { FurniEditorView } from './furni-editor/FurniEditorView';
|
||||
import { FriendsView } from './friends/FriendsView';
|
||||
@@ -27,6 +28,8 @@ import { ExternalPluginLoader } from './plugins/ExternalPluginLoader';
|
||||
import { RightSideView } from './right-side/RightSideView';
|
||||
import { RoomView } from './room/RoomView';
|
||||
import { ToolbarView } from './toolbar/ToolbarView';
|
||||
import { TranslationBootstrap } from './translation/TranslationBootstrap';
|
||||
import { TranslationSettingsView } from './translation/TranslationSettingsView';
|
||||
import { UserProfileView } from './user-profile/UserProfileView';
|
||||
import { UserSettingsView } from './user-settings/UserSettingsView';
|
||||
import { WiredView } from './wired/WiredView';
|
||||
@@ -37,6 +40,7 @@ export const MainView: FC<{}> = props =>
|
||||
{
|
||||
const [ isReady, setIsReady ] = useState(false);
|
||||
const [ landingViewVisible, setLandingViewVisible ] = useState(true);
|
||||
const [ localizationVersion, setLocalizationVersion ] = useState(0);
|
||||
|
||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event => setLandingViewVisible(false));
|
||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.ENDED, event => setLandingViewVisible(event.openLandingView));
|
||||
@@ -86,8 +90,18 @@ export const MainView: FC<{}> = props =>
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const refreshLocalization = () => setLocalizationVersion(value => (value + 1));
|
||||
|
||||
window.addEventListener('nitro-localization-updated', refreshLocalization);
|
||||
|
||||
return () => window.removeEventListener('nitro-localization-updated', refreshLocalization);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden" data-localization-version={ localizationVersion } />
|
||||
<AnimatePresence>
|
||||
{ landingViewVisible &&
|
||||
<motion.div
|
||||
@@ -98,10 +112,12 @@ export const MainView: FC<{}> = props =>
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
<ToolbarView isInRoom={ !landingViewVisible } />
|
||||
<TranslationBootstrap />
|
||||
<ModToolsView />
|
||||
<WiredCreatorToolsView />
|
||||
<RoomView />
|
||||
<ChatHistoryView />
|
||||
<CustomizeNickIconView />
|
||||
<WiredView />
|
||||
<AvatarEditorView />
|
||||
<AchievementsView />
|
||||
@@ -112,6 +128,7 @@ export const MainView: FC<{}> = props =>
|
||||
<FriendsView />
|
||||
<RightSideView />
|
||||
<UserSettingsView />
|
||||
<TranslationSettingsView />
|
||||
<UserProfileView />
|
||||
<GroupsView />
|
||||
<GroupForumView />
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue } from '../../api';
|
||||
import { useCatalog } from '../../hooks';
|
||||
import { CatalogClassicView } from './CatalogClassicView';
|
||||
import { CatalogModernView } from './CatalogModernView';
|
||||
|
||||
export const CatalogView: FC<{}> = () =>
|
||||
{
|
||||
const { catalogLocalizationVersion = 0 } = useCatalog();
|
||||
const useNewStyle = GetConfigurationValue<boolean>('catalog.style.new', false);
|
||||
|
||||
if(useNewStyle) return <CatalogModernView />;
|
||||
if(useNewStyle) return (
|
||||
<>
|
||||
<div className="hidden" data-catalog-localization-version={ catalogLocalizationVersion } />
|
||||
<CatalogModernView />
|
||||
</>
|
||||
);
|
||||
|
||||
return <CatalogClassicView />;
|
||||
return (
|
||||
<>
|
||||
<div className="hidden" data-catalog-localization-version={ catalogLocalizationVersion } />
|
||||
<CatalogClassicView />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { GetSessionDataManager, IFurnitureData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaSearch, FaTimes } from 'react-icons/fa';
|
||||
import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
|
||||
import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
export const CatalogSearchView: FC<{}> = () =>
|
||||
{
|
||||
const [ searchValue, setSearchValue ] = useState('');
|
||||
const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
|
||||
const { currentType = null, rootNode = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
|
||||
|
||||
const normalizeSearchText = (value: string) => (value || '')
|
||||
.toLocaleLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
let search = searchValue?.toLocaleLowerCase().replace(' ', '');
|
||||
const search = normalizeSearchText(searchValue);
|
||||
|
||||
if(!search || !search.length)
|
||||
{
|
||||
@@ -22,7 +29,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
|
||||
const timeout = setTimeout(() =>
|
||||
{
|
||||
if(!offersToNodes || !rootNode) return;
|
||||
if(!rootNode) return;
|
||||
|
||||
const furnitureDatas = GetSessionDataManager().getAllFurnitureData();
|
||||
|
||||
@@ -39,34 +46,35 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
|
||||
if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue;
|
||||
|
||||
const searchValues = [ furniture.className || '', furniture.name || '', furniture.description || '' ].join(' ').replace(/ /gi, '').toLowerCase();
|
||||
const name = normalizeSearchText(furniture.name || '');
|
||||
const matchesSearch = name.includes(search);
|
||||
|
||||
if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1))
|
||||
{
|
||||
if((furniture.furniLine !== '') && (foundFurniLines.indexOf(furniture.furniLine) < 0))
|
||||
{
|
||||
if(searchValues.indexOf(search) >= 0) foundFurniLines.push(furniture.furniLine);
|
||||
if(matchesSearch) foundFurniLines.push(furniture.furniLine);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if(matchesSearch)
|
||||
{
|
||||
const foundNodes = [
|
||||
...GetOfferNodes(offersToNodes, furniture.purchaseOfferId),
|
||||
...GetOfferNodes(offersToNodes, furniture.rentOfferId)
|
||||
];
|
||||
foundFurniture.push(furniture);
|
||||
|
||||
if(foundNodes.length)
|
||||
if(furniture.furniLine && furniture.furniLine.length && (foundFurniLines.indexOf(furniture.furniLine) < 0))
|
||||
{
|
||||
if(searchValues.indexOf(search) >= 0) foundFurniture.push(furniture);
|
||||
|
||||
if(foundFurniture.length === 250) break;
|
||||
foundFurniLines.push(furniture.furniLine);
|
||||
}
|
||||
|
||||
if(foundFurniture.length === 250) break;
|
||||
}
|
||||
}
|
||||
|
||||
const offers: IPurchasableOffer[] = [];
|
||||
|
||||
for(const furniture of foundFurniture) offers.push(new FurnitureOffer(furniture));
|
||||
for(const furniture of foundFurniture)
|
||||
{
|
||||
offers.push(new FurnitureOffer(furniture));
|
||||
}
|
||||
|
||||
let nodes: ICatalogNode[] = [];
|
||||
|
||||
@@ -77,7 +85,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]);
|
||||
}, [ currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { IPurchasableOffer, ProductTypeEnum } from '../../../../../api';
|
||||
import { IPurchasableOffer } from '../../../../../api';
|
||||
import { AutoGrid, AutoGridProps } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../../CatalogAdminContext';
|
||||
@@ -13,7 +13,7 @@ interface CatalogItemGridWidgetViewProps extends AutoGridProps
|
||||
export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const { currentOffer = null, setCurrentOffer = null, currentPage = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null, selectCatalogOffer = null } = useCatalog();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
@@ -29,23 +29,7 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
|
||||
|
||||
const selectOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
offer.activate();
|
||||
|
||||
if(offer.isLazy) return;
|
||||
|
||||
setCurrentOffer(offer);
|
||||
|
||||
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
|
||||
{
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraData = (offer.product.extraParam || null);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}
|
||||
selectCatalogOffer(offer);
|
||||
};
|
||||
|
||||
const handleDragStart = useCallback((index: number) =>
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, PurchaseCatalogPrefixComposer, PurchaseNickIconComposer, PurchasePrefixComposer, RemoveLinkEventTracker, RequestNickIconsComposer, SetActiveNickIconComposer, SetActivePrefixComposer, SetDisplayOrderComposer, UserNickIconsEvent } from '@nitrots/nitro-renderer';
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { INickIconItem, IPrefixItem, PRESET_PREFIX_EFFECTS, PRESET_PREFIX_FONTS, SendMessageComposer, getPrefixEffectStyle, getPrefixFontStyle, parsePrefixColors } from '../../api';
|
||||
import { GetNickIconUrl } from '../../assets/images/user_custom/nick_icons';
|
||||
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text, UserIdentityView } from '../../common';
|
||||
import { LayoutCurrencyIcon } from '../../common/layout/LayoutCurrencyIcon';
|
||||
import { useMessageEvent } from '../../hooks';
|
||||
|
||||
type CustomizeTab = 'icons' | 'prefix' | 'settings';
|
||||
type PrefixSubTab = 'library' | 'custom';
|
||||
|
||||
interface ICatalogPrefixItem extends IPrefixItem
|
||||
{
|
||||
points: number;
|
||||
pointsType: number;
|
||||
owned: boolean;
|
||||
ownedPrefixId: number;
|
||||
}
|
||||
|
||||
interface ICombinedPrefixItem extends IPrefixItem
|
||||
{
|
||||
points: number;
|
||||
pointsType: number;
|
||||
owned: boolean;
|
||||
ownedPrefixId: number;
|
||||
}
|
||||
|
||||
const ORDER_LABELS: Record<string, string> = {
|
||||
'icon-prefix-name': 'Icon / Prefix / Name',
|
||||
'prefix-icon-name': 'Prefix / Icon / Name',
|
||||
'name-icon-prefix': 'Name / Icon / Prefix',
|
||||
'name-prefix-icon': 'Name / Prefix / Icon',
|
||||
'icon-name-prefix': 'Icon / Name / Prefix',
|
||||
'prefix-name-icon': 'Prefix / Name / Icon'
|
||||
};
|
||||
|
||||
const PRESET_COLORS: string[] = [
|
||||
'#D62828', '#E85D04', '#F77F00', '#2A9D8F',
|
||||
'#0077B6', '#4361EE', '#6A4C93', '#C1121F',
|
||||
'#B5179E', '#3A86FF', '#3F8E00', '#8D5524'
|
||||
];
|
||||
|
||||
export const CustomizeNickIconView: FC<{}> = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ activeTab, setActiveTab ] = useState<CustomizeTab>('icons');
|
||||
const [ activePrefixSubTab, setActivePrefixSubTab ] = useState<PrefixSubTab>('library');
|
||||
const [ iconItems, setIconItems ] = useState<INickIconItem[]>([]);
|
||||
const [ prefixItems, setPrefixItems ] = useState<IPrefixItem[]>([]);
|
||||
const [ catalogPrefixes, setCatalogPrefixes ] = useState<ICatalogPrefixItem[]>([]);
|
||||
const [ displayOrder, setDisplayOrder ] = useState('icon-prefix-name');
|
||||
const [ customPrefixMaxLength, setCustomPrefixMaxLength ] = useState(15);
|
||||
const [ customPrefixPriceCredits, setCustomPrefixPriceCredits ] = useState(0);
|
||||
const [ customPrefixPricePoints, setCustomPrefixPricePoints ] = useState(0);
|
||||
const [ customPrefixPointsType, setCustomPrefixPointsType ] = useState(0);
|
||||
const [ customPrefixFontPriceCredits, setCustomPrefixFontPriceCredits ] = useState(0);
|
||||
const [ customPrefixFontPricePoints, setCustomPrefixFontPricePoints ] = useState(0);
|
||||
const [ customPrefixFontPointsType, setCustomPrefixFontPointsType ] = useState(0);
|
||||
const [ customPrefixText, setCustomPrefixText ] = useState('');
|
||||
const [ customPrefixColor, setCustomPrefixColor ] = useState('#FFFFFF');
|
||||
const [ customPrefixIcon, setCustomPrefixIcon ] = useState('');
|
||||
const [ customPrefixEffect, setCustomPrefixEffect ] = useState('');
|
||||
const [ customPrefixFont, setCustomPrefixFont ] = useState('');
|
||||
const [ showEmojiPicker, setShowEmojiPicker ] = useState(false);
|
||||
|
||||
useMessageEvent<UserNickIconsEvent>(UserNickIconsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setIconItems(parser.nickIcons.map(icon => ({
|
||||
id: icon.id,
|
||||
iconKey: icon.iconKey,
|
||||
displayName: icon.displayName,
|
||||
points: icon.points,
|
||||
pointsType: icon.pointsType,
|
||||
owned: icon.owned,
|
||||
active: icon.active
|
||||
})));
|
||||
setPrefixItems(parser.ownedPrefixes.map(prefix => ({
|
||||
id: prefix.id,
|
||||
displayName: prefix.displayName,
|
||||
text: prefix.text,
|
||||
color: prefix.color,
|
||||
icon: prefix.icon || '',
|
||||
effect: prefix.effect || '',
|
||||
font: prefix.font || '',
|
||||
active: prefix.active,
|
||||
isCustom: prefix.isCustom,
|
||||
points: prefix.points,
|
||||
pointsType: prefix.pointsType,
|
||||
catalogPrefixId: prefix.catalogPrefixId
|
||||
})));
|
||||
setCatalogPrefixes(parser.prefixCatalog.map(prefix => ({
|
||||
id: prefix.id,
|
||||
displayName: prefix.displayName,
|
||||
text: prefix.text,
|
||||
color: prefix.color,
|
||||
icon: prefix.icon || '',
|
||||
effect: prefix.effect || '',
|
||||
font: prefix.font || '',
|
||||
active: prefix.active,
|
||||
points: prefix.points,
|
||||
pointsType: prefix.pointsType,
|
||||
owned: prefix.owned,
|
||||
ownedPrefixId: prefix.ownedPrefixId
|
||||
})));
|
||||
setDisplayOrder(parser.displayOrder || 'icon-prefix-name');
|
||||
setCustomPrefixMaxLength(parser.customPrefixMaxLength || 15);
|
||||
setCustomPrefixPriceCredits(parser.customPrefixPriceCredits || 0);
|
||||
setCustomPrefixPricePoints(parser.customPrefixPricePoints || 0);
|
||||
setCustomPrefixPointsType(parser.customPrefixPointsType || 0);
|
||||
setCustomPrefixFontPriceCredits(parser.customPrefixFontPriceCredits || 0);
|
||||
setCustomPrefixFontPricePoints(parser.customPrefixFontPricePoints || 0);
|
||||
setCustomPrefixFontPointsType(parser.customPrefixFontPointsType || 0);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(previousValue => !previousValue);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'customize/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
setIsLoading(true);
|
||||
SendMessageComposer(new RequestNickIconsComposer());
|
||||
}, [ isVisible ]);
|
||||
|
||||
const activeIcon = useMemo(() => iconItems.find(item => item.active) || null, [ iconItems ]);
|
||||
const activePrefix = useMemo(() => prefixItems.find(item => item.active) || null, [ prefixItems ]);
|
||||
const combinedPrefixes = useMemo(() =>
|
||||
{
|
||||
const ownedByCatalogId = new Map<number, IPrefixItem>();
|
||||
|
||||
for(const prefix of prefixItems)
|
||||
{
|
||||
if(prefix.catalogPrefixId && (prefix.catalogPrefixId > 0)) ownedByCatalogId.set(prefix.catalogPrefixId, prefix);
|
||||
}
|
||||
|
||||
const catalogEntries: ICombinedPrefixItem[] = catalogPrefixes.map(prefix =>
|
||||
{
|
||||
const ownedPrefix = ownedByCatalogId.get(prefix.id);
|
||||
|
||||
return {
|
||||
id: ownedPrefix?.id || prefix.id,
|
||||
displayName: ownedPrefix?.displayName || prefix.displayName,
|
||||
text: ownedPrefix?.text || prefix.text,
|
||||
color: ownedPrefix?.color || prefix.color,
|
||||
icon: ownedPrefix?.icon || prefix.icon,
|
||||
effect: ownedPrefix?.effect || prefix.effect,
|
||||
font: ownedPrefix?.font || prefix.font,
|
||||
active: ownedPrefix?.active || prefix.active,
|
||||
isCustom: false,
|
||||
points: prefix.points,
|
||||
pointsType: prefix.pointsType,
|
||||
catalogPrefixId: prefix.id,
|
||||
owned: prefix.owned || !!ownedPrefix,
|
||||
ownedPrefixId: prefix.ownedPrefixId || ownedPrefix?.id || 0
|
||||
};
|
||||
});
|
||||
|
||||
const customEntries: ICombinedPrefixItem[] = prefixItems
|
||||
.filter(prefix => !prefix.catalogPrefixId || (prefix.catalogPrefixId <= 0))
|
||||
.map(prefix => ({
|
||||
id: prefix.id,
|
||||
displayName: prefix.displayName,
|
||||
text: prefix.text,
|
||||
color: prefix.color,
|
||||
icon: prefix.icon,
|
||||
effect: prefix.effect,
|
||||
font: prefix.font || '',
|
||||
active: prefix.active,
|
||||
isCustom: true,
|
||||
points: prefix.points || customPrefixPricePoints,
|
||||
pointsType: prefix.pointsType || customPrefixPointsType,
|
||||
catalogPrefixId: 0,
|
||||
owned: true,
|
||||
ownedPrefixId: prefix.id
|
||||
}));
|
||||
|
||||
return [ ...catalogEntries, ...customEntries ];
|
||||
}, [ catalogPrefixes, customPrefixPointsType, customPrefixPricePoints, prefixItems ]);
|
||||
const selectedEffectOption = useMemo(() => PRESET_PREFIX_EFFECTS.find(effect => effect.id === customPrefixEffect) || PRESET_PREFIX_EFFECTS[0], [ customPrefixEffect ]);
|
||||
const selectedFontOption = useMemo(() => PRESET_PREFIX_FONTS.find(font => font.id === customPrefixFont) || PRESET_PREFIX_FONTS[0], [ customPrefixFont ]);
|
||||
const basicEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'basic'), []);
|
||||
const premiumEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'premium'), []);
|
||||
const basicFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'basic'), []);
|
||||
const premiumFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'premium'), []);
|
||||
const prefixPreviewColors = useMemo(() => parsePrefixColors(customPrefixText || 'Preview', customPrefixColor || '#FFFFFF'), [ customPrefixText, customPrefixColor ]);
|
||||
const customPrefixPreviewStyle = useMemo(() => getPrefixEffectStyle(customPrefixEffect, prefixPreviewColors[0] || '#FFFFFF'), [ customPrefixEffect, prefixPreviewColors ]);
|
||||
const customPrefixFontStyle = useMemo(() => getPrefixFontStyle(customPrefixFont), [ customPrefixFont ]);
|
||||
const customPrefixTotalCredits = useMemo(() => customPrefixPriceCredits + (customPrefixFont ? customPrefixFontPriceCredits : 0), [ customPrefixFont, customPrefixFontPriceCredits, customPrefixPriceCredits ]);
|
||||
const customPrefixTotalPoints = useMemo(() => customPrefixPricePoints + ((customPrefixFont && (customPrefixFontPointsType === customPrefixPointsType)) ? customPrefixFontPricePoints : 0), [ customPrefixFont, customPrefixFontPointsType, customPrefixFontPricePoints, customPrefixPointsType, customPrefixPricePoints ]);
|
||||
const customPrefixIsValid = useMemo(() =>
|
||||
{
|
||||
const trimmed = customPrefixText.trim();
|
||||
|
||||
if(!trimmed.length || (trimmed.length > customPrefixMaxLength)) return false;
|
||||
|
||||
return customPrefixColor.split(',').every(color => /^#[0-9A-Fa-f]{6}$/.test(color));
|
||||
}, [ customPrefixColor, customPrefixMaxLength, customPrefixText ]);
|
||||
|
||||
const refreshCustomizeData = () =>
|
||||
{
|
||||
setIsLoading(true);
|
||||
SendMessageComposer(new RequestNickIconsComposer());
|
||||
};
|
||||
|
||||
const handleIconAction = (item: INickIconItem) =>
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
if(!item.owned)
|
||||
{
|
||||
SendMessageComposer(new PurchaseNickIconComposer(item.iconKey));
|
||||
return;
|
||||
}
|
||||
|
||||
SendMessageComposer(new SetActiveNickIconComposer(item.active ? 0 : item.id));
|
||||
};
|
||||
|
||||
const handleCombinedPrefixAction = (item: ICombinedPrefixItem) =>
|
||||
{
|
||||
setIsLoading(true);
|
||||
|
||||
if(item.owned)
|
||||
{
|
||||
SendMessageComposer(new SetActivePrefixComposer(item.active ? 0 : item.ownedPrefixId));
|
||||
return;
|
||||
}
|
||||
|
||||
SendMessageComposer(new PurchaseCatalogPrefixComposer(item.catalogPrefixId || item.id));
|
||||
};
|
||||
|
||||
const handleCustomPrefixPurchase = () =>
|
||||
{
|
||||
if(!customPrefixIsValid) return;
|
||||
|
||||
setIsLoading(true);
|
||||
SendMessageComposer(new PurchasePrefixComposer(customPrefixText.trim(), customPrefixColor, customPrefixIcon, customPrefixEffect, customPrefixFont));
|
||||
};
|
||||
|
||||
const handleDisplayOrderChange = (nextDisplayOrder: string) =>
|
||||
{
|
||||
if(nextDisplayOrder === displayOrder) return;
|
||||
|
||||
setDisplayOrder(nextDisplayOrder);
|
||||
setIsLoading(true);
|
||||
SendMessageComposer(new SetDisplayOrderComposer(nextDisplayOrder));
|
||||
};
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="customize-nick-icon-window w-[680px] max-w-[95vw]" theme="primary-slim" uniqueKey="customize-nick-icons">
|
||||
<NitroCardHeaderView headerText="Customize Bubble Identity" onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardTabsView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === 'icons' } onClick={ () => setActiveTab('icons') }>
|
||||
<Text>Icons</Text>
|
||||
</NitroCardTabsItemView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === 'prefix' } onClick={ () => setActiveTab('prefix') }>
|
||||
<Text>Prefix</Text>
|
||||
</NitroCardTabsItemView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === 'settings' } onClick={ () => setActiveTab('settings') }>
|
||||
<Text>Settings</Text>
|
||||
</NitroCardTabsItemView>
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView className="flex max-h-[78vh] flex-col gap-3 overflow-y-auto text-black">
|
||||
<div className="rounded border border-black/10 bg-black/5 p-3">
|
||||
<Text bold>Live preview</Text>
|
||||
<div className="mt-2 flex min-h-[54px] items-center justify-center rounded border border-black/10 bg-[#1f2937] px-3 py-2 text-white">
|
||||
<UserIdentityView
|
||||
displayOrder={ displayOrder }
|
||||
nickIcon={ activeIcon?.iconKey || '' }
|
||||
prefixColor={ activePrefix?.color || customPrefixColor }
|
||||
prefixEffect={ activePrefix?.effect || customPrefixEffect }
|
||||
prefixFont={ activePrefix?.font || customPrefixFont }
|
||||
prefixIcon={ activePrefix?.icon || customPrefixIcon }
|
||||
prefixText={ activePrefix?.text || customPrefixText }
|
||||
username="Username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ activeTab === 'icons' &&
|
||||
<>
|
||||
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
||||
Choose the icon shown in your bubble identity.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{ iconItems.map(item =>
|
||||
{
|
||||
const iconUrl = GetNickIconUrl(item.iconKey);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ item.iconKey }
|
||||
className={ `relative flex min-h-[126px] flex-col items-center justify-between gap-2 rounded border p-3 transition-colors ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
||||
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
||||
<img className="h-auto max-h-[28px] w-auto object-contain" src={ iconUrl } alt={ item.iconKey } />
|
||||
<div className="flex flex-col items-center gap-1 text-center text-[11px]">
|
||||
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
||||
<span className="max-w-[140px] truncate">{ item.displayName || `Icon #${ item.iconKey }` }</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutCurrencyIcon type={ item.pointsType } />
|
||||
{ item.points }
|
||||
</span>
|
||||
</div>
|
||||
<Button disabled={ isLoading } onClick={ () => handleIconAction(item) }>
|
||||
{ !item.owned && 'Buy' }
|
||||
{ item.owned && !item.active && 'Activate' }
|
||||
{ item.owned && item.active && 'Deactivate' }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
</> }
|
||||
|
||||
{ activeTab === 'prefix' &&
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="rounded border border-black/10 bg-black/5 p-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activePrefixSubTab === 'library' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
||||
type="button"
|
||||
onClick={ () => setActivePrefixSubTab('library') }>
|
||||
Library
|
||||
</button>
|
||||
<button
|
||||
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activePrefixSubTab === 'custom' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
||||
type="button"
|
||||
onClick={ () => setActivePrefixSubTab('custom') }>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ activePrefixSubTab === 'library' &&
|
||||
<>
|
||||
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
||||
Choose a preset or custom prefix for your bubble identity.
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{ combinedPrefixes.map(item => (
|
||||
<div key={ `${ item.catalogPrefixId || 'custom' }-${ item.ownedPrefixId || item.id }` } className={ `relative flex min-h-[96px] flex-col gap-2 rounded border p-2.5 ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
||||
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
||||
<UserIdentityView
|
||||
displayOrder={ displayOrder }
|
||||
nickIcon={ activeIcon?.iconKey || '' }
|
||||
prefixColor={ item.color }
|
||||
prefixEffect={ item.effect }
|
||||
prefixFont={ item.font || '' }
|
||||
prefixIcon={ item.icon }
|
||||
prefixText={ item.text }
|
||||
username="Username" />
|
||||
<div className="flex flex-col gap-1 text-[11px]">
|
||||
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
||||
<span className="truncate">{ item.displayName || item.text }{ item.isCustom ? ' - Custom' : '' }</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutCurrencyIcon type={ item.pointsType } />
|
||||
{ item.points }
|
||||
</span>
|
||||
</div>
|
||||
<Button disabled={ isLoading } onClick={ () => handleCombinedPrefixAction(item) }>
|
||||
{ !item.owned && 'Buy' }
|
||||
{ item.owned && !item.active && 'Activate' }
|
||||
{ item.owned && item.active && 'Deactivate' }
|
||||
</Button>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</> }
|
||||
|
||||
{ activePrefixSubTab === 'custom' &&
|
||||
<div className="rounded border border-black/10 bg-black/5 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Text bold>Custom prefix</Text>
|
||||
<Button disabled={ isLoading } onClick={ refreshCustomizeData }>Refresh</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 rounded border border-black/10 bg-white px-3 py-2 text-sm"
|
||||
maxLength={ customPrefixMaxLength }
|
||||
placeholder="Enter your prefix"
|
||||
type="text"
|
||||
value={ customPrefixText }
|
||||
onChange={ event => setCustomPrefixText(event.target.value) } />
|
||||
<span className="text-[11px] text-black/60">{ customPrefixText.length }/{ customPrefixMaxLength }</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button className="rounded border border-black/10 bg-white px-3 py-2 text-sm" type="button" onClick={ () => setShowEmojiPicker(true) }>
|
||||
{ customPrefixIcon || 'Emoji' }
|
||||
</button>
|
||||
{ !!customPrefixIcon && <Button onClick={ () => setCustomPrefixIcon('') }>Clear</Button> }
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-white p-2">
|
||||
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
||||
Safe colors only, chosen to stay readable on both light and dark backgrounds.
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{ PRESET_COLORS.map(color => (
|
||||
<button
|
||||
key={ color }
|
||||
className={ `flex h-[28px] items-center justify-center rounded border text-[10px] font-bold uppercase ${ customPrefixColor === color ? 'border-[#1e7295] ring-1 ring-[#1e7295]' : 'border-black/10' }` }
|
||||
style={ { backgroundColor: color } }
|
||||
type="button"
|
||||
onClick={ () => setCustomPrefixColor(color) }>
|
||||
{ customPrefixColor === color ? 'ON' : '' }
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-white p-2">
|
||||
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
||||
Effect
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="flex-1 rounded border border-black/10 bg-white px-2 py-2 text-sm"
|
||||
value={ customPrefixEffect }
|
||||
onChange={ event => setCustomPrefixEffect(event.target.value) }>
|
||||
<optgroup label="Basic">
|
||||
{ basicEffects.map(effect => (
|
||||
<option key={ effect.id || 'none' } value={ effect.id }>
|
||||
{ effect.label }
|
||||
</option>
|
||||
)) }
|
||||
</optgroup>
|
||||
<optgroup label="Premium">
|
||||
{ premiumEffects.map(effect => (
|
||||
<option key={ effect.id } value={ effect.id }>
|
||||
{ effect.label }
|
||||
</option>
|
||||
)) }
|
||||
</optgroup>
|
||||
</select>
|
||||
<div className="min-w-[130px] rounded border border-black/10 bg-black/5 px-2 py-2 text-center text-[11px] font-bold">
|
||||
{ selectedEffectOption.icon } { selectedEffectOption.label }
|
||||
<div className="mt-1 text-[9px] uppercase text-black/60">
|
||||
{ selectedEffectOption.tier }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-white p-2">
|
||||
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
||||
Font
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="flex-1 rounded border border-black/10 bg-white px-2 py-2 text-sm"
|
||||
value={ customPrefixFont }
|
||||
onChange={ event => setCustomPrefixFont(event.target.value) }>
|
||||
<optgroup label="Basic">
|
||||
{ basicFonts.map(font => (
|
||||
<option key={ font.id || 'default' } value={ font.id }>
|
||||
{ font.label }
|
||||
</option>
|
||||
)) }
|
||||
</optgroup>
|
||||
<optgroup label="Premium">
|
||||
{ premiumFonts.map(font => (
|
||||
<option key={ font.id } value={ font.id }>
|
||||
{ font.label }
|
||||
</option>
|
||||
)) }
|
||||
</optgroup>
|
||||
</select>
|
||||
<div className="min-w-[130px] rounded border border-black/10 bg-black/5 px-2 py-2 text-center text-[11px] font-bold">
|
||||
<span style={ customPrefixFontStyle }>{ selectedFontOption.label }</span>
|
||||
<div className="mt-1 text-[9px] uppercase text-black/60">
|
||||
{ selectedFontOption.tier }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ !!customPrefixFont &&
|
||||
<div className="mt-2 text-[10px] leading-4 text-black/60">
|
||||
Premium fonts add an extra price on top of the custom prefix.
|
||||
</div> }
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-[#1f2937] px-3 py-2 text-white" style={ customPrefixPreviewStyle }>
|
||||
<UserIdentityView
|
||||
displayOrder={ displayOrder }
|
||||
nickIcon={ activeIcon?.iconKey || '' }
|
||||
prefixColor={ customPrefixColor }
|
||||
prefixEffect={ customPrefixEffect }
|
||||
prefixFont={ customPrefixFont }
|
||||
prefixIcon={ customPrefixIcon }
|
||||
prefixText={ customPrefixText || 'Preview' }
|
||||
username="Username" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-[12px]">
|
||||
{ customPrefixTotalCredits > 0 && <span>{ customPrefixTotalCredits } credits</span> }
|
||||
{ customPrefixTotalPoints > 0 &&
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutCurrencyIcon type={ customPrefixPointsType } />
|
||||
{ customPrefixTotalPoints }
|
||||
</span> }
|
||||
{ !!customPrefixFont && (customPrefixFontPointsType !== customPrefixPointsType) && (customPrefixFontPricePoints > 0) &&
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutCurrencyIcon type={ customPrefixFontPointsType } />
|
||||
{ customPrefixFontPricePoints }
|
||||
</span> }
|
||||
</div>
|
||||
<Button disabled={ !customPrefixIsValid || isLoading } onClick={ handleCustomPrefixPurchase }>
|
||||
Buy custom prefix
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div> }
|
||||
</div> }
|
||||
|
||||
{ activeTab === 'settings' &&
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="rounded border border-black/10 bg-black/5 p-3">
|
||||
<Text bold>Display order</Text>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
{ Object.entries(ORDER_LABELS).map(([ key, label ]) => (
|
||||
<Button key={ key } disabled={ isLoading && (displayOrder === key) } onClick={ () => handleDisplayOrderChange(key) }>
|
||||
{ displayOrder === key ? '* ' : '' }{ label }
|
||||
</Button>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-black/5 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Text bold>Refresh data</Text>
|
||||
<Button disabled={ isLoading } onClick={ refreshCustomizeData }>Refresh</Button>
|
||||
</div>
|
||||
<div className="text-[11px] leading-4 text-black/70">
|
||||
Use this tab to control how your icon, prefix and username are ordered in bubbles, profile and infostand.
|
||||
</div>
|
||||
</div>
|
||||
</div> }
|
||||
</NitroCardContentView>
|
||||
{ showEmojiPicker &&
|
||||
<>
|
||||
<div className="fixed inset-0 z-[999]" onClick={ () => setShowEmojiPicker(false) } />
|
||||
<div className="fixed left-1/2 top-1/2 z-[1000] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-xl shadow-2xl">
|
||||
<Picker
|
||||
data={ data }
|
||||
locale="en"
|
||||
onEmojiSelect={ (emoji: { native: string }) => { setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false); } }
|
||||
previewPosition="none"
|
||||
set="native"
|
||||
theme="dark" />
|
||||
</div>
|
||||
</> }
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,13 +13,19 @@ interface FriendsRemoveConfirmationViewProps
|
||||
export const FriendsRemoveConfirmationView: FC<FriendsRemoveConfirmationViewProps> = props =>
|
||||
{
|
||||
const { selectedFriendsIds = null, removeFriendsText = null, removeSelectedFriends = null, onCloseClick = null } = props;
|
||||
const separatorIndex = removeFriendsText.indexOf(':');
|
||||
const removeFriendsLeadText = (separatorIndex >= 0) ? removeFriendsText.substring(0, separatorIndex + 1) : removeFriendsText;
|
||||
const removeFriendsNamesText = (separatorIndex >= 0) ? removeFriendsText.substring(separatorIndex + 1).trimStart() : '';
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-friends-remove-confirmation" theme="primary-slim">
|
||||
<NitroCardView className="nitro-friends-remove-confirmation" theme="primary-slim" isResizable={ false } style={ { width: 270, height: 225, minWidth: 270, minHeight: 225, maxWidth: 270, maxHeight: 225 } }>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('friendlist.removefriendconfirm.title') } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<div>{ removeFriendsText }</div>
|
||||
<div className="flex gap-1">
|
||||
<NitroCardContentView className="nitro-friends-remove-confirmation-content text-black">
|
||||
<div className="nitro-friends-remove-confirmation-text">
|
||||
<div>{ removeFriendsLeadText }</div>
|
||||
{ removeFriendsNamesText.length > 0 && <div className="nitro-friends-remove-confirmation-names">{ removeFriendsNamesText }</div> }
|
||||
</div>
|
||||
<div className="nitro-friends-remove-confirmation-actions">
|
||||
<Button fullWidth disabled={ (selectedFriendsIds.length === 0) } variant="danger" onClick={ removeSelectedFriends }>{ LocalizeText('generic.ok') }</Button>
|
||||
<Button fullWidth onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</Button>
|
||||
</div>
|
||||
|
||||
@@ -15,13 +15,13 @@ export const FriendsRoomInviteView: FC<FriendsRoomInviteViewProps> = props =>
|
||||
const [ roomInviteMessage, setRoomInviteMessage ] = useState<string>('');
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-friends-room-invite" theme="primary-slim" uniqueKey="nitro-friends-room-invite">
|
||||
<NitroCardView className="nitro-friends-room-invite" theme="primary-slim" uniqueKey="nitro-friends-room-invite" isResizable={ false } style={ { width: 270, height: 225, minWidth: 270, minHeight: 225, maxWidth: 270, maxHeight: 225 } }>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('friendlist.invite.title') } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black">
|
||||
{ LocalizeText('friendlist.invite.summary', [ 'count' ], [ selectedFriendsIds.length.toString() ]) }
|
||||
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" maxLength={ 255 } value={ roomInviteMessage } onChange={ event => setRoomInviteMessage(event.target.value) }></textarea>
|
||||
<Text center className="bg-muted rounded p-1">{ LocalizeText('friendlist.invite.note') }</Text>
|
||||
<div className="flex gap-1">
|
||||
<NitroCardContentView className="nitro-friends-room-invite-content text-black" gap={ 2 }>
|
||||
<Text className="nitro-friends-room-invite-summary">{ LocalizeText('friendlist.invite.summary', [ 'count' ], [ selectedFriendsIds.length.toString() ]) }</Text>
|
||||
<textarea className="nitro-friends-room-invite-textarea" maxLength={ 255 } value={ roomInviteMessage } onChange={ event => setRoomInviteMessage(event.target.value) }></textarea>
|
||||
<Text center className="nitro-friends-room-invite-note">{ LocalizeText('friendlist.invite.note') }</Text>
|
||||
<div className="nitro-friends-room-invite-actions">
|
||||
<Button fullWidth disabled={ ((roomInviteMessage.length === 0) || (selectedFriendsIds.length === 0)) } variant="success" onClick={ () => sendRoomInvite(roomInviteMessage) }>{ LocalizeText('friendlist.invite.send') }</Button>
|
||||
<Button fullWidth onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { HabboSearchComposer, HabboSearchResultData, HabboSearchResultEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, OpenMessengerChat, SendMessageComposer } from '../../../../api';
|
||||
import { Column, NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionSetViewProps, Text, UserProfileIconView } from '../../../../common';
|
||||
import { Column, LayoutAvatarImageView, NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionSetViewProps, Text, UserProfileIconView } from '../../../../common';
|
||||
import { useFriends, useMessageEvent } from '../../../../hooks';
|
||||
import { resolveAvatarFigure } from './resolveAvatarFigure';
|
||||
import { resolveAvatarGender } from './resolveAvatarGender';
|
||||
|
||||
interface FriendsSearchViewProps extends NitroCardAccordionSetViewProps
|
||||
{
|
||||
@@ -17,6 +19,22 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
|
||||
const [ otherResults, setOtherResults ] = useState<HabboSearchResultData[]>(null);
|
||||
const { canRequestFriend = null, requestFriend = null } = useFriends();
|
||||
|
||||
const getSearchResultFigure = (result: HabboSearchResultData) =>
|
||||
{
|
||||
if(!result) return null;
|
||||
|
||||
const typedResult = (result as HabboSearchResultData & { figureString?: string; avatarFigure?: string; figure?: string; avatarFigureString?: string });
|
||||
|
||||
return typedResult.figureString || typedResult.avatarFigure || typedResult.figure || typedResult.avatarFigureString || null;
|
||||
};
|
||||
|
||||
const getSearchResultGender = (result: HabboSearchResultData) =>
|
||||
{
|
||||
const typedResult = (result as HabboSearchResultData & { gender?: string | number; avatarGender?: string | number });
|
||||
|
||||
return resolveAvatarGender(typedResult.avatarGender ?? typedResult.gender);
|
||||
};
|
||||
|
||||
useMessageEvent<HabboSearchResultEvent>(HabboSearchResultEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
@@ -55,10 +73,15 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
|
||||
{ friendResults.map(result =>
|
||||
{
|
||||
return (
|
||||
<NitroCardAccordionItemView key={ result.avatarId } className="px-2 py-1" justifyContent="between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ result.avatarId } />
|
||||
<div>{ result.avatarName }</div>
|
||||
<NitroCardAccordionItemView key={ result.avatarId } className="friends-list-item px-2 py-1" justifyContent="between">
|
||||
<div className="friends-list-user">
|
||||
<div className="friends-list-avatar">
|
||||
<LayoutAvatarImageView figure={ resolveAvatarFigure(getSearchResultFigure(result), getSearchResultGender(result)) } gender={ getSearchResultGender(result) } headOnly={ true } direction={ 2 } />
|
||||
</div>
|
||||
<div>
|
||||
<UserProfileIconView userId={ result.avatarId } />
|
||||
</div>
|
||||
<div className="friends-list-name">{ result.avatarName }</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{ result.isAvatarOnline &&
|
||||
@@ -82,10 +105,15 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
|
||||
{ otherResults.map(result =>
|
||||
{
|
||||
return (
|
||||
<NitroCardAccordionItemView key={ result.avatarId } className="px-2 py-1" justifyContent="between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ result.avatarId } />
|
||||
<div>{ result.avatarName }</div>
|
||||
<NitroCardAccordionItemView key={ result.avatarId } className="friends-list-item px-2 py-1" justifyContent="between">
|
||||
<div className="friends-list-user">
|
||||
<div className="friends-list-avatar">
|
||||
<LayoutAvatarImageView figure={ resolveAvatarFigure(getSearchResultFigure(result), getSearchResultGender(result)) } gender={ getSearchResultGender(result) } headOnly={ true } direction={ 2 } />
|
||||
</div>
|
||||
<div>
|
||||
<UserProfileIconView userId={ result.avatarId } />
|
||||
</div>
|
||||
<div className="friends-list-name">{ result.avatarName }</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{ canRequestFriend(result.avatarId) &&
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FriendsListView: FC<{}> = props =>
|
||||
userNames.push(existingFriend.name);
|
||||
}
|
||||
|
||||
return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join(', ') ]);
|
||||
return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join('\n') ]);
|
||||
}, [ offlineFriends, onlineFriends, selectedFriendsIds ]);
|
||||
|
||||
const selectFriend = useCallback((userId: number) =>
|
||||
@@ -60,6 +60,27 @@ export const FriendsListView: FC<{}> = props =>
|
||||
});
|
||||
}, [ setSelectedFriendsIds ]);
|
||||
|
||||
const toggleSelectFriends = useCallback((friendIds: number[]) =>
|
||||
{
|
||||
if(!friendIds.length) return;
|
||||
|
||||
setSelectedFriendsIds(prevValue =>
|
||||
{
|
||||
const allSelected = friendIds.every(friendId => (prevValue.indexOf(friendId) >= 0));
|
||||
|
||||
if(allSelected) return prevValue.filter(friendId => (friendIds.indexOf(friendId) === -1));
|
||||
|
||||
const nextValue = [ ...prevValue ];
|
||||
|
||||
for(const friendId of friendIds)
|
||||
{
|
||||
if(nextValue.indexOf(friendId) === -1) nextValue.push(friendId);
|
||||
}
|
||||
|
||||
return nextValue;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sendRoomInvite = (message: string) =>
|
||||
{
|
||||
if(!selectedFriendsIds.length || !message || !message.length || (message.length > 255)) return;
|
||||
@@ -125,10 +146,24 @@ export const FriendsListView: FC<{}> = props =>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardContentView className="text-black p-0" gap={ 1 } overflow="hidden">
|
||||
<NitroCardAccordionView fullHeight overflow="hidden">
|
||||
<NitroCardAccordionSetView 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">
|
||||
<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))
|
||||
? LocalizeText('friendlist.unselect_all')
|
||||
: LocalizeText('friendlist.select_all') }
|
||||
</span>
|
||||
</Flex>
|
||||
<FriendsListGroupView list={ onlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
|
||||
</NitroCardAccordionSetView>
|
||||
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
|
||||
<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)); } }>
|
||||
{ offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
|
||||
? LocalizeText('friendlist.unselect_all')
|
||||
: LocalizeText('friendlist.select_all') }
|
||||
</span>
|
||||
</Flex>
|
||||
<FriendsListGroupView list={ offlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
|
||||
</NitroCardAccordionSetView>
|
||||
<FriendsListRequestView headerText={ LocalizeText('friendlist.tab.friendrequests') + ` (${ requests.length })` } isExpanded={ true } />
|
||||
|
||||
+10
-5
@@ -1,7 +1,9 @@
|
||||
import { FC, MouseEvent, useState } from 'react';
|
||||
import { LocalizeText, MessengerFriend, OpenMessengerChat } from '../../../../../api';
|
||||
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
|
||||
import { LayoutAvatarImageView, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
|
||||
import { useFriends } from '../../../../../hooks';
|
||||
import { resolveAvatarFigure } from '../resolveAvatarFigure';
|
||||
import { resolveAvatarGender } from '../resolveAvatarGender';
|
||||
|
||||
export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: boolean, selectFriend: (userId: number) => void }> = props =>
|
||||
{
|
||||
@@ -55,14 +57,17 @@ export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: b
|
||||
if(!friend) return null;
|
||||
|
||||
return (
|
||||
<NitroCardAccordionItemView className={ `px-2 py-1 ${ selected && 'bg-primary text-white' }` } justifyContent="between" onClick={ event => selectFriend(friend.id) }>
|
||||
<div className="flex items-center gap-1">
|
||||
<NitroCardAccordionItemView className={ `friends-list-item ${ selected ? 'selected' : '' }` } justifyContent="between" onClick={ event => selectFriend(friend.id) }>
|
||||
<div className="friends-list-user">
|
||||
<div className="friends-list-avatar">
|
||||
<LayoutAvatarImageView figure={ resolveAvatarFigure(friend.figure, friend.gender) } gender={ resolveAvatarGender(friend.gender) } headOnly={ true } direction={ 2 } />
|
||||
</div>
|
||||
<div onClick={ event => event.stopPropagation() }>
|
||||
<UserProfileIconView userId={ friend.id } />
|
||||
</div>
|
||||
<div>{ friend.name }</div>
|
||||
<div className="friends-list-name">{ friend.name }</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="friends-list-actions">
|
||||
{ !isRelationshipOpen &&
|
||||
<>
|
||||
{ friend.online &&
|
||||
|
||||
+19
-8
@@ -1,7 +1,9 @@
|
||||
import { FC } from 'react';
|
||||
import { MessengerRequest } from '../../../../../api';
|
||||
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
|
||||
import { LocalizeText, MessengerRequest } from '../../../../../api';
|
||||
import { Button, LayoutAvatarImageView, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
|
||||
import { useFriends } from '../../../../../hooks';
|
||||
import { resolveAvatarFigure } from '../resolveAvatarFigure';
|
||||
import { resolveAvatarGender } from '../resolveAvatarGender';
|
||||
|
||||
export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = props =>
|
||||
{
|
||||
@@ -11,14 +13,23 @@ export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = pro
|
||||
if(!request) return null;
|
||||
|
||||
return (
|
||||
<NitroCardAccordionItemView className="px-2 py-1" justifyContent="between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ request.id } />
|
||||
<div>{ request.name }</div>
|
||||
<NitroCardAccordionItemView className="friends-list-item px-2 py-1" justifyContent="between">
|
||||
<div className="friends-list-user">
|
||||
<div className="friends-list-avatar">
|
||||
<LayoutAvatarImageView figure={ resolveAvatarFigure(request.figureString) } gender={ resolveAvatarGender(undefined) } headOnly={ true } direction={ 2 } />
|
||||
</div>
|
||||
<div>
|
||||
<UserProfileIconView userId={ request.requesterUserId } />
|
||||
</div>
|
||||
<div className="friends-list-name">{ request.name }</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="nitro-friends-spritesheet icon-accept cursor-pointer" onClick={ event => requestResponse(request.id, true) } />
|
||||
<div className="nitro-friends-spritesheet icon-deny cursor-pointer" onClick={ event => requestResponse(request.id, false) } />
|
||||
<Button size="sm" onClick={ event => requestResponse(request.id, true) }>
|
||||
{ LocalizeText('friendlist.request_accept') }
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={ event => requestResponse(request.id, false) }>
|
||||
{ LocalizeText('friendlist.request_decline') }
|
||||
</Button>
|
||||
</div>
|
||||
</NitroCardAccordionItemView>
|
||||
);
|
||||
|
||||
+5
-2
@@ -17,8 +17,11 @@ export const FriendsListRequestView: FC<NitroCardAccordionSetViewProps> = props
|
||||
<Column gap={ 0 }>
|
||||
{ requests.map((request, index) => <FriendsListRequestItemView key={ index } request={ request } />) }
|
||||
</Column>
|
||||
<div className="flex justify-center px-2 py-1">
|
||||
<Button onClick={ event => requestResponse(-1, false) }>
|
||||
<div className="flex justify-center gap-2 px-2 py-1">
|
||||
<Button onClick={ event => requests.forEach(request => requestResponse(request.id, true)) }>
|
||||
{ LocalizeText('friendlist.requests.acceptall') }
|
||||
</Button>
|
||||
<Button variant="danger" onClick={ event => requestResponse(-1, false) }>
|
||||
{ LocalizeText('friendlist.requests.dismissall') }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { resolveAvatarGender } from './resolveAvatarGender';
|
||||
|
||||
const DEFAULT_AVATAR_FIGURES: Record<string, string> = {
|
||||
M: 'hd-180-1.ch-210-66.lg-270-82.sh-290-80',
|
||||
F: 'hd-600-1.ch-630-66.lg-695-82.sh-725-80'
|
||||
};
|
||||
|
||||
export const resolveAvatarFigure = (figure: string | null | undefined, gender?: string | number | null) =>
|
||||
{
|
||||
const normalizedFigure = (figure || '').trim();
|
||||
|
||||
if(normalizedFigure.length && normalizedFigure.includes('hd-')) return normalizedFigure;
|
||||
|
||||
return DEFAULT_AVATAR_FIGURES[resolveAvatarGender(gender)] || DEFAULT_AVATAR_FIGURES.M;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export const resolveAvatarGender = (value: string | number | null | undefined) =>
|
||||
{
|
||||
if(typeof value === 'string')
|
||||
{
|
||||
const normalized = value.trim().toUpperCase();
|
||||
|
||||
if(normalized === 'F') return 'F';
|
||||
if(normalized === 'M') return 'M';
|
||||
if(normalized === 'FEMALE') return 'F';
|
||||
if(normalized === 'MALE') return 'M';
|
||||
}
|
||||
|
||||
if(typeof value === 'number')
|
||||
{
|
||||
if(value === 2) return 'F';
|
||||
if(value === 1) return 'M';
|
||||
}
|
||||
|
||||
return 'M';
|
||||
};
|
||||
@@ -2,9 +2,8 @@ import { AddLinkEventTracker, FollowFriendMessageComposer, GetSessionDataManager
|
||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { GetUserProfile, LocalizeText, ReportType, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutGridItem, LayoutItemCountView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { useHelp, useMessenger } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { DraggableWindowPosition, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
|
||||
import { useHelp, useMessenger, useTranslation } from '../../../../hooks';
|
||||
import { FriendsMessengerThreadView } from './messenger-thread/FriendsMessengerThreadView';
|
||||
|
||||
export const FriendsMessengerView: FC<{}> = props =>
|
||||
@@ -14,15 +13,35 @@ export const FriendsMessengerView: FC<{}> = props =>
|
||||
const [ messageText, setMessageText ] = useState('');
|
||||
const { visibleThreads = [], activeThread = null, getMessageThread = null, sendMessage = null, setActiveThreadId = null, closeThread = null } = useMessenger();
|
||||
const { report = null } = useHelp();
|
||||
const { settings, translateOutgoing } = useTranslation();
|
||||
const messagesBox = useRef<HTMLDivElement>();
|
||||
|
||||
const followFriend = () => (activeThread && activeThread.participant && SendMessageComposer(new FollowFriendMessageComposer(activeThread.participant.id)));
|
||||
const openProfile = () => (activeThread && activeThread.participant && GetUserProfile(activeThread.participant.id));
|
||||
|
||||
const send = () =>
|
||||
const send = async () =>
|
||||
{
|
||||
if(!activeThread || !messageText.length) return;
|
||||
|
||||
const trimmedText = messageText.trimStart();
|
||||
const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':');
|
||||
|
||||
if(!shouldTranslateOutgoing)
|
||||
{
|
||||
sendMessage(activeThread, GetSessionDataManager().userId, messageText);
|
||||
setMessageText('');
|
||||
return;
|
||||
}
|
||||
|
||||
const translation = await translateOutgoing(messageText);
|
||||
|
||||
if(translation && translation.translatedText?.length && (translation.translatedText.length <= 255))
|
||||
{
|
||||
sendMessage(activeThread, GetSessionDataManager().userId, translation.translatedText, 0, null, undefined, translation);
|
||||
setMessageText('');
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(activeThread, GetSessionDataManager().userId, messageText);
|
||||
|
||||
setMessageText('');
|
||||
@@ -32,7 +51,7 @@ export const FriendsMessengerView: FC<{}> = props =>
|
||||
{
|
||||
if(event.key !== 'Enter') return;
|
||||
|
||||
send();
|
||||
void send();
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
@@ -107,71 +126,60 @@ export const FriendsMessengerView: FC<{}> = props =>
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-friends-messenger w-[800px] h-[720px]" theme="primary-slim" uniqueKey="nitro-friends-messenger">
|
||||
<NitroCardView className="messenger-card" theme="primary-slim" uniqueKey={ null } windowPosition={ DraggableWindowPosition.TOP_CENTER } offsetTop={ 8 } isResizable={ false }>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('messenger.window.title', [ 'OPEN_CHAT_COUNT' ], [ visibleThreads.length.toString() ]) } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardContentView>
|
||||
<Grid overflow="hidden">
|
||||
<Column overflow="hidden" size={ 4 }>
|
||||
<Text bold>{ LocalizeText('toolbar.icon.label.messenger') }</Text>
|
||||
<Column fit overflow="auto">
|
||||
<Column>
|
||||
{ visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ thread.threadId } itemActive={ (activeThread === thread) } onClick={ event => setActiveThreadId(thread.threadId) } className="py-1 px-2">
|
||||
{ thread.unread && <LayoutItemCountView className="text-black" count={ thread.unreadCount } /> }
|
||||
<Flex fullWidth gap={ 1 } style={{ minHeight: '50px' }}>
|
||||
<LayoutAvatarImageView
|
||||
figure={ thread.participant.id > 0 ? thread.participant.figure : thread.participant.figure === 'ADM' ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : thread.participant.figure }
|
||||
headOnly={ true }
|
||||
direction={ thread.participant.id > 0 ? 2 : 3 }
|
||||
style={{ width: '50px', height: '80px', backgroundPosition: 'center 45%', flexShrink: 0, alignSelf: 'flex-end' }}
|
||||
/>
|
||||
<Text truncate grow className="self-center">{ thread.participant.name }</Text>
|
||||
</Flex>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</Column>
|
||||
</Column>
|
||||
</Column>
|
||||
<Column overflow="hidden" size={ 8 }>
|
||||
{ activeThread &&
|
||||
<>
|
||||
<Text bold center>{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }</Text>
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<NitroCardContentView className="text-black p-0" gap={ 0 } overflow="hidden">
|
||||
<div className="messenger-card-body">
|
||||
<div className="messenger-avatar-bar">
|
||||
{ visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread =>
|
||||
{
|
||||
return (
|
||||
<button key={ thread.threadId } className={ 'messenger-avatar-tab' + ((activeThread === thread) ? ' active' : '') + (thread.unread ? ' unread' : '') } onClick={ event => setActiveThreadId(thread.threadId) }>
|
||||
<LayoutAvatarImageView
|
||||
figure={ thread.participant.id > 0 ? thread.participant.figure : thread.participant.figure === 'ADM' ? 'ha-3409-1413-70.lg-285-89.ch-3032-1334-109.sh-3016-110.hd-185-1359.ca-3225-110-62.wa-3264-62-62.fa-1206-90.hr-3322-1403' : thread.participant.figure }
|
||||
headOnly={ true }
|
||||
direction={ thread.participant.id > 0 ? 2 : 3 }
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
|
||||
{ activeThread &&
|
||||
<>
|
||||
<div className="messenger-thread-header">
|
||||
<span className="messenger-thread-name">{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }</span>
|
||||
<div className="messenger-actions">
|
||||
{ (activeThread.participant.id > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button onClick={ followFriend }>
|
||||
<div className="nitro-friends-spritesheet icon-follow" />
|
||||
</Button>
|
||||
<Button onClick={ openProfile }>
|
||||
<div className="nitro-friends-spritesheet icon-profile-sm" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="danger" onClick={ () => report(ReportType.IM, { reportedUserId: activeThread.participant.id }) }>
|
||||
<>
|
||||
<button className="messenger-btn icon-btn" onClick={ followFriend }>
|
||||
<div className="nitro-friends-spritesheet icon-follow" />
|
||||
</button>
|
||||
<button className="messenger-btn icon-btn" onClick={ openProfile }>
|
||||
<div className="nitro-friends-spritesheet icon-profile-sm" />
|
||||
</button>
|
||||
<button className="messenger-btn danger" onClick={ () => report(ReportType.IM, { reportedUserId: activeThread.participant.id }) }>
|
||||
{ LocalizeText('messenger.window.button.report') }
|
||||
</Button>
|
||||
</div> }
|
||||
<Button onClick={ event => closeThread(activeThread.threadId) }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</Button>
|
||||
</Flex>
|
||||
<Column fit className="bg-muted p-2 rounded chat-messages">
|
||||
<Column innerRef={ messagesBox } overflow="auto">
|
||||
<FriendsMessengerThreadView thread={ activeThread } />
|
||||
</Column>
|
||||
</Column>
|
||||
<div className="flex gap-1">
|
||||
<NitroInput maxLength={ 255 } placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } type="text" value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } />
|
||||
<Button variant="success" onClick={ send }>
|
||||
{ LocalizeText('widgets.chatinput.say') }
|
||||
</Button>
|
||||
</button>
|
||||
</> }
|
||||
<button className="messenger-btn close-btn" onClick={ event => closeThread(activeThread.threadId) }>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<div ref={ messagesBox } className="chat-messages">
|
||||
<FriendsMessengerThreadView thread={ activeThread } />
|
||||
</div>
|
||||
|
||||
<div className="messenger-input-row">
|
||||
<input maxLength={ 255 } placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } type="text" value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } />
|
||||
<button className="messenger-btn send" onClick={ () => void send() }>
|
||||
{ LocalizeText('widgets.chatinput.say') }
|
||||
</button>
|
||||
</div>
|
||||
</> }
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
|
||||
+32
-13
@@ -28,14 +28,11 @@ export const FriendsMessengerThreadGroup: FC<{ thread: MessengerThread, group: M
|
||||
<>
|
||||
{ group.chats.map((chat, index) =>
|
||||
{
|
||||
if(chat.type === MessengerThreadChat.SECURITY_NOTIFICATION) return null;
|
||||
|
||||
return (
|
||||
<Flex key={ index } fullWidth gap={ 2 } justifyContent="start">
|
||||
<Base className="w-full text-break">
|
||||
{ (chat.type === MessengerThreadChat.SECURITY_NOTIFICATION) &&
|
||||
<Flex alignItems="center" className="bg-light rounded mb-2 px-2 py-1 small text-muted" gap={ 2 }>
|
||||
<Base className="nitro-friends-spritesheet icon-warning shrink-0" />
|
||||
<Base>{ chat.message }</Base>
|
||||
</Flex> }
|
||||
{ (chat.type === MessengerThreadChat.ROOM_INVITE) &&
|
||||
<Flex alignItems="center" className="bg-light rounded mb-2 px-2 py-1 small text-black" gap={ 2 }>
|
||||
<Base className="messenger-notification-icon shrink-0" />
|
||||
@@ -50,24 +47,46 @@ export const FriendsMessengerThreadGroup: FC<{ thread: MessengerThread, group: M
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex fullWidth gap={ 2 } justifyContent={ isOwnChat ? 'end' : 'start' }>
|
||||
<Flex fullWidth gap={ 2 } justifyContent={ isOwnChat ? 'end' : 'start' } className={ 'messenger-message-row ' + (isOwnChat ? 'own' : '') }>
|
||||
<Base shrink className="message-avatar">
|
||||
{ ((group.type === MessengerGroupType.PRIVATE_CHAT) && !isOwnChat) &&
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ thread.participant.figure } /> }
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ thread.participant.figure } headOnly={ true } /> }
|
||||
{ (groupChatData && !isOwnChat) &&
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ groupChatData.figure } /> }
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ groupChatData.figure } headOnly={ true } /> }
|
||||
</Base>
|
||||
<Base className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (isOwnChat ? 'right' : 'left') }>
|
||||
<Base className="font-bold">
|
||||
<Base className="small text-muted">{ group.chats[0].date.toLocaleTimeString() }</Base>
|
||||
<Base className="messenger-message-body">
|
||||
<Base className={ 'messenger-message-name ' + (isOwnChat ? 'text-end' : '') }>
|
||||
{ isOwnChat && GetSessionDataManager().userName }
|
||||
{ !isOwnChat && (groupChatData ? groupChatData.username : thread.participant.name) }
|
||||
:
|
||||
</Base>
|
||||
{ group.chats.map((chat, index) => <Base key={ index } className="text-break">{ chat.message }</Base>) }
|
||||
<Base className={ 'messenger-message-bubble messages-group-' + (isOwnChat ? 'right' : 'left') }>
|
||||
{ group.chats.map((chat, index) =>
|
||||
{
|
||||
if(!chat.showTranslation)
|
||||
{
|
||||
return <Base key={ index } className="text-break">{ chat.message }</Base>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Base key={ index } className="messenger-translation-block">
|
||||
<Base className="messenger-translation-row">
|
||||
<span className="messenger-translation-label">original:</span>
|
||||
<span className="text-break">{ chat.originalMessage || chat.message }</span>
|
||||
</Base>
|
||||
<Base className="messenger-translation-row">
|
||||
<span className="messenger-translation-label">translate:</span>
|
||||
<span className="text-break">{ chat.translatedMessage || chat.message }</span>
|
||||
</Base>
|
||||
</Base>
|
||||
);
|
||||
}) }
|
||||
</Base>
|
||||
<Base className="messenger-message-time">{ group.chats[0].date.toLocaleTimeString() }</Base>
|
||||
</Base>
|
||||
{ isOwnChat &&
|
||||
<Base shrink className="message-avatar">
|
||||
<LayoutAvatarImageView direction={ 4 } figure={ GetSessionDataManager().figure } />
|
||||
<LayoutAvatarImageView direction={ 4 } figure={ GetSessionDataManager().figure } headOnly={ true } />
|
||||
</Base> }
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { FaTrashAlt } from 'react-icons/fa';
|
||||
import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
|
||||
import { useInventoryPrefixes, useNotification } from '../../../../hooks';
|
||||
import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, getPrefixFontStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
|
||||
import { Button } from '../../../../common';
|
||||
import { GetNickIconUrl } from '../../../../assets/images/user_custom/nick_icons';
|
||||
import { useInventoryNickIcons, useInventoryPrefixes, useNotification } from '../../../../hooks';
|
||||
import { NitroButton } from '../../../../layout';
|
||||
|
||||
const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', className = '', textSize = 'text-sm' }) =>
|
||||
type InventoryIdentityTab = 'prefixes' | 'icons';
|
||||
|
||||
const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; font?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', font = '', className = '', textSize = 'text-sm' }) =>
|
||||
{
|
||||
const colors = parsePrefixColors(text, color);
|
||||
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
||||
const fxStyle = getPrefixEffectStyle(effect, colors[0] || '#FFFFFF');
|
||||
const fontStyle = getPrefixFontStyle(font);
|
||||
|
||||
return (
|
||||
<span className={ `font-bold ${ textSize } ${ className }` } style={ fxStyle }>
|
||||
{ effect === 'pulse' && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
|
||||
<span className={ `font-bold ${ textSize } ${ className }` } style={ { ...fontStyle, ...fxStyle } }>
|
||||
{ !!effect && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
|
||||
{ icon && <span className="mr-0.5">{ icon }</span> }
|
||||
<span style={ hasMultiColor ? fxStyle : { ...fxStyle, color: colors[0] || '#FFFFFF' } }>
|
||||
<span style={ hasMultiColor ? { ...fontStyle, ...fxStyle } : { ...fontStyle, ...fxStyle, color: colors[0] || '#FFFFFF' } }>
|
||||
{'{'}
|
||||
{ hasMultiColor
|
||||
? [ ...text ].map((char, i) => (
|
||||
<span key={ i } style={ { color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(effect, colors[i]) } }>{ char }</span>
|
||||
<span key={ i } style={ { ...fontStyle, color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(effect, colors[i]) } }>{ char }</span>
|
||||
))
|
||||
: text
|
||||
}
|
||||
@@ -40,7 +45,30 @@ const PrefixItemView: FC<{
|
||||
${ isSelected ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ prefix.active ? 'ring-2 ring-green-400' : '' }` }
|
||||
onClick={ onClick }>
|
||||
<PrefixPreview className="truncate" color={ prefix.color } effect={ prefix.effect } icon={ prefix.icon } text={ prefix.text } />
|
||||
<PrefixPreview className="truncate" color={ prefix.color } effect={ prefix.effect } font={ prefix.font } icon={ prefix.icon } text={ prefix.text } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NickIconItemView: FC<{
|
||||
iconKey: string;
|
||||
displayName: string;
|
||||
isSelected: boolean;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}> = ({ iconKey, displayName, isSelected, isActive, onClick }) =>
|
||||
{
|
||||
return (
|
||||
<div
|
||||
className={ `relative flex cursor-pointer items-center justify-center rounded-md border-2 p-2 transition-colors
|
||||
${ isSelected ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ isActive ? 'ring-2 ring-green-400' : '' }` }
|
||||
onClick={ onClick }>
|
||||
{ isActive && <span className="absolute right-1 top-1 rounded bg-[#15954c] px-1 py-0.5 text-[8px] font-bold uppercase text-white">Active</span> }
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<img className="h-auto max-h-[28px] w-auto object-contain" src={ GetNickIconUrl(iconKey) } alt={ displayName || iconKey } />
|
||||
<span className="max-w-[90px] truncate text-center text-[11px] font-bold">{ displayName || iconKey }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -48,8 +76,13 @@ const PrefixItemView: FC<{
|
||||
export const InventoryPrefixView: FC<{}> = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ activeTab, setActiveTab ] = useState<InventoryIdentityTab>('prefixes');
|
||||
const { prefixes = [], activePrefix = null, selectedPrefix = null, setSelectedPrefix = null, activatePrefix = null, deactivatePrefix = null, deletePrefix = null, activate = null, deactivate = null } = useInventoryPrefixes();
|
||||
const { nickIcons = [], activeNickIcon = null, selectedNickIcon = null, setSelectedNickIcon = null, activateNickIcon = null, deactivateNickIcon = null, activate: activateNickIcons = null, deactivate: deactivateNickIcons = null } = useInventoryNickIcons();
|
||||
const { showConfirm = null } = useNotification();
|
||||
const hasPrefixes = prefixes && (prefixes.length > 0);
|
||||
const hasNickIcons = nickIcons && (nickIcons.length > 0);
|
||||
const selectedIconUrl = useMemo(() => selectedNickIcon ? GetNickIconUrl(selectedNickIcon.iconKey) : '', [ selectedNickIcon ]);
|
||||
|
||||
const attemptDeletePrefix = () =>
|
||||
{
|
||||
@@ -69,10 +102,15 @@ export const InventoryPrefixView: FC<{}> = () =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
const id = activate();
|
||||
const prefixVisibilityId = activate();
|
||||
const iconVisibilityId = activateNickIcons();
|
||||
|
||||
return () => deactivate(id);
|
||||
}, [ isVisible, activate, deactivate ]);
|
||||
return () =>
|
||||
{
|
||||
deactivate(prefixVisibilityId);
|
||||
deactivateNickIcons(iconVisibilityId);
|
||||
};
|
||||
}, [ isVisible, activate, activateNickIcons, deactivate, deactivateNickIcons ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -82,55 +120,115 @@ export const InventoryPrefixView: FC<{}> = () =>
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="flex flex-col col-span-7 gap-1 overflow-auto">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ prefixes.map(prefix => (
|
||||
<PrefixItemView
|
||||
key={ prefix.id }
|
||||
isSelected={ selectedPrefix?.id === prefix.id }
|
||||
prefix={ prefix }
|
||||
onClick={ () => setSelectedPrefix(prefix) } />
|
||||
)) }
|
||||
<div className="flex h-full flex-col gap-2">
|
||||
<div className="shrink-0 rounded border border-black/10 bg-[#C9C9C9] p-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activeTab === 'prefixes' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
||||
type="button"
|
||||
onClick={ () => setActiveTab('prefixes') }>
|
||||
Prefixes
|
||||
</button>
|
||||
<button
|
||||
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activeTab === 'icons' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
||||
type="button"
|
||||
onClick={ () => setActiveTab('icons') }>
|
||||
Icons
|
||||
</button>
|
||||
</div>
|
||||
{ (!prefixes || prefixes.length === 0) &&
|
||||
<div className="flex items-center justify-center h-full text-sm opacity-50">
|
||||
{ LocalizeText('inventory.empty.title') }
|
||||
</div> }
|
||||
</div>
|
||||
<div className="flex flex-col justify-between col-span-5 overflow-auto">
|
||||
{ activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center p-3 rounded-md border-2 border-green-400 bg-card-grid-item">
|
||||
<PrefixPreview color={ activePrefix.color } effect={ activePrefix.effect } icon={ activePrefix.icon } text={ activePrefix.text } textSize="text-lg" />
|
||||
|
||||
{ activeTab === 'prefixes' &&
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="col-span-7 flex flex-col gap-1 overflow-auto pr-1">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ prefixes.map(prefix => (
|
||||
<PrefixItemView
|
||||
key={ prefix.id }
|
||||
isSelected={ selectedPrefix?.id === prefix.id }
|
||||
prefix={ prefix }
|
||||
onClick={ () => setSelectedPrefix(prefix) } />
|
||||
)) }
|
||||
</div>
|
||||
</div> }
|
||||
{ !activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center p-3 rounded-md border-2 border-dashed border-card-grid-item-border bg-card-grid-item opacity-50">
|
||||
<span className="text-sm">No active prefix</span>
|
||||
{ !hasPrefixes &&
|
||||
<div className="flex h-full items-center justify-center text-sm opacity-50">
|
||||
{ LocalizeText('inventory.empty.title') }
|
||||
</div> }
|
||||
</div>
|
||||
<div className="col-span-5 flex flex-col justify-between overflow-auto">
|
||||
{ activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center rounded-md border-2 border-green-400 bg-card-grid-item p-3">
|
||||
<PrefixPreview color={ activePrefix.color } effect={ activePrefix.effect } font={ activePrefix.font } icon={ activePrefix.icon } text={ activePrefix.text } textSize="text-lg" />
|
||||
</div>
|
||||
</div> }
|
||||
{ !activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center rounded-md border-2 border-dashed border-card-grid-item-border bg-card-grid-item p-3 opacity-50">
|
||||
<span className="text-sm">No active prefix</span>
|
||||
</div>
|
||||
</div> }
|
||||
{ !!selectedPrefix &&
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-center gap-2 rounded bg-card-grid-item p-2">
|
||||
<PrefixPreview color={ selectedPrefix.color } effect={ selectedPrefix.effect } font={ selectedPrefix.font } icon={ selectedPrefix.icon } text={ selectedPrefix.text } textSize="text-lg" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<NitroButton
|
||||
className="grow"
|
||||
onClick={ () => selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
|
||||
{ selectedPrefix.active ? 'Deactivate' : 'Activate' }
|
||||
</NitroButton>
|
||||
{ !selectedPrefix.active &&
|
||||
<NitroButton className="bg-danger! hover:bg-danger/80! p-1" onClick={ attemptDeletePrefix }>
|
||||
<FaTrashAlt className="fa-icon" />
|
||||
</NitroButton> }
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
</div> }
|
||||
|
||||
{ activeTab === 'icons' &&
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="col-span-7 flex flex-col gap-1 overflow-auto pr-1">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ nickIcons.map(icon => (
|
||||
<NickIconItemView
|
||||
key={ icon.id }
|
||||
displayName={ icon.displayName }
|
||||
iconKey={ icon.iconKey }
|
||||
isActive={ !!icon.active }
|
||||
isSelected={ selectedNickIcon?.id === icon.id }
|
||||
onClick={ () => setSelectedNickIcon(icon) } />
|
||||
)) }
|
||||
</div>
|
||||
</div> }
|
||||
{ !!selectedPrefix &&
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex items-center justify-center gap-2 p-2 rounded bg-card-grid-item">
|
||||
<PrefixPreview color={ selectedPrefix.color } effect={ selectedPrefix.effect } icon={ selectedPrefix.icon } text={ selectedPrefix.text } textSize="text-lg" />
|
||||
{ !hasNickIcons &&
|
||||
<div className="flex h-full items-center justify-center text-sm opacity-50">
|
||||
No purchased icons yet
|
||||
</div> }
|
||||
</div>
|
||||
<div className="col-span-5 flex flex-col justify-between overflow-auto">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active icon</span>
|
||||
<div className={ `flex min-h-[88px] items-center justify-center rounded-md border-2 bg-card-grid-item p-3 ${ activeNickIcon ? 'border-green-400' : 'border-dashed border-card-grid-item-border opacity-50' }` }>
|
||||
{ activeNickIcon && <img className="h-auto max-h-[36px] w-auto object-contain" src={ GetNickIconUrl(activeNickIcon.iconKey) } alt={ activeNickIcon.displayName || activeNickIcon.iconKey } /> }
|
||||
{ !activeNickIcon && <span className="text-sm">No active icon</span> }
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<NitroButton
|
||||
className="grow"
|
||||
onClick={ () => selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
|
||||
{ selectedPrefix.active ? 'Deactivate' : 'Activate' }
|
||||
</NitroButton>
|
||||
{ !selectedPrefix.active &&
|
||||
<NitroButton className="bg-danger! hover:bg-danger/80! p-1" onClick={ attemptDeletePrefix }>
|
||||
<FaTrashAlt className="fa-icon" />
|
||||
</NitroButton> }
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
{ !!selectedNickIcon &&
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<div className="flex min-h-[100px] flex-col items-center justify-center gap-2 rounded bg-card-grid-item p-3 text-center">
|
||||
<img className="h-auto max-h-[40px] w-auto object-contain" src={ selectedIconUrl } alt={ selectedNickIcon.displayName || selectedNickIcon.iconKey } />
|
||||
<span className="text-sm font-bold">{ selectedNickIcon.displayName || selectedNickIcon.iconKey }</span>
|
||||
</div>
|
||||
<Button disabled={ false } onClick={ () => selectedNickIcon.active ? deactivateNickIcon() : activateNickIcon(selectedNickIcon.id) }>
|
||||
{ selectedNickIcon.active ? 'Deactivate' : 'Activate' }
|
||||
</Button>
|
||||
</div> }
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { FaChevronDown, FaQuestionCircle } from 'react-icons/fa';
|
||||
import { FaChevronDown, FaLanguage, FaQuestionCircle } from 'react-icons/fa';
|
||||
import { FriendlyTime, GetConfigurationValue, LocalizeText } from '../../api';
|
||||
import { Column, Flex, LayoutCurrencyIcon, Text } from '../../common';
|
||||
import { usePurse } from '../../hooks';
|
||||
@@ -91,6 +91,9 @@ export const PurseView: FC<{}> = props => {
|
||||
</div>
|
||||
</div> }
|
||||
<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">
|
||||
<FaLanguage />
|
||||
</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') }>
|
||||
<FaQuestionCircle />
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusI
|
||||
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common';
|
||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||||
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
||||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||||
@@ -29,7 +29,6 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
||||
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
||||
|
||||
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
|
||||
|
||||
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
|
||||
@@ -79,6 +78,12 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
newValue.figure = event.figure;
|
||||
newValue.motto = event.customInfo;
|
||||
newValue.achievementScore = event.activityPoints;
|
||||
newValue.nickIcon = event.nickIcon;
|
||||
newValue.prefixText = event.prefixText;
|
||||
newValue.prefixColor = event.prefixColor;
|
||||
newValue.prefixIcon = event.prefixIcon;
|
||||
newValue.prefixEffect = event.prefixEffect;
|
||||
newValue.displayOrder = event.displayOrder;
|
||||
newValue.backgroundId = event.backgroundId;
|
||||
newValue.standId = event.standId;
|
||||
newValue.overlayId = event.overlayId;
|
||||
@@ -139,7 +144,17 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.webID} />
|
||||
<Text small wrap variant="white">{avatarInfo.name}</Text>
|
||||
<UserIdentityView
|
||||
className="text-[12px]"
|
||||
displayOrder={ avatarInfo.displayOrder }
|
||||
nameClassName="text-white"
|
||||
nickIcon={ avatarInfo.nickIcon }
|
||||
prefixColor={ avatarInfo.prefixColor }
|
||||
prefixEffect={ avatarInfo.prefixEffect }
|
||||
prefixFont={ avatarInfo.prefixFont }
|
||||
prefixIcon={ avatarInfo.prefixIcon }
|
||||
prefixText={ avatarInfo.prefixText }
|
||||
username={ avatarInfo.name } />
|
||||
</div>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatBubbleMessage, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
|
||||
import { ChatBubbleMessage } from '../../../../api';
|
||||
import { UserIdentityView } from '../../../../common';
|
||||
import { useOnClickChat } from '../../../../hooks';
|
||||
|
||||
interface ChatWidgetMessageViewProps
|
||||
@@ -38,11 +39,11 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsVisible(false);
|
||||
|
||||
const element = elementRef.current;
|
||||
if(!element) return;
|
||||
|
||||
const previousWidth = chat.width;
|
||||
const previousHeight = chat.height;
|
||||
const { offsetWidth: width, offsetHeight: height } = element;
|
||||
|
||||
chat.width = width;
|
||||
@@ -62,10 +63,14 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
|
||||
|
||||
setIsReady(true);
|
||||
|
||||
if(isVisible && ((previousWidth !== width) || (previousHeight !== height)) && makeRoom) makeRoom(chat);
|
||||
}, [ chat, chat.formattedText, chat.originalFormattedText, chat.showTranslation, chat.translatedFormattedText, isVisible, makeRoom ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
chat.elementRef = null;
|
||||
setIsReady(false);
|
||||
};
|
||||
}, [ chat ]);
|
||||
|
||||
@@ -77,6 +82,8 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
|
||||
setIsVisible(true);
|
||||
}, [ chat, isReady, isVisible, makeRoom ]);
|
||||
|
||||
const messageClassName = `message [overflow-wrap:anywhere] break-words${ chat.type === 1 ? ' italic text-[#595959]' : '' }${ chat.type === 2 ? ' font-bold' : '' }`;
|
||||
|
||||
return (
|
||||
<div ref={ elementRef } className={ `bubble-container newbubblehe ${ isVisible ? 'visible' : 'invisible' } w-max absolute select-none pointer-events-auto` }
|
||||
onClick={ () => GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT) }>
|
||||
@@ -90,29 +97,33 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
|
||||
) }
|
||||
</div>
|
||||
<div className="chat-content py-[5px] px-[6px] ml-[27px] leading-none min-h-[25px]">
|
||||
{ chat.prefixEffect === 'pulse' && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
|
||||
{ chat.prefixText && (() => {
|
||||
const colors = parsePrefixColors(chat.prefixText, chat.prefixColor);
|
||||
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
||||
const fxStyle = getPrefixEffectStyle(chat.prefixEffect, colors[0] || '#FFFFFF');
|
||||
return (
|
||||
<span className="prefix font-bold mr-1" style={ fxStyle }>
|
||||
{ chat.prefixIcon && <span className="mr-0.5 text-[13px]">{ chat.prefixIcon }</span> }
|
||||
<span style={ hasMultiColor ? fxStyle : { ...fxStyle, color: colors[0] || '#FFFFFF' } }>
|
||||
{'{'}
|
||||
{ hasMultiColor
|
||||
? [ ...chat.prefixText ].map((char, i) => (
|
||||
<span key={ i } style={ { color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(chat.prefixEffect, colors[i]) } }>{ char }</span>
|
||||
))
|
||||
: chat.prefixText
|
||||
}
|
||||
{'}'}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})() }
|
||||
<b className="username" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } />
|
||||
<span className={ `message${ chat.type === 1 ? ' italic text-[#595959]' : '' }` } dangerouslySetInnerHTML={ { __html: `${ chat.formattedText }` } } onClick={ onClickChat } />
|
||||
<UserIdentityView
|
||||
className="mr-1 align-middle"
|
||||
displayOrder={ chat.displayOrder }
|
||||
iconClassName="inline-block w-auto h-auto align-[-1px]"
|
||||
nameClassName="username font-bold"
|
||||
nickIcon={ chat.nickIcon }
|
||||
prefixClassName=""
|
||||
prefixColor={ chat.prefixColor }
|
||||
prefixEffect={ chat.prefixEffect }
|
||||
prefixFont={ chat.prefixFont }
|
||||
prefixIcon={ chat.prefixIcon }
|
||||
prefixText={ chat.prefixText }
|
||||
showColon={ true }
|
||||
username={ chat.username } />
|
||||
{ !chat.showTranslation &&
|
||||
<span className={ `${ messageClassName } align-middle` } dangerouslySetInnerHTML={ { __html: `${ chat.formattedText }` } } onClick={ onClickChat } /> }
|
||||
{ chat.showTranslation &&
|
||||
<div className="mt-[2px] flex flex-col gap-[2px]" onClick={ onClickChat }>
|
||||
<div className="flex items-start gap-1 leading-[1.1]">
|
||||
<span className="inline-block min-w-[52px] font-bold" style={ { opacity: 0.75 } }>original:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: `${ chat.originalFormattedText || chat.formattedText }` } } />
|
||||
</div>
|
||||
<div className="flex items-start gap-1 leading-[1.1]">
|
||||
<span className="inline-block min-w-[52px] font-bold" style={ { opacity: 0.75 } }>translate:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: `${ chat.translatedFormattedText || chat.formattedText }` } } />
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
<div className="pointer absolute left-[50%] translate-x-[-50%] w-[9px] h-[6px] bottom-[-5px]" />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GetSessionDataManager, RoomObjectType } from '@nitrots/nitro-renderer';
|
||||
import { FC, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatEntryType, LocalizeText } from '../../../../api';
|
||||
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
|
||||
import { useChatHistory, useChatWindow } from '../../../../hooks';
|
||||
import { useChatHistory, useChatWindow, useOnClickChat } from '../../../../hooks';
|
||||
import { useRoom } from '../../../../hooks/rooms';
|
||||
|
||||
const BOTTOM_SCROLL_THRESHOLD = 20;
|
||||
@@ -19,6 +19,7 @@ export const ChatWidgetWindowView: FC<{}> = () =>
|
||||
const { chatHistory = [], clearChatHistory = null } = useChatHistory();
|
||||
const [ , setChatWindowEnabled ] = useChatWindow();
|
||||
const { roomSession = null } = useRoom();
|
||||
const { onClickChat } = useOnClickChat();
|
||||
const ownUserId = (GetSessionDataManager()?.userId || -1);
|
||||
|
||||
const roomChatHistory = useMemo(() =>
|
||||
@@ -33,7 +34,7 @@ export const ChatWidgetWindowView: FC<{}> = () =>
|
||||
|
||||
if(!normalizedSearch.length) return true;
|
||||
|
||||
return (`${ chat.name } ${ chat.message }`.toLowerCase().includes(normalizedSearch));
|
||||
return (`${ chat.name } ${ chat.message || '' } ${ chat.originalMessage || '' } ${ chat.translatedMessage || '' }`.toLowerCase().includes(normalizedSearch));
|
||||
});
|
||||
}, [ chatHistory, roomSession?.roomId, hidePets, search ]);
|
||||
|
||||
@@ -125,14 +126,27 @@ export const ChatWidgetWindowView: FC<{}> = () =>
|
||||
{
|
||||
const isOwnMessage = (chat.webId === ownUserId);
|
||||
const rowClassName = `mb-1 flex items-start gap-1 break-words ${ isOwnMessage ? 'justify-end' : '' }`;
|
||||
const messageClassName = `message${ chat.chatType === 1 ? ' italic text-[#595959]' : '' }${ chat.chatType === 2 ? ' font-bold' : '' }`;
|
||||
|
||||
return (
|
||||
<div key={ `${ chat.timestamp }-${ chat.id }` } className={ rowClassName }>
|
||||
{ hideBalloons && !hideAvatars && <div className={ `w-[65px] h-[55px] shrink-0 mt-[-18px] rounded-sm bg-no-repeat bg-center scale-70 ${ isOwnMessage ? 'order-2' : '' }` } style={ chat.imageUrl ? { backgroundImage: `url(${ chat.imageUrl })` } : undefined } /> }
|
||||
{ hideBalloons && (
|
||||
<div>
|
||||
<div onClick={ onClickChat }>
|
||||
<b dangerouslySetInnerHTML={ { __html: `${ chat.name }: ` } } />
|
||||
<span dangerouslySetInnerHTML={ { __html: chat.message } } />
|
||||
{ !chat.showTranslation &&
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: chat.message } } /> }
|
||||
{ chat.showTranslation &&
|
||||
<div className="mt-[2px] flex flex-col gap-[2px]">
|
||||
<div className="flex items-start gap-1 leading-[1.15]">
|
||||
<span className="inline-block min-w-[52px] font-bold opacity-75">original:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: chat.originalMessage || chat.message || '' } } />
|
||||
</div>
|
||||
<div className="flex items-start gap-1 leading-[1.15]">
|
||||
<span className="inline-block min-w-[52px] font-bold opacity-75">translate:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: chat.translatedMessage || chat.message || '' } } />
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
) }
|
||||
{ !hideBalloons && (
|
||||
@@ -148,7 +162,19 @@ export const ChatWidgetWindowView: FC<{}> = () =>
|
||||
</div>
|
||||
<div className={ `chat-content py-[5px] px-[6px] leading-none min-h-[25px] ${ !hideAvatars ? (isOwnMessage ? 'mr-[27px]' : 'ml-[27px]') : '' }` }>
|
||||
<b className="username" dangerouslySetInnerHTML={ { __html: `${ chat.name }: ` } } />
|
||||
<span className="message" dangerouslySetInnerHTML={ { __html: `${ chat.message }` } } />
|
||||
{ !chat.showTranslation &&
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: `${ chat.message }` } } onClick={ onClickChat } /> }
|
||||
{ chat.showTranslation &&
|
||||
<div className="mt-[2px] flex flex-col gap-[2px]" onClick={ onClickChat }>
|
||||
<div className="flex items-start gap-1 leading-[1.1]">
|
||||
<span className="inline-block min-w-[52px] font-bold" style={ { opacity: 0.75 } }>original:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: `${ chat.originalMessage || chat.message || '' }` } } />
|
||||
</div>
|
||||
<div className="flex items-start gap-1 leading-[1.1]">
|
||||
<span className="inline-block min-w-[52px] font-bold" style={ { opacity: 0.75 } }>translate:</span>
|
||||
<span className={ messageClassName } dangerouslySetInnerHTML={ { __html: `${ chat.translatedMessage || chat.message || '' }` } } />
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from '../../hooks';
|
||||
|
||||
export const TranslationBootstrap: FC<{}> = () =>
|
||||
{
|
||||
useTranslation();
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useTranslation } from '../../hooks';
|
||||
|
||||
export const TranslationSettingsView: FC<{}> = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const {
|
||||
settings,
|
||||
supportedLanguages = [],
|
||||
availableTextLocales = [],
|
||||
languagesLoading = false,
|
||||
localizationTextsLoading = false,
|
||||
lastIncomingLanguage = '',
|
||||
lastOutgoingLanguage = '',
|
||||
lastError = '',
|
||||
updateSettings,
|
||||
ensureSupportedLanguagesLoaded,
|
||||
getLanguageName
|
||||
} = useTranslation();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'translation-settings/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
ensureSupportedLanguagesLoaded();
|
||||
}, [ ensureSupportedLanguagesLoaded, isVisible ]);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="translation-settings-window w-[360px]" theme="primary-slim" uniqueKey="translation-settings">
|
||||
<NitroCardHeaderView headerText="Google Translate" onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardContentView className="flex flex-col gap-3 text-black">
|
||||
<div className="flex items-center gap-2">
|
||||
<input checked={ settings.enabled } className="form-check-input" type="checkbox" onChange={ event => updateSettings({ enabled: event.target.checked }) } />
|
||||
<Text>Enable automatic translation</Text>
|
||||
</div>
|
||||
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
||||
When enabled, chat bubbles always show two lines: <strong>original:</strong> and <strong>translate:</strong>.
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>Interface texts</Text>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="flex flex-col gap-1 text-[12px]">
|
||||
<span>Localized text pack</span>
|
||||
<select
|
||||
className="rounded border border-black/20 bg-white px-2 py-1"
|
||||
disabled={ localizationTextsLoading }
|
||||
value={ settings.uiTextLanguage || '' }
|
||||
onChange={ event => updateSettings({ uiTextLanguage: event.target.value }) }>
|
||||
<option value="">Default (gamedata)</option>
|
||||
{ availableTextLocales.map(locale => <option key={ locale.code } value={ locale.code }>{ locale.name }</option>) }
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>Incoming messages</Text>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>Detected language (auto): { getLanguageName(lastIncomingLanguage) }</Text>
|
||||
<label className="flex flex-col gap-1 text-[12px]">
|
||||
<span>Translate into</span>
|
||||
<select
|
||||
className="rounded border border-black/20 bg-white px-2 py-1"
|
||||
disabled={ languagesLoading || !supportedLanguages.length }
|
||||
value={ settings.incomingTargetLanguage }
|
||||
onChange={ event => updateSettings({ incomingTargetLanguage: event.target.value }) }>
|
||||
{ supportedLanguages.map(language => <option key={ language.code } value={ language.code }>{ language.name }</option>) }
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>Outgoing messages</Text>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text>Detected writing language (auto): { getLanguageName(lastOutgoingLanguage) }</Text>
|
||||
<label className="flex flex-col gap-1 text-[12px]">
|
||||
<span>Send text as</span>
|
||||
<select
|
||||
className="rounded border border-black/20 bg-white px-2 py-1"
|
||||
disabled={ languagesLoading || !supportedLanguages.length }
|
||||
value={ settings.outgoingTargetLanguage }
|
||||
onChange={ event => updateSettings({ outgoingTargetLanguage: event.target.value }) }>
|
||||
{ supportedLanguages.map(language => <option key={ language.code } value={ language.code }>{ language.name }</option>) }
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[11px] text-black/60">
|
||||
<span>{ languagesLoading ? 'Loading languages...' : `${ supportedLanguages.length } languages available` }</span>
|
||||
<button className="rounded border border-black/15 bg-white px-2 py-1 text-[11px] text-black hover:bg-black/5" type="button" onClick={ () => ensureSupportedLanguagesLoaded(true) }>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
{ localizationTextsLoading &&
|
||||
<div className="rounded border border-blue-300 bg-blue-50 px-2 py-1 text-[11px] leading-4 text-blue-700">
|
||||
Loading localized interface texts...
|
||||
</div> }
|
||||
{ lastError.length > 0 &&
|
||||
<div className="rounded border border-red-300 bg-red-50 px-2 py-1 text-[11px] leading-4 text-red-700">
|
||||
{ lastError }
|
||||
</div> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GetSessionDataManager, RequestFriendComposer, UserProfileParser } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { LayoutAvatarImageView, Text } from '../../common';
|
||||
import { LayoutAvatarImageView, Text, UserIdentityView } from '../../common';
|
||||
|
||||
export const UserContainerView: FC<{
|
||||
userProfile: UserProfileParser;
|
||||
@@ -18,7 +18,6 @@ export const UserContainerView: FC<{
|
||||
const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`;
|
||||
const infostandStandClass = `stand-${userProfile.standId ?? 'default'}`;
|
||||
const infostandOverlayClass = `overlay-${userProfile.overlayId ?? 'default'}`;
|
||||
|
||||
const addFriend = () =>
|
||||
{
|
||||
setRequestSent(true);
|
||||
@@ -41,7 +40,16 @@ export const UserContainerView: FC<{
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-0">
|
||||
<p className="leading-tight font-bold">{ userProfile.username }</p>
|
||||
<UserIdentityView
|
||||
className="leading-tight"
|
||||
displayOrder={ userProfile.displayOrder }
|
||||
nickIcon={ userProfile.nickIcon }
|
||||
prefixColor={ userProfile.prefixColor }
|
||||
prefixEffect={ userProfile.prefixEffect }
|
||||
prefixFont={ userProfile.prefixFont }
|
||||
prefixIcon={ userProfile.prefixIcon }
|
||||
prefixText={ userProfile.prefixText }
|
||||
username={ userProfile.username } />
|
||||
<p className="text-sm italic leading-tight">{ userProfile.motto }</p>
|
||||
</div>
|
||||
|
||||
@@ -115,4 +123,4 @@ export const UserContainerView: FC<{
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, GetStage, GetTicker, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
|
||||
import { WiredMonitorDataEvent, WiredMonitorRequestComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import furniInspectionIcon from '../../assets/images/wiredtools/furni.png';
|
||||
import globalInspectionIcon from '../../assets/images/wiredtools/global.png';
|
||||
import userInspectionIcon from '../../assets/images/wiredtools/user.png';
|
||||
import contextInspectionIcon from '../../assets/images/wiredtools/context.png';
|
||||
import wiredGlobalPlaceholderImage from '../../assets/images/wiredtools/wired_global_placeholder.png';
|
||||
import wiredMonitorImage from '../../assets/images/wiredtools/wired_monitor.png';
|
||||
import { AvatarInfoFurni, AvatarInfoUtilities, LocalizeText, NotificationAlertType, SendMessageComposer } from '../../api';
|
||||
import { AvatarInfoFurni, AvatarInfoUtilities, GetRoomObjectBounds, GetRoomObjectScreenLocation, LocalizeText, NotificationAlertType, SendMessageComposer, WiredSelectionVisualizer } from '../../api';
|
||||
import { Button, DraggableWindowPosition, LayoutAvatarImageView, LayoutPetImageView, LayoutRoomObjectImageView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common';
|
||||
import { useInventoryTrade, useMessageEvent, useNotification, useObjectSelectedEvent, useRoom, useWiredTools } from '../../hooks';
|
||||
import { WiredToolsSettingsTabView } from './WiredToolsSettingsTabView';
|
||||
@@ -184,6 +184,21 @@ interface VariableManageEntry
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
interface VariableHighlightTarget
|
||||
{
|
||||
category: number;
|
||||
hasValue: boolean;
|
||||
objectId: number;
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
interface VariableHighlightOverlay extends VariableHighlightTarget
|
||||
{
|
||||
key: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ManagedHolderVariableEntry
|
||||
{
|
||||
availability: string;
|
||||
@@ -631,6 +646,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
||||
const [ isManagedGiveOpen, setIsManagedGiveOpen ] = useState(false);
|
||||
const [ managedGiveVariableItemId, setManagedGiveVariableItemId ] = useState(0);
|
||||
const [ managedGiveValue, setManagedGiveValue ] = useState('0');
|
||||
const [ isVariableHighlightActive, setIsVariableHighlightActive ] = useState(false);
|
||||
const [ variableHighlightOverlays, setVariableHighlightOverlays ] = useState<VariableHighlightOverlay[]>([]);
|
||||
const variableHighlightObjectsRef = useRef<Array<{ category: number; objectId: number; }>>([]);
|
||||
const shouldPauseVariableSnapshotRefresh = (!!editingVariable || !!editingManagedHolderVariableId || isInspectionGiveOpen || isManagedGiveOpen);
|
||||
const [ selectedVariableKeys, setSelectedVariableKeys ] = useState<Record<VariablesElementType, string>>({
|
||||
furni: VARIABLE_DEFINITIONS.furni[0].key,
|
||||
@@ -2400,6 +2418,155 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
||||
manageLabel: 'Manage'
|
||||
} ];
|
||||
}, [ selectedVariableDefinition, variablesType, roomSession, userVariableAssignments, furniVariableAssignments, roomVariableAssignmentMap ]);
|
||||
const canVariableHighlight = !!selectedVariableDefinition?.itemId
|
||||
&& (selectedVariableDefinition.type === 'Custom')
|
||||
&& ((variablesType === 'user') || (variablesType === 'furni'))
|
||||
&& !!roomSession;
|
||||
const variableHighlightTargets = useMemo((): VariableHighlightTarget[] =>
|
||||
{
|
||||
if(!isVariableHighlightActive || !canVariableHighlight || !roomSession || !selectedVariableDefinition?.itemId) return [];
|
||||
|
||||
if(variablesType === 'user')
|
||||
{
|
||||
const targets: VariableHighlightTarget[] = [];
|
||||
|
||||
for(const [ userIdString, assignments ] of Object.entries(userVariableAssignments))
|
||||
{
|
||||
const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
|
||||
|
||||
if(!assignment) continue;
|
||||
|
||||
const userId = Number(userIdString);
|
||||
const userData = roomSession.userDataManager.getUserData(userId)
|
||||
?? roomSession.userDataManager.getBotData(userId)
|
||||
?? roomSession.userDataManager.getRentableBotData(userId)
|
||||
?? roomSession.userDataManager.getPetData(userId);
|
||||
const roomIndex = Number(userData?.roomIndex ?? -1);
|
||||
|
||||
if(roomIndex < 0) continue;
|
||||
|
||||
targets.push({
|
||||
category: RoomObjectCategory.UNIT,
|
||||
objectId: roomIndex,
|
||||
hasValue: !!assignment.hasValue && !!selectedVariableDefinition.hasValue && (assignment.value !== null) && (assignment.value !== undefined),
|
||||
value: assignment.value
|
||||
});
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
if(variablesType === 'furni')
|
||||
{
|
||||
const targets: VariableHighlightTarget[] = [];
|
||||
|
||||
for(const [ furniIdString, assignments ] of Object.entries(furniVariableAssignments))
|
||||
{
|
||||
const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
|
||||
|
||||
if(!assignment) continue;
|
||||
|
||||
const furniId = Number(furniIdString);
|
||||
const floorObject = GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.FLOOR);
|
||||
const wallObject = floorObject ? null : GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.WALL);
|
||||
const category = floorObject ? RoomObjectCategory.FLOOR : (wallObject ? RoomObjectCategory.WALL : -1);
|
||||
|
||||
if(category < 0) continue;
|
||||
|
||||
targets.push({
|
||||
category,
|
||||
objectId: furniId,
|
||||
hasValue: !!assignment.hasValue && !!selectedVariableDefinition.hasValue && (assignment.value !== null) && (assignment.value !== undefined),
|
||||
value: assignment.value
|
||||
});
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [ canVariableHighlight, furniVariableAssignments, isVariableHighlightActive, roomSession, selectedVariableDefinition, userVariableAssignments, variablesType ]);
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isVisible && (activeTab === 'variables') && canVariableHighlight) return;
|
||||
|
||||
setIsVariableHighlightActive(false);
|
||||
}, [ activeTab, canVariableHighlight, isVisible ]);
|
||||
useEffect(() =>
|
||||
{
|
||||
if(variableHighlightObjectsRef.current.length)
|
||||
{
|
||||
WiredSelectionVisualizer.clearVariableHighlightFromObjects(variableHighlightObjectsRef.current);
|
||||
variableHighlightObjectsRef.current = [];
|
||||
}
|
||||
|
||||
if(!isVariableHighlightActive || !variableHighlightTargets.length)
|
||||
{
|
||||
|
||||
setVariableHighlightOverlays([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const objects = variableHighlightTargets.map(target => ({
|
||||
category: target.category,
|
||||
objectId: target.objectId
|
||||
}));
|
||||
|
||||
WiredSelectionVisualizer.applyVariableHighlightToObjects(objects);
|
||||
variableHighlightObjectsRef.current = objects;
|
||||
|
||||
return () =>
|
||||
{
|
||||
if(!variableHighlightObjectsRef.current.length) return;
|
||||
|
||||
WiredSelectionVisualizer.clearVariableHighlightFromObjects(variableHighlightObjectsRef.current);
|
||||
variableHighlightObjectsRef.current = [];
|
||||
};
|
||||
}, [ isVariableHighlightActive, variableHighlightTargets ]);
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVariableHighlightActive || !roomSession?.roomId || !variableHighlightTargets.length)
|
||||
{
|
||||
setVariableHighlightOverlays([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOverlays = () =>
|
||||
{
|
||||
const stage = GetStage();
|
||||
const nextOverlays: VariableHighlightOverlay[] = [];
|
||||
|
||||
for(const target of variableHighlightTargets)
|
||||
{
|
||||
const bounds = GetRoomObjectBounds(roomSession.roomId, target.objectId, target.category);
|
||||
const location = GetRoomObjectScreenLocation(roomSession.roomId, target.objectId, target.category);
|
||||
|
||||
if(!bounds || !location) continue;
|
||||
|
||||
const x = Math.max(8, Math.min(Math.round(location.x), (stage.width - 8)));
|
||||
const y = Math.max(8, Math.min(Math.round(bounds.top), (stage.height - 40)));
|
||||
|
||||
nextOverlays.push({
|
||||
...target,
|
||||
key: `${ target.category }:${ target.objectId }`,
|
||||
x,
|
||||
y
|
||||
});
|
||||
}
|
||||
|
||||
setVariableHighlightOverlays(nextOverlays);
|
||||
};
|
||||
|
||||
updateOverlays();
|
||||
|
||||
const ticker = GetTicker();
|
||||
|
||||
ticker.add(updateOverlays);
|
||||
|
||||
return () => ticker.remove(updateOverlays);
|
||||
}, [ isVariableHighlightActive, roomSession?.roomId, variableHighlightTargets ]);
|
||||
const variableManageTypeOptions = useMemo(() =>
|
||||
{
|
||||
switch(variablesType)
|
||||
@@ -3465,6 +3632,27 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isVariableHighlightActive && !!variableHighlightOverlays.length &&
|
||||
<div className="pointer-events-none absolute left-0 top-0 z-30">
|
||||
{ variableHighlightOverlays.map(overlay => (
|
||||
<div
|
||||
key={ overlay.key }
|
||||
className="pointer-events-none absolute"
|
||||
style={ {
|
||||
left: overlay.x,
|
||||
top: overlay.y,
|
||||
transform: 'translateX(-50%)'
|
||||
} }>
|
||||
{ overlay.hasValue &&
|
||||
<div className="absolute left-1/2 top-[-30px] -translate-x-1/2">
|
||||
<div className="relative min-w-[24px] rounded-[8px] bg-[#86aebccc] px-[8px] py-[4px] text-center text-[12px] font-bold leading-none text-white shadow-[inset_0_0_0_1px_rgba(176,211,225,.7)]">
|
||||
{ overlay.value ?? 0 }
|
||||
<span className="absolute left-1/2 bottom-[-7px] z-10 h-0 w-0 -translate-x-1/2 border-x-[6px] border-t-[7px] border-x-transparent border-t-[#86aebccc]" />
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
)) }
|
||||
</div> }
|
||||
<NitroCardView className="min-w-[520px] max-w-[520px]" theme="primary-slim" uniqueKey="wired-creator-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||
<NitroCardHeaderView headerText="Wired Creator Tools (:wired)" onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardTabsView justifyContent="start">
|
||||
@@ -3830,7 +4018,12 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button disabled variant="secondary">Highlight</Button>
|
||||
<Button
|
||||
disabled={ !canVariableHighlight }
|
||||
variant="secondary"
|
||||
onClick={ () => setIsVariableHighlightActive(value => !value) }>
|
||||
{ isVariableHighlightActive ? 'Undo' : 'Highlight' }
|
||||
</Button>
|
||||
<Button
|
||||
disabled={ !variableManageCanOpen }
|
||||
variant="secondary"
|
||||
|
||||
@@ -5,7 +5,14 @@ import { useWired } from '../../../../hooks';
|
||||
import { WiredSourcesSelector } from '../WiredSourcesSelector';
|
||||
import { WiredActionBaseView } from './WiredActionBaseView';
|
||||
|
||||
const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ];
|
||||
const COUNTER_INTERACTION_TYPES = [
|
||||
'game_upcounter',
|
||||
'game_timer',
|
||||
'wf_game_upcounter*',
|
||||
'fball_counter',
|
||||
'bb_counter',
|
||||
'es_counter'
|
||||
];
|
||||
|
||||
const CONTROL_OPTIONS = [
|
||||
{ value: 0, label: 'wiredfurni.params.clock_control.0' },
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import sourceFurniIcon from '../../../../assets/images/wired/source_furni.png';
|
||||
import sourceUserIcon from '../../../../assets/images/wired/source_user.png';
|
||||
import { Button, Slider, Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
|
||||
import { sortWiredSourceOptions, useAvailableUserSources } from '../WiredSourcesSelector';
|
||||
import { WiredConditionBaseView } from './WiredConditionBaseView';
|
||||
|
||||
const SOURCE_GROUP_USERS = 0;
|
||||
const SOURCE_GROUP_FURNI = 1;
|
||||
const SOURCE_TRIGGER = 0;
|
||||
const SOURCE_SELECTED = 100;
|
||||
const COMPARISON_OPTIONS = [ 0, 1, 2 ];
|
||||
const MIN_QUANTITY = 0;
|
||||
const MAX_QUANTITY = 100;
|
||||
const QUANTITY_PATTERN = /^\d*$/;
|
||||
|
||||
const USER_SOURCES = [
|
||||
const USER_SOURCES = sortWiredSourceOptions([
|
||||
{ value: 0, label: 'wiredfurni.params.sources.users.0' },
|
||||
{ value: 200, label: 'wiredfurni.params.sources.users.200' },
|
||||
{ value: 201, label: 'wiredfurni.params.sources.users.201' }
|
||||
];
|
||||
], 'users');
|
||||
|
||||
const FURNI_SOURCES = sortWiredSourceOptions([
|
||||
{ value: 0, label: 'wiredfurni.params.sources.furni.0' },
|
||||
{ value: 100, label: 'wiredfurni.params.sources.furni.100' },
|
||||
{ value: 200, label: 'wiredfurni.params.sources.furni.200' },
|
||||
{ value: 201, label: 'wiredfurni.params.sources.furni.201' }
|
||||
{ value: 201, label: 'wiredfurni.params.sources.furni.201' },
|
||||
{ value: 0, label: 'wiredfurni.params.sources.furni.0' }
|
||||
], 'furni');
|
||||
|
||||
const SOURCE_GROUP_BUTTONS = [
|
||||
{ key: 'user', icon: sourceUserIcon, isUserGroup: true },
|
||||
{ key: 'furni', icon: sourceFurniIcon, isUserGroup: false }
|
||||
] as const;
|
||||
|
||||
const clampQuantity = (value: number) =>
|
||||
{
|
||||
if(isNaN(value)) return MIN_QUANTITY;
|
||||
@@ -32,21 +42,22 @@ const clampQuantity = (value: number) =>
|
||||
return Math.max(MIN_QUANTITY, Math.min(MAX_QUANTITY, Math.floor(value)));
|
||||
};
|
||||
|
||||
const normalizeSource = (value: number, allowed: number[]) =>
|
||||
const normalizeSourceType = (value: number, allowed: number[]) =>
|
||||
{
|
||||
return allowed.includes(value) ? value : 0;
|
||||
return allowed.includes(value) ? value : SOURCE_TRIGGER;
|
||||
};
|
||||
|
||||
export const WiredConditionSelectionQuantityView: FC<{}> = () =>
|
||||
{
|
||||
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const availableUserSources = sortWiredSourceOptions(useAvailableUserSources(trigger, USER_SOURCES), 'users');
|
||||
const { trigger = null, furniIds = [], setFurniIds = null, setIntParams = null, setStringParam = null } = useWired();
|
||||
const rawAvailableUserSources = useAvailableUserSources(trigger, USER_SOURCES);
|
||||
const availableUserSources = useMemo(() => sortWiredSourceOptions(rawAvailableUserSources, 'users'), [ rawAvailableUserSources ]);
|
||||
const [ comparison, setComparison ] = useState(1);
|
||||
const [ quantity, setQuantity ] = useState(0);
|
||||
const [ quantityInput, setQuantityInput ] = useState('0');
|
||||
const [ sourceGroup, setSourceGroup ] = useState(SOURCE_GROUP_USERS);
|
||||
const [ userSource, setUserSource ] = useState(0);
|
||||
const [ furniSource, setFurniSource ] = useState(0);
|
||||
const [ userSource, setUserSource ] = useState(SOURCE_TRIGGER);
|
||||
const [ furniSource, setFurniSource ] = useState(SOURCE_TRIGGER);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -55,19 +66,46 @@ export const WiredConditionSelectionQuantityView: FC<{}> = () =>
|
||||
const nextComparison = (trigger.intData.length > 0) ? trigger.intData[0] : 1;
|
||||
const nextQuantity = clampQuantity((trigger.intData.length > 1) ? trigger.intData[1] : 0);
|
||||
const nextSourceGroup = (trigger.intData.length > 2 && trigger.intData[2] === SOURCE_GROUP_FURNI) ? SOURCE_GROUP_FURNI : SOURCE_GROUP_USERS;
|
||||
const nextSourceType = (trigger.intData.length > 3) ? trigger.intData[3] : 0;
|
||||
const nextSourceType = (trigger.intData.length > 3) ? trigger.intData[3] : SOURCE_TRIGGER;
|
||||
|
||||
setComparison(COMPARISON_OPTIONS.includes(nextComparison) ? nextComparison : 1);
|
||||
setQuantity(nextQuantity);
|
||||
setQuantityInput(nextQuantity.toString());
|
||||
setSourceGroup(nextSourceGroup);
|
||||
setUserSource(nextSourceGroup === SOURCE_GROUP_USERS ? normalizeSource(nextSourceType, availableUserSources.map(source => source.value)) : 0);
|
||||
setFurniSource(nextSourceGroup === SOURCE_GROUP_FURNI ? normalizeSource(nextSourceType, FURNI_SOURCES.map(source => source.value)) : 0);
|
||||
setUserSource(nextSourceGroup === SOURCE_GROUP_USERS ? normalizeSourceType(nextSourceType, availableUserSources.map(source => source.value)) : SOURCE_TRIGGER);
|
||||
setFurniSource(nextSourceGroup === SOURCE_GROUP_FURNI ? normalizeSourceType(nextSourceType, FURNI_SOURCES.map(source => source.value)) : SOURCE_TRIGGER);
|
||||
}, [ availableUserSources, trigger ]);
|
||||
|
||||
const activeSources = useMemo(() => (sourceGroup === SOURCE_GROUP_FURNI) ? FURNI_SOURCES : availableUserSources, [ availableUserSources, sourceGroup ]);
|
||||
const activeSource = (sourceGroup === SOURCE_GROUP_FURNI) ? furniSource : userSource;
|
||||
const isUserGroup = sourceGroup === SOURCE_GROUP_USERS;
|
||||
const activeSources = useMemo(() => isUserGroup ? availableUserSources : FURNI_SOURCES, [ availableUserSources, isUserGroup ]);
|
||||
const activeSource = isUserGroup ? userSource : furniSource;
|
||||
const activeSourceIndex = Math.max(0, activeSources.findIndex(source => source.value === activeSource));
|
||||
const currentSourceType = activeSources[activeSourceIndex]?.value ?? SOURCE_TRIGGER;
|
||||
const requiresFurni = (!isUserGroup && furniSource === SOURCE_SELECTED) ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(currentSourceType === activeSource) return;
|
||||
|
||||
if(isUserGroup) setUserSource(currentSourceType);
|
||||
else setFurniSource(currentSourceType);
|
||||
}, [ activeSource, currentSourceType, isUserGroup ]);
|
||||
|
||||
const changeGroup = (nextIsUserGroup: boolean) =>
|
||||
{
|
||||
if(nextIsUserGroup === isUserGroup) return;
|
||||
|
||||
const nextOptions = nextIsUserGroup ? availableUserSources : FURNI_SOURCES;
|
||||
const nextIndex = Math.min(activeSourceIndex, Math.max(0, nextOptions.length - 1));
|
||||
const nextOption = nextOptions[nextIndex] ?? nextOptions[0];
|
||||
|
||||
setSourceGroup(nextIsUserGroup ? SOURCE_GROUP_USERS : SOURCE_GROUP_FURNI);
|
||||
|
||||
if(!nextOption) return;
|
||||
|
||||
if(nextIsUserGroup) setUserSource(nextOption.value);
|
||||
else setFurniSource(nextOption.value);
|
||||
};
|
||||
|
||||
const updateQuantity = (value: number) =>
|
||||
{
|
||||
@@ -92,30 +130,23 @@ export const WiredConditionSelectionQuantityView: FC<{}> = () =>
|
||||
updateQuantity(parseInt(value));
|
||||
};
|
||||
|
||||
const cycleSource = (direction: -1 | 1) =>
|
||||
{
|
||||
const nextIndex = (activeSourceIndex + direction + activeSources.length) % activeSources.length;
|
||||
const nextValue = activeSources[nextIndex].value;
|
||||
|
||||
if(sourceGroup === SOURCE_GROUP_FURNI) setFurniSource(nextValue);
|
||||
else setUserSource(nextValue);
|
||||
};
|
||||
|
||||
const save = () =>
|
||||
{
|
||||
setIntParams([
|
||||
comparison,
|
||||
clampQuantity(quantity),
|
||||
sourceGroup,
|
||||
(sourceGroup === SOURCE_GROUP_FURNI) ? furniSource : userSource
|
||||
isUserGroup ? userSource : furniSource
|
||||
]);
|
||||
setStringParam('');
|
||||
|
||||
if(requiresFurni <= WiredFurniType.STUFF_SELECTION_OPTION_NONE) setFurniIds([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<WiredConditionBaseView
|
||||
hasSpecialInput={ true }
|
||||
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
|
||||
requiresFurni={ requiresFurni }
|
||||
save={ save }>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.comparison_selection') }</Text>
|
||||
@@ -147,34 +178,34 @@ export const WiredConditionSelectionQuantityView: FC<{}> = () =>
|
||||
onChange={ event => updateQuantity(event as number) } />
|
||||
<Text small>{ quantity }</Text>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text bold>{ LocalizeText('wiredfurni.params.sources.merged.title') }</Text>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ (sourceGroup === SOURCE_GROUP_USERS) ? 'primary' : 'secondary' }
|
||||
onClick={ () => setSourceGroup(SOURCE_GROUP_USERS) }>
|
||||
{ LocalizeText('wiredfurni.params.sources.users.title') }
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={ (sourceGroup === SOURCE_GROUP_FURNI) ? 'primary' : 'secondary' }
|
||||
onClick={ () => setSourceGroup(SOURCE_GROUP_FURNI) }>
|
||||
{ LocalizeText('wiredfurni.params.sources.furni.title') }
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ () => cycleSource(-1) }>
|
||||
<FaChevronLeft />
|
||||
</Button>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<Text small>{ LocalizeText(activeSources[activeSourceIndex].label) }</Text>
|
||||
<WiredFurniSelectionSourceRow
|
||||
title="wiredfurni.params.sources.merged.title"
|
||||
options={ activeSources }
|
||||
value={ activeSource }
|
||||
selectionKind={ isUserGroup ? 'primary' : 'secondary' }
|
||||
selectionActive={ !isUserGroup && furniSource === SOURCE_SELECTED }
|
||||
selectionCount={ furniIds.length }
|
||||
selectionLimit={ trigger?.maximumItemSelectionCount ?? 20 }
|
||||
selectionEnabledValues={ [ SOURCE_SELECTED ] }
|
||||
showSelectionToggle={ false }
|
||||
headerContent={
|
||||
<div className="nitro-wired__give-var-targets">
|
||||
{ SOURCE_GROUP_BUTTONS.map(button => (
|
||||
<button
|
||||
key={ button.key }
|
||||
type="button"
|
||||
className={ `nitro-wired__give-var-target nitro-wired__give-var-target--${ button.key } ${ isUserGroup === button.isUserGroup ? 'is-active' : '' }` }
|
||||
onClick={ () => changeGroup(button.isUserGroup) }>
|
||||
<img src={ button.icon } alt={ button.key } />
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
<Button variant="primary" className="px-2 py-1" onClick={ () => cycleSource(1) }>
|
||||
<FaChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
onChange={ value =>
|
||||
{
|
||||
if(isUserGroup) setUserSource(value);
|
||||
else setFurniSource(value);
|
||||
} } />
|
||||
</WiredConditionBaseView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, WiredFurniType } from '../../../../api';
|
||||
import { Text } from '../../../../common';
|
||||
import { useWired } from '../../../../hooks';
|
||||
import { WiredTextFormattingHelp } from '../common/WiredTextFormattingHelp';
|
||||
import { WiredExtraBaseView } from './WiredExtraBaseView';
|
||||
|
||||
const DEFAULT_CONNECTOR_PLACEHOLDER = '0=text 1\n1=text 2\n2 = text 3';
|
||||
@@ -70,6 +71,7 @@ export const WiredExtraVariableTextConnectorView: FC<{}> = () =>
|
||||
value={ mappingsText }
|
||||
onChange={ event => handleTextChange(event.target.value) } />
|
||||
<Text small>{ `${ lineCount }/${ MAX_CONNECTOR_LINES } righe - ${ characterCount }/${ MAX_CONNECTOR_CHARACTERS } caratteri` }</Text>
|
||||
<WiredTextFormattingHelp />
|
||||
</div>
|
||||
</WiredExtraBaseView>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ const normalizeFurniSource = (value: number) => (FURNI_SOURCE_OPTIONS.some(optio
|
||||
export const WiredTriggerReceiveSignalView: FC<{}> = () =>
|
||||
{
|
||||
const [ senderCount, setSenderCount ] = useState(0);
|
||||
const [ maxSenders, setMaxSenders ] = useState(5);
|
||||
const [ channel, setChannel ] = useState(0);
|
||||
const [ furniSource, setFurniSource ] = useState(100);
|
||||
|
||||
@@ -30,7 +29,6 @@ export const WiredTriggerReceiveSignalView: FC<{}> = () =>
|
||||
const p = trigger.intData;
|
||||
if(p.length >= 1) setChannel(p[0]);
|
||||
if(p.length >= 2) setSenderCount(p[1]);
|
||||
if(p.length >= 3) setMaxSenders(p[2]);
|
||||
if(p.length >= 4) setFurniSource(normalizeFurniSource(p[3]));
|
||||
else setFurniSource(100);
|
||||
}, [ trigger ]);
|
||||
@@ -43,7 +41,7 @@ export const WiredTriggerReceiveSignalView: FC<{}> = () =>
|
||||
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } furniSources={ FURNI_SOURCE_OPTIONS } onChangeFurni={ setFurniSource } /> }>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text small>{ LocalizeText('wiredfurni.params.signal.senders_connected') }</Text>
|
||||
<Text bold small>{ senderCount }/{ maxSenders }</Text>
|
||||
<Text bold small>{ senderCount }</Text>
|
||||
</div>
|
||||
</WiredTriggerBaseView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user