mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { Dispatch, FC, SetStateAction, useState } from 'react';
|
||||
import { FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { GroupBadgePart } from '../../../api';
|
||||
import { Column, Flex, Grid, LayoutBadgeImageView } from '../../../common';
|
||||
import { useGroup } from '../../../hooks';
|
||||
|
||||
interface GroupBadgeCreatorViewProps
|
||||
{
|
||||
badgeParts: GroupBadgePart[];
|
||||
setBadgeParts: Dispatch<SetStateAction<GroupBadgePart[]>>;
|
||||
}
|
||||
|
||||
const POSITIONS: number[] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ];
|
||||
|
||||
export const GroupBadgeCreatorView: FC<GroupBadgeCreatorViewProps> = props =>
|
||||
{
|
||||
const { badgeParts = [], setBadgeParts = null } = props;
|
||||
const [ selectedIndex, setSelectedIndex ] = useState<number>(-1);
|
||||
const { groupCustomize = null } = useGroup();
|
||||
|
||||
const setPartProperty = (partIndex: number, property: string, value: number) =>
|
||||
{
|
||||
const newBadgeParts = [ ...badgeParts ];
|
||||
|
||||
newBadgeParts[partIndex][property] = value;
|
||||
|
||||
setBadgeParts(newBadgeParts);
|
||||
|
||||
if(property === 'key') setSelectedIndex(-1);
|
||||
};
|
||||
|
||||
if(!badgeParts || !badgeParts.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ ((selectedIndex < 0) && badgeParts && (badgeParts.length > 0)) && badgeParts.map((part, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } alignItems="center" className="bg-muted rounded px-2 py-1" gap={ 2 } justifyContent="between">
|
||||
<Flex center pointer className="bg-muted rounded p-1" onClick={ event => setSelectedIndex(index) }>
|
||||
{ (badgeParts[index].code && (badgeParts[index].code.length > 0)) &&
|
||||
<LayoutBadgeImageView badgeCode={ badgeParts[index].code } isGroup={ true } /> }
|
||||
{ (!badgeParts[index].code || !badgeParts[index].code.length) &&
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center group-badge">
|
||||
<FaPlus className="fa-icon" />
|
||||
</Flex> }
|
||||
</Flex>
|
||||
{ (part.type !== GroupBadgePart.BASE) &&
|
||||
<Grid columnCount={ 3 } gap={ 1 }>
|
||||
{ POSITIONS.map((position, posIndex) =>
|
||||
{
|
||||
return <div key={ posIndex } className={ `relative rounded-[.25rem] w-[16px] h-[16px] bg-[#fff] border-[2px] border-[solid] border-[#fff] [box-shadow:inset_3px_3px_#0000001a] cursor-pointer ${ (badgeParts[index].position === position) ? 'bg-primary [box-shadow:none]' : '' }` } onClick={ event => setPartProperty(index, 'position', position) } />;
|
||||
}) }
|
||||
</Grid> }
|
||||
<Grid columnCount={ 8 } gap={ 1 }>
|
||||
{ (groupCustomize.badgePartColors.length > 0) && groupCustomize.badgePartColors.map((item, colorIndex) =>
|
||||
{
|
||||
return <div key={ colorIndex } className={ `relative [box-shadow:inset_2px_2px_#0003] rounded-[.25rem] w-[16px] h-[16px] bg-[#fff] border-[2px] border-[solid] border-[#fff] [box-shadow:inset_3px_3px_#0000001a]cursor-pointer ${ (badgeParts[index].color === (colorIndex + 1)) ? 'bg-primary [box-shadow:none]' : '' }` } style={ { backgroundColor: '#' + item.color } } onClick={ event => setPartProperty(index, 'color', (colorIndex + 1)) } />;
|
||||
}) }
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
{ (selectedIndex >= 0) &&
|
||||
<Grid columnCount={ 5 } gap={ 1 }>
|
||||
{ (badgeParts[selectedIndex].type === GroupBadgePart.SYMBOL) &&
|
||||
<Column center pointer className="bg-muted rounded p-1" onClick={ event => setPartProperty(selectedIndex, 'key', 0) }>
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center group-badge">
|
||||
<FaTimes className="fa-icon" />
|
||||
</Flex>
|
||||
</Column> }
|
||||
{ ((badgeParts[selectedIndex].type === GroupBadgePart.BASE) ? groupCustomize.badgeBases : groupCustomize.badgeSymbols).map((item, index) =>
|
||||
{
|
||||
return (
|
||||
<Column key={ index } center pointer className="bg-muted rounded p-1" onClick={ event => setPartProperty(selectedIndex, 'key', item.id) }>
|
||||
<LayoutBadgeImageView badgeCode={ GroupBadgePart.getCode(badgeParts[selectedIndex].type, item.id, badgeParts[selectedIndex].color, 4) } isGroup={ true } />
|
||||
</Column>
|
||||
);
|
||||
}) }
|
||||
</Grid> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import { GroupBuyComposer, GroupBuyDataComposer, GroupBuyDataEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { HasHabboClub, IGroupData, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
import { GroupTabBadgeView } from './tabs/GroupTabBadgeView';
|
||||
import { GroupTabColorsView } from './tabs/GroupTabColorsView';
|
||||
import { GroupTabCreatorConfirmationView } from './tabs/GroupTabCreatorConfirmationView';
|
||||
import { GroupTabIdentityView } from './tabs/GroupTabIdentityView';
|
||||
|
||||
interface GroupCreatorViewProps
|
||||
{
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TABS: number[] = [ 1, 2, 3, 4 ];
|
||||
|
||||
export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
|
||||
{
|
||||
const { onClose = null } = props;
|
||||
const [ currentTab, setCurrentTab ] = useState<number>(1);
|
||||
const [ closeAction, setCloseAction ] = useState<{ action: () => boolean }>(null);
|
||||
const [ groupData, setGroupData ] = useState<IGroupData>(null);
|
||||
const [ availableRooms, setAvailableRooms ] = useState<{ id: number, name: string }[]>(null);
|
||||
const [ purchaseCost, setPurchaseCost ] = useState<number>(0);
|
||||
|
||||
const onCloseClose = () =>
|
||||
{
|
||||
setCloseAction(null);
|
||||
setGroupData(null);
|
||||
|
||||
if(onClose) onClose();
|
||||
};
|
||||
|
||||
const buyGroup = () =>
|
||||
{
|
||||
if(!groupData) return;
|
||||
|
||||
const badge = [];
|
||||
|
||||
groupData.groupBadgeParts.forEach(part =>
|
||||
{
|
||||
if(part.code)
|
||||
{
|
||||
badge.push(part.key);
|
||||
badge.push(part.color);
|
||||
badge.push(part.position);
|
||||
}
|
||||
});
|
||||
|
||||
SendMessageComposer(new GroupBuyComposer(groupData.groupName, groupData.groupDescription, groupData.groupHomeroomId, groupData.groupColors[0], groupData.groupColors[1], badge));
|
||||
};
|
||||
|
||||
const previousStep = () =>
|
||||
{
|
||||
if(closeAction && closeAction.action)
|
||||
{
|
||||
if(!closeAction.action()) return;
|
||||
}
|
||||
|
||||
if(currentTab === 1)
|
||||
{
|
||||
onClose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentTab(value => value - 1);
|
||||
};
|
||||
|
||||
const nextStep = () =>
|
||||
{
|
||||
if(closeAction && closeAction.action)
|
||||
{
|
||||
if(!closeAction.action()) return;
|
||||
}
|
||||
|
||||
if(currentTab === 4)
|
||||
{
|
||||
buyGroup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentTab(value => (value === 4 ? value : value + 1));
|
||||
};
|
||||
|
||||
useMessageEvent<GroupBuyDataEvent>(GroupBuyDataEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
const rooms: { id: number, name: string }[] = [];
|
||||
|
||||
parser.availableRooms.forEach((name, id) => rooms.push({ id, name }));
|
||||
|
||||
setAvailableRooms(rooms);
|
||||
setPurchaseCost(parser.groupCost);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setCurrentTab(1);
|
||||
|
||||
setGroupData({
|
||||
groupId: -1,
|
||||
groupName: null,
|
||||
groupDescription: null,
|
||||
groupHomeroomId: -1,
|
||||
groupState: 1,
|
||||
groupCanMembersDecorate: true,
|
||||
groupColors: null,
|
||||
groupBadgeParts: null
|
||||
});
|
||||
|
||||
SendMessageComposer(new GroupBuyDataComposer());
|
||||
}, [ setGroupData ]);
|
||||
|
||||
if(!groupData) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="h-[355px] w-[390px] border-[1px] border-[solid] border-[#283F5D] " theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('group.create.title') } onCloseClick={ onCloseClose } />
|
||||
<NitroCardContentView>
|
||||
<div className="flex items-center justify-center creator-tabs">
|
||||
{ TABS.map((tab, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } center className={ `relative -ml-[6px] bg-[url('@/assets/images/groups/creator_tabs.png')] bg-no-repeat ${ ((tab === 1) ? 'w-[84px] h-[24px] bg-[0px_0px]' : (tab === 4) ? 'w-[133px] h-[28px] bg-[0px_-104px]' : 'w-[83px] h-[24px] bg-[0px_-52px]') } ${ (currentTab === tab) ? 'active' : '' }` }>
|
||||
<Text variant="white">{ LocalizeText(`group.create.steplabel.${ tab }`) }</Text>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<Column overflow="hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={ `bg-no-repeat w-[122px] h-[68px] bg-[url('@/assets/images/groups/creator_images.png')] ${ currentTab === 1 && 'bg-[0px_0px] !w-[99px] !h-[50px]' }
|
||||
${ currentTab == 2 && '!bg-[-99px_0px] !w-[98px] !h-[62px]' } ${ currentTab === 3 && '!bg-[0px_-50px] !w-[96px] !h-[45px]' } ${ currentTab === 4 || currentTab === 5 && '!bg-[0px_-95px] !w-[114px] !h-[61px]' } ` } />
|
||||
<Column grow gap={ 0 }>
|
||||
<Text bold fontSize={ 4 }>{ LocalizeText(`group.create.stepcaption.${ currentTab }`) }</Text>
|
||||
<Text>{ LocalizeText(`group.create.stepdesc.${ currentTab }`) }</Text>
|
||||
</Column>
|
||||
</div>
|
||||
<Column overflow="hidden">
|
||||
{ (currentTab === 1) &&
|
||||
<GroupTabIdentityView availableRooms={ availableRooms } groupData={ groupData } isCreator={ true } setCloseAction={ setCloseAction } setGroupData={ setGroupData } onClose={ null } /> }
|
||||
{ (currentTab === 2) &&
|
||||
<GroupTabBadgeView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } /> }
|
||||
{ (currentTab === 3) &&
|
||||
<GroupTabColorsView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } /> }
|
||||
{ (currentTab === 4) &&
|
||||
<GroupTabCreatorConfirmationView groupData={ groupData } purchaseCost={ purchaseCost } setGroupData={ setGroupData } /> }
|
||||
</Column>
|
||||
<div className="flex justify-between">
|
||||
<Button className="text-black" variant="link" onClick={ previousStep }>
|
||||
{ LocalizeText(currentTab === 1 ? 'generic.cancel' : 'group.create.previousstep') }
|
||||
</Button>
|
||||
<Button disabled={ ((currentTab === 4) && !HasHabboClub()) } variant={ ((currentTab === 4) ? HasHabboClub() ? 'success' : 'danger' : 'primary') } onClick={ nextStep }>
|
||||
{ LocalizeText((currentTab === 4) ? HasHabboClub() ? 'group.create.confirm.buy' : 'group.create.confirm.viprequired' : 'group.create.nextstep') }
|
||||
</Button>
|
||||
</div>
|
||||
</Column>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { GroupInformationEvent, GroupInformationParser } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { LocalizeText } from '../../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
import { GroupInformationView } from './GroupInformationView';
|
||||
|
||||
export const GroupInformationStandaloneView: FC<{}> = props =>
|
||||
{
|
||||
const [ groupInformation, setGroupInformation ] = useState<GroupInformationParser>(null);
|
||||
|
||||
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if((groupInformation && (groupInformation.id === parser.id)) || parser.flag) setGroupInformation(parser);
|
||||
});
|
||||
|
||||
if(!groupInformation) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-group-information-standalone" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('group.window.title') } onCloseClick={ event => setGroupInformation(null) } />
|
||||
<NitroCardContentView>
|
||||
<GroupInformationView groupInformation={ groupInformation } onClose={ () => setGroupInformation(null) } />
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import { CreateLinkEvent, GetSessionDataManager, GroupInformationParser, GroupRemoveMemberComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { CatalogPageName, GetGroupManager, GetGroupMembers, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup, TryVisitRoom } from '../../../api';
|
||||
import { Button, Column, Grid, GridProps, LayoutBadgeImageView, Text } from '../../../common';
|
||||
import { useNotification } from '../../../hooks';
|
||||
|
||||
const STATES: string[] = [ 'regular', 'exclusive', 'private' ];
|
||||
|
||||
interface GroupInformationViewProps extends GridProps
|
||||
{
|
||||
groupInformation: GroupInformationParser;
|
||||
onJoin?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const GroupInformationView: FC<GroupInformationViewProps> = props =>
|
||||
{
|
||||
const { groupInformation = null, onClose = null, overflow = 'hidden', ...rest } = props;
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const isRealOwner = (groupInformation && (groupInformation.ownerName === GetSessionDataManager().userName));
|
||||
|
||||
const joinGroup = () => (groupInformation && TryJoinGroup(groupInformation.id));
|
||||
|
||||
const leaveGroup = () =>
|
||||
{
|
||||
showConfirm(LocalizeText('group.leaveconfirm.desc'), () =>
|
||||
{
|
||||
SendMessageComposer(new GroupRemoveMemberComposer(groupInformation.id, GetSessionDataManager().userId));
|
||||
|
||||
if(onClose) onClose();
|
||||
}, null);
|
||||
};
|
||||
|
||||
const getRoleIcon = () =>
|
||||
{
|
||||
if(groupInformation.membershipType === GroupMembershipType.NOT_MEMBER || groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return null;
|
||||
|
||||
if(isRealOwner) return <i className="nitro-icon icon-group-owner" title={ LocalizeText('group.youareowner') } />;
|
||||
|
||||
if(groupInformation.isAdmin) return <i className="nitro-icon icon-group-admin" title={ LocalizeText('group.youareadmin') } />;
|
||||
|
||||
return <i className="nitro-icon icon-group-member" title={ LocalizeText('group.youaremember') } />;
|
||||
};
|
||||
|
||||
const getButtonText = () =>
|
||||
{
|
||||
if(isRealOwner) return 'group.youareowner';
|
||||
|
||||
if(groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType !== GroupMembershipType.MEMBER) return '';
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.MEMBER) return 'group.leave';
|
||||
|
||||
if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.REGULAR) return 'group.join';
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return 'group.membershippending';
|
||||
|
||||
if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.EXCLUSIVE) return 'group.requestmembership';
|
||||
};
|
||||
|
||||
const handleButtonClick = () =>
|
||||
{
|
||||
if((groupInformation.type === GroupType.PRIVATE) && (groupInformation.membershipType === GroupMembershipType.NOT_MEMBER)) return;
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.MEMBER)
|
||||
{
|
||||
leaveGroup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
joinGroup();
|
||||
};
|
||||
|
||||
const handleAction = (action: string) =>
|
||||
{
|
||||
switch(action)
|
||||
{
|
||||
case 'members':
|
||||
GetGroupMembers(groupInformation.id);
|
||||
break;
|
||||
case 'members_pending':
|
||||
GetGroupMembers(groupInformation.id, 2);
|
||||
break;
|
||||
case 'manage':
|
||||
GetGroupManager(groupInformation.id);
|
||||
break;
|
||||
case 'homeroom':
|
||||
TryVisitRoom(groupInformation.roomId);
|
||||
break;
|
||||
case 'furniture':
|
||||
CreateLinkEvent('catalog/open/' + CatalogPageName.GUILD_CUSTOM_FURNI);
|
||||
break;
|
||||
case 'popular_groups':
|
||||
CreateLinkEvent('navigator/search/groups');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if(!groupInformation) return null;
|
||||
|
||||
return (
|
||||
<Grid overflow={ overflow } { ...rest }>
|
||||
<Column center overflow="hidden" size={ 3 }>
|
||||
<div className="flex items-center overflow-hidden group-badge">
|
||||
<LayoutBadgeImageView badgeCode={ groupInformation.badge } isGroup={ true } scale={ 2 } />
|
||||
</div>
|
||||
<Column alignItems="center" gap={ 1 }>
|
||||
<Text pointer small underline onClick={ () => handleAction('members') }>{ LocalizeText('group.membercount', [ 'totalMembers' ], [ groupInformation.membersCount.toString() ]) }</Text>
|
||||
{ (groupInformation.pendingRequestsCount > 0) &&
|
||||
<Text pointer small underline onClick={ () => handleAction('members_pending') }>{ LocalizeText('group.pendingmembercount', [ 'amount' ], [ groupInformation.pendingRequestsCount.toString() ]) }</Text> }
|
||||
{ groupInformation.isOwner &&
|
||||
<Text pointer small underline onClick={ () => handleAction('manage') }>{ LocalizeText('group.manage') }</Text> }
|
||||
</Column>
|
||||
{ getRoleIcon() }
|
||||
</Column>
|
||||
<div className="flex flex-col justify-between overflow-auto col-span-9">
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text bold>{ groupInformation.title }</Text>
|
||||
<div className="flex gap-1">
|
||||
<i className={ 'nitro-icon icon-group-type-' + groupInformation.type } title={ LocalizeText(`group.edit.settings.type.${ STATES[groupInformation.type] }.help`) } />
|
||||
{ groupInformation.canMembersDecorate &&
|
||||
<i className="nitro-icon icon-group-decorate" title={ LocalizeText('group.memberscandecorate') } /> }
|
||||
</div>
|
||||
</div>
|
||||
<Text small>{ LocalizeText('group.created', [ 'date', 'owner' ], [ groupInformation.createdAt, groupInformation.ownerName ]) }</Text>
|
||||
</div>
|
||||
<Text small className="group-description" overflow="auto">{ groupInformation.description }</Text>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text pointer small underline onClick={ () => handleAction('homeroom') }>{ LocalizeText('group.linktobase') }</Text>
|
||||
<Text pointer small underline onClick={ () => handleAction('furniture') }>{ LocalizeText('group.buyfurni') }</Text>
|
||||
<Text pointer small underline onClick={ () => handleAction('popular_groups') }>{ LocalizeText('group.showgroups') }</Text>
|
||||
</div>
|
||||
{ (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) &&
|
||||
<Button disabled={ (groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) || isRealOwner } onClick={ handleButtonClick }>
|
||||
{ LocalizeText(getButtonText()) }
|
||||
</Button> }
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { GroupBadgePart, GroupInformationEvent, GroupSettingsEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { IGroupData, LocalizeText } from '../../../api';
|
||||
import { Column, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
import { GroupTabBadgeView } from './tabs/GroupTabBadgeView';
|
||||
import { GroupTabColorsView } from './tabs/GroupTabColorsView';
|
||||
import { GroupTabIdentityView } from './tabs/GroupTabIdentityView';
|
||||
import { GroupTabSettingsView } from './tabs/GroupTabSettingsView';
|
||||
|
||||
const TABS: number[] = [ 1, 2, 3, 5 ];
|
||||
|
||||
export const GroupManagerView: FC<{}> = props =>
|
||||
{
|
||||
const [ currentTab, setCurrentTab ] = useState<number>(1);
|
||||
const [ closeAction, setCloseAction ] = useState<{ action: () => boolean }>(null);
|
||||
const [ groupData, setGroupData ] = useState<IGroupData>(null);
|
||||
|
||||
const onClose = () =>
|
||||
{
|
||||
setCloseAction(prevValue =>
|
||||
{
|
||||
if(prevValue && prevValue.action) prevValue.action();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
setGroupData(null);
|
||||
};
|
||||
|
||||
const changeTab = (tab: number) =>
|
||||
{
|
||||
if(closeAction && closeAction.action) closeAction.action();
|
||||
|
||||
setCurrentTab(tab);
|
||||
};
|
||||
|
||||
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!groupData || (groupData.groupId !== parser.id)) return;
|
||||
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.groupName = parser.title;
|
||||
newValue.groupDescription = parser.description;
|
||||
newValue.groupState = parser.type;
|
||||
newValue.groupCanMembersDecorate = parser.canMembersDecorate;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<GroupSettingsEvent>(GroupSettingsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
const groupBadgeParts: GroupBadgePart[] = [];
|
||||
|
||||
parser.badgeParts.forEach((part, id) =>
|
||||
{
|
||||
groupBadgeParts.push(new GroupBadgePart(
|
||||
part.isBase ? GroupBadgePart.BASE : GroupBadgePart.SYMBOL,
|
||||
part.key,
|
||||
part.color,
|
||||
part.position
|
||||
));
|
||||
});
|
||||
|
||||
setGroupData({
|
||||
groupId: parser.id,
|
||||
groupName: parser.title,
|
||||
groupDescription: parser.description,
|
||||
groupHomeroomId: parser.roomId,
|
||||
groupState: parser.state,
|
||||
groupCanMembersDecorate: parser.canMembersDecorate,
|
||||
groupColors: [ parser.colorA, parser.colorB ],
|
||||
groupBadgeParts
|
||||
});
|
||||
});
|
||||
|
||||
if(!groupData || (groupData.groupId <= 0)) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-group-manager">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('group.window.title') } onCloseClick={ onClose } />
|
||||
<NitroCardTabsView>
|
||||
{ TABS.map(tab =>
|
||||
{
|
||||
return (<NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ () => changeTab(tab) }>
|
||||
{ LocalizeText(`group.edit.tab.${ tab }`) }
|
||||
</NitroCardTabsItemView>);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<div className="items-center gap-2">
|
||||
<div className={ `nitro-group-tab-image tab-${ currentTab }` } />
|
||||
<Column grow gap={ 0 }>
|
||||
<Text bold fontSize={ 4 }>{ LocalizeText(`group.edit.tabcaption.${ currentTab }`) }</Text>
|
||||
<Text>{ LocalizeText(`group.edit.tabdesc.${ currentTab }`) }</Text>
|
||||
</Column>
|
||||
</div>
|
||||
<Column grow overflow="hidden">
|
||||
{ (currentTab === 1) &&
|
||||
<GroupTabIdentityView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } onClose={ onClose } /> }
|
||||
{ (currentTab === 2) &&
|
||||
<GroupTabBadgeView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } skipDefault={ true } /> }
|
||||
{ (currentTab === 3) &&
|
||||
<GroupTabColorsView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } /> }
|
||||
{ (currentTab === 5) &&
|
||||
<GroupTabSettingsView groupData={ groupData } setCloseAction={ setCloseAction } setGroupData={ setGroupData } /> }
|
||||
</Column>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, GroupAdminGiveComposer, GroupAdminTakeComposer, GroupConfirmMemberRemoveEvent, GroupConfirmRemoveMemberComposer, GroupMemberParser, GroupMembersComposer, GroupMembersEvent, GroupMembershipAcceptComposer, GroupMembershipDeclineComposer, GroupMembersParser, GroupRank, GroupRemoveMemberComposer, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { GetUserProfile, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../hooks';
|
||||
import { classNames } from '../../../layout';
|
||||
|
||||
export const GroupMembersView: FC<{}> = props =>
|
||||
{
|
||||
const [ groupId, setGroupId ] = useState<number>(-1);
|
||||
const [ levelId, setLevelId ] = useState<number>(-1);
|
||||
const [ membersData, setMembersData ] = useState<GroupMembersParser>(null);
|
||||
const [ pageId, setPageId ] = useState<number>(-1);
|
||||
const [ totalPages, setTotalPages ] = useState<number>(0);
|
||||
const [ searchQuery, setSearchQuery ] = useState<string>('');
|
||||
const [ removingMemberName, setRemovingMemberName ] = useState<string>(null);
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const getRankDescription = (member: GroupMemberParser) =>
|
||||
{
|
||||
if(member.rank === GroupRank.OWNER) return 'group.members.owner';
|
||||
|
||||
if(membersData.admin)
|
||||
{
|
||||
if(member.rank === GroupRank.ADMIN) return 'group.members.removerights';
|
||||
|
||||
if(member.rank === GroupRank.MEMBER) return 'group.members.giverights';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const refreshMembers = useCallback(() =>
|
||||
{
|
||||
if((groupId === -1) || (levelId === -1) || (pageId === -1)) return;
|
||||
|
||||
SendMessageComposer(new GroupMembersComposer(groupId, pageId, searchQuery, levelId));
|
||||
}, [ groupId, levelId, pageId, searchQuery ]);
|
||||
|
||||
const toggleAdmin = (member: GroupMemberParser) =>
|
||||
{
|
||||
if(!membersData.admin || (member.rank === GroupRank.OWNER)) return;
|
||||
|
||||
if(member.rank !== GroupRank.ADMIN) SendMessageComposer(new GroupAdminGiveComposer(membersData.groupId, member.id));
|
||||
else SendMessageComposer(new GroupAdminTakeComposer(membersData.groupId, member.id));
|
||||
|
||||
refreshMembers();
|
||||
};
|
||||
|
||||
const acceptMembership = (member: GroupMemberParser) =>
|
||||
{
|
||||
if(!membersData.admin || (member.rank !== GroupRank.REQUESTED)) return;
|
||||
|
||||
SendMessageComposer(new GroupMembershipAcceptComposer(membersData.groupId, member.id));
|
||||
|
||||
refreshMembers();
|
||||
};
|
||||
|
||||
const removeMemberOrDeclineMembership = (member: GroupMemberParser) =>
|
||||
{
|
||||
if(!membersData.admin) return;
|
||||
|
||||
if(member.rank === GroupRank.REQUESTED)
|
||||
{
|
||||
SendMessageComposer(new GroupMembershipDeclineComposer(membersData.groupId, member.id));
|
||||
|
||||
refreshMembers();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setRemovingMemberName(member.name);
|
||||
SendMessageComposer(new GroupConfirmRemoveMemberComposer(membersData.groupId, member.id));
|
||||
};
|
||||
|
||||
useMessageEvent<GroupMembersEvent>(GroupMembersEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setMembersData(parser);
|
||||
setLevelId(parser.level);
|
||||
setTotalPages(Math.ceil(parser.totalMembersCount / parser.pageSize));
|
||||
});
|
||||
|
||||
useMessageEvent<GroupConfirmMemberRemoveEvent>(GroupConfirmMemberRemoveEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
showConfirm(LocalizeText(((parser.furnitureCount > 0) ? 'group.kickconfirm.desc' : 'group.kickconfirm_nofurni.desc'), [ 'user', 'amount' ], [ removingMemberName, parser.furnitureCount.toString() ]), () =>
|
||||
{
|
||||
SendMessageComposer(new GroupRemoveMemberComposer(membersData.groupId, parser.userId));
|
||||
|
||||
refreshMembers();
|
||||
}, null);
|
||||
|
||||
setRemovingMemberName(null);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
const groupId = (parseInt(parts[1]) || -1);
|
||||
const levelId = (parseInt(parts[2]) || 3);
|
||||
|
||||
setGroupId(groupId);
|
||||
setLevelId(levelId);
|
||||
setPageId(0);
|
||||
},
|
||||
eventUrlPrefix: 'group-members/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setPageId(0);
|
||||
}, [ groupId, levelId, searchQuery ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if((groupId === -1) || (levelId === -1) || (pageId === -1)) return;
|
||||
|
||||
SendMessageComposer(new GroupMembersComposer(groupId, pageId, searchQuery, levelId));
|
||||
}, [ groupId, levelId, pageId, searchQuery ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(groupId === -1) return;
|
||||
|
||||
setLevelId(-1);
|
||||
setMembersData(null);
|
||||
setTotalPages(0);
|
||||
setSearchQuery('');
|
||||
setRemovingMemberName(null);
|
||||
}, [ groupId ]);
|
||||
|
||||
if((groupId === -1) || !membersData) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[400px] max-h-[380px] " theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('group.members.title', [ 'groupName' ], [ membersData ? membersData.groupTitle : '' ]) } onCloseClick={ event => setGroupId(-1) } />
|
||||
<NitroCardContentView overflow="hidden">
|
||||
<div className="flex gap-2">
|
||||
<Flex center className="group-badge">
|
||||
<LayoutBadgeImageView badgeCode={ membersData.badge } className="mx-auto block" isGroup={ true } />
|
||||
</Flex>
|
||||
<Column fullWidth gap={ 1 }>
|
||||
<input className="min-h-[calc(1.5em+.5rem+2px)] px-[.5rem] py-[.25rem] text-[.7875rem] rounded-[.2rem] w-full" placeholder={ LocalizeText('group.members.searchinfo') } type="text" value={ searchQuery } onChange={ event => setSearchQuery(event.target.value) } />
|
||||
<select className="form-select form-select-sm w-full" value={ levelId } onChange={ event => setLevelId(parseInt(event.target.value)) }>
|
||||
<option value="0">{ LocalizeText('group.members.search.all') }</option>
|
||||
<option value="1">{ LocalizeText('group.members.search.admins') }</option>
|
||||
<option value="2">{ LocalizeText('group.members.search.pending') }</option>
|
||||
</select>
|
||||
</Column>
|
||||
</div>
|
||||
<Grid className="nitro-group-members-list-grid" columnCount={ 2 } overflow="auto">
|
||||
{ membersData.result.map((member, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } alignItems="center" className="p-2 bg-white rounded h-[50px] max-h-[50px]" gap={ 2 } overflow="hidden">
|
||||
<div className="cursor-pointer relative overflow-hidden w-[40px] h-[50px]" onClick={ () => GetUserProfile(member.id) }>
|
||||
<LayoutAvatarImageView className="absolute -left-[25px] -top-[20px]" direction={ 2 } figure={ member.figure } headOnly={ true } />
|
||||
</div>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text bold pointer small onClick={ event => GetUserProfile(member.id) }>{ member.name }</Text>
|
||||
{ (member.rank !== GroupRank.REQUESTED) &&
|
||||
<Text italics small variant="muted">{ LocalizeText('group.members.since', [ 'date' ], [ member.joinedAt ]) }</Text> }
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (member.rank !== GroupRank.REQUESTED) &&
|
||||
<div className="flex items-center justify-center">
|
||||
<div className={ classNames(`nitro-icon icon-group-small-${ ((member.rank === GroupRank.OWNER) ? 'owner' : (member.rank === GroupRank.ADMIN) ? 'admin' : (membersData.admin && (member.rank === GroupRank.MEMBER)) ? 'not-admin' : '') }`, membersData.admin && 'cursor-pointer') } title={ LocalizeText(getRankDescription(member)) } onClick={ event => toggleAdmin(member) } />
|
||||
</div> }
|
||||
{ membersData.admin && (member.rank === GroupRank.REQUESTED) &&
|
||||
<Flex alignItems="center">
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-accept" title={ LocalizeText('group.members.accept') } onClick={ event => acceptMembership(member) } />
|
||||
</Flex> }
|
||||
{ membersData.admin && (member.rank !== GroupRank.OWNER) && (member.id !== GetSessionDataManager().userId) &&
|
||||
<Flex alignItems="center">
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-deny" title={ LocalizeText(member.rank === GroupRank.REQUESTED ? 'group.members.reject' : 'group.members.kick') } onClick={ event => removeMemberOrDeclineMembership(member) } />
|
||||
</Flex> }
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</Grid>
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<Button disabled={ (membersData.pageIndex === 0) } onClick={ event => setPageId(prevValue => (prevValue - 1)) }>
|
||||
<FaChevronLeft className="fa-icon" />
|
||||
</Button>
|
||||
<Text small>
|
||||
{ LocalizeText('group.members.pageinfo', [ 'amount', 'page', 'totalPages' ], [ membersData.totalMembersCount.toString(), (membersData.pageIndex + 1).toString(), totalPages.toString() ]) }
|
||||
</Text>
|
||||
<Button disabled={ (membersData.pageIndex === (totalPages - 1)) } onClick={ event => setPageId(prevValue => (prevValue + 1)) }>
|
||||
<FaChevronRight className="fa-icon" />
|
||||
</Button>
|
||||
</Flex>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { DesktopViewEvent, GetGuestRoomResultEvent, GetSessionDataManager, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupRemoveMemberComposer, HabboGroupDeactivatedMessageEvent, RoomEntryInfoMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
|
||||
import { GetGroupInformation, GetGroupManager, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup } from '../../../api';
|
||||
import { Button, Flex, LayoutBadgeImageView, Text } from '../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../hooks';
|
||||
|
||||
export const GroupRoomInformationView: FC<{}> = props =>
|
||||
{
|
||||
const [ expectedGroupId, setExpectedGroupId ] = useState<number>(0);
|
||||
const [ groupInformation, setGroupInformation ] = useState<GroupInformationParser>(null);
|
||||
const [ isOpen, setIsOpen ] = useState<boolean>(true);
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
useMessageEvent<DesktopViewEvent>(DesktopViewEvent, event =>
|
||||
{
|
||||
setExpectedGroupId(0);
|
||||
setGroupInformation(null);
|
||||
});
|
||||
|
||||
useMessageEvent<RoomEntryInfoMessageEvent>(RoomEntryInfoMessageEvent, event =>
|
||||
{
|
||||
setExpectedGroupId(0);
|
||||
setGroupInformation(null);
|
||||
});
|
||||
|
||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser.roomEnter) return;
|
||||
|
||||
if(parser.data.habboGroupId > 0)
|
||||
{
|
||||
setExpectedGroupId(parser.data.habboGroupId);
|
||||
SendMessageComposer(new GroupInformationComposer(parser.data.habboGroupId, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
setExpectedGroupId(0);
|
||||
setGroupInformation(null);
|
||||
}
|
||||
});
|
||||
|
||||
useMessageEvent<HabboGroupDeactivatedMessageEvent>(HabboGroupDeactivatedMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!groupInformation || ((parser.groupId !== groupInformation.id) && (parser.groupId !== expectedGroupId))) return;
|
||||
|
||||
setExpectedGroupId(0);
|
||||
setGroupInformation(null);
|
||||
});
|
||||
|
||||
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.id !== expectedGroupId) return;
|
||||
|
||||
setGroupInformation(parser);
|
||||
});
|
||||
|
||||
const leaveGroup = () =>
|
||||
{
|
||||
showConfirm(LocalizeText('group.leaveconfirm.desc'), () =>
|
||||
{
|
||||
SendMessageComposer(new GroupRemoveMemberComposer(groupInformation.id, GetSessionDataManager().userId));
|
||||
}, null);
|
||||
};
|
||||
|
||||
const isRealOwner = (groupInformation && (groupInformation.ownerName === GetSessionDataManager().userName));
|
||||
|
||||
const getButtonText = () =>
|
||||
{
|
||||
if(isRealOwner) return 'group.manage';
|
||||
|
||||
if(groupInformation.type === GroupType.PRIVATE) return '';
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.MEMBER) return 'group.leave';
|
||||
|
||||
if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.REGULAR) return 'group.join';
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return 'group.membershippending';
|
||||
|
||||
if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.EXCLUSIVE) return 'group.requestmembership';
|
||||
};
|
||||
|
||||
const handleButtonClick = () =>
|
||||
{
|
||||
if(isRealOwner) return GetGroupManager(groupInformation.id);
|
||||
|
||||
if((groupInformation.type === GroupType.PRIVATE) && (groupInformation.membershipType === GroupMembershipType.NOT_MEMBER)) return;
|
||||
|
||||
if(groupInformation.membershipType === GroupMembershipType.MEMBER)
|
||||
{
|
||||
leaveGroup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TryJoinGroup(groupInformation.id);
|
||||
};
|
||||
|
||||
if(!groupInformation) return null;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] bg-[#1c1c20f2] rounded text-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Flex pointer alignItems="center" justifyContent="between" onClick={ event => setIsOpen(value => !value) }>
|
||||
<Text variant="white">{ LocalizeText('group.homeroominfo.title') }</Text>
|
||||
{ isOpen && <FaChevronUp className="fa-icon" /> }
|
||||
{ !isOpen && <FaChevronDown className="fa-icon" /> }
|
||||
</Flex>
|
||||
{ isOpen &&
|
||||
<>
|
||||
<Flex pointer alignItems="center" gap={ 2 } onClick={ event => GetGroupInformation(groupInformation.id) }>
|
||||
<div className="group-badge">
|
||||
<LayoutBadgeImageView badgeCode={ groupInformation.badge } isGroup={ true } />
|
||||
</div>
|
||||
<Text variant="white">{ groupInformation.title }</Text>
|
||||
</Flex>
|
||||
{ (groupInformation.type !== GroupType.PRIVATE || isRealOwner) &&
|
||||
<Button fullWidth disabled={ (groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) } variant="success" onClick={ handleButtonClick }>
|
||||
{ LocalizeText(getButtonText()) }
|
||||
</Button>
|
||||
}
|
||||
</> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { GroupSaveBadgeComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { GroupBadgePart, IGroupData, SendMessageComposer } from '../../../../api';
|
||||
import { Column, Flex, Grid, LayoutBadgeImageView } from '../../../../common';
|
||||
import { useGroup } from '../../../../hooks';
|
||||
import { GroupBadgeCreatorView } from '../GroupBadgeCreatorView';
|
||||
|
||||
interface GroupTabBadgeViewProps
|
||||
{
|
||||
skipDefault?: boolean;
|
||||
setCloseAction: Dispatch<SetStateAction<{ action: () => boolean }>>;
|
||||
groupData: IGroupData;
|
||||
setGroupData: Dispatch<SetStateAction<IGroupData>>;
|
||||
}
|
||||
|
||||
export const GroupTabBadgeView: FC<GroupTabBadgeViewProps> = props =>
|
||||
{
|
||||
const { groupData = null, setGroupData = null, setCloseAction = null, skipDefault = null } = props;
|
||||
const [ badgeParts, setBadgeParts ] = useState<GroupBadgePart[]>(null);
|
||||
const { groupCustomize = null } = useGroup();
|
||||
|
||||
const getModifiedBadgeCode = () =>
|
||||
{
|
||||
if(!badgeParts || !badgeParts.length) return '';
|
||||
|
||||
let badgeCode = '';
|
||||
|
||||
badgeParts.forEach(part => (part.code && (badgeCode += part.code)));
|
||||
|
||||
return badgeCode;
|
||||
};
|
||||
|
||||
const saveBadge = useCallback(() =>
|
||||
{
|
||||
if(!groupData || !badgeParts || !badgeParts.length) return false;
|
||||
|
||||
if((groupData.groupBadgeParts === badgeParts)) return true;
|
||||
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.groupBadgeParts = badgeParts;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const badge = [];
|
||||
|
||||
badgeParts.forEach(part =>
|
||||
{
|
||||
if(!part.code) return;
|
||||
|
||||
badge.push(part.key);
|
||||
badge.push(part.color);
|
||||
badge.push(part.position);
|
||||
});
|
||||
|
||||
SendMessageComposer(new GroupSaveBadgeComposer(groupData.groupId, badge));
|
||||
|
||||
return true;
|
||||
}, [ groupData, badgeParts, setGroupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(groupData.groupBadgeParts) return;
|
||||
|
||||
const badgeParts = [
|
||||
new GroupBadgePart(GroupBadgePart.BASE, groupCustomize.badgeBases[0].id, groupCustomize.badgePartColors[0].id),
|
||||
new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id),
|
||||
new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id),
|
||||
new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id),
|
||||
new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id)
|
||||
];
|
||||
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
const groupBadgeParts = badgeParts;
|
||||
|
||||
return { ...prevValue, groupBadgeParts };
|
||||
});
|
||||
}, [ groupData.groupBadgeParts, groupCustomize, setGroupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
setBadgeParts(groupData.groupBadgeParts ? [ ...groupData.groupBadgeParts ] : null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setBadgeParts(groupData.groupBadgeParts);
|
||||
}, [ groupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setCloseAction({ action: saveBadge });
|
||||
|
||||
return () => setCloseAction(null);
|
||||
}, [ setCloseAction, saveBadge ]);
|
||||
|
||||
return (
|
||||
<Grid gap={ 1 } overflow="hidden">
|
||||
<Column size={ 2 }>
|
||||
<Flex center className="bg-muted rounded p-1">
|
||||
<LayoutBadgeImageView badgeCode={ getModifiedBadgeCode() } isGroup={ true } />
|
||||
</Flex>
|
||||
</Column>
|
||||
<Column overflow="auto" size={ 10 }>
|
||||
<GroupBadgeCreatorView badgeParts={ badgeParts } setBadgeParts={ setBadgeParts } />
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { GroupSaveColorsComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { IGroupData, LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { AutoGrid, Column, Grid, Text } from '../../../../common';
|
||||
import { useGroup } from '../../../../hooks';
|
||||
import { classNames } from '../../../../layout';
|
||||
|
||||
interface GroupTabColorsViewProps
|
||||
{
|
||||
groupData: IGroupData;
|
||||
setGroupData: Dispatch<SetStateAction<IGroupData>>;
|
||||
setCloseAction: Dispatch<SetStateAction<{ action: () => boolean }>>;
|
||||
}
|
||||
|
||||
export const GroupTabColorsView: FC<GroupTabColorsViewProps> = props =>
|
||||
{
|
||||
const { groupData = null, setGroupData = null, setCloseAction = null } = props;
|
||||
const [ colors, setColors ] = useState<number[]>(null);
|
||||
const { groupCustomize = null } = useGroup();
|
||||
|
||||
const getGroupColor = (colorIndex: number) =>
|
||||
{
|
||||
if(colorIndex === 0) return groupCustomize.groupColorsA.find(color => (color.id === colors[colorIndex])).color;
|
||||
|
||||
return groupCustomize.groupColorsB.find(color => (color.id === colors[colorIndex])).color;
|
||||
};
|
||||
|
||||
const selectColor = (colorIndex: number, colorId: number) =>
|
||||
{
|
||||
setColors(prevValue =>
|
||||
{
|
||||
const newColors = [ ...prevValue ];
|
||||
|
||||
newColors[colorIndex] = colorId;
|
||||
|
||||
return newColors;
|
||||
});
|
||||
};
|
||||
|
||||
const saveColors = useCallback(() =>
|
||||
{
|
||||
if(!groupData || !colors || !colors.length) return false;
|
||||
|
||||
if(groupData.groupColors === colors) return true;
|
||||
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.groupColors = [ ...colors ];
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SendMessageComposer(new GroupSaveColorsComposer(groupData.groupId, colors[0], colors[1]));
|
||||
|
||||
return true;
|
||||
}, [ groupData, colors, setGroupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!groupCustomize.groupColorsA || !groupCustomize.groupColorsB || groupData.groupColors) return;
|
||||
|
||||
const groupColors = [ groupCustomize.groupColorsA[0].id, groupCustomize.groupColorsB[0].id ];
|
||||
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
return { ...prevValue, groupColors };
|
||||
});
|
||||
}, [ groupCustomize, groupData.groupColors, setGroupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
setColors(groupData.groupColors ? [ ...groupData.groupColors ] : null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setColors(groupData.groupColors);
|
||||
}, [ groupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setCloseAction({ action: saveColors });
|
||||
|
||||
return () => setCloseAction(null);
|
||||
}, [ setCloseAction, saveColors ]);
|
||||
|
||||
if(!colors) return null;
|
||||
|
||||
return (
|
||||
<Grid overflow="hidden">
|
||||
<Column gap={ 1 } size={ 2 }>
|
||||
<Text bold>{ LocalizeText('group.edit.color.guild.color') }</Text>
|
||||
{ groupData.groupColors && (groupData.groupColors.length > 0) &&
|
||||
<div className="flex overflow-hidden border rounded">
|
||||
<div className="w-[30px] h-[40px]" style={ { backgroundColor: '#' + getGroupColor(0) } } />
|
||||
<div className="w-[30px] h-[40px]" style={ { backgroundColor: '#' + getGroupColor(1) } } />
|
||||
</div> }
|
||||
</Column>
|
||||
<Column gap={ 1 } overflow="hidden" size={ 5 }>
|
||||
<Text bold>{ LocalizeText('group.edit.color.primary.color') }</Text>
|
||||
<AutoGrid columnCount={ 7 } columnMinHeight={ 16 } columnMinWidth={ 16 } gap={ 1 }>
|
||||
{ groupData.groupColors && groupCustomize.groupColorsA && groupCustomize.groupColorsA.map((item, index) =>
|
||||
{
|
||||
return <div key={ index } className={ classNames('relative rounded-[.25rem] w-[16px] h-[16px] bg-[#fff] border-[2px] border-[solid] border-[#fff] [box-shadow:inset_3px_3px_#0000001a] [box-shadow:inset_2px_2px_#0003] cursor-pointer', ((groupData.groupColors[0] === item.id) && 'bg-primary [box-shadow:none]')) } style={ { backgroundColor: '#' + item.color } } onClick={ () => selectColor(0, item.id) }></div>;
|
||||
}) }
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
<Column gap={ 1 } overflow="hidden" size={ 5 }>
|
||||
<Text bold>{ LocalizeText('group.edit.color.secondary.color') }</Text>
|
||||
<AutoGrid columnCount={ 7 } columnMinHeight={ 16 } columnMinWidth={ 16 } gap={ 1 }>
|
||||
{ groupData.groupColors && groupCustomize.groupColorsB && groupCustomize.groupColorsB.map((item, index) =>
|
||||
{
|
||||
return <div key={ index } className={ classNames('relative rounded-[.25rem] w-[16px] h-[16px] bg-[#fff] border-[2px] border-[solid] border-[#fff] [box-shadow:inset_3px_3px_#0000001a] [box-shadow:inset_2px_2px_#0003] cursor-pointer', ((groupData.groupColors[1] === item.id) && 'bg-primary [box-shadow:none]')) } style={ { backgroundColor: '#' + item.color } } onClick={ () => selectColor(1, item.id) }></div>;
|
||||
}) }
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { IGroupData, LocalizeText } from '../../../../api';
|
||||
import { Column, Flex, Grid, LayoutBadgeImageView, Text } from '../../../../common';
|
||||
import { useGroup } from '../../../../hooks';
|
||||
|
||||
interface GroupTabCreatorConfirmationViewProps
|
||||
{
|
||||
groupData: IGroupData;
|
||||
setGroupData: Dispatch<SetStateAction<IGroupData>>;
|
||||
purchaseCost: number;
|
||||
}
|
||||
|
||||
export const GroupTabCreatorConfirmationView: FC<GroupTabCreatorConfirmationViewProps> = props =>
|
||||
{
|
||||
const { groupData = null, setGroupData = null, purchaseCost = 0 } = props;
|
||||
const { groupCustomize = null } = useGroup();
|
||||
|
||||
const getCompleteBadgeCode = () =>
|
||||
{
|
||||
if(!groupData || !groupData.groupBadgeParts || !groupData.groupBadgeParts.length) return '';
|
||||
|
||||
let badgeCode = '';
|
||||
|
||||
groupData.groupBadgeParts.forEach(part => (part.code && (badgeCode += part.code)));
|
||||
|
||||
return badgeCode;
|
||||
};
|
||||
|
||||
const getGroupColor = (colorIndex: number) =>
|
||||
{
|
||||
if(colorIndex === 0) return groupCustomize.groupColorsA.find(c => c.id === groupData.groupColors[colorIndex]).color;
|
||||
|
||||
return groupCustomize.groupColorsB.find(c => c.id === groupData.groupColors[colorIndex]).color;
|
||||
};
|
||||
|
||||
if(!groupData) return null;
|
||||
|
||||
return (
|
||||
<Grid gap={ 1 } overflow="hidden">
|
||||
<Column size={ 3 }>
|
||||
<Column center className="bg-muted rounded p-1" gap={ 2 }>
|
||||
<Text bold center>{ LocalizeText('group.create.confirm.guildbadge') }</Text>
|
||||
<LayoutBadgeImageView badgeCode={ getCompleteBadgeCode() } isGroup={ true } />
|
||||
</Column>
|
||||
<Column center className="bg-muted rounded p-1" gap={ 2 }>
|
||||
<Text bold center>{ LocalizeText('group.edit.color.guild.color') }</Text>
|
||||
<Flex className="rounded border" overflow="hidden">
|
||||
<div className="w-[30px] h-[40px]" style={ { backgroundColor: '#' + getGroupColor(0) } } />
|
||||
<div className="w-[30px] h-[40px]" style={ { backgroundColor: '#' + getGroupColor(1) } } />
|
||||
</Flex>
|
||||
</Column>
|
||||
</Column>
|
||||
<Column justifyContent="between" size={ 9 }>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ groupData.groupName }</Text>
|
||||
<Text>{ groupData.groupDescription }</Text>
|
||||
</div>
|
||||
<Text overflow="auto">{ LocalizeText('group.create.confirm.info') }</Text>
|
||||
</div>
|
||||
<Text center className="bg-primary rounded p-1" variant="white">
|
||||
{ LocalizeText('group.create.confirm.buyinfo', [ 'amount' ], [ purchaseCost.toString() ]) }
|
||||
</Text>
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
import
|
||||
{
|
||||
CreateLinkEvent,
|
||||
GroupDeleteComposer,
|
||||
GroupSaveInformationComposer,
|
||||
} from '@nitrots/nitro-renderer';
|
||||
import
|
||||
{
|
||||
Dispatch,
|
||||
FC,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { IGroupData, LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Text } from '../../../../common';
|
||||
import { useNotification } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
|
||||
interface GroupTabIdentityViewProps {
|
||||
groupData: IGroupData;
|
||||
setGroupData: Dispatch<SetStateAction<IGroupData>>;
|
||||
setCloseAction: Dispatch<SetStateAction<{ action: () => boolean }>>;
|
||||
onClose: () => void;
|
||||
isCreator?: boolean;
|
||||
availableRooms?: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
export const GroupTabIdentityView: FC<GroupTabIdentityViewProps> = (props) =>
|
||||
{
|
||||
const {
|
||||
groupData = null,
|
||||
setGroupData = null,
|
||||
setCloseAction = null,
|
||||
onClose = null,
|
||||
isCreator = false,
|
||||
availableRooms = [],
|
||||
} = props;
|
||||
const [groupName, setGroupName] = useState<string>('');
|
||||
const [groupDescription, setGroupDescription] = useState<string>('');
|
||||
const [groupHomeroomId, setGroupHomeroomId] = useState<number>(-1);
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const deleteGroup = () =>
|
||||
{
|
||||
if(!groupData || groupData.groupId <= 0) return;
|
||||
|
||||
showConfirm(
|
||||
LocalizeText('group.deleteconfirm.desc'),
|
||||
() =>
|
||||
{
|
||||
SendMessageComposer(new GroupDeleteComposer(groupData.groupId));
|
||||
|
||||
if(onClose) onClose();
|
||||
},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
LocalizeText('group.deleteconfirm.title')
|
||||
);
|
||||
};
|
||||
|
||||
const saveIdentity = useCallback(() =>
|
||||
{
|
||||
if(!groupData || !groupName || !groupName.length) return false;
|
||||
|
||||
if(
|
||||
groupName === groupData.groupName &&
|
||||
groupDescription === groupData.groupDescription
|
||||
)
|
||||
return true;
|
||||
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
if(groupHomeroomId <= 0) return false;
|
||||
|
||||
setGroupData((prevValue) =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.groupName = groupName;
|
||||
newValue.groupDescription = groupDescription;
|
||||
newValue.groupHomeroomId = groupHomeroomId;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SendMessageComposer(
|
||||
new GroupSaveInformationComposer(
|
||||
groupData.groupId,
|
||||
groupName,
|
||||
groupDescription || ''
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}, [groupData, groupName, groupDescription, groupHomeroomId, setGroupData]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setGroupName(groupData.groupName || '');
|
||||
setGroupDescription(groupData.groupDescription || '');
|
||||
setGroupHomeroomId(groupData.groupHomeroomId);
|
||||
}, [groupData]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setCloseAction({ action: saveIdentity });
|
||||
|
||||
return () => setCloseAction(null);
|
||||
}, [setCloseAction, saveIdentity]);
|
||||
|
||||
if(!groupData) return null;
|
||||
|
||||
return (
|
||||
<Column justifyContent="between" overflow="auto">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Text center className="col-span-3">
|
||||
{LocalizeText('group.edit.name')}
|
||||
</Text>
|
||||
<NitroInput
|
||||
maxLength={29}
|
||||
type="text"
|
||||
value={groupName}
|
||||
onChange={(event) => setGroupName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text center className="col-span-3">
|
||||
{LocalizeText('group.edit.desc')}
|
||||
</Text>
|
||||
<textarea
|
||||
className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm"
|
||||
maxLength={254}
|
||||
value={groupDescription}
|
||||
onChange={(event) =>
|
||||
setGroupDescription(event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isCreator && (
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text center className="col-span-3">
|
||||
{LocalizeText('group.edit.base')}
|
||||
</Text>
|
||||
<Column fullWidth gap={1}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={groupHomeroomId}
|
||||
onChange={(event) =>
|
||||
setGroupHomeroomId(
|
||||
parseInt(event.target.value)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option disabled value={-1}>
|
||||
{LocalizeText(
|
||||
'group.edit.base.select.room'
|
||||
)}
|
||||
</option>
|
||||
{availableRooms &&
|
||||
availableRooms.map((room, index) => (
|
||||
<option key={index} value={room.id}>
|
||||
{room.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Column>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div className="col-span-3"> </div>
|
||||
<Text small>
|
||||
{LocalizeText('group.edit.base.warning')}
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isCreator && (
|
||||
<Button variant="danger" onClick={deleteGroup}>
|
||||
{LocalizeText('group.delete')}
|
||||
</Button>
|
||||
)}
|
||||
{isCreator && (
|
||||
<Text
|
||||
center
|
||||
fullWidth
|
||||
pointer
|
||||
underline
|
||||
onClick={(event) => CreateLinkEvent('navigator/create')}
|
||||
>
|
||||
{LocalizeText('group.createroom')}
|
||||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { GroupSavePreferencesComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { IGroupData, LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Flex, HorizontalRule, Text } from '../../../../common';
|
||||
|
||||
const STATES: string[] = [ 'regular', 'exclusive', 'private' ];
|
||||
|
||||
interface GroupTabSettingsViewProps
|
||||
{
|
||||
groupData: IGroupData;
|
||||
setGroupData: Dispatch<SetStateAction<IGroupData>>;
|
||||
setCloseAction: Dispatch<SetStateAction<{ action: () => boolean }>>;
|
||||
}
|
||||
|
||||
export const GroupTabSettingsView: FC<GroupTabSettingsViewProps> = props =>
|
||||
{
|
||||
const { groupData = null, setGroupData = null, setCloseAction = null } = props;
|
||||
const [ groupState, setGroupState ] = useState<number>(groupData.groupState);
|
||||
const [ groupDecorate, setGroupDecorate ] = useState<boolean>(groupData.groupCanMembersDecorate);
|
||||
|
||||
const saveSettings = useCallback(() =>
|
||||
{
|
||||
if(!groupData) return false;
|
||||
|
||||
if((groupState === groupData.groupState) && (groupDecorate === groupData.groupCanMembersDecorate)) return true;
|
||||
|
||||
if(groupData.groupId <= 0)
|
||||
{
|
||||
setGroupData(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.groupState = groupState;
|
||||
newValue.groupCanMembersDecorate = groupDecorate;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SendMessageComposer(new GroupSavePreferencesComposer(groupData.groupId, groupState, groupDecorate ? 0 : 1));
|
||||
|
||||
return true;
|
||||
}, [ groupData, groupState, groupDecorate, setGroupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setGroupState(groupData.groupState);
|
||||
setGroupDecorate(groupData.groupCanMembersDecorate);
|
||||
}, [ groupData ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setCloseAction({ action: saveSettings });
|
||||
|
||||
return () => setCloseAction(null);
|
||||
}, [ setCloseAction, saveSettings ]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<div className="flex flex-col">
|
||||
{ STATES.map((state, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } alignItems="center" gap={ 1 }>
|
||||
<input checked={ (groupState === index) } className="form-check-input" name="groupState" type="radio" onChange={ event => setGroupState(index) } />
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex gap-1">
|
||||
<i className={ `icon icon-group-type-${ index }` } />
|
||||
<Text bold>{ LocalizeText(`group.edit.settings.type.${ state }.label`) }</Text>
|
||||
</div>
|
||||
<Text>{ LocalizeText(`group.edit.settings.type.${ state }.help`) }</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<HorizontalRule />
|
||||
<div className="flex items-center gap-1">
|
||||
<input checked={ groupDecorate } className="form-check-input" type="checkbox" onChange={ event => setGroupDecorate(prevValue => !prevValue) } />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ LocalizeText('group.edit.settings.rights.caption') }</Text>
|
||||
<Text>{ LocalizeText('group.edit.settings.rights.members.help') }</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user