mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
🆙 Stage 1
This commit is contained in:
@@ -78,6 +78,7 @@ export const GroupForumNewThreadView: FC<GroupForumNewThreadViewProps> = props =
|
|||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm flex-1"
|
className="form-control form-control-sm flex-1"
|
||||||
placeholder={ LocalizeText('messageboard.forum.compose.message.header') }
|
placeholder={ LocalizeText('messageboard.forum.compose.message.header') }
|
||||||
|
maxLength={ 4000 }
|
||||||
value={ message }
|
value={ message }
|
||||||
onChange={ e => setMessage(e.target.value) }
|
onChange={ e => setMessage(e.target.value) }
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { GetThreadsMessageComposer, GuildForumThreadsEvent, PostThreadMessageEvent } from '@nitrots/nitro-renderer';
|
import { ExtendedForumData, GetThreadsMessageComposer, GuildForumThread, GuildForumThreadsEvent, ModerateThreadMessageComposer, PostThreadMessageEvent } from '@nitrots/nitro-renderer';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { LocalizeText, SendMessageComposer, GetUserProfile } from '../../../../api';
|
import { LocalizeText, SendMessageComposer, GetUserProfile } from '../../../../api';
|
||||||
import { Button, Column, Flex, LayoutBadgeImageView, Text } from '../../../../common';
|
import { Button, Column, Flex, LayoutBadgeImageView, Text } from '../../../../common';
|
||||||
import { useMessageEvent } from '../../../../hooks';
|
import { useMessageEvent } from '../../../../hooks';
|
||||||
import { ExtendedForumData, GuildForumThread } from '@nitrots/nitro-renderer';
|
|
||||||
|
|
||||||
const THREADS_PER_PAGE = 20;
|
const THREADS_PER_PAGE = 20;
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ interface GroupForumThreadListViewProps
|
|||||||
{
|
{
|
||||||
groupId: number;
|
groupId: number;
|
||||||
forumData: ExtendedForumData;
|
forumData: ExtendedForumData;
|
||||||
onOpenThread: (groupId: number, threadId: number) => void;
|
onOpenThread: (groupId: number, threadId: number, thread?: GuildForumThread) => void;
|
||||||
onNewThread: () => void;
|
onNewThread: () => void;
|
||||||
onOpenSettings: () => void;
|
onOpenSettings: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@@ -78,10 +77,17 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canModerate = forumData && forumData.hasModeratePermissionError;
|
||||||
|
|
||||||
const pinnedThreads = threads.filter(t => t.isPinned);
|
const pinnedThreads = threads.filter(t => t.isPinned);
|
||||||
const normalThreads = threads.filter(t => !t.isPinned);
|
const normalThreads = threads.filter(t => !t.isPinned);
|
||||||
const sortedThreads = [ ...pinnedThreads, ...normalThreads ];
|
const sortedThreads = [ ...pinnedThreads, ...normalThreads ];
|
||||||
|
|
||||||
|
const restoreThread = (thread: GuildForumThread) =>
|
||||||
|
{
|
||||||
|
SendMessageComposer(new ModerateThreadMessageComposer(effectiveGroupId, thread.threadId, 1));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column className="h-full" gap={ 0 }>
|
<Column className="h-full" gap={ 0 }>
|
||||||
<Flex className="bg-muted p-2 border-b" gap={ 2 } alignItems="center" justifyContent="between">
|
<Flex className="bg-muted p-2 border-b" gap={ 2 } alignItems="center" justifyContent="between">
|
||||||
@@ -121,8 +127,16 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
|
|||||||
if(stateText)
|
if(stateText)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Flex key={ thread.threadId } className="p-2 border-b bg-danger bg-opacity-10" alignItems="center">
|
<Flex key={ thread.threadId } className="p-2 border-b bg-danger bg-opacity-10" alignItems="center" justifyContent="between">
|
||||||
<Text small variant="muted">{ stateText }</Text>
|
<Column gap={ 0 }>
|
||||||
|
<Text small variant="muted">{ stateText }</Text>
|
||||||
|
{ canModerate &&
|
||||||
|
<Text small variant="muted">{ thread.header }</Text> }
|
||||||
|
</Column>
|
||||||
|
{ canModerate &&
|
||||||
|
<Button variant="outline-success" className="btn-sm" onClick={ () => restoreThread(thread) }>
|
||||||
|
{ LocalizeText('groupforum.thread.restore') }
|
||||||
|
</Button> }
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -132,7 +146,7 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
|
|||||||
className={ `p-2 border-b hover:bg-muted cursor-pointer ${ thread.isPinned ? 'bg-warning bg-opacity-10' : '' } ${ thread.unreadMessagesCount > 0 ? 'fw-bold' : '' }` }
|
className={ `p-2 border-b hover:bg-muted cursor-pointer ${ thread.isPinned ? 'bg-warning bg-opacity-10' : '' } ${ thread.unreadMessagesCount > 0 ? 'fw-bold' : '' }` }
|
||||||
gap={ 2 }
|
gap={ 2 }
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
onClick={ () => onOpenThread(effectiveGroupId, thread.threadId) }>
|
onClick={ () => onOpenThread(effectiveGroupId, thread.threadId, thread) }>
|
||||||
<Column className="flex-1 overflow-hidden" gap={ 0 }>
|
<Column className="flex-1 overflow-hidden" gap={ 0 }>
|
||||||
<Flex gap={ 1 } alignItems="center">
|
<Flex gap={ 1 } alignItems="center">
|
||||||
{ thread.isPinned && <i className="fas fa-thumbtack text-warning" /> }
|
{ thread.isPinned && <i className="fas fa-thumbtack text-warning" /> }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const MESSAGES_PER_PAGE = 20;
|
|||||||
|
|
||||||
// Message states
|
// Message states
|
||||||
const STATE_NORMAL = 0;
|
const STATE_NORMAL = 0;
|
||||||
|
const STATE_VISIBLE = 1;
|
||||||
const STATE_HIDDEN_BY_ADMIN = 10;
|
const STATE_HIDDEN_BY_ADMIN = 10;
|
||||||
const STATE_DELETED_BY_MODERATOR = 20;
|
const STATE_DELETED_BY_MODERATOR = 20;
|
||||||
|
|
||||||
@@ -16,18 +17,20 @@ interface GroupForumThreadViewProps
|
|||||||
{
|
{
|
||||||
groupId: number;
|
groupId: number;
|
||||||
threadId: number;
|
threadId: number;
|
||||||
|
initialThread?: GuildForumThread;
|
||||||
forumData: ExtendedForumData;
|
forumData: ExtendedForumData;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
||||||
{
|
{
|
||||||
const { groupId = 0, threadId = 0, forumData = null, onBack = null } = props;
|
const { groupId = 0, threadId = 0, initialThread = null, forumData = null, onBack = null } = props;
|
||||||
const effectiveGroupId = forumData?.groupId || groupId;
|
const effectiveGroupId = forumData?.groupId || groupId;
|
||||||
const [ messages, setMessages ] = useState<MessageData[]>([]);
|
const [ messages, setMessages ] = useState<MessageData[]>([]);
|
||||||
const [ totalMessages, setTotalMessages ] = useState<number>(0);
|
const [ totalMessages, setTotalMessages ] = useState<number>(0);
|
||||||
const [ replyText, setReplyText ] = useState<string>('');
|
const [ replyText, setReplyText ] = useState<string>('');
|
||||||
const [ threadInfo, setThreadInfo ] = useState<GuildForumThread>(null);
|
const [ threadInfo, setThreadInfo ] = useState<GuildForumThread>(initialThread);
|
||||||
|
const [ isSubmitting, setIsSubmitting ] = useState<boolean>(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useMessageEvent<ThreadMessagesMessageEvent>(ThreadMessagesMessageEvent, event =>
|
useMessageEvent<ThreadMessagesMessageEvent>(ThreadMessagesMessageEvent, event =>
|
||||||
@@ -109,24 +112,29 @@ export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
|||||||
|
|
||||||
const sendReply = useCallback(() =>
|
const sendReply = useCallback(() =>
|
||||||
{
|
{
|
||||||
if(!replyText.trim().length) return;
|
if(replyText.trim().length < 10 || isSubmitting) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
SendMessageComposer(new PostMessageMessageComposer(effectiveGroupId, threadId, '', replyText.trim()));
|
SendMessageComposer(new PostMessageMessageComposer(effectiveGroupId, threadId, '', replyText.trim()));
|
||||||
setReplyText('');
|
setReplyText('');
|
||||||
}, [ effectiveGroupId, threadId, replyText ]);
|
|
||||||
|
setTimeout(() => setIsSubmitting(false), 1000);
|
||||||
|
}, [ effectiveGroupId, threadId, replyText, isSubmitting ]);
|
||||||
|
|
||||||
const togglePinThread = useCallback(() =>
|
const togglePinThread = useCallback(() =>
|
||||||
{
|
{
|
||||||
if(!threadInfo) return;
|
if(!threadInfo) return;
|
||||||
|
|
||||||
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, !threadInfo.isPinned, threadInfo.isLocked));
|
// UpdateThreadMessageComposer swaps 3rd/4th params internally: (groupId, threadId, isLocked, isPinned)
|
||||||
|
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, threadInfo.isLocked, !threadInfo.isPinned));
|
||||||
}, [ effectiveGroupId, threadId, threadInfo ]);
|
}, [ effectiveGroupId, threadId, threadInfo ]);
|
||||||
|
|
||||||
const toggleLockThread = useCallback(() =>
|
const toggleLockThread = useCallback(() =>
|
||||||
{
|
{
|
||||||
if(!threadInfo) return;
|
if(!threadInfo) return;
|
||||||
|
|
||||||
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, threadInfo.isPinned, !threadInfo.isLocked));
|
// UpdateThreadMessageComposer swaps 3rd/4th params internally: (groupId, threadId, isLocked, isPinned)
|
||||||
|
SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, !threadInfo.isLocked, threadInfo.isPinned));
|
||||||
}, [ effectiveGroupId, threadId, threadInfo ]);
|
}, [ effectiveGroupId, threadId, threadInfo ]);
|
||||||
|
|
||||||
const hideMessage = useCallback((messageId: number) =>
|
const hideMessage = useCallback((messageId: number) =>
|
||||||
@@ -136,7 +144,7 @@ export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
|||||||
|
|
||||||
const restoreMessage = useCallback((messageId: number) =>
|
const restoreMessage = useCallback((messageId: number) =>
|
||||||
{
|
{
|
||||||
SendMessageComposer(new ModerateMessageMessageComposer(effectiveGroupId, threadId, messageId, STATE_NORMAL));
|
SendMessageComposer(new ModerateMessageMessageComposer(effectiveGroupId, threadId, messageId, STATE_VISIBLE));
|
||||||
}, [ effectiveGroupId, threadId ]);
|
}, [ effectiveGroupId, threadId ]);
|
||||||
|
|
||||||
const hideThread = useCallback(() =>
|
const hideThread = useCallback(() =>
|
||||||
@@ -215,9 +223,11 @@ export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
|||||||
<Flex key={ message.messageId }
|
<Flex key={ message.messageId }
|
||||||
className={ `p-3 border-b ${ (message.state !== STATE_NORMAL) ? 'bg-danger bg-opacity-10' : '' }` }
|
className={ `p-3 border-b ${ (message.state !== STATE_NORMAL) ? 'bg-danger bg-opacity-10' : '' }` }
|
||||||
gap={ 3 }>
|
gap={ 3 }>
|
||||||
<Column className="flex-shrink-0 text-center" gap={ 1 } style={ { width: '80px' } }>
|
<Column className="flex-shrink-0 items-center w-[50px]" gap={ 1 }>
|
||||||
<div className="mx-auto" style={ { height: '80px', width: '50px', overflow: 'hidden' } }>
|
<div className="w-[40px] h-[40px] rounded-full mx-auto overflow-hidden bg-[rgba(255,255,255,0.1)] flex justify-center">
|
||||||
<LayoutAvatarImageView figure={ message.authorFigure } direction={ 2 } />
|
<div className="mt-[-25px]">
|
||||||
|
<LayoutAvatarImageView figure={ message.authorFigure } headOnly={ true } direction={ 2 } />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Text small bold pointer underline onClick={ () => GetUserProfile(message.authorId) }>
|
<Text small bold pointer underline onClick={ () => GetUserProfile(message.authorId) }>
|
||||||
{ message.authorName }
|
{ message.authorName }
|
||||||
@@ -262,6 +272,7 @@ export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
|||||||
className="form-control form-control-sm flex-1"
|
className="form-control form-control-sm flex-1"
|
||||||
placeholder={ LocalizeText('messageboard.message.replying.to') }
|
placeholder={ LocalizeText('messageboard.message.replying.to') }
|
||||||
rows={ 2 }
|
rows={ 2 }
|
||||||
|
maxLength={ 4000 }
|
||||||
value={ replyText }
|
value={ replyText }
|
||||||
onChange={ e => setReplyText(e.target.value) }
|
onChange={ e => setReplyText(e.target.value) }
|
||||||
onKeyDown={ e =>
|
onKeyDown={ e =>
|
||||||
@@ -273,7 +284,7 @@ export const GroupForumThreadView: FC<GroupForumThreadViewProps> = props =>
|
|||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
<Button variant="primary" className="btn-sm align-self-end" onClick={ sendReply } disabled={ !replyText.trim().length }>
|
<Button variant="primary" className="btn-sm align-self-end" onClick={ sendReply } disabled={ replyText.trim().length < 10 || isSubmitting }>
|
||||||
{ LocalizeText('messageboard.reply.button') }
|
{ LocalizeText('messageboard.reply.button') }
|
||||||
</Button>
|
</Button>
|
||||||
</Flex> }
|
</Flex> }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AddLinkEventTracker, ForumDataMessageEvent, GetForumStatsMessageComposer, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
import { AddLinkEventTracker, ForumDataMessageEvent, GetForumStatsMessageComposer, GuildForumThread, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||||
import { FC, useCallback, useEffect, useState } from 'react';
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
import { LocalizeText, SendMessageComposer } from '../../../../api';
|
import { LocalizeText, SendMessageComposer } from '../../../../api';
|
||||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
|
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
|
||||||
@@ -22,6 +22,7 @@ export const GroupForumView: FC<{}> = props =>
|
|||||||
const [ currentView, setCurrentView ] = useState<number>(VIEW_FORUM_LIST);
|
const [ currentView, setCurrentView ] = useState<number>(VIEW_FORUM_LIST);
|
||||||
const [ groupId, setGroupId ] = useState<number>(0);
|
const [ groupId, setGroupId ] = useState<number>(0);
|
||||||
const [ threadId, setThreadId ] = useState<number>(0);
|
const [ threadId, setThreadId ] = useState<number>(0);
|
||||||
|
const [ currentThread, setCurrentThread ] = useState<GuildForumThread>(null);
|
||||||
const [ forumData, setForumData ] = useState<ExtendedForumData>(null);
|
const [ forumData, setForumData ] = useState<ExtendedForumData>(null);
|
||||||
|
|
||||||
useMessageEvent<ForumDataMessageEvent>(ForumDataMessageEvent, event =>
|
useMessageEvent<ForumDataMessageEvent>(ForumDataMessageEvent, event =>
|
||||||
@@ -39,10 +40,11 @@ export const GroupForumView: FC<{}> = props =>
|
|||||||
SendMessageComposer(new GetForumStatsMessageComposer(id));
|
SendMessageComposer(new GetForumStatsMessageComposer(id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openThread = useCallback((gId: number, tId: number) =>
|
const openThread = useCallback((gId: number, tId: number, thread: GuildForumThread = null) =>
|
||||||
{
|
{
|
||||||
setGroupId(gId);
|
setGroupId(gId);
|
||||||
setThreadId(tId);
|
setThreadId(tId);
|
||||||
|
setCurrentThread(thread);
|
||||||
setCurrentView(VIEW_THREAD);
|
setCurrentView(VIEW_THREAD);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -148,6 +150,7 @@ export const GroupForumView: FC<{}> = props =>
|
|||||||
<GroupForumThreadView
|
<GroupForumThreadView
|
||||||
groupId={ groupId }
|
groupId={ groupId }
|
||||||
threadId={ threadId }
|
threadId={ threadId }
|
||||||
|
initialThread={ currentThread }
|
||||||
forumData={ forumData }
|
forumData={ forumData }
|
||||||
onBack={ backToThreadList } /> }
|
onBack={ backToThreadList } /> }
|
||||||
{ (currentView === VIEW_NEW_THREAD) &&
|
{ (currentView === VIEW_NEW_THREAD) &&
|
||||||
|
|||||||
Reference in New Issue
Block a user