mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #18936 from overleaf/rh-viewer-no-comment
[web] Remove access to commenting/chat for collaborators with Viewer permission GitOrigin-RevId: 03957cb8c04866318a4b94bdb72843e7d7a5a003
This commit is contained in:
parent
ce9b531892
commit
bf90932f40
9 changed files with 66 additions and 18 deletions
|
@ -11,13 +11,14 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
|
|||
import { FetchError } from '../../../infrastructure/fetch-json'
|
||||
import { useChatContext } from '../context/chat-context'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
|
||||
const MessageList = lazy(() => import('./message-list'))
|
||||
|
||||
const ChatPane = React.memo(function ChatPane() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
const { setChatIsOpen, chatIsOpen } = useLayoutContext()
|
||||
const user = useUserContext()
|
||||
|
||||
const {
|
||||
|
@ -46,13 +47,17 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
0
|
||||
)
|
||||
|
||||
const shouldShowChat = !useViewerPermissions()
|
||||
|
||||
// Keep the chat pane in the DOM to avoid resetting the form input and re-rendering MathJax content.
|
||||
const [chatOpenedOnce, setChatOpenedOnce] = useState(chatIsOpen)
|
||||
useEffect(() => {
|
||||
if (chatIsOpen) {
|
||||
if (chatIsOpen && shouldShowChat) {
|
||||
setChatOpenedOnce(true)
|
||||
} else if (chatIsOpen && !shouldShowChat) {
|
||||
setChatIsOpen(false)
|
||||
}
|
||||
}, [chatIsOpen])
|
||||
}, [chatIsOpen, setChatIsOpen, shouldShowChat])
|
||||
|
||||
if (error) {
|
||||
// let user try recover from fetch errors
|
||||
|
|
|
@ -17,6 +17,7 @@ import { appendMessage, prependMessages } from '../utils/message-list-appender'
|
|||
import useBrowserWindow from '../../../shared/hooks/use-browser-window'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
|
@ -286,9 +287,10 @@ export const ChatProvider: FC = ({ children }) => {
|
|||
}, [])
|
||||
|
||||
// Handling receiving messages over the socket
|
||||
const hasViewerPermissions = useViewerPermissions()
|
||||
const { socket } = useIdeContext()
|
||||
useEffect(() => {
|
||||
if (!socket) return
|
||||
if (!socket || hasViewerPermissions) return
|
||||
|
||||
function receivedMessage(message: any) {
|
||||
// If the message is from the current client id, then we are receiving the sent message back from the socket.
|
||||
|
@ -304,7 +306,7 @@ export const ChatProvider: FC = ({ children }) => {
|
|||
|
||||
socket.removeListener('new-chat-message', receivedMessage)
|
||||
}
|
||||
}, [socket])
|
||||
}, [socket, hasViewerPermissions])
|
||||
|
||||
// Handle unread messages
|
||||
useEffect(() => {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useLayoutContext } from '../../../shared/context/layout-context'
|
|||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
|
||||
function isOpentoString(open: boolean) {
|
||||
return open ? 'open' : 'close'
|
||||
|
@ -44,6 +45,7 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
} = useLayoutContext()
|
||||
|
||||
const { markMessagesAsRead, unreadMessageCount } = useChatContext()
|
||||
const canViewChatAndTrackChanges = !useViewerPermissions()
|
||||
|
||||
const toggleChatOpen = useCallback(() => {
|
||||
if (!chatIsOpen) {
|
||||
|
@ -119,11 +121,12 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
hasPublishPermissions={
|
||||
permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'
|
||||
}
|
||||
chatVisible={!(isRestrictedTokenMember || !canViewChatAndTrackChanges)}
|
||||
projectName={projectName}
|
||||
renameProject={renameProject}
|
||||
hasRenamePermissions={permissionsLevel === 'owner'}
|
||||
openShareModal={openShareModal}
|
||||
trackChangesVisible={trackChangesVisible}
|
||||
trackChangesVisible={canViewChatAndTrackChanges && trackChangesVisible}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
goToUser,
|
||||
isRestrictedTokenMember,
|
||||
hasPublishPermissions,
|
||||
chatVisible,
|
||||
projectName,
|
||||
renameProject,
|
||||
hasRenamePermissions,
|
||||
|
@ -89,7 +90,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
|
||||
<LayoutDropdownButton />
|
||||
|
||||
{!isRestrictedTokenMember && (
|
||||
{chatVisible && (
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
|
@ -117,6 +118,7 @@ ToolbarHeader.propTypes = {
|
|||
goToUser: PropTypes.func.isRequired,
|
||||
isRestrictedTokenMember: PropTypes.bool,
|
||||
hasPublishPermissions: PropTypes.bool,
|
||||
chatVisible: PropTypes.bool,
|
||||
projectName: PropTypes.string.isRequired,
|
||||
renameProject: PropTypes.func.isRequired,
|
||||
hasRenamePermissions: PropTypes.bool,
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '@/features/ide-react/types/permissions'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { DeepReadonly } from '../../../../../types/utils'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
|
||||
export const PermissionsContext = createContext<Permissions | undefined>(
|
||||
undefined
|
||||
|
@ -40,6 +41,12 @@ const anonymousPermissionsMap: typeof permissionsMap = {
|
|||
owner: { ...permissionsMap.owner, comment: false },
|
||||
}
|
||||
|
||||
const linkSharingWarningPermissionsMap: typeof permissionsMap = {
|
||||
readOnly: { ...permissionsMap.readOnly, comment: false },
|
||||
readAndWrite: permissionsMap.readAndWrite,
|
||||
owner: permissionsMap.owner,
|
||||
}
|
||||
|
||||
export const PermissionsProvider: React.FC = ({ children }) => {
|
||||
const [permissions, setPermissions] =
|
||||
useScopeValue<Readonly<Permissions>>('permissions')
|
||||
|
@ -47,14 +54,20 @@ export const PermissionsProvider: React.FC = ({ children }) => {
|
|||
const { permissionsLevel } = useEditorContext() as {
|
||||
permissionsLevel: PermissionsLevel
|
||||
}
|
||||
const hasViewerPermissions = useViewerPermissions()
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
|
||||
useEffect(() => {
|
||||
const activePermissionsMap = anonymous
|
||||
? anonymousPermissionsMap
|
||||
: permissionsMap
|
||||
let activePermissionsMap
|
||||
if (hasViewerPermissions) {
|
||||
activePermissionsMap = linkSharingWarningPermissionsMap
|
||||
} else {
|
||||
activePermissionsMap = anonymous
|
||||
? anonymousPermissionsMap
|
||||
: permissionsMap
|
||||
}
|
||||
setPermissions(activePermissionsMap[permissionsLevel])
|
||||
}, [anonymous, permissionsLevel, setPermissions])
|
||||
}, [anonymous, permissionsLevel, setPermissions, hasViewerPermissions])
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionState.forceDisconnected) {
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
useEditorManagerContext,
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import ColorManager from '@/ide/colors/ColorManager'
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
|
@ -65,7 +64,9 @@ import {
|
|||
EditOperation,
|
||||
} from '../../../../../../../types/change'
|
||||
import { RangesTrackerWithResolvedThreadIds } from '@/features/ide-react/editor/document-container'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
|
||||
const dispatchReviewPanelEvent = (type: string, payload?: any) => {
|
||||
window.dispatchEvent(
|
||||
|
@ -151,6 +152,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
const permissions = usePermissionsContext()
|
||||
const { showGenericMessageModal } = useModalsContext()
|
||||
const addCommentEmitter = useScopeEventEmitter('comment:start_adding')
|
||||
const hasViewerPermissions = useViewerPermissions()
|
||||
|
||||
const layoutToLeft = useLayoutToLeft('.ide-react-editor-panel')
|
||||
const [subView, setSubView] =
|
||||
|
@ -418,7 +420,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
}
|
||||
|
||||
if (!users[change.metadata.user_id]) {
|
||||
if (!isRestrictedTokenMember) {
|
||||
if (!(isRestrictedTokenMember || hasViewerPermissions)) {
|
||||
refreshChangeUsers(change.metadata.user_id)
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +428,10 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
|
||||
let localResolvedThreadIds = resolvedThreadIds
|
||||
|
||||
if (!isRestrictedTokenMember && rangesTracker.comments.length > 0) {
|
||||
if (
|
||||
!(isRestrictedTokenMember || hasViewerPermissions) &&
|
||||
rangesTracker.comments.length > 0
|
||||
) {
|
||||
const threadsLoadResult = await ensureThreadsAreLoaded()
|
||||
if (threadsLoadResult?.resolvedThreadIds) {
|
||||
localResolvedThreadIds = threadsLoadResult.resolvedThreadIds
|
||||
|
@ -483,13 +488,14 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
getChangeTracker,
|
||||
getDocEntries,
|
||||
getDocResolvedComments,
|
||||
isRestrictedTokenMember,
|
||||
refreshChangeUsers,
|
||||
resolvedThreadIds,
|
||||
users,
|
||||
ensureThreadsAreLoaded,
|
||||
loadingThreads,
|
||||
setLoadingThreads,
|
||||
hasViewerPermissions,
|
||||
isRestrictedTokenMember,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { dispatchTimer } from '../../../infrastructure/cm6-performance'
|
|||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import { FigureModal } from './figure-modal/figure-modal'
|
||||
import ReviewPanel from './review-panel/review-panel'
|
||||
import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
|
||||
|
||||
const sourceEditorComponents = importOverleafModules(
|
||||
'sourceEditorComponents'
|
||||
|
@ -36,6 +37,8 @@ function CodeMirrorEditor() {
|
|||
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const shouldShowReviewPanel = !useViewerPermissions()
|
||||
|
||||
// create the view using the initial state and intercept transactions
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
if (viewRef.current === null) {
|
||||
|
@ -69,7 +72,8 @@ function CodeMirrorEditor() {
|
|||
)
|
||||
)}
|
||||
<CodeMirrorCommandTooltip />
|
||||
<ReviewPanel />
|
||||
|
||||
{shouldShowReviewPanel && <ReviewPanel />}
|
||||
{sourceEditorComponents.map(
|
||||
({ import: { default: Component }, path }) => (
|
||||
<Component key={path} />
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { useEditorContext } from '../context/editor-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function useViewerPermissions() {
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
const hasViewerPermissions =
|
||||
getMeta('ol-linkSharingWarning') && permissionsLevel === 'readOnly'
|
||||
return hasViewerPermissions
|
||||
}
|
||||
|
||||
export default useViewerPermissions
|
|
@ -17,6 +17,7 @@ describe('<ToolbarHeader />', function () {
|
|||
renameProject: () => {},
|
||||
openShareModal: () => {},
|
||||
hasPublishPermissions: true,
|
||||
chatVisible: true,
|
||||
trackChangesVisible: true,
|
||||
handleChangeLayout: () => {},
|
||||
pdfLayout: 'sideBySide',
|
||||
|
@ -83,10 +84,10 @@ describe('<ToolbarHeader />', function () {
|
|||
screen.getByText('Chat')
|
||||
})
|
||||
|
||||
it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () {
|
||||
it('is not displayed when "chatVisible" prop is set to false', function () {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isRestrictedTokenMember: true,
|
||||
chatVisible: false,
|
||||
}
|
||||
renderWithEditorContext(<ToolbarHeader {...props} />)
|
||||
expect(screen.queryByText('Chat')).to.not.exist
|
||||
|
|
Loading…
Reference in a new issue