diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 93aad89bdb..2d871c4830 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -927,6 +927,7 @@
"remove_or_replace_figure": "",
"remove_secondary_email_addresses": "",
"remove_tag": "",
+ "removed_from_project": "",
"removing": "",
"rename": "",
"rename_project": "",
@@ -1372,6 +1373,7 @@
"you_have_added_x_of_group_size_y": "",
"you_have_been_invited_to_transfer_management_of_your_account": "",
"you_have_been_invited_to_transfer_management_of_your_account_to": "",
+ "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "",
"you_may_be_able_to_prevent_a_compile_timeout": "",
"you_need_to_configure_your_sso_settings": "",
"you_will_be_able_to_reassign_subscription": "",
diff --git a/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
new file mode 100644
index 0000000000..684a804b10
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
@@ -0,0 +1,38 @@
+import { useState, useCallback } from 'react'
+import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
+import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import * as eventTracking from '@/infrastructure/event-tracking'
+import EditorNavigationToolbarRoot from '@/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root'
+import ShareProjectModal from '@/features/share-project-modal/components/share-project-modal'
+
+function EditorNavigationToolbar() {
+ const [showShareModal, setShowShareModal] = useState(false)
+ const { onlineUsersArray } = useOnlineUsersContext()
+ const { openDoc } = useEditorManagerContext()
+
+ const handleOpenShareModal = () => {
+ eventTracking.sendMBOnce('ide-open-share-modal-once')
+ setShowShareModal(true)
+ }
+
+ const handleHideShareModal = useCallback(() => {
+ setShowShareModal(false)
+ }, [])
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default EditorNavigationToolbar
diff --git a/services/web/frontend/js/features/ide-react/components/header.tsx b/services/web/frontend/js/features/ide-react/components/header.tsx
deleted file mode 100644
index 11fd158705..0000000000
--- a/services/web/frontend/js/features/ide-react/components/header.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { useCallback } from 'react'
-import ChatToggleButton from '@/features/editor-navigation-toolbar/components/chat-toggle-button'
-import HistoryToggleButton from '@/features/editor-navigation-toolbar/components/history-toggle-button'
-import LayoutDropdownButton from '@/features/editor-navigation-toolbar/components/layout-dropdown-button'
-import MenuButton from '@/features/editor-navigation-toolbar/components/menu-button'
-import { useLayoutContext } from '@/shared/context/layout-context'
-import { sendMB } from '@/infrastructure/event-tracking'
-import OnlineUsersWidget from '@/features/editor-navigation-toolbar/components/online-users-widget'
-import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
-
-type HeaderProps = {
- chatIsOpen: boolean
- setChatIsOpen: (chatIsOpen: boolean) => void
- historyIsOpen: boolean
- setHistoryIsOpen: (historyIsOpen: boolean) => void
-}
-
-export default function Header({
- chatIsOpen,
- setChatIsOpen,
- historyIsOpen,
- setHistoryIsOpen,
-}: HeaderProps) {
- const { setLeftMenuShown } = useLayoutContext()
- const { onlineUsersArray } = useOnlineUsersContext()
-
- function toggleChatOpen() {
- setChatIsOpen(!chatIsOpen)
- }
-
- function toggleHistoryOpen() {
- setHistoryIsOpen(!historyIsOpen)
- }
-
- const handleShowLeftMenuClick = useCallback(() => {
- sendMB('navigation-clicked-menu')
- setLeftMenuShown(value => !value)
- }, [setLeftMenuShown])
-
- return (
-
- )
-}
diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
index 3f2643b863..f1c9f10595 100644
--- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
+++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
@@ -7,14 +7,16 @@ import PlaceholderChat from '@/features/ide-react/components/layout/placeholder/
import PlaceholderHistory from '@/features/ide-react/components/layout/placeholder/placeholder-history'
import MainLayout from '@/features/ide-react/components/layout/main-layout'
import { EditorAndSidebar } from '@/features/ide-react/components/editor-and-sidebar'
-import Header from '@/features/ide-react/components/header'
import EditorLeftMenu from '@/features/editor-left-menu/components/editor-left-menu'
+import EditorNavigationToolbar from '@/features/ide-react/components/editor-navigation-toolbar'
import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
+import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
// This is filled with placeholder content while the real content is migrated
// away from Angular
export default function IdePage() {
useLayoutEventTracking()
+ useSocketListeners()
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
const { registerUserActivity } = useConnectionContext()
@@ -32,24 +34,8 @@ export default function IdePage() {
return () => document.body.removeEventListener('click', listener)
}, [listener])
- const { chatIsOpen, setChatIsOpen, view, setView } = useLayoutContext()
+ const { chatIsOpen, view } = useLayoutContext()
const historyIsOpen = view === 'history'
- const setHistoryIsOpen = useCallback(
- (historyIsOpen: boolean) => {
- setView(historyIsOpen ? 'history' : 'editor')
- },
- [setView]
- )
-
- const headerContent = (
-
- )
- const chatContent =
const mainContent = historyIsOpen ? (
}
+ chatContent={}
mainContent={mainContent}
chatIsOpen={chatIsOpen}
shouldPersistLayout
diff --git a/services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx b/services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx
index 2e1190bf95..c3f8d67013 100644
--- a/services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx
+++ b/services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx
@@ -1,5 +1,6 @@
import React from 'react'
import ChatToggleButton from '@/features/editor-navigation-toolbar/components/chat-toggle-button'
+import ShareProjectButton from '@/features/editor-navigation-toolbar/components/share-project-button'
import HistoryToggleButton from '@/features/editor-navigation-toolbar/components/history-toggle-button'
import LayoutDropdownButton from '@/features/editor-navigation-toolbar/components/layout-dropdown-button'
@@ -16,6 +17,8 @@ export default function PlaceholderHeader({
historyIsOpen,
setHistoryIsOpen,
}: PlaceholderHeaderProps) {
+ function handleOpenShareModal() {}
+
function toggleChatOpen() {
setChatIsOpen(!chatIsOpen)
}
@@ -28,11 +31,12 @@ export default function PlaceholderHeader({
Header placeholder
-
+
+
&
+ GenericMessageModalOwnProps
+
+function GenericMessageModal({
+ title,
+ message,
+ ...modalProps
+}: GenericMessageModalProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {title}
+
+
+ {message}
+
+
+
+
+
+ )
+}
+
+export default GenericMessageModal
diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
index d8c263b95b..b80a696efe 100644
--- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
+++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
@@ -18,7 +18,6 @@ import { JoinProjectPayload } from '@/features/ide-react/connection/join-project
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { getMockIde } from '@/shared/context/mock/mock-ide'
import { populateEditorScope } from '@/features/ide-react/context/editor-manager-context'
-import { debugConsole } from '@/utils/debugging'
import { postJSON } from '@/infrastructure/fetch-json'
import { EventLog } from '@/features/ide-react/editor/event-log'
import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter'
@@ -41,10 +40,6 @@ const IdeReactContext = createContext(
undefined
)
-function showGenericMessageModal(title: string, message: string) {
- debugConsole.log('*** showGenericMessageModal ***', title, message)
-}
-
function populateIdeReactScope(store: ReactScopeValueStore) {
store.set('sync_tex_error', false)
store.set('settings', window.userSettings)
@@ -156,7 +151,6 @@ export const IdeReactProvider: FC = ({ children }) => {
return {
...getMockIde(),
socket,
- showGenericMessageModal,
reportError,
// TODO: MIGRATION: Remove this once it's no longer used
fileTreeManager: {
diff --git a/services/web/frontend/js/features/ide-react/context/modals-context.tsx b/services/web/frontend/js/features/ide-react/context/modals-context.tsx
new file mode 100644
index 0000000000..71abdb82c7
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/context/modals-context.tsx
@@ -0,0 +1,71 @@
+import {
+ createContext,
+ useContext,
+ FC,
+ useCallback,
+ useMemo,
+ useState,
+} from 'react'
+import GenericMessageModal, {
+ GenericMessageModalOwnProps,
+} from '@/features/ide-react/components/modals/generic-message-modal'
+
+type ModalsContextValue = {
+ showGenericMessageModal: (
+ title: GenericMessageModalOwnProps['title'],
+ message: GenericMessageModalOwnProps['message']
+ ) => void
+}
+
+const ModalsContext = createContext(undefined)
+
+export const ModalsContextProvider: FC = ({ children }) => {
+ const [showGenericModal, setShowGenericModal] = useState(false)
+ const [genericMessageModalData, setGenericMessageModalData] =
+ useState({ title: '', message: '' })
+
+ const handleHideGenericModal = useCallback(() => {
+ setShowGenericModal(false)
+ }, [])
+
+ const showGenericMessageModal = useCallback(
+ (
+ title: GenericMessageModalOwnProps['title'],
+ message: GenericMessageModalOwnProps['message']
+ ) => {
+ setGenericMessageModalData({ title, message })
+ setShowGenericModal(true)
+ },
+ []
+ )
+
+ const value = useMemo(
+ () => ({
+ showGenericMessageModal,
+ }),
+ [showGenericMessageModal]
+ )
+
+ return (
+
+ {children}
+
+
+ )
+}
+
+export function useModalsContext(): ModalsContextValue {
+ const context = useContext(ModalsContext)
+
+ if (!context) {
+ throw new Error(
+ 'useModalsContext is only available inside ModalsContextProvider'
+ )
+ }
+
+ return context
+}
diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
index 4e21cffd8a..106667a5b1 100644
--- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
+++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
@@ -16,6 +16,7 @@ import { OnlineUsersProvider } from '@/features/ide-react/context/online-users-c
import { MetadataProvider } from '@/features/ide-react/context/metadata-context'
import { ReferencesProvider } from '@/features/ide-react/context/references-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
+import { ModalsContextProvider } from '@/features/ide-react/context/modals-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
export const ReactContextRoot: FC = ({ children }) => {
@@ -38,7 +39,9 @@ export const ReactContextRoot: FC = ({ children }) => {
- {children}
+
+ {children}
+
diff --git a/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts b/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
new file mode 100644
index 0000000000..4b593f769e
--- /dev/null
+++ b/services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
@@ -0,0 +1,64 @@
+import { useTranslation } from 'react-i18next'
+import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
+import {
+ listProjectInvites,
+ listProjectMembers,
+} from '@/features/share-project-modal/utils/api'
+import useScopeValue from '@/shared/hooks/use-scope-value'
+import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
+import { useModalsContext } from '@/features/ide-react/context/modals-context'
+import { debugConsole } from '@/utils/debugging'
+
+function useSocketListeners() {
+ const { t } = useTranslation()
+ const { socket } = useConnectionContext()
+ const { projectId } = useIdeReactContext()
+ const { showGenericMessageModal } = useModalsContext()
+ const [, setPublicAccessLevel] = useScopeValue('project.publicAccesLevel')
+ const [, setProjectMembers] = useScopeValue('project.members')
+ const [, setProjectInvites] = useScopeValue('project.invites')
+
+ useSocketListener(socket, 'project:access:revoked', () => {
+ showGenericMessageModal(
+ t('removed_from_project'),
+ t(
+ 'you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard'
+ )
+ )
+ })
+
+ useSocketListener(socket, 'project:publicAccessLevel:changed', data => {
+ if (data.newAccessLevel) {
+ setPublicAccessLevel(data.newAccessLevel)
+ }
+ })
+
+ useSocketListener(socket, 'project:membership:changed', data => {
+ if (data.members) {
+ listProjectMembers(projectId)
+ .then(({ members }) => {
+ if (members) {
+ setProjectMembers(members)
+ }
+ })
+ .catch(err => {
+ debugConsole.error('Error fetching members for project', err)
+ })
+ }
+
+ if (data.invites) {
+ listProjectInvites(projectId)
+ .then(({ invites }) => {
+ if (invites) {
+ setProjectInvites(invites)
+ }
+ })
+ .catch(err => {
+ debugConsole.error('Error fetching invites for project', err)
+ })
+ }
+ })
+}
+
+export default useSocketListeners
diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx
similarity index 76%
rename from services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx
rename to services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx
index 118ddbd884..77d173b317 100644
--- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx
@@ -2,7 +2,6 @@ import { Button, Modal, Grid } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import AccessibleModal from '../../../shared/components/accessible-modal'
-import PropTypes from 'prop-types'
import { useEditorContext } from '../../../shared/context/editor-context'
import { lazy, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
@@ -16,18 +15,24 @@ const ReadOnlyTokenLink = lazy(() =>
const ShareModalBody = lazy(() => import('./share-modal-body'))
+type ShareProjectModalContentProps = {
+ cancel: () => void
+ show: boolean
+ animation: boolean
+ inFlight: boolean
+ error: string | undefined
+}
+
export default function ShareProjectModalContent({
show,
cancel,
animation,
inFlight,
error,
-}) {
+}: ShareProjectModalContentProps) {
const { t } = useTranslation()
- const { isRestrictedTokenMember } = useEditorContext({
- isRestrictedTokenMember: PropTypes.bool,
- })
+ const { isRestrictedTokenMember } = useEditorContext()
return (
@@ -72,37 +77,26 @@ export default function ShareProjectModalContent({
)
}
-ShareProjectModalContent.propTypes = {
- cancel: PropTypes.func.isRequired,
- show: PropTypes.bool,
- animation: PropTypes.bool,
- inFlight: PropTypes.bool,
- error: PropTypes.string,
-}
-function ErrorMessage({ error }) {
+function ErrorMessage({ error }: Pick) {
const { t } = useTranslation()
-
switch (error) {
case 'cannot_invite_non_user':
- return t('cannot_invite_non_user')
+ return <>{t('cannot_invite_non_user')}>
case 'cannot_verify_user_not_robot':
- return t('cannot_verify_user_not_robot')
+ return <>{t('cannot_verify_user_not_robot')}>
case 'cannot_invite_self':
- return t('cannot_invite_self')
+ return <>{t('cannot_invite_self')}>
case 'invalid_email':
- return t('invalid_email')
+ return <>{t('invalid_email')}>
case 'too_many_requests':
- return t('too_many_requests')
+ return <>{t('too_many_requests')}>
default:
- return t('generic_something_went_wrong')
+ return <>{t('generic_something_went_wrong')}>
}
}
-ErrorMessage.propTypes = {
- error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.jsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
similarity index 75%
rename from services/web/frontend/js/features/share-project-modal/components/share-project-modal.jsx
rename to services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
index c11ed8785d..96e4d0887a 100644
--- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
@@ -14,19 +14,23 @@ import {
import { useSplitTestContext } from '../../../shared/context/split-test-context'
import { sendMB } from '../../../infrastructure/event-tracking'
-const ShareProjectContext = createContext()
-
-ShareProjectContext.Provider.propTypes = {
- value: PropTypes.shape({
- updateProject: PropTypes.func.isRequired,
- monitorRequest: PropTypes.func.isRequired,
- inFlight: PropTypes.bool,
- setInFlight: PropTypes.func,
- error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
- setError: PropTypes.func,
- }),
+type ShareProjectContextValue = {
+ updateProject: (data: unknown) => void
+ monitorRequest: >(request: () => T) => T
+ inFlight: boolean
+ setInFlight: React.Dispatch<
+ React.SetStateAction
+ >
+ error: string | undefined
+ setError: React.Dispatch<
+ React.SetStateAction
+ >
}
+const ShareProjectContext = createContext(
+ undefined
+)
+
export function useShareProjectContext() {
const context = useContext(ShareProjectContext)
@@ -39,13 +43,20 @@ export function useShareProjectContext() {
return context
}
+type ShareProjectModalProps = {
+ handleHide: () => void
+ show: boolean
+ animation?: boolean
+}
+
const ShareProjectModal = React.memo(function ShareProjectModal({
handleHide,
show,
animation = true,
-}) {
- const [inFlight, setInFlight] = useState(false)
- const [error, setError] = useState()
+}: ShareProjectModalProps) {
+ const [inFlight, setInFlight] =
+ useState(false)
+ const [error, setError] = useState()
const project = useProjectContext(projectShape)
@@ -84,7 +95,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
const promise = request()
- promise.catch(error => {
+ promise.catch((error: { data?: Record }) => {
setError(
error.data?.errorReason ||
error.data?.error ||
@@ -130,10 +141,5 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
)
})
-ShareProjectModal.propTypes = {
- animation: PropTypes.bool,
- handleHide: PropTypes.func.isRequired,
- show: PropTypes.bool.isRequired,
-}
export default ShareProjectModal
diff --git a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
index c546c6169d..cda460db1b 100644
--- a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
+++ b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
@@ -8,10 +8,11 @@ import { debugConsole } from '@/utils/debugging'
App.component(
'shareProjectModal',
- react2angular(
- rootContext.use(ShareProjectModal),
- Object.keys(ShareProjectModal.propTypes)
- )
+ react2angular(rootContext.use(ShareProjectModal), [
+ 'animation',
+ 'handleHide',
+ 'show',
+ ])
)
export default App.controller('ReactShareProjectModalController', [
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 6d0320c943..7f5ab1eb2c 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1445,6 +1445,7 @@
"remove_secondary_email_addresses": "Remove any secondary email addresses associated with your account. <0>Remove them in account settings.0>",
"remove_tag": "Remove tag __tagName__",
"removed": "removed",
+ "removed_from_project": "Removed from project",
"removing": "Removing",
"rename": "Rename",
"rename_project": "Rename Project",
@@ -2052,6 +2053,7 @@
"you_have_added_x_of_group_size_y": "You have added <0>__addedUsersSize__0> of <1>__groupSize__1> available members",
"you_have_been_invited_to_transfer_management_of_your_account": "You have been invited to transfer management of your account.",
"you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
+ "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.",
"you_introed_high_number": " You’ve introduced <0>__numberOfPeople__0> people to __appName__. Good job!",
"you_introed_small_number": " You’ve introduced <0>__numberOfPeople__0> person to __appName__. Good job, but can you get some more?",
"you_may_be_able_to_prevent_a_compile_timeout": "You may be able to prevent a compile timeout using the following tips.",