From 4eaea36d4f1586d7b43175d9e5f69f65f4a3e8fc Mon Sep 17 00:00:00 2001 From: Mehdi Torabi <46302001+mehdi-torabiv@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:27:49 +0300 Subject: [PATCH] Feat/discourse platform integration (#340) * integrate filters on category for heatmap chart * integrate discourse platform * fix * add violation detection * fix * fix --- .../TcCommunityPlatforms.tsx | 134 +++++-- .../communityPlatforms/TcDiscourse.tsx | 345 ++++++++++++++++++ .../communityPlatforms/TcMediaWiki.tsx | 2 +- src/components/layouts/Sidebar.tsx | 20 +- .../violation-detection/index.tsx | 277 ++++++++++++++ src/utils/enums.ts | 2 +- 6 files changed, 743 insertions(+), 37 deletions(-) create mode 100644 src/components/communitySettings/communityPlatforms/TcDiscourse.tsx create mode 100644 src/pages/community-settings/violation-detection/index.tsx diff --git a/src/components/communitySettings/communityPlatforms/TcCommunityPlatforms.tsx b/src/components/communitySettings/communityPlatforms/TcCommunityPlatforms.tsx index e095b519..387b4aea 100644 --- a/src/components/communitySettings/communityPlatforms/TcCommunityPlatforms.tsx +++ b/src/components/communitySettings/communityPlatforms/TcCommunityPlatforms.tsx @@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react'; import TcCommunityPlatformIcon from './TcCommunityPlatformIcon'; import TcDiscordIntgration from './TcDiscordIntgration'; +import TcDiscourse from './TcDiscourse'; import TcGdriveIntegration from './TcGdriveIntegration'; import TcGithubIntegration from './TcGithubIntegration'; import TcMediaWiki from './TcMediaWiki'; @@ -54,6 +55,10 @@ function TcCommunityPlatforms() { const [activeTab, setActiveTab] = useState(0); const [hivemindManageIsLoading, setHivemindManageIsLoading] = useState(false); + const [ + violationDetectionManageIsLoading, + setViolationDetectionManageIsLoading, + ] = useState(false); const router = useRouter(); const communityId = @@ -66,11 +71,12 @@ function TcCommunityPlatforms() { 'github', 'notion', 'mediaWiki', + 'discourse', ]; + const platformName = platformNames[activeTab]; if (!platformName) { - console.log('Unexpected tab index'); return; } @@ -115,6 +121,31 @@ function TcCommunityPlatforms() { } }; + const handleViolationDetectionModule = async () => { + try { + setViolationDetectionManageIsLoading(true); + const hivemindModules = await retrieveModules({ + community: communityId, + name: 'violationDetection', + }); + + if (hivemindModules.results.length > 0) { + router.push('/community-settings/violation-detection'); + } else { + await createModule({ + name: 'violationDetection', + community: communityId, + }); + router.push('/community-settings/violation-detection'); + } + setViolationDetectionManageIsLoading(false); + } catch (error) { + console.log('error', error); + } finally { + setViolationDetectionManageIsLoading(false); + } + }; + const handleUpdateCommunityPlatform = async () => { await fetchPlatformsByType(); }; @@ -124,10 +155,7 @@ function TcCommunityPlatforms() {
- +
- { - platform === "GDrive" && ( - - ) - } + {platform === 'GDrive' && ( + + )} } disabled={ ![ 'Discord', + 'Discourse', 'Github', 'Notion', 'MediaWiki', @@ -221,6 +253,15 @@ function TcCommunityPlatforms() { /> )} + {activeTab === 5 && ( + + + + )}
@@ -232,25 +273,50 @@ function TcCommunityPlatforms() { />
- - - - ) : ( - 'Manage' - ) - } - variant='text' - onClick={() => handleManageHivemindModule()} - /> - - } - /> +
+ + + + ) : ( + 'Manage' + ) + } + variant='text' + onClick={() => handleManageHivemindModule()} + /> +
+ } + /> + + + + ) : ( + 'Manage' + ) + } + variant='text' + onClick={() => handleViolationDetectionModule()} + /> + + } + /> + ); diff --git a/src/components/communitySettings/communityPlatforms/TcDiscourse.tsx b/src/components/communitySettings/communityPlatforms/TcDiscourse.tsx new file mode 100644 index 00000000..9f258b55 --- /dev/null +++ b/src/components/communitySettings/communityPlatforms/TcDiscourse.tsx @@ -0,0 +1,345 @@ +import { + Alert, + AlertTitle, + CircularProgress, + FormControl, + Paper, + TextField, + Typography, +} from '@mui/material'; +import moment from 'moment'; +import React, { useState } from 'react'; +import { AiOutlineClose } from 'react-icons/ai'; +import { BiPlus } from 'react-icons/bi'; +import { IoClose, IoSettingsSharp } from 'react-icons/io5'; +import { MdDelete } from 'react-icons/md'; + +import TcAvatar from '@/components/shared/TcAvatar'; +import TcButton from '@/components/shared/TcButton'; +import TcDialog from '@/components/shared/TcDialog'; +import TcText from '@/components/shared/TcText'; + +import useAppStore from '@/store/useStore'; + +import { useSnackbar } from '@/context/SnackbarContext'; +import { useToken } from '@/context/TokenContext'; +import { truncateCenter } from '@/helpers/helper'; +import { IPlatformProps } from '@/utils/interfaces'; + +import TcCommunityPlatformIcon from './TcCommunityPlatformIcon'; + +interface TcDiscourseProps { + isLoading: boolean; + connectedPlatforms: IPlatformProps[]; + handleUpdateCommunityPlatform: () => void; +} + +function TcDiscourse({ + isLoading, + connectedPlatforms, + handleUpdateCommunityPlatform, +}: TcDiscourseProps) { + const { createNewPlatform, deletePlatform } = useAppStore(); + const [activePlatform, setActivePlatform] = useState( + null + ); + const [url, setUrl] = useState(''); + const [urlError, setUrlError] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const { showMessage } = useSnackbar(); + + const { community } = useToken(); + + const handleCreateNewPlatform = async () => { + const data = await createNewPlatform({ + community: community?.id, + name: 'discourse', + metadata: { + id: url.replaceAll('https://', '').replaceAll('http://', ''), + period: new Date( + new Date().setDate(new Date().getDate() - 90) + ).toISOString(), + analyzerStartedAt: new Date().toISOString(), + resources: [], + }, + }); + if (data) { + handleUpdateCommunityPlatform(); + setIsOpen(false); + setUrl(''); + showMessage('Platform connected successfully.', 'success'); + } + }; + + const handleUrlChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setUrl(value); + validateUrl(value); + }; + + const validateUrl = (value: string) => { + const regex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*\.[^\s]{2,}([^\s]*)?$/i; + if (!regex.test(value)) { + setUrlError('Invalid URL. Please enter a valid site URL.'); + } else { + setUrlError(''); + } + }; + + const handleOpenDialog = (platform: IPlatformProps | null = null) => { + setActivePlatform(platform); + setUrl(platform?.metadata?.id || ''); + setIsOpen(true); + }; + + const handleDisconnectPlatform = async (deleteType: 'hard' | 'soft') => { + try { + const data = await deletePlatform({ id: activePlatform?.id, deleteType }); + if (data === '') { + setIsDeleteDialogOpen(false); + setActivePlatform(null); + setUrl(''); + showMessage('Platform disconnected successfully.', 'success'); + handleUpdateCommunityPlatform(); + } + } catch (error) { + showMessage('Error disconnecting platform.', 'error'); + } + }; + + return ( +
+ + + + +
+ } + onClick={() => handleOpenDialog()} + /> +
+
+ {isLoading ? ( + + ) : ( + connectedPlatforms && + connectedPlatforms[0]?.name === 'discourse' && + connectedPlatforms.map((platform, index) => ( + + + } + onClick={() => handleOpenDialog(platform)} + /> + + )) + )} + +
+
+ setIsOpen(false)} + /> +
+
+
+ +
+ +
+
+ {activePlatform ? ( + <> +
+
+ + +
+
+ + +
+
+
+ } + variant='outlined' + onClick={() => { + setIsDeleteDialogOpen(true); + setIsOpen(false); + }} + /> +
+ + ) : ( + <> + + Analyzing Your Community Data + + We're currently analyzing 90 days of your community's data. + This process may take up to 6 hours. Once the analysis is + complete, you will receive a message on Discord. + + + + + + + )} +
+ {!activePlatform && ( +
+ setIsOpen(false)} + /> + +
+ )} +
+
+ +
+ setIsDeleteDialogOpen(false)} + /> +
+
+
+ +
+
+
+ + + Importing new data will be stopped. Already imported and + analyzed data will be deleted. + + } + variant='body2' + /> + handleDisconnectPlatform('hard')} + /> +
+
+ + + Importing new data will be stopped. Already imported and + analyzed data will be kept. + + } + variant='body2' + /> + handleDisconnectPlatform('soft')} + /> +
+
+
+
+
+ ); +} + +export default TcDiscourse; diff --git a/src/components/communitySettings/communityPlatforms/TcMediaWiki.tsx b/src/components/communitySettings/communityPlatforms/TcMediaWiki.tsx index e4a45cc6..8de8fdf2 100644 --- a/src/components/communitySettings/communityPlatforms/TcMediaWiki.tsx +++ b/src/components/communitySettings/communityPlatforms/TcMediaWiki.tsx @@ -176,7 +176,7 @@ function TcMediaWiki({ fontWeight='bold' /> diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index 4bb290e8..e868f5b8 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -4,6 +4,7 @@ type items = { name: string; path: string; icon: any; + isVisible?: boolean; }; import { faHeartPulse, faUserGroup } from '@fortawesome/free-solid-svg-icons'; @@ -24,7 +25,8 @@ import useAppStore from '../../store/useStore'; const Sidebar = () => { const router = useRouter(); const currentRoute = router.pathname; - const { community } = useToken(); + const { community, selectedPlatform } = useToken(); + const [isDiscourse, setIsDiscourse] = useState(false); const userPermissions = useAppStore( (state) => state.userRolePermissions || [] @@ -46,6 +48,18 @@ const Sidebar = () => { } }, [community]); + useEffect(() => { + const discoursePlatformId = community?.platforms?.find((platform) => { + return platform.name === 'discourse' && platform.disconnectedAt === null; + })?.id; + + if (discoursePlatformId && selectedPlatform) { + setIsDiscourse(selectedPlatform === discoursePlatformId); + } else { + setIsDiscourse(false); + } + }, [community, selectedPlatform]); + let menuItems: items[] = [ { name: 'Community Insights', @@ -95,6 +109,10 @@ const Sidebar = () => { ); } + if (isDiscourse) { + menuItems = menuItems.filter((item) => item.name !== 'Smart Announcements'); + } + const menuItem = menuItems.map((el) => (
  • diff --git a/src/pages/community-settings/violation-detection/index.tsx b/src/pages/community-settings/violation-detection/index.tsx new file mode 100644 index 00000000..09070bdb --- /dev/null +++ b/src/pages/community-settings/violation-detection/index.tsx @@ -0,0 +1,277 @@ +/* eslint-disable react/jsx-key */ +import { + Alert, + AlertTitle, + Autocomplete, + Chip, + CircularProgress, + FormControl, + FormControlLabel, + FormHelperText, + FormLabel, + Switch, + TextField, + Typography, +} from '@mui/material'; +import router from 'next/router'; +import React, { useEffect, useState } from 'react'; + +import TcButton from '@/components/shared/TcButton'; + +import useAppStore from '@/store/useStore'; + +import { useSnackbar } from '@/context/SnackbarContext'; +import { useToken } from '@/context/TokenContext'; +import { StorageService } from '@/services/StorageService'; +import { IDiscordModifiedCommunity, IPlatformProps } from '@/utils/interfaces'; + +import SEO from '../../../components/global/SEO'; +import TcBoxContainer from '../../../components/shared/TcBox/TcBoxContainer'; +import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; +import { defaultLayout } from '../../../layouts/defaultLayout'; +import { withRoles } from '../../../utils/withRoles'; + +function Index() { + const { retrieveModules, patchModule } = useAppStore(); + const [isLoading, setIsLoading] = useState(false); + const [emails, setEmails] = useState([]); + const [activePlatform, setActivePlatform] = useState( + null + ); + const [violationModules, setViolationModules] = useState([]); + const [isViolationDetectionOn, setIsViolationDetectionOn] = + useState(false); + const [emailError, setEmailError] = useState(null); + + const { community } = useToken(); + const { showMessage } = useSnackbar(); + + const fetchDiscourseViolation = async () => { + const communityId = + StorageService.readLocalStorage( + 'community' + )?.id; + + const discoursePlatform = community?.platforms.find( + (platform) => + platform.name === 'discourse' && platform.disconnectedAt === null + ) as unknown as IPlatformProps; + + setActivePlatform(discoursePlatform); + + if (communityId) { + const { results } = await retrieveModules({ + community: communityId, + name: 'violationDetection', + }); + + setViolationModules(results); + + if (results.length > 0) { + if (results[0]?.options?.platforms.length === 0) return; + setEmails( + results[0]?.options?.platforms[0]?.metadata?.selectedEmails || [] + ); + + const hasActiveModerators = + results[0].options.platforms[0].metadata.selectedEmails.length > 0; + setIsViolationDetectionOn(hasActiveModerators ? true : false); + } else { + setEmails([]); + setIsViolationDetectionOn(false); + } + } + }; + + useEffect(() => { + fetchDiscourseViolation(); + }, []); + + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleEmailChange = (event: any, newValue: string[] | null) => { + if (newValue) { + const invalidEmail = newValue.find((email) => !validateEmail(email)); + if (invalidEmail) { + setEmailError(`Invalid email: ${invalidEmail}`); + } else { + setEmailError(null); + setEmails(newValue); + } + } + }; + + const handleDiscourseViolation = async () => { + if (!activePlatform) return; + + const updatedEmails = isViolationDetectionOn ? emails : []; + + const payload = { + platforms: [ + { + platform: activePlatform.id, + name: activePlatform.name, + metadata: { + selectedEmails: updatedEmails, + fromDate: activePlatform.metadata.period, + toDate: null, + selectedResources: [], + }, + }, + ], + }; + + try { + setIsLoading(true); + const data = await patchModule({ + moduleId: violationModules[0].id, + payload, + }); + + if (data) { + router.push('/community-settings'); + showMessage( + 'Violation detection settings updated successfully', + 'success' + ); + } + } catch (error) { + console.error('Error updating violation module:', error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + +
    + + +
    + + Violation Detection Settings + + + Configure the settings for the violation detection system. + This system will automatically detect and take action on any + violations of the community guidelines. You can configure the + system to automatically take action on violations or to notify + moderators for manual review. + + + Module working for discourse only + This module is currently only available for Discourse. + + + + + setIsViolationDetectionOn(event.target.checked) + } + /> + } + label='Violation Detection' + /> + + Activate/Deactivate the violation detection module. + + + + {isViolationDetectionOn && ( + + Moderator Emails + ( + + )} + options={[]} + renderTags={(value, getTagProps) => { + return value.map((option, index) => ( + + )); + }} + /> + + Enter the email addresses of the moderators who should be + notified when a violation is detected. + + + )} + +
    + router.push('/community-settings')} + /> + + ) : ( + 'Save Changes' + ) + } + disabled={isLoading || !!emailError} + variant='contained' + className='md:w-1/4' + onClick={handleDiscourseViolation} + /> +
    +
    +
    + } + /> + + + ); +} + +Index.pageLayout = defaultLayout; + +export default withRoles(Index, ['admin']); diff --git a/src/utils/enums.ts b/src/utils/enums.ts index e011721b..f5afed0e 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -4,8 +4,8 @@ export enum IntegrationPlatform { Github = 'Github', Notion = 'Notion', MediaWiki = 'MediaWiki', - Twitter = 'Twitter', Discourse = 'Discourse', + Twitter = 'Twitter', Telegram = 'Telegram', Snapshot = 'Snapshot', }