mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
⭐ Start the forum framework
This commit is contained in:
@@ -12,6 +12,7 @@ import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
|
||||
import { FriendsView } from './friends/FriendsView';
|
||||
import { GameCenterView } from './game-center/GameCenterView';
|
||||
import { GroupsView } from './groups/GroupsView';
|
||||
import { GroupForumView } from './groups/views/forums/GroupForumView';
|
||||
import { GuideToolView } from './guide-tool/GuideToolView';
|
||||
import { HcCenterView } from './hc-center/HcCenterView';
|
||||
import { HelpView } from './help/HelpView';
|
||||
@@ -112,6 +113,7 @@ export const MainView: FC<{}> = props =>
|
||||
<UserSettingsView />
|
||||
<UserProfileView />
|
||||
<GroupsView />
|
||||
<GroupForumView />
|
||||
<CameraWidgetView />
|
||||
<HelpView />
|
||||
<NitropediaView />
|
||||
|
||||
@@ -94,6 +94,9 @@ export const GroupInformationView: FC<GroupInformationViewProps> = props =>
|
||||
case 'popular_groups':
|
||||
CreateLinkEvent('navigator/search/groups');
|
||||
break;
|
||||
case 'forum':
|
||||
CreateLinkEvent('groupforum/' + groupInformation.id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,6 +137,8 @@ export const GroupInformationView: FC<GroupInformationViewProps> = props =>
|
||||
<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>
|
||||
{ groupInformation.hasForum &&
|
||||
<Text pointer small underline onClick={ () => handleAction('forum') }>{ LocalizeText('group.showforum') }</Text> }
|
||||
</div>
|
||||
{ (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) &&
|
||||
<Button disabled={ (groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) || isRealOwner } onClick={ handleButtonClick }>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ForumsListMessageEvent, GetForumsListMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer, GetUserProfile } from '../../../../api';
|
||||
import { Column, Flex, Text, LayoutBadgeImageView } from '../../../../common';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { ForumData } from '@nitrots/nitro-renderer';
|
||||
|
||||
const FORUMS_PER_PAGE = 20;
|
||||
|
||||
interface GroupForumListViewProps
|
||||
{
|
||||
onOpenForum: (groupId: number) => void;
|
||||
}
|
||||
|
||||
export const GroupForumListView: FC<GroupForumListViewProps> = props =>
|
||||
{
|
||||
const { onOpenForum = null } = props;
|
||||
const [ forums, setForums ] = useState<ForumData[]>([]);
|
||||
const [ listMode, setListMode ] = useState<number>(2); // 2 = most active
|
||||
const [ startIndex, setStartIndex ] = useState<number>(0);
|
||||
const [ totalForums, setTotalForums ] = useState<number>(0);
|
||||
|
||||
useMessageEvent<ForumsListMessageEvent>(ForumsListMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setTotalForums(parser.totalAmount);
|
||||
|
||||
if(parser.startIndex === 0)
|
||||
{
|
||||
setForums(parser.forums);
|
||||
}
|
||||
else
|
||||
{
|
||||
setForums(prev => [ ...prev, ...parser.forums ]);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetForumsListMessageComposer(listMode, startIndex, FORUMS_PER_PAGE));
|
||||
}, [ listMode, startIndex ]);
|
||||
|
||||
const formatTimeAgo = (seconds: number): string =>
|
||||
{
|
||||
if(seconds < 60) return `${ seconds }s ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 3600) return `${ Math.floor(seconds / 60) }m ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 86400) return `${ Math.floor(seconds / 3600) }h ${ LocalizeText('messageboard.time.ago') }`;
|
||||
|
||||
return `${ Math.floor(seconds / 86400) }d ${ LocalizeText('messageboard.time.ago') }`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Column className="h-full" gap={ 0 }>
|
||||
<Flex className="bg-muted p-2 border-b" gap={ 2 } alignItems="center" justifyContent="between">
|
||||
<Text bold>{ LocalizeText('messageboard.all.threads.header') }</Text>
|
||||
<Flex gap={ 1 }>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={ listMode }
|
||||
onChange={ e => { setListMode(parseInt(e.target.value)); setStartIndex(0); } }>
|
||||
<option value={ 2 }>{ LocalizeText('groupforum.list.tab.most_active') }</option>
|
||||
<option value={ 1 }>{ LocalizeText('groupforum.list.tab.my_forums') }</option>
|
||||
</select>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Column className="overflow-auto flex-1 p-2" gap={ 1 }>
|
||||
{ forums.map((forum, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ forum.groupId }
|
||||
className="p-2 rounded bg-white hover:bg-muted cursor-pointer border"
|
||||
gap={ 2 }
|
||||
alignItems="center"
|
||||
onClick={ () => onOpenForum(forum.groupId) }>
|
||||
<div className="flex-shrink-0">
|
||||
<LayoutBadgeImageView badgeCode={ forum.icon } isGroup={ true } />
|
||||
</div>
|
||||
<Column className="flex-1 overflow-hidden" gap={ 0 }>
|
||||
<Text bold className="truncate">{ forum.name }</Text>
|
||||
<Text small variant="muted" className="truncate">{ forum.description }</Text>
|
||||
</Column>
|
||||
<Column className="flex-shrink-0 text-end" gap={ 0 }>
|
||||
<Text small>{ forum.totalThreads } { LocalizeText('groupforum.view.threads') }</Text>
|
||||
<Text small>{ forum.totalMessages } { LocalizeText('messageboard.messages') }</Text>
|
||||
{ (forum.unreadMessages > 0) &&
|
||||
<Text small bold variant="danger">{ forum.unreadMessages } { LocalizeText('messageboard.unread') }</Text> }
|
||||
</Column>
|
||||
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
||||
{ (forum.lastMessageAuthorId > 0) && <>
|
||||
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(forum.lastMessageAuthorId); } }>
|
||||
{ forum.lastMessageAuthorName }
|
||||
</Text>
|
||||
<Text small variant="muted">{ formatTimeAgo(forum.lastMessageTimeAsSecondsAgo) }</Text>
|
||||
</> }
|
||||
</Column>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
{ (forums.length === 0) &&
|
||||
<Flex className="p-4" justifyContent="center">
|
||||
<Text variant="muted">{ LocalizeText('groupforum.list.no_forums') }</Text>
|
||||
</Flex> }
|
||||
{ (forums.length < totalForums) &&
|
||||
<Flex justifyContent="center" className="p-2">
|
||||
<Text pointer underline onClick={ () => setStartIndex(forums.length) }>
|
||||
{ LocalizeText('groupforum.list.load_more') }
|
||||
</Text>
|
||||
</Flex> }
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { PostMessageMessageComposer, PostThreadMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, Text } from '../../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../../hooks';
|
||||
import { ExtendedForumData } from '@nitrots/nitro-renderer';
|
||||
|
||||
interface GroupForumNewThreadViewProps
|
||||
{
|
||||
groupId: number;
|
||||
forumData: ExtendedForumData;
|
||||
onBack: () => void;
|
||||
onThreadCreated: (threadId: number) => void;
|
||||
}
|
||||
|
||||
export const GroupForumNewThreadView: FC<GroupForumNewThreadViewProps> = props =>
|
||||
{
|
||||
const { groupId = 0, forumData = null, onBack = null, onThreadCreated = null } = props;
|
||||
const effectiveGroupId = forumData?.groupId || groupId;
|
||||
const [ subject, setSubject ] = useState<string>('');
|
||||
const [ message, setMessage ] = useState<string>('');
|
||||
const [ isSubmitting, setIsSubmitting ] = useState<boolean>(false);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
useMessageEvent<PostThreadMessageEvent>(PostThreadMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId) return;
|
||||
|
||||
setIsSubmitting(false);
|
||||
setSubject('');
|
||||
setMessage('');
|
||||
|
||||
if(onThreadCreated) onThreadCreated(parser.thread.threadId);
|
||||
});
|
||||
|
||||
const submitThread = useCallback(() =>
|
||||
{
|
||||
if(subject.trim().length < 10)
|
||||
{
|
||||
simpleAlert(LocalizeText('groupforum.compose.subject_too_short'));
|
||||
return;
|
||||
}
|
||||
|
||||
if(message.trim().length < 10)
|
||||
{
|
||||
simpleAlert(LocalizeText('groupforum.compose.message_too_short'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
// PostMessageMessageComposer with threadId=0 creates a new thread
|
||||
// params: groupId, threadId (0 for new), subject, message
|
||||
SendMessageComposer(new PostMessageMessageComposer(effectiveGroupId, 0, subject.trim(), message.trim()));
|
||||
}, [ effectiveGroupId, subject, message, simpleAlert ]);
|
||||
|
||||
return (
|
||||
<Column className="h-full p-3" gap={ 2 }>
|
||||
<Flex gap={ 2 } alignItems="center">
|
||||
<Text pointer underline onClick={ onBack }>
|
||||
« { LocalizeText('groupforum.view.back') }
|
||||
</Text>
|
||||
</Flex>
|
||||
<Column gap={ 1 }>
|
||||
<Text bold>{ LocalizeText('messageboard.message.thread.subject') }</Text>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder={ LocalizeText('messageboard.message.thread.subject') }
|
||||
maxLength={ 120 }
|
||||
value={ subject }
|
||||
onChange={ e => setSubject(e.target.value) }
|
||||
/>
|
||||
</Column>
|
||||
<Column className="flex-1" gap={ 1 }>
|
||||
<Text bold>{ LocalizeText('messageboard.forum.compose.message.header') }</Text>
|
||||
<textarea
|
||||
className="form-control form-control-sm flex-1"
|
||||
placeholder={ LocalizeText('messageboard.forum.compose.message.header') }
|
||||
value={ message }
|
||||
onChange={ e => setMessage(e.target.value) }
|
||||
/>
|
||||
</Column>
|
||||
<Flex gap={ 2 } justifyContent="end">
|
||||
<Button variant="secondary" className="btn-sm" onClick={ onBack }>
|
||||
{ LocalizeText('generic.cancel') }
|
||||
</Button>
|
||||
<Button variant="primary" className="btn-sm" onClick={ submitThread } disabled={ isSubmitting || subject.trim().length < 10 || message.trim().length < 10 }>
|
||||
{ isSubmitting ? '...' : LocalizeText('messageboard.new.thread.button') }
|
||||
</Button>
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { UpdateForumSettingsMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, Text } from '../../../../common';
|
||||
import { ExtendedForumData } from '@nitrots/nitro-renderer';
|
||||
|
||||
// Permission levels: 0 = EVERYONE, 1 = MEMBERS, 2 = ADMINS, 3 = OWNER
|
||||
const PERMISSION_EVERYONE = 0;
|
||||
const PERMISSION_MEMBERS = 1;
|
||||
const PERMISSION_ADMINS = 2;
|
||||
const PERMISSION_OWNER = 3;
|
||||
|
||||
interface GroupForumSettingsViewProps
|
||||
{
|
||||
groupId: number;
|
||||
forumData: ExtendedForumData;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const GroupForumSettingsView: FC<GroupForumSettingsViewProps> = props =>
|
||||
{
|
||||
const { groupId = 0, forumData = null, onBack = null } = props;
|
||||
const effectiveGroupId = forumData?.groupId || groupId;
|
||||
const [ readPermission, setReadPermission ] = useState<number>(forumData?.readPermissions ?? PERMISSION_EVERYONE);
|
||||
const [ postMessagePermission, setPostMessagePermission ] = useState<number>(forumData?.postMessagePermissions ?? PERMISSION_MEMBERS);
|
||||
const [ postThreadPermission, setPostThreadPermission ] = useState<number>(forumData?.postThreadPermissions ?? PERMISSION_MEMBERS);
|
||||
const [ moderatePermission, setModeratePermission ] = useState<number>(forumData?.moderatePermissions ?? PERMISSION_ADMINS);
|
||||
|
||||
const saveSettings = useCallback(() =>
|
||||
{
|
||||
SendMessageComposer(new UpdateForumSettingsMessageComposer(
|
||||
effectiveGroupId,
|
||||
readPermission,
|
||||
postMessagePermission,
|
||||
postThreadPermission,
|
||||
moderatePermission
|
||||
));
|
||||
|
||||
onBack();
|
||||
}, [ effectiveGroupId, readPermission, postMessagePermission, postThreadPermission, moderatePermission, onBack ]);
|
||||
|
||||
const getPermissionOptions = (includeOwner: boolean = false) =>
|
||||
{
|
||||
const options = [
|
||||
{ value: PERMISSION_EVERYONE, label: LocalizeText('groupforum.permissions.option_all') },
|
||||
{ value: PERMISSION_MEMBERS, label: LocalizeText('groupforum.permissions.option_group_members') },
|
||||
{ value: PERMISSION_ADMINS, label: LocalizeText('groupforum.permissions.option_group_admins') }
|
||||
];
|
||||
|
||||
if(includeOwner)
|
||||
{
|
||||
options.push({ value: PERMISSION_OWNER, label: LocalizeText('groupforum.permissions.option_owner') });
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
return (
|
||||
<Column className="h-full p-3" gap={ 3 }>
|
||||
<Flex gap={ 2 } alignItems="center">
|
||||
<Text pointer underline onClick={ onBack }>
|
||||
« { LocalizeText('groupforum.view.back') }
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text bold>{ LocalizeText('groupforum.settings.window_title') }</Text>
|
||||
<Column gap={ 2 }>
|
||||
<Column gap={ 1 }>
|
||||
<Text bold small>{ LocalizeText('groupforum.permissions.read_label') }</Text>
|
||||
<select className="form-select form-select-sm" value={ readPermission } onChange={ e => setReadPermission(parseInt(e.target.value)) }>
|
||||
{ getPermissionOptions().map(opt => (
|
||||
<option key={ opt.value } value={ opt.value }>{ opt.label }</option>
|
||||
)) }
|
||||
</select>
|
||||
</Column>
|
||||
<Column gap={ 1 }>
|
||||
<Text bold small>{ LocalizeText('groupforum.permissions.post_message_label') }</Text>
|
||||
<select className="form-select form-select-sm" value={ postMessagePermission } onChange={ e => setPostMessagePermission(parseInt(e.target.value)) }>
|
||||
{ getPermissionOptions(true).map(opt => (
|
||||
<option key={ opt.value } value={ opt.value }>{ opt.label }</option>
|
||||
)) }
|
||||
</select>
|
||||
</Column>
|
||||
<Column gap={ 1 }>
|
||||
<Text bold small>{ LocalizeText('groupforum.permissions.post_thread_label') }</Text>
|
||||
<select className="form-select form-select-sm" value={ postThreadPermission } onChange={ e => setPostThreadPermission(parseInt(e.target.value)) }>
|
||||
{ getPermissionOptions(true).map(opt => (
|
||||
<option key={ opt.value } value={ opt.value }>{ opt.label }</option>
|
||||
)) }
|
||||
</select>
|
||||
</Column>
|
||||
<Column gap={ 1 }>
|
||||
<Text bold small>{ LocalizeText('groupforum.permissions.moderate_label') }</Text>
|
||||
<select className="form-select form-select-sm" value={ moderatePermission } onChange={ e => setModeratePermission(parseInt(e.target.value)) }>
|
||||
{ [
|
||||
{ value: PERMISSION_ADMINS, label: LocalizeText('groupforum.permissions.option_group_admins') },
|
||||
{ value: PERMISSION_OWNER, label: LocalizeText('groupforum.permissions.option_owner') }
|
||||
].map(opt => (
|
||||
<option key={ opt.value } value={ opt.value }>{ opt.label }</option>
|
||||
)) }
|
||||
</select>
|
||||
</Column>
|
||||
</Column>
|
||||
<Flex className="mt-auto" gap={ 2 } justifyContent="end">
|
||||
<Button variant="secondary" className="btn-sm" onClick={ onBack }>
|
||||
{ LocalizeText('generic.cancel') }
|
||||
</Button>
|
||||
<Button variant="primary" className="btn-sm" onClick={ saveSettings }>
|
||||
{ LocalizeText('groupforum.settings.ok') }
|
||||
</Button>
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import { GetThreadsMessageComposer, GuildForumThreadsEvent, PostThreadMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer, GetUserProfile } from '../../../../api';
|
||||
import { Button, Column, Flex, LayoutBadgeImageView, Text } from '../../../../common';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { ExtendedForumData, GuildForumThread } from '@nitrots/nitro-renderer';
|
||||
|
||||
const THREADS_PER_PAGE = 20;
|
||||
|
||||
interface GroupForumThreadListViewProps
|
||||
{
|
||||
groupId: number;
|
||||
forumData: ExtendedForumData;
|
||||
onOpenThread: (groupId: number, threadId: number) => void;
|
||||
onNewThread: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props =>
|
||||
{
|
||||
const { groupId = 0, forumData = null, onOpenThread = null, onNewThread = null, onOpenSettings = null, onBack = null } = props;
|
||||
const effectiveGroupId = forumData?.groupId || groupId;
|
||||
const [ threads, setThreads ] = useState<GuildForumThread[]>([]);
|
||||
const [ startIndex, setStartIndex ] = useState<number>(0);
|
||||
const [ totalThreads, setTotalThreads ] = useState<number>(0);
|
||||
|
||||
useMessageEvent<GuildForumThreadsEvent>(GuildForumThreadsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId) return;
|
||||
|
||||
setTotalThreads(parser.amount);
|
||||
|
||||
if(parser.startIndex === 0)
|
||||
{
|
||||
setThreads(parser.threads);
|
||||
}
|
||||
else
|
||||
{
|
||||
setThreads(prev => [ ...prev, ...parser.threads ]);
|
||||
}
|
||||
});
|
||||
|
||||
useMessageEvent<PostThreadMessageEvent>(PostThreadMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId) return;
|
||||
|
||||
setThreads(prev => [ parser.thread, ...prev ]);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!effectiveGroupId) return;
|
||||
|
||||
setThreads([]);
|
||||
setStartIndex(0);
|
||||
SendMessageComposer(new GetThreadsMessageComposer(effectiveGroupId, 0, THREADS_PER_PAGE));
|
||||
}, [ effectiveGroupId ]);
|
||||
|
||||
const formatTimeAgo = (seconds: number): string =>
|
||||
{
|
||||
if(seconds < 60) return `${ seconds }s ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 3600) return `${ Math.floor(seconds / 60) }m ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 86400) return `${ Math.floor(seconds / 3600) }h ${ LocalizeText('messageboard.time.ago') }`;
|
||||
|
||||
return `${ Math.floor(seconds / 86400) }d ${ LocalizeText('messageboard.time.ago') }`;
|
||||
};
|
||||
|
||||
const getThreadStateText = (thread: GuildForumThread): string =>
|
||||
{
|
||||
if(thread.state === 10) return LocalizeText('messageboard.thread.hidden.by.admin');
|
||||
if(thread.state === 20) return LocalizeText('messageboard.thread.permanently.deleted.by.moderator');
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const pinnedThreads = threads.filter(t => t.isPinned);
|
||||
const normalThreads = threads.filter(t => !t.isPinned);
|
||||
const sortedThreads = [ ...pinnedThreads, ...normalThreads ];
|
||||
|
||||
return (
|
||||
<Column className="h-full" gap={ 0 }>
|
||||
<Flex className="bg-muted p-2 border-b" gap={ 2 } alignItems="center" justifyContent="between">
|
||||
<Flex gap={ 2 } alignItems="center">
|
||||
<Text pointer underline onClick={ onBack }>
|
||||
« { LocalizeText('groupforum.view.back') }
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex gap={ 1 } alignItems="center">
|
||||
{ forumData && forumData.canChangeSettings &&
|
||||
<Button variant="link" className="btn-sm" onClick={ onOpenSettings }>
|
||||
{ LocalizeText('groupforum.view.settings.header') }
|
||||
</Button> }
|
||||
{ forumData && forumData.hasPostThreadPermissionError &&
|
||||
<Button variant="primary" className="btn-sm" onClick={ onNewThread }>
|
||||
{ LocalizeText('messageboard.new.thread.button') }
|
||||
</Button> }
|
||||
</Flex>
|
||||
</Flex>
|
||||
{ forumData &&
|
||||
<Flex className="bg-light p-2 border-b" gap={ 2 } alignItems="center">
|
||||
<LayoutBadgeImageView badgeCode={ forumData.icon } isGroup={ true } />
|
||||
<Column className="flex-1" gap={ 0 }>
|
||||
<Text bold>{ forumData.name }</Text>
|
||||
<Text small variant="muted">{ forumData.description }</Text>
|
||||
</Column>
|
||||
<Column className="text-end" gap={ 0 }>
|
||||
<Text small>{ forumData.totalThreads } { LocalizeText('groupforum.view.threads') }</Text>
|
||||
<Text small>{ forumData.totalMessages } { LocalizeText('messageboard.messages') }</Text>
|
||||
</Column>
|
||||
</Flex> }
|
||||
<Column className="overflow-auto flex-1" gap={ 0 }>
|
||||
{ sortedThreads.map((thread, index) =>
|
||||
{
|
||||
const stateText = getThreadStateText(thread);
|
||||
|
||||
if(stateText)
|
||||
{
|
||||
return (
|
||||
<Flex key={ thread.threadId } className="p-2 border-b bg-danger bg-opacity-10" alignItems="center">
|
||||
<Text small variant="muted">{ stateText }</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex key={ thread.threadId }
|
||||
className={ `p-2 border-b hover:bg-muted cursor-pointer ${ thread.isPinned ? 'bg-warning bg-opacity-10' : '' } ${ thread.unreadMessagesCount > 0 ? 'fw-bold' : '' }` }
|
||||
gap={ 2 }
|
||||
alignItems="center"
|
||||
onClick={ () => onOpenThread(effectiveGroupId, thread.threadId) }>
|
||||
<Column className="flex-1 overflow-hidden" gap={ 0 }>
|
||||
<Flex gap={ 1 } alignItems="center">
|
||||
{ thread.isPinned && <i className="fas fa-thumbtack text-warning" /> }
|
||||
{ thread.isLocked && <i className="fas fa-lock text-muted" /> }
|
||||
<Text bold={ thread.unreadMessagesCount > 0 } className="truncate">{ thread.header }</Text>
|
||||
</Flex>
|
||||
<Flex gap={ 1 }>
|
||||
<Text small variant="muted">{ LocalizeText('messageboard.started.by') }</Text>
|
||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.authorId); } }>
|
||||
{ thread.authorName }
|
||||
</Text>
|
||||
<Text small variant="muted">- { formatTimeAgo(thread.creationTimeAsSecondsAgo) }</Text>
|
||||
</Flex>
|
||||
</Column>
|
||||
<Column className="flex-shrink-0 text-center min-w-[60px]" gap={ 0 }>
|
||||
<Text small>{ thread.totalMessages }</Text>
|
||||
<Text small variant="muted">{ LocalizeText('messageboard.messages') }</Text>
|
||||
</Column>
|
||||
{ (thread.unreadMessagesCount > 0) &&
|
||||
<Column className="flex-shrink-0 text-center min-w-[60px]" gap={ 0 }>
|
||||
<Text small bold variant="danger">{ thread.unreadMessagesCount }</Text>
|
||||
<Text small variant="danger">{ LocalizeText('messageboard.unread') }</Text>
|
||||
</Column> }
|
||||
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
||||
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.lastUserId); } }>
|
||||
{ thread.lastUserName }
|
||||
</Text>
|
||||
<Text small variant="muted">{ formatTimeAgo(thread.lastCommentTime) }</Text>
|
||||
</Column>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
{ (sortedThreads.length === 0) &&
|
||||
<Flex className="p-4" justifyContent="center">
|
||||
<Text variant="muted">{ LocalizeText('groupforum.view.no_threads') }</Text>
|
||||
</Flex> }
|
||||
{ (threads.length < totalThreads) &&
|
||||
<Flex justifyContent="center" className="p-2">
|
||||
<Text pointer underline onClick={ () =>
|
||||
{
|
||||
const nextIndex = threads.length;
|
||||
setStartIndex(nextIndex);
|
||||
SendMessageComposer(new GetThreadsMessageComposer(effectiveGroupId, nextIndex, THREADS_PER_PAGE));
|
||||
} }>
|
||||
{ LocalizeText('groupforum.list.load_more') }
|
||||
</Text>
|
||||
</Flex> }
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
import { GetMessagesMessageComposer, ModerateMessageMessageComposer, ModerateThreadMessageComposer, PostMessageMessageComposer, PostMessageMessageEvent, PostThreadMessageEvent, ThreadMessagesMessageEvent, UpdateMessageMessageEvent, UpdateThreadMessageComposer, UpdateThreadMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer, GetUserProfile } from '../../../../api';
|
||||
import { Button, Column, Flex, LayoutAvatarImageView, Text } from '../../../../common';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { ExtendedForumData, GuildForumThread, MessageData } from '@nitrots/nitro-renderer';
|
||||
|
||||
const MESSAGES_PER_PAGE = 20;
|
||||
|
||||
// Message states
|
||||
const STATE_NORMAL = 0;
|
||||
const STATE_HIDDEN_BY_ADMIN = 10;
|
||||
const STATE_DELETED_BY_MODERATOR = 20;
|
||||
|
||||
interface GroupForumThreadViewProps
|
||||
{
|
||||
groupId: number;
|
||||
threadId: number;
|
||||
forumData: ExtendedForumData;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
||||
{
|
||||
const { groupId = 0, threadId = 0, forumData = null, onBack = null } = props;
|
||||
const effectiveGroupId = forumData?.groupId || groupId;
|
||||
const [ messages, setMessages ] = useState<MessageData[]>([]);
|
||||
const [ totalMessages, setTotalMessages ] = useState<number>(0);
|
||||
const [ replyText, setReplyText ] = useState<string>('');
|
||||
const [ threadInfo, setThreadInfo ] = useState<GuildForumThread>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useMessageEvent<ThreadMessagesMessageEvent>(ThreadMessagesMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId || parser.threadId !== threadId) return;
|
||||
|
||||
setTotalMessages(parser.amount);
|
||||
|
||||
if(parser.startIndex === 0)
|
||||
{
|
||||
setMessages(parser.messages);
|
||||
}
|
||||
else
|
||||
{
|
||||
setMessages(prev => [ ...prev, ...parser.messages ]);
|
||||
}
|
||||
});
|
||||
|
||||
useMessageEvent<PostMessageMessageEvent>(PostMessageMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId || parser.threadId !== threadId) return;
|
||||
|
||||
setMessages(prev => [ ...prev, parser.message ]);
|
||||
});
|
||||
|
||||
useMessageEvent<PostThreadMessageEvent>(PostThreadMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId) return;
|
||||
|
||||
// Update thread info if this is our thread
|
||||
if(parser.thread.threadId === threadId)
|
||||
{
|
||||
setThreadInfo(parser.thread);
|
||||
}
|
||||
});
|
||||
|
||||
useMessageEvent<UpdateMessageMessageEvent>(UpdateMessageMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId || parser.threadId !== threadId) return;
|
||||
|
||||
setMessages(prev => prev.map(msg =>
|
||||
{
|
||||
if(msg.messageId === parser.message.messageId)
|
||||
{
|
||||
return parser.message;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}));
|
||||
});
|
||||
|
||||
useMessageEvent<UpdateThreadMessageEvent>(UpdateThreadMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.groupId !== effectiveGroupId) return;
|
||||
|
||||
if(parser.thread.threadId === threadId)
|
||||
{
|
||||
setThreadInfo(parser.thread);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!effectiveGroupId || !threadId) return;
|
||||
|
||||
setMessages([]);
|
||||
SendMessageComposer(new GetMessagesMessageComposer(effectiveGroupId, threadId, 0, MESSAGES_PER_PAGE));
|
||||
}, [ effectiveGroupId, threadId ]);
|
||||
|
||||
const sendReply = useCallback(() =>
|
||||
{
|
||||
if(!replyText.trim().length) return;
|
||||
|
||||
SendMessageComposer(new PostMessageMessageComposer(effectiveGroupId, threadId, '', replyText.trim()));
|
||||
setReplyText('');
|
||||
}, [ effectiveGroupId, threadId, replyText ]);
|
||||
|
||||
const togglePinThread = useCallback(() =>
|
||||
{
|
||||
if(!threadInfo) return;
|
||||
|
||||
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, !threadInfo.isPinned, threadInfo.isLocked));
|
||||
}, [ effectiveGroupId, threadId, threadInfo ]);
|
||||
|
||||
const toggleLockThread = useCallback(() =>
|
||||
{
|
||||
if(!threadInfo) return;
|
||||
|
||||
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, threadInfo.isPinned, !threadInfo.isLocked));
|
||||
}, [ effectiveGroupId, threadId, threadInfo ]);
|
||||
|
||||
const hideMessage = useCallback((messageId: number) =>
|
||||
{
|
||||
SendMessageComposer(new ModerateMessageMessageComposer(effectiveGroupId, threadId, messageId, STATE_HIDDEN_BY_ADMIN));
|
||||
}, [ effectiveGroupId, threadId ]);
|
||||
|
||||
const restoreMessage = useCallback((messageId: number) =>
|
||||
{
|
||||
SendMessageComposer(new ModerateMessageMessageComposer(effectiveGroupId, threadId, messageId, STATE_NORMAL));
|
||||
}, [ effectiveGroupId, threadId ]);
|
||||
|
||||
const hideThread = useCallback(() =>
|
||||
{
|
||||
SendMessageComposer(new ModerateThreadMessageComposer(effectiveGroupId, threadId, STATE_HIDDEN_BY_ADMIN));
|
||||
onBack();
|
||||
}, [ effectiveGroupId, threadId, onBack ]);
|
||||
|
||||
const formatTimeAgo = (seconds: number): string =>
|
||||
{
|
||||
if(seconds < 60) return `${ seconds }s ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 3600) return `${ Math.floor(seconds / 60) }m ${ LocalizeText('messageboard.time.ago') }`;
|
||||
if(seconds < 86400) return `${ Math.floor(seconds / 3600) }h ${ LocalizeText('messageboard.time.ago') }`;
|
||||
|
||||
return `${ Math.floor(seconds / 86400) }d ${ LocalizeText('messageboard.time.ago') }`;
|
||||
};
|
||||
|
||||
const getMessageStateText = (message: MessageData): string =>
|
||||
{
|
||||
if(message.state === STATE_HIDDEN_BY_ADMIN)
|
||||
{
|
||||
return LocalizeText('messageboard.message.hidden.by.admin');
|
||||
}
|
||||
|
||||
if(message.state === STATE_DELETED_BY_MODERATOR)
|
||||
{
|
||||
return LocalizeText('messageboard.message.permanently.deleted.by.moderator');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const canModerate = forumData && forumData.hasModeratePermissionError;
|
||||
const canPost = forumData && forumData.hasPostMessagePermissionError;
|
||||
const isLocked = threadInfo ? threadInfo.isLocked : false;
|
||||
|
||||
// Derive thread info from first message if we don't have explicit thread info
|
||||
const threadHeader = (messages.length > 0 && messages[0]) ? messages[0].messageText : '';
|
||||
|
||||
return (
|
||||
<Column className="h-full" gap={ 0 }>
|
||||
<Flex className="bg-muted p-2 border-b" gap={ 2 } alignItems="center" justifyContent="between">
|
||||
<Flex gap={ 2 } alignItems="center">
|
||||
<Text pointer underline onClick={ onBack }>
|
||||
« { LocalizeText('groupforum.view.back') }
|
||||
</Text>
|
||||
</Flex>
|
||||
{ canModerate &&
|
||||
<Flex gap={ 1 }>
|
||||
<Button variant={ threadInfo?.isPinned ? 'warning' : 'outline-secondary' } className="btn-sm" onClick={ togglePinThread }>
|
||||
{ threadInfo?.isPinned ? LocalizeText('groupforum.thread.unpin') : LocalizeText('groupforum.thread.pin') }
|
||||
</Button>
|
||||
<Button variant={ isLocked ? 'danger' : 'outline-secondary' } className="btn-sm" onClick={ toggleLockThread }>
|
||||
{ isLocked ? LocalizeText('groupforum.thread.unlock') : LocalizeText('groupforum.thread.lock') }
|
||||
</Button>
|
||||
<Button variant="outline-danger" className="btn-sm" onClick={ hideThread }>
|
||||
{ LocalizeText('groupforum.thread.hide') }
|
||||
</Button>
|
||||
</Flex> }
|
||||
</Flex>
|
||||
<Column className="overflow-auto flex-1" gap={ 0 }>
|
||||
{ messages.map((message, index) =>
|
||||
{
|
||||
const stateText = getMessageStateText(message);
|
||||
|
||||
if(stateText && !canModerate)
|
||||
{
|
||||
return (
|
||||
<Flex key={ message.messageId } className="p-2 border-b bg-danger bg-opacity-10" alignItems="center">
|
||||
<Text small variant="muted">{ stateText }</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex key={ message.messageId }
|
||||
className={ `p-3 border-b ${ (message.state !== STATE_NORMAL) ? 'bg-danger bg-opacity-10' : '' }` }
|
||||
gap={ 3 }>
|
||||
<Column className="flex-shrink-0 text-center" gap={ 1 } style={ { width: '80px' } }>
|
||||
<div className="mx-auto" style={ { height: '80px', width: '50px', overflow: 'hidden' } }>
|
||||
<LayoutAvatarImageView figure={ message.authorFigure } direction={ 2 } />
|
||||
</div>
|
||||
<Text small bold pointer underline onClick={ () => GetUserProfile(message.authorId) }>
|
||||
{ message.authorName }
|
||||
</Text>
|
||||
<Text small variant="muted">{ message.authorPostCount } { LocalizeText('messageboard.messages') }</Text>
|
||||
</Column>
|
||||
<Column className="flex-1" gap={ 1 }>
|
||||
<Flex justifyContent="between" alignItems="center">
|
||||
<Text small variant="muted">{ formatTimeAgo(message.creationTime) }</Text>
|
||||
{ canModerate && (message.state !== STATE_NORMAL) &&
|
||||
<Flex gap={ 1 }>
|
||||
<Text small variant="muted">{ stateText }</Text>
|
||||
<Text small pointer underline variant="primary" onClick={ () => restoreMessage(message.messageId) }>
|
||||
{ LocalizeText('groupforum.message.restore') }
|
||||
</Text>
|
||||
</Flex> }
|
||||
{ canModerate && (message.state === STATE_NORMAL) &&
|
||||
<Text small pointer underline variant="danger" onClick={ () => hideMessage(message.messageId) }>
|
||||
{ LocalizeText('groupforum.message.hide') }
|
||||
</Text> }
|
||||
</Flex>
|
||||
{ (message.state === STATE_NORMAL || canModerate) &&
|
||||
<Text className="whitespace-pre-wrap break-words">{ message.messageText }</Text> }
|
||||
</Column>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
{ (messages.length < totalMessages) &&
|
||||
<Flex justifyContent="center" className="p-2">
|
||||
<Text pointer underline onClick={ () =>
|
||||
{
|
||||
SendMessageComposer(new GetMessagesMessageComposer(effectiveGroupId, threadId, messages.length, MESSAGES_PER_PAGE));
|
||||
} }>
|
||||
{ LocalizeText('groupforum.thread.load_more') }
|
||||
</Text>
|
||||
</Flex> }
|
||||
<div ref={ messagesEndRef } />
|
||||
</Column>
|
||||
{ canPost && !isLocked &&
|
||||
<Flex className="p-2 border-t bg-light" gap={ 2 }>
|
||||
<textarea
|
||||
className="form-control form-control-sm flex-1"
|
||||
placeholder={ LocalizeText('messageboard.message.replying.to') }
|
||||
rows={ 2 }
|
||||
value={ replyText }
|
||||
onChange={ e => setReplyText(e.target.value) }
|
||||
onKeyDown={ e =>
|
||||
{
|
||||
if(e.key === 'Enter' && !e.shiftKey)
|
||||
{
|
||||
e.preventDefault();
|
||||
sendReply();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
<Button variant="primary" className="btn-sm align-self-end" onClick={ sendReply } disabled={ !replyText.trim().length }>
|
||||
{ LocalizeText('messageboard.reply.button') }
|
||||
</Button>
|
||||
</Flex> }
|
||||
{ isLocked &&
|
||||
<Flex className="p-2 border-t bg-warning bg-opacity-10" justifyContent="center">
|
||||
<Text small variant="muted">{ LocalizeText('groupforum.thread.locked') }</Text>
|
||||
</Flex> }
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { AddLinkEventTracker, ForumDataMessageEvent, GetForumStatsMessageComposer, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { GroupForumThreadListView } from './GroupForumThreadListView';
|
||||
import { GroupForumThreadView } from './GroupForumThreadView';
|
||||
import { GroupForumNewThreadView } from './GroupForumNewThreadView';
|
||||
import { GroupForumSettingsView } from './GroupForumSettingsView';
|
||||
import { GroupForumListView } from './GroupForumListView';
|
||||
import { ExtendedForumData } from '@nitrots/nitro-renderer';
|
||||
|
||||
const VIEW_FORUM_LIST = 0;
|
||||
const VIEW_THREAD_LIST = 1;
|
||||
const VIEW_THREAD = 2;
|
||||
const VIEW_NEW_THREAD = 3;
|
||||
const VIEW_SETTINGS = 4;
|
||||
|
||||
export const GroupForumView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState<boolean>(false);
|
||||
const [ currentView, setCurrentView ] = useState<number>(VIEW_FORUM_LIST);
|
||||
const [ groupId, setGroupId ] = useState<number>(0);
|
||||
const [ threadId, setThreadId ] = useState<number>(0);
|
||||
const [ forumData, setForumData ] = useState<ExtendedForumData>(null);
|
||||
|
||||
useMessageEvent<ForumDataMessageEvent>(ForumDataMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setForumData(parser.extendedForumData);
|
||||
});
|
||||
|
||||
const openForum = useCallback((id: number) =>
|
||||
{
|
||||
setGroupId(id);
|
||||
setCurrentView(VIEW_THREAD_LIST);
|
||||
setIsVisible(true);
|
||||
SendMessageComposer(new GetForumStatsMessageComposer(id));
|
||||
}, []);
|
||||
|
||||
const openThread = useCallback((gId: number, tId: number) =>
|
||||
{
|
||||
setGroupId(gId);
|
||||
setThreadId(tId);
|
||||
setCurrentView(VIEW_THREAD);
|
||||
}, []);
|
||||
|
||||
const openNewThread = useCallback(() =>
|
||||
{
|
||||
setCurrentView(VIEW_NEW_THREAD);
|
||||
}, []);
|
||||
|
||||
const openSettings = useCallback(() =>
|
||||
{
|
||||
setCurrentView(VIEW_SETTINGS);
|
||||
}, []);
|
||||
|
||||
const backToThreadList = useCallback(() =>
|
||||
{
|
||||
setCurrentView(VIEW_THREAD_LIST);
|
||||
setThreadId(0);
|
||||
}, []);
|
||||
|
||||
const backToForumList = useCallback(() =>
|
||||
{
|
||||
setCurrentView(VIEW_FORUM_LIST);
|
||||
setGroupId(0);
|
||||
setForumData(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'toggle':
|
||||
setIsVisible(prev => !prev);
|
||||
if(!isVisible) setCurrentView(VIEW_FORUM_LIST);
|
||||
return;
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
default: {
|
||||
const id = parseInt(parts[1]);
|
||||
|
||||
if(!isNaN(id) && id > 0)
|
||||
{
|
||||
openForum(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'groupforum/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ isVisible, openForum ]);
|
||||
|
||||
const getHeaderText = () =>
|
||||
{
|
||||
switch(currentView)
|
||||
{
|
||||
case VIEW_FORUM_LIST:
|
||||
return LocalizeText('groupforum.view.window_title');
|
||||
case VIEW_THREAD_LIST:
|
||||
return forumData ? forumData.name : LocalizeText('messageboard.forum.header');
|
||||
case VIEW_THREAD:
|
||||
return forumData ? forumData.name : LocalizeText('messageboard.forum.header');
|
||||
case VIEW_NEW_THREAD:
|
||||
return LocalizeText('messageboard.new.thread.button');
|
||||
case VIEW_SETTINGS:
|
||||
return LocalizeText('groupforum.settings.window_title');
|
||||
default:
|
||||
return LocalizeText('messageboard.forum.header');
|
||||
}
|
||||
};
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-group-forum w-[600px] h-[500px]" theme="primary" uniqueKey="group-forum">
|
||||
<NitroCardHeaderView headerText={ getHeaderText() } onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardContentView overflow="hidden" className="p-0">
|
||||
{ (currentView === VIEW_FORUM_LIST) &&
|
||||
<GroupForumListView onOpenForum={ openForum } /> }
|
||||
{ (currentView === VIEW_THREAD_LIST) &&
|
||||
<GroupForumThreadListView
|
||||
groupId={ groupId }
|
||||
forumData={ forumData }
|
||||
onOpenThread={ openThread }
|
||||
onNewThread={ openNewThread }
|
||||
onOpenSettings={ openSettings }
|
||||
onBack={ backToForumList } /> }
|
||||
{ (currentView === VIEW_THREAD) &&
|
||||
<GroupForumThreadView
|
||||
groupId={ groupId }
|
||||
threadId={ threadId }
|
||||
forumData={ forumData }
|
||||
onBack={ backToThreadList } /> }
|
||||
{ (currentView === VIEW_NEW_THREAD) &&
|
||||
<GroupForumNewThreadView
|
||||
groupId={ groupId }
|
||||
forumData={ forumData }
|
||||
onBack={ backToThreadList }
|
||||
onThreadCreated={ (tId: number) => openThread(groupId, tId) } /> }
|
||||
{ (currentView === VIEW_SETTINGS) &&
|
||||
<GroupForumSettingsView
|
||||
groupId={ groupId }
|
||||
forumData={ forumData }
|
||||
onBack={ backToThreadList } /> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './GroupForumView';
|
||||
export * from './GroupForumListView';
|
||||
export * from './GroupForumNewThreadView';
|
||||
export * from './GroupForumSettingsView';
|
||||
export * from './GroupForumThreadListView';
|
||||
export * from './GroupForumThreadView';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CreateLinkEvent, GetRoomEngine, GetSessionDataManager, MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react';
|
||||
import { DispatchUiEvent, GetConfigurationValue, GetRoomSession, GetUserProfile } from '../../api';
|
||||
import { DispatchUiEvent, GetConfigurationValue, GetRoomSession, GetUserProfile, LocalizeText } from '../../api';
|
||||
import { Flex, LayoutItemCountView } from '../../common';
|
||||
import { GuideToolEvent } from '../../events';
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ToolbarMeView: FC<PropsWithChildren<{
|
||||
<div className="navigation-item relative nitro-icon icon-me-rooms cursor-pointer" onClick={ event => CreateLinkEvent('navigator/search/myworld_view') } />
|
||||
<div className="navigation-item relative nitro-icon icon-me-clothing cursor-pointer" onClick={ event => CreateLinkEvent('avatar-editor/toggle') } />
|
||||
<div className="navigation-item relative nitro-icon icon-me-settings cursor-pointer" onClick={ event => CreateLinkEvent('user-settings/toggle') } />
|
||||
<div className="navigation-item relative nitro-icon icon-me-forums cursor-pointer" onClick={ event => CreateLinkEvent('groupforum/toggle') } title={ LocalizeText('toolbar.icon.label.forums') } />
|
||||
{ children }
|
||||
</Flex>
|
||||
);
|
||||
|
||||
+5
-5
@@ -875,12 +875,12 @@ body {
|
||||
|
||||
.avatar-parts {
|
||||
border: none !important;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
max-width: 42px;
|
||||
border-radius: 2rem !important;
|
||||
overflow: visible !important;
|
||||
overflow: hidden !important;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -132,7 +132,12 @@ const InfiniteGridItem = forwardRef<HTMLDivElement, {
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!itemImage || !itemImage.length) return;
|
||||
if(!itemImage || !itemImage.length)
|
||||
{
|
||||
setBackgroundImageUrl(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user