import { GetMessagesMessageComposer, ModerateMessageMessageComposer, ModerateThreadMessageComposer, PostMessageMessageComposer, PostMessageMessageEvent, PostThreadMessageEvent, ThreadMessagesMessageEvent, UpdateForumReadMarkerMessageComposer, UpdateForumReadMarkerEntry, 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_VISIBLE = 1; const STATE_HIDDEN_BY_ADMIN = 10; const STATE_DELETED_BY_MODERATOR = 20; interface GroupForumThreadViewProps { groupId: number; threadId: number; initialThread?: GuildForumThread; forumData: ExtendedForumData; onBack: () => void; } export const GroupForumThreadView: FC = props => { const { groupId = 0, threadId = 0, initialThread = null, forumData = null, onBack = null } = props; const effectiveGroupId = forumData?.groupId || groupId; const [ messages, setMessages ] = useState([]); const [ totalMessages, setTotalMessages ] = useState(0); const [ replyText, setReplyText ] = useState(''); const [ threadInfo, setThreadInfo ] = useState(initialThread); const [ isSubmitting, setIsSubmitting ] = useState(false); const messagesEndRef = useRef(null); useMessageEvent(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 ]); } // Mark messages as read if(parser.messages.length > 0) { const lastMessage = parser.messages[parser.messages.length - 1]; SendMessageComposer(new UpdateForumReadMarkerMessageComposer( new UpdateForumReadMarkerEntry(effectiveGroupId, lastMessage.messageId, true) )); } }); useMessageEvent(PostMessageMessageEvent, event => { const parser = event.getParser(); if(parser.groupId !== effectiveGroupId || parser.threadId !== threadId) return; setMessages(prev => [ ...prev, parser.message ]); }); useMessageEvent(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, 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, 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 < 10 || isSubmitting) return; setIsSubmitting(true); SendMessageComposer(new PostMessageMessageComposer(effectiveGroupId, threadId, '', replyText.trim())); setReplyText(''); setTimeout(() => setIsSubmitting(false), 1000); }, [ effectiveGroupId, threadId, replyText, isSubmitting ]); const togglePinThread = useCallback(() => { if(!threadInfo) return; // UpdateThreadMessageComposer swaps 3rd/4th params internally: (groupId, threadId, isLocked, isPinned) SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, threadInfo.isLocked, !threadInfo.isPinned)); }, [ effectiveGroupId, threadId, threadInfo ]); const toggleLockThread = useCallback(() => { if(!threadInfo) return; // UpdateThreadMessageComposer swaps 3rd/4th params internally: (groupId, threadId, isLocked, isPinned) SendMessageComposer(new UpdateThreadMessageComposer(effectiveGroupId, threadId, !threadInfo.isLocked, threadInfo.isPinned)); }, [ 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_VISIBLE)); }, [ effectiveGroupId, threadId ]); const hideThread = useCallback(() => { SendMessageComposer(new ModerateThreadMessageComposer(effectiveGroupId, threadId, STATE_HIDDEN_BY_ADMIN)); onBack(); }, [ effectiveGroupId, threadId, onBack ]); const deleteThread = useCallback(() => { SendMessageComposer(new ModerateThreadMessageComposer(effectiveGroupId, threadId, STATE_DELETED_BY_MODERATOR)); 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 ( « { LocalizeText('groupforum.view.back') } { canModerate && } { messages.map((message, index) => { const stateText = getMessageStateText(message); if(stateText && !canModerate) { return ( { stateText } ); } return (
GetUserProfile(message.authorId) }> { message.authorName } { message.authorPostCount } { LocalizeText('messageboard.messages') }
{ formatTimeAgo(message.creationTime) } { canModerate && (message.state !== STATE_NORMAL) && { stateText } restoreMessage(message.messageId) }> { LocalizeText('groupforum.message.restore') } } { canModerate && (message.state === STATE_NORMAL) && hideMessage(message.messageId) }> { LocalizeText('groupforum.message.hide') } } { (message.state === STATE_NORMAL || canModerate) && { message.messageText } }
); }) } { (messages.length < totalMessages) && { SendMessageComposer(new GetMessagesMessageComposer(effectiveGroupId, threadId, messages.length, MESSAGES_PER_PAGE)); } }> { LocalizeText('groupforum.thread.load_more') } }
{ canPost && !isLocked &&