diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index f9a5ac786a86..b6ec0e20b348 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -1052,7 +1052,7 @@ export interface ClearPostActionSession { export interface UpdateProjectServerState { action: 'UPDATE_PROJECT_SERVER_STATE' - serverState: ProjectServerState + serverState: Partial } export interface UpdateTopLevelElementsFromCollaborationUpdate { diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 7f07558a3735..7d2eee3a5c5e 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -1659,7 +1659,7 @@ export function clearPostActionData(): ClearPostActionSession { } export function updateProjectServerState( - projectServerState: ProjectServerState, + projectServerState: Partial, ): UpdateProjectServerState { return { action: 'UPDATE_PROJECT_SERVER_STATE', diff --git a/editor/src/components/editor/editor-component.tsx b/editor/src/components/editor/editor-component.tsx index f3dc342707c6..4cf0b9324734 100644 --- a/editor/src/components/editor/editor-component.tsx +++ b/editor/src/components/editor/editor-component.tsx @@ -70,6 +70,7 @@ import { useDisplayOwnershipWarning } from './project-owner-hooks' import { EditorModes } from './editor-modes' import { allowedToEditProject } from './store/collaborative-editing' import { useDataThemeAttributeOnBody } from '../../core/commenting/comment-hooks' +import { CollaborationStateUpdater } from './store/collaboration-state' const liveModeToastId = 'play-mode-toast' @@ -556,7 +557,9 @@ export function EditorComponent(props: EditorProps) { forkedFromProjectId={forkedFromProjectId} dispatch={dispatch} > - + + + diff --git a/editor/src/components/editor/persistence/persistence.ts b/editor/src/components/editor/persistence/persistence.ts index f6a5244488a7..cffcec11fd00 100644 --- a/editor/src/components/editor/persistence/persistence.ts +++ b/editor/src/components/editor/persistence/persistence.ts @@ -27,7 +27,7 @@ import { userLogOutEvent, } from './generic/persistence-machine' import type { PersistenceBackendAPI, PersistenceContext } from './generic/persistence-types' -import { releaseControl } from '../store/collaborative-editing' +import { clearAllControlFromThisEditor } from '../store/collaborative-editing' export class PersistenceMachine { private interpreter: Interpreter< @@ -136,7 +136,7 @@ export class PersistenceMachine { window.addEventListener('beforeunload', async (e) => { if (this.isSafeToClose()) { - void releaseControl() + void clearAllControlFromThisEditor() } else { this.sendThrottledSave() e.preventDefault() diff --git a/editor/src/components/editor/store/collaboration-state.tsx b/editor/src/components/editor/store/collaboration-state.tsx new file mode 100644 index 000000000000..d8e5736927f6 --- /dev/null +++ b/editor/src/components/editor/store/collaboration-state.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import type { EditorDispatch } from '../action-types' +import { releaseControlOverProject, snatchControlOverProject } from './collaborative-editing' +import { Substores, useEditorState } from './store-hook' +import { updateProjectServerState } from '../actions/action-creators' + +interface CollaborationStateUpdaterProps { + projectId: string | null + dispatch: EditorDispatch +} + +export const CollaborationStateUpdater = React.memo( + (props: React.PropsWithChildren) => { + const { projectId, dispatch, children } = props + const isMyProject = useEditorState( + Substores.projectServerState, + (store) => store.projectServerState.isMyProject, + 'CollaborationStateUpdater isMyProject', + ) + React.useEffect(() => { + function handleVisibilityChange(): void { + if (projectId != null) { + // If the document is hidden, that means the editor is in the background + // or minimised, so release control. Otherwise if it has become unhidden + // then snatch control of the project. + if (document.hidden) { + void releaseControlOverProject(projectId).then(() => { + dispatch([updateProjectServerState({ currentlyHolderOfTheBaton: false })]) + }) + } else { + // Only attempt to do any kind of snatching of control if the project is "mine". + if (isMyProject === 'yes') { + void snatchControlOverProject(projectId).then((controlResult) => { + dispatch([ + updateProjectServerState({ currentlyHolderOfTheBaton: controlResult ?? false }), + ]) + }) + } + } + } + } + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, [dispatch, projectId, isMyProject]) + return <>{children} + }, +) diff --git a/editor/src/components/editor/store/collaborative-editing.ts b/editor/src/components/editor/store/collaborative-editing.ts index 8af0d772856c..2743f996b2ee 100644 --- a/editor/src/components/editor/store/collaborative-editing.ts +++ b/editor/src/components/editor/store/collaborative-editing.ts @@ -630,6 +630,40 @@ export function claimProjectControl( } } +export interface SnatchProjectControl { + type: 'SNATCH_PROJECT_CONTROL' + projectID: string + collaborationEditor: string +} + +export function snatchProjectControl( + projectID: string, + collaborationEditor: string, +): SnatchProjectControl { + return { + type: 'SNATCH_PROJECT_CONTROL', + projectID: projectID, + collaborationEditor: collaborationEditor, + } +} + +export interface ReleaseProjectControl { + type: 'RELEASE_PROJECT_CONTROL' + projectID: string + collaborationEditor: string +} + +export function releaseProjectControl( + projectID: string, + collaborationEditor: string, +): ReleaseProjectControl { + return { + type: 'RELEASE_PROJECT_CONTROL', + projectID: projectID, + collaborationEditor: collaborationEditor, + } +} + export interface ClearAllOfCollaboratorsControl { type: 'CLEAR_ALL_OF_COLLABORATORS_CONTROL' collaborationEditor: string @@ -644,18 +678,22 @@ export function clearAllOfCollaboratorsControl( } } -export type CollaborationRequest = ClaimProjectControl | ClearAllOfCollaboratorsControl +export type CollaborationRequest = + | ClaimProjectControl + | SnatchProjectControl + | ReleaseProjectControl + | ClearAllOfCollaboratorsControl -export interface ClaimProjectControlResult { - type: 'CLAIM_PROJECT_CONTROL_RESULT' +export interface RequestProjectControlResult { + type: 'REQUEST_PROJECT_CONTROL_RESULT' successfullyClaimed: boolean } -export interface ClearAllOfCollaboratorsControlResult { - type: 'CLEAR_ALL_OF_COLLABORATORS_CONTROL_RESULT' +export interface ReleaseControlResponse { + type: 'RELEASE_CONTROL_RESULT' } -export type CollaborationResponse = ClaimProjectControlResult | ClearAllOfCollaboratorsControlResult +export type CollaborationResponse = RequestProjectControlResult | ReleaseControlResponse const collaborationEditor = UUID() @@ -681,22 +719,51 @@ export async function claimControlOverProject(projectID: string | null): Promise if (projectID == null || !isFeatureEnabled('Baton Passing For Control')) { return null } + if (document.hidden) { + return false + } const request = claimProjectControl(projectID, collaborationEditor) const response = await callCollaborationEndpoint(request) - if (response.type === 'CLAIM_PROJECT_CONTROL_RESULT') { + if (response.type === 'REQUEST_PROJECT_CONTROL_RESULT') { + return response.successfullyClaimed + } else { + throw new Error(`Unexpected response: ${JSON.stringify(response)}`) + } +} + +export async function snatchControlOverProject(projectID: string | null): Promise { + if (projectID == null || !isFeatureEnabled('Baton Passing For Control')) { + return null + } + + const request = snatchProjectControl(projectID, collaborationEditor) + const response = await callCollaborationEndpoint(request) + if (response.type === 'REQUEST_PROJECT_CONTROL_RESULT') { return response.successfullyClaimed } else { throw new Error(`Unexpected response: ${JSON.stringify(response)}`) } } -export async function releaseControl(): Promise { +export async function releaseControlOverProject(projectID: string | null): Promise { + if (projectID == null || !isFeatureEnabled('Baton Passing For Control')) { + return + } + + const request = releaseProjectControl(projectID, collaborationEditor) + const response = await callCollaborationEndpoint(request) + if (response.type !== 'RELEASE_CONTROL_RESULT') { + throw new Error(`Unexpected response: ${JSON.stringify(response)}`) + } +} + +export async function clearAllControlFromThisEditor(): Promise { if (isFeatureEnabled('Baton Passing For Control')) { const request = clearAllOfCollaboratorsControl(collaborationEditor) const response = await callCollaborationEndpoint(request) - if (response.type !== 'CLEAR_ALL_OF_COLLABORATORS_CONTROL_RESULT') { + if (response.type !== 'RELEASE_CONTROL_RESULT') { throw new Error(`Unexpected response: ${JSON.stringify(response)}`) } } diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 197706abfe11..c4f7ce9ebb4e 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -628,7 +628,7 @@ export function editorDispatchClosingOut( ]), saveCountThisSession: saveCountThisSession + (shouldSave ? 1 : 0), builtInDependencies: storedState.builtInDependencies, - projectServerState: storedState.projectServerState, + projectServerState: result.projectServerState, collaborativeEditingSupport: storedState.collaborativeEditingSupport, } @@ -830,6 +830,7 @@ function editorDispatchInner( storedState.nothingChanged && storedState.unpatchedEditor === result.unpatchedEditor && storedState.userState === result.userState && + storedState.projectServerState === result.projectServerState && storedState.postActionInteractionSession === result.postActionInteractionSession const domMetadataChanged = diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index d117e7d26e63..8140a33f2607 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -521,6 +521,9 @@ export function runUpdateProjectServerState( ): EditorStoreUnpatched { return { ...working, - projectServerState: action.serverState, + projectServerState: { + ...working.projectServerState, + ...action.serverState, + }, } } diff --git a/editor/src/components/editor/store/project-server-state.tsx b/editor/src/components/editor/store/project-server-state.tsx index 023e714201e5..22e3e9810393 100644 --- a/editor/src/components/editor/store/project-server-state.tsx +++ b/editor/src/components/editor/store/project-server-state.tsx @@ -80,7 +80,11 @@ export async function getProjectServerState( forkedFromProjectId: string | null, ): Promise { if (IS_TEST_ENVIRONMENT) { - return emptyProjectServerState() + return { + ...emptyProjectServerState(), + isMyProject: 'yes', + currentlyHolderOfTheBaton: true, + } } else { const projectListing = projectId == null ? null : await fetchProjectMetadata(projectId) const forkedFromProjectListing = @@ -95,7 +99,9 @@ export async function getProjectServerState( } const ownership = await getOwnership() - const holderOfTheBaton = (await claimControlOverProject(projectId)) ?? ownership.isOwner + const holderOfTheBaton = ownership.isOwner + ? (await claimControlOverProject(projectId)) ?? ownership.isOwner + : false return { isMyProject: ownership.isOwner ? 'yes' : 'no', diff --git a/server/migrations/005.sql b/server/migrations/005.sql new file mode 100644 index 000000000000..5414eeb204ae --- /dev/null +++ b/server/migrations/005.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY "public"."project_collaboration" + ADD COLUMN "owner_id" character varying; diff --git a/server/src/Utopia/Web/Database.hs b/server/src/Utopia/Web/Database.hs index bc21883a48a4..5b8bfaf21cf4 100644 --- a/server/src/Utopia/Web/Database.hs +++ b/server/src/Utopia/Web/Database.hs @@ -60,8 +60,9 @@ data DatabaseMetrics = DatabaseMetrics , _updateGithubAuthenticationDetailsMetrics :: InvocationMetric , _getGithubAuthenticationDetailsMetrics :: InvocationMetric , _maybeClaimCollaborationControlMetrics :: InvocationMetric + , _forceClaimCollaborationControlMetrics :: InvocationMetric + , _releaseCollaborationControlMetrics :: InvocationMetric , _deleteCollaborationControlByCollaboratorMetrics :: InvocationMetric - , _deleteCollaborationControlByProjectMetrics :: InvocationMetric } createDatabaseMetrics :: Store -> IO DatabaseMetrics @@ -86,8 +87,9 @@ createDatabaseMetrics store = DatabaseMetrics <*> createInvocationMetric "utopia.database.updategithubauthenticationdetails" store <*> createInvocationMetric "utopia.database.getgithubauthenticationdetails" store <*> createInvocationMetric "utopia.database.maybeclaimcollaborationownership" store + <*> createInvocationMetric "utopia.database.forceclaimcollaborationownership" store + <*> createInvocationMetric "utopia.database.releasecollaborationownership" store <*> createInvocationMetric "utopia.database.deletecollaborationownershipbycollaborator" store - <*> createInvocationMetric "utopia.database.deletecollaborationownershipbyproject" store data UserIDIncorrectException = UserIDIncorrectException deriving (Eq, Show) @@ -445,12 +447,12 @@ lookupGithubAuthenticationDetails metrics pool userId = invokeAndMeasure (_getGi pure githubAuthenticationDetailsRow pure $ fmap githubAuthenticationDetailsFromRow $ listToMaybe githubAuthenticationDetails -insertCollaborationControl :: Connection -> Text -> Text -> UTCTime -> IO () -insertCollaborationControl connection projectId collaborationEditor currentTime = do +insertCollaborationControl :: Connection -> Text -> Text -> Text -> UTCTime -> IO () +insertCollaborationControl connection ownerId projectId collaborationEditor currentTime = do let newLastSeenTimeout = addUTCTime collaborationLastSeenTimeoutWindow currentTime void $ runInsert_ connection $ Insert { iTable = projectCollaborationTable - , iRows = [toFields (projectId, collaborationEditor, newLastSeenTimeout)] + , iRows = [toFields (projectId, collaborationEditor, newLastSeenTimeout, Just ownerId)] , iReturning = rCount , iOnConflict = Nothing } @@ -458,38 +460,45 @@ insertCollaborationControl connection projectId collaborationEditor currentTime collaborationLastSeenTimeoutWindow :: NominalDiffTime collaborationLastSeenTimeoutWindow = secondsToNominalDiffTime 20 -updateCollaborationLastSeenTimeout :: Connection -> Text -> Text -> UTCTime -> IO () -updateCollaborationLastSeenTimeout connection projectId newCollaborationEditor newLastSeenTimeout = do +sameOwner :: FieldNullable SqlText -> Text -> Field SqlBool +sameOwner rowPossibleOwnerId ownerId = matchNullable (toFields True) (.== toFields ownerId) rowPossibleOwnerId + +sameProjectAndOwner :: Field SqlText -> FieldNullable SqlText -> Text -> Text -> Field SqlBool +sameProjectAndOwner rowProjectId rowPossibleOwnerId projectId ownerId = + rowProjectId .== toFields projectId .&& sameOwner rowPossibleOwnerId ownerId + +updateCollaborationLastSeenTimeout :: Connection -> Text -> Text -> Text -> UTCTime -> IO () +updateCollaborationLastSeenTimeout connection ownerId projectId newCollaborationEditor newLastSeenTimeout = do void $ runUpdate_ connection $ Update { uTable = projectCollaborationTable - , uUpdateWith = updateEasy (\(rowProjectId, _, _) -> (rowProjectId, toFields newCollaborationEditor, toFields newLastSeenTimeout)) - , uWhere = (\(rowProjectId, _, _) -> rowProjectId .=== toFields projectId) + , uUpdateWith = updateEasy (\(rowProjectId, _, _, _) -> (rowProjectId, toFields newCollaborationEditor, toFields newLastSeenTimeout, toFields $ Just ownerId)) + , uWhere = (\(rowProjectId, _, _, rowPossibleOwnerId) -> sameProjectAndOwner rowProjectId rowPossibleOwnerId projectId ownerId) , uReturning = rCount } -maybeClaimExistingCollaborationControl :: Connection -> Text -> Text -> Text -> UTCTime -> UTCTime -> IO Bool -maybeClaimExistingCollaborationControl connection projectId currentCollaborationEditor newCollaborationEditor currentLastSeenTimeout currentTime +maybeClaimExistingCollaborationControl :: Connection -> Text -> Text -> Text -> Text -> UTCTime -> UTCTime -> IO Bool +maybeClaimExistingCollaborationControl connection ownerId projectId currentCollaborationEditor newCollaborationEditor currentLastSeenTimeout currentTime | currentCollaborationEditor == newCollaborationEditor = updateLastSeen | currentLastSeenTimeout < currentTime = updateLastSeen | otherwise = pure False where newTimeout = addUTCTime collaborationLastSeenTimeoutWindow currentTime - updateLastSeen = updateCollaborationLastSeenTimeout connection projectId newCollaborationEditor newTimeout >> pure True + updateLastSeen = updateCollaborationLastSeenTimeout connection ownerId projectId newCollaborationEditor newTimeout >> pure True -maybeClaimCollaborationControl :: DatabaseMetrics -> DBPool -> Text -> Text -> IO Bool -maybeClaimCollaborationControl metrics pool projectId collaborationEditor = do +maybeClaimCollaborationControl :: DatabaseMetrics -> DBPool -> Text -> Text -> Text -> IO Bool +maybeClaimCollaborationControl metrics pool ownerId projectId collaborationEditor = do currentTime <- getCurrentTime invokeAndMeasure (_maybeClaimCollaborationControlMetrics metrics) $ usePool pool $ \connection -> do withTransaction connection $ do -- Find any existing entries (should only be one). collaborationEditorIdsWithLastSeen <- runSelect connection $ do - (rowProjectId, rowCollaborationEditor, rowLastSeenTimeout) <- projectCollaborationSelect - where_ $ rowProjectId .== toFields projectId + (rowProjectId, rowCollaborationEditor, rowLastSeenTimeout, rowPossibleOwnerId) <- projectCollaborationSelect + where_ $ sameProjectAndOwner rowProjectId rowPossibleOwnerId projectId ownerId pure (rowCollaborationEditor, rowLastSeenTimeout) -- Get the first if there is one. let maybeCurrentCollaborationEditorAndLastSeen = listToMaybe collaborationEditorIdsWithLastSeen -- Create the expression that inserts an entry and returns true to indicate this was successfully claimed. - let insertCurrent = insertCollaborationControl connection projectId collaborationEditor currentTime >> pure True + let insertCurrent = insertCollaborationControl connection ownerId projectId collaborationEditor currentTime >> pure True -- Handle the current state. case maybeCurrentCollaborationEditorAndLastSeen of -- There's no current entry in the table, so add one. @@ -497,21 +506,36 @@ maybeClaimCollaborationControl metrics pool projectId collaborationEditor = do -- If the current entry is the same as the one we're trying to insert return true, otherwise -- return false but in either case make no changes to the database. Just (currentCollaborationEditor, currentLastSeenTimeout) -> - maybeClaimExistingCollaborationControl connection projectId currentCollaborationEditor collaborationEditor currentLastSeenTimeout currentTime + maybeClaimExistingCollaborationControl connection ownerId projectId currentCollaborationEditor collaborationEditor currentLastSeenTimeout currentTime -deleteCollaborationControlByCollaborator :: DatabaseMetrics -> DBPool -> Text -> IO () -deleteCollaborationControlByCollaborator metrics pool collaborationEditor = invokeAndMeasure (_deleteCollaborationControlByCollaboratorMetrics metrics) $ usePool pool $ \connection -> do - void $ runDelete_ connection $ Delete +forceClaimCollaborationControl :: DatabaseMetrics -> DBPool -> Text -> Text -> Text -> IO () +forceClaimCollaborationControl metrics pool ownerId projectId collaborationEditor = do + currentTime <- getCurrentTime + invokeAndMeasure (_forceClaimCollaborationControlMetrics metrics) $ usePool pool $ \connection -> do + withTransaction connection $ do + -- Delete any and all values for this project. + void $ runDelete_ connection $ Delete { dTable = projectCollaborationTable - , dWhere = (\(_, rowCollaborationEditor, _) -> rowCollaborationEditor .== toFields collaborationEditor) + , dWhere = (\(rowProjectId, _, _, _) -> rowProjectId .=== toFields projectId) , dReturning = rCount } - -deleteCollaborationControlByProject :: DatabaseMetrics -> DBPool -> Text -> IO () -deleteCollaborationControlByProject metrics pool projectId = invokeAndMeasure (_deleteCollaborationControlByProjectMetrics metrics) $ usePool pool $ \connection -> do + -- Inserts an entry for this collaborator. + insertCollaborationControl connection ownerId projectId collaborationEditor currentTime + +releaseCollaborationControl :: DatabaseMetrics -> DBPool -> Text -> Text -> Text -> IO () +releaseCollaborationControl metrics pool ownerId projectId collaborationEditor = do + invokeAndMeasure (_releaseCollaborationControlMetrics metrics) $ usePool pool $ \connection -> do + -- Delete any matching values for this project and collaboration editor. + void $ runDelete_ connection $ Delete + { dTable = projectCollaborationTable + , dWhere = (\(rowProjectId, rowCollaborationEditor, _, rowPossibleOwnerId) -> sameProjectAndOwner rowProjectId rowPossibleOwnerId projectId ownerId .&& rowCollaborationEditor .== toFields collaborationEditor) + , dReturning = rCount + } + +deleteCollaborationControlByCollaborator :: DatabaseMetrics -> DBPool -> Text -> Text -> IO () +deleteCollaborationControlByCollaborator metrics pool ownerId collaborationEditor = invokeAndMeasure (_deleteCollaborationControlByCollaboratorMetrics metrics) $ usePool pool $ \connection -> do void $ runDelete_ connection $ Delete { dTable = projectCollaborationTable - , dWhere = (\(rowProjectId, _, _) -> rowProjectId .== toFields projectId) + , dWhere = (\(_, rowCollaborationEditor, _, rowPossibleOwnerId) -> sameOwner rowPossibleOwnerId ownerId .&& rowCollaborationEditor .== toFields collaborationEditor) , dReturning = rCount } - diff --git a/server/src/Utopia/Web/Database/Migrations.hs b/server/src/Utopia/Web/Database/Migrations.hs index 29d80a7983e5..c9582837d618 100644 --- a/server/src/Utopia/Web/Database/Migrations.hs +++ b/server/src/Utopia/Web/Database/Migrations.hs @@ -26,6 +26,7 @@ migrateDatabase verbose includeInitial pool = withResource pool $ \connection -> , MigrationFile "002.sql" "./migrations/002.sql" , MigrationFile "003.sql" "./migrations/003.sql" , MigrationFile "004.sql" "./migrations/004.sql" + , MigrationFile "005.sql" "./migrations/005.sql" ] let initialMigrationCommand = if includeInitial then [MigrationFile "initial.sql" "./migrations/initial.sql"] diff --git a/server/src/Utopia/Web/Database/Types.hs b/server/src/Utopia/Web/Database/Types.hs index 0eacf8478f0c..a78e14aecd4e 100644 --- a/server/src/Utopia/Web/Database/Types.hs +++ b/server/src/Utopia/Web/Database/Types.hs @@ -141,13 +141,14 @@ data GithubAuthenticationDetails = GithubAuthenticationDetails , expiresAt :: Maybe UTCTime } deriving (Eq, Show, Generic) -type ProjectCollaborationFields = (Field SqlText, Field SqlText, Field SqlTimestamptz) +type ProjectCollaborationFields = (Field SqlText, Field SqlText, Field SqlTimestamptz, FieldNullable SqlText) projectCollaborationTable :: Table ProjectCollaborationFields ProjectCollaborationFields -projectCollaborationTable = table "project_collaboration" (p3 +projectCollaborationTable = table "project_collaboration" (p4 ( tableField "project_id" , tableField "collaboration_editor" , tableField "last_seen_timeout" + , tableField "owner_id" ) ) @@ -158,4 +159,5 @@ data ProjectCollaborationDetails = ProjectCollaborationDetails { projectId :: Text , collaborationEditor :: Text , lastSeenTimeout :: UTCTime + , ownerId :: Maybe Text } deriving (Eq, Show, Generic) diff --git a/server/src/Utopia/Web/Endpoints/Collaboration.hs b/server/src/Utopia/Web/Endpoints/Collaboration.hs index 7742e980acaf..cbd8db774830 100644 --- a/server/src/Utopia/Web/Endpoints/Collaboration.hs +++ b/server/src/Utopia/Web/Endpoints/Collaboration.hs @@ -66,7 +66,14 @@ collaborationEndpoint cookie collaborationRequest = do case collaborationRequest of (ClaimProjectControlRequest ClaimProjectControl{..}) -> do successfullyClaimed <- claimCollaborationControl idOfUser projectID collaborationEditor - pure $ ClaimProjectControlResultResponse $ ClaimProjectControlResult{..} + pure $ RequestProjectControlResultResponse $ RequestProjectControlResult{..} + (SnatchProjectControlRequest SnatchProjectControl{..}) -> do + snatchCollaborationControl idOfUser projectID collaborationEditor + let successfullyClaimed = True + pure $ RequestProjectControlResultResponse $ RequestProjectControlResult{..} + (ReleaseProjectControlRequest ReleaseProjectControl{..}) -> do + releaseCollaborationControl idOfUser projectID collaborationEditor + pure $ ReleaseControlResponse ReleaseControlResult (ClearAllOfCollaboratorsControlRequest ClearAllOfCollaboratorsControl{..}) -> do - _ <- clearCollaboratorOwnership collaborationEditor - pure $ ClearAllOfCollaboratorsControlResponse ClearAllOfCollaboratorsControlResult + _ <- clearCollaboratorOwnership idOfUser collaborationEditor + pure $ ReleaseControlResponse ReleaseControlResult diff --git a/server/src/Utopia/Web/Executors/Development.hs b/server/src/Utopia/Web/Executors/Development.hs index dad96c641237..f6eee73213b4 100644 --- a/server/src/Utopia/Web/Executors/Development.hs +++ b/server/src/Utopia/Web/Executors/Development.hs @@ -451,12 +451,24 @@ innerServerExecutor (ClaimCollaborationControl user projectID collaborationEdito pool <- fmap _projectPool ask projectOwnershipResult <- liftIO $ DB.checkIfProjectOwner metrics pool user projectID unless projectOwnershipResult $ throwError err400 - ownershipResult <- liftIO $ DB.maybeClaimCollaborationControl metrics pool projectID collaborationEditor + ownershipResult <- liftIO $ DB.maybeClaimCollaborationControl metrics pool user projectID collaborationEditor pure $ action ownershipResult -innerServerExecutor (ClearCollaboratorOwnership collaborationEditor action) = do +innerServerExecutor (SnatchCollaborationControl user projectID collaborationEditor action) = do metrics <- fmap _databaseMetrics ask pool <- fmap _projectPool ask - liftIO $ DB.deleteCollaborationControlByCollaborator metrics pool collaborationEditor + projectOwnershipResult <- liftIO $ DB.checkIfProjectOwner metrics pool user projectID + unless projectOwnershipResult $ throwError err400 + liftIO $ DB.forceClaimCollaborationControl metrics pool user projectID collaborationEditor + pure action +innerServerExecutor (ReleaseCollaborationControl user projectID collaborationEditor action) = do + metrics <- fmap _databaseMetrics ask + pool <- fmap _projectPool ask + liftIO $ DB.releaseCollaborationControl metrics pool user projectID collaborationEditor + pure action +innerServerExecutor (ClearCollaboratorOwnership user collaborationEditor action) = do + metrics <- fmap _databaseMetrics ask + pool <- fmap _projectPool ask + liftIO $ DB.deleteCollaborationControlByCollaborator metrics pool user collaborationEditor pure action {-| diff --git a/server/src/Utopia/Web/Executors/Production.hs b/server/src/Utopia/Web/Executors/Production.hs index 7dd067150feb..cf66bfa68889 100644 --- a/server/src/Utopia/Web/Executors/Production.hs +++ b/server/src/Utopia/Web/Executors/Production.hs @@ -356,12 +356,24 @@ innerServerExecutor (ClaimCollaborationControl user projectID collaborationEdito pool <- fmap _projectPool ask projectOwnershipResult <- liftIO $ DB.checkIfProjectOwner metrics pool user projectID unless projectOwnershipResult $ throwError err400 - ownershipResult <- liftIO $ DB.maybeClaimCollaborationControl metrics pool projectID collaborationEditor + ownershipResult <- liftIO $ DB.maybeClaimCollaborationControl metrics pool user projectID collaborationEditor pure $ action ownershipResult -innerServerExecutor (ClearCollaboratorOwnership collaborationEditor action) = do +innerServerExecutor (SnatchCollaborationControl user projectID collaborationEditor action) = do metrics <- fmap _databaseMetrics ask pool <- fmap _projectPool ask - liftIO $ DB.deleteCollaborationControlByCollaborator metrics pool collaborationEditor + projectOwnershipResult <- liftIO $ DB.checkIfProjectOwner metrics pool user projectID + unless projectOwnershipResult $ throwError err400 + liftIO $ DB.forceClaimCollaborationControl metrics pool user projectID collaborationEditor + pure action +innerServerExecutor (ReleaseCollaborationControl user projectID collaborationEditor action) = do + metrics <- fmap _databaseMetrics ask + pool <- fmap _projectPool ask + liftIO $ DB.releaseCollaborationControl metrics pool user projectID collaborationEditor + pure action +innerServerExecutor (ClearCollaboratorOwnership user collaborationEditor action) = do + metrics <- fmap _databaseMetrics ask + pool <- fmap _projectPool ask + liftIO $ DB.deleteCollaborationControlByCollaborator metrics pool user collaborationEditor pure action readEditorContentFromDisk :: Maybe BranchDownloads -> Maybe Text -> Text -> IO Text @@ -396,7 +408,7 @@ assetPathsAndBuilders = shouldUseFakeUser :: Maybe Text -> Bool shouldUseFakeUser (Just "USE_FAKE_USER") = True -shouldUseFakeUser _ = False +shouldUseFakeUser _ = False initialiseResources :: IO ProductionServerResources initialiseResources = do diff --git a/server/src/Utopia/Web/ServiceTypes.hs b/server/src/Utopia/Web/ServiceTypes.hs index 2f6e1a3df780..9300ef68d34c 100644 --- a/server/src/Utopia/Web/ServiceTypes.hs +++ b/server/src/Utopia/Web/ServiceTypes.hs @@ -145,7 +145,9 @@ data ServiceCallsF a = NotFound | AuthLiveblocksUser Text Text (Text -> a) | IsLiveblocksEnabled (Bool -> a) | ClaimCollaborationControl Text Text Text (Bool -> a) - | ClearCollaboratorOwnership Text a + | SnatchCollaborationControl Text Text Text a + | ReleaseCollaborationControl Text Text Text a + | ClearCollaboratorOwnership Text Text a deriving Functor {- diff --git a/server/src/Utopia/Web/Types/Collaboration.hs b/server/src/Utopia/Web/Types/Collaboration.hs index f96304c6baf9..6ece4753cf76 100644 --- a/server/src/Utopia/Web/Types/Collaboration.hs +++ b/server/src/Utopia/Web/Types/Collaboration.hs @@ -41,6 +41,22 @@ data ClaimProjectControl = ClaimProjectControl instance FromJSON ClaimProjectControl where parseJSON = genericParseJSON defaultOptions +data SnatchProjectControl = SnatchProjectControl + { projectID :: Text + , collaborationEditor :: Text + } deriving (Eq, Show, Generic) + +instance FromJSON SnatchProjectControl where + parseJSON = genericParseJSON defaultOptions + +data ReleaseProjectControl = ReleaseProjectControl + { projectID :: Text + , collaborationEditor :: Text + } deriving (Eq, Show, Generic) + +instance FromJSON ReleaseProjectControl where + parseJSON = genericParseJSON defaultOptions + data ClearAllOfCollaboratorsControl = ClearAllOfCollaboratorsControl { collaborationEditor :: Text } deriving (Eq, Show, Generic) @@ -49,6 +65,8 @@ instance FromJSON ClearAllOfCollaboratorsControl where parseJSON = genericParseJSON defaultOptions data CollaborationRequest = ClaimProjectControlRequest ClaimProjectControl + | SnatchProjectControlRequest SnatchProjectControl + | ReleaseProjectControlRequest ReleaseProjectControl | ClearAllOfCollaboratorsControlRequest ClearAllOfCollaboratorsControl deriving (Eq, Show, Generic) @@ -57,29 +75,31 @@ instance FromJSON CollaborationRequest where let fileType = firstOf (key "type" . _String) value in case fileType of (Just "CLAIM_PROJECT_CONTROL") -> fmap ClaimProjectControlRequest $ parseJSON value + (Just "SNATCH_PROJECT_CONTROL") -> fmap SnatchProjectControlRequest $ parseJSON value + (Just "RELEASE_PROJECT_CONTROL") -> fmap ReleaseProjectControlRequest $ parseJSON value (Just "CLEAR_ALL_OF_COLLABORATORS_CONTROL") -> fmap ClearAllOfCollaboratorsControlRequest $ parseJSON value (Just unknownType) -> fail ("Unknown type: " <> T.unpack unknownType) _ -> fail "No type for CollaborationRequest specified." -data ClaimProjectControlResult = ClaimProjectControlResult +data RequestProjectControlResult = RequestProjectControlResult { successfullyClaimed :: Bool } deriving (Eq, Show, Generic) -instance ToJSON ClaimProjectControlResult where +instance ToJSON RequestProjectControlResult where toJSON = genericToJSON defaultOptions -data ClearAllOfCollaboratorsControlResult = ClearAllOfCollaboratorsControlResult +data ReleaseControlResult = ReleaseControlResult deriving (Eq, Show, Generic) -instance ToJSON ClearAllOfCollaboratorsControlResult where +instance ToJSON ReleaseControlResult where toJSON _ = object [] -data CollaborationResponse = ClaimProjectControlResultResponse ClaimProjectControlResult - | ClearAllOfCollaboratorsControlResponse ClearAllOfCollaboratorsControlResult +data CollaborationResponse = RequestProjectControlResultResponse RequestProjectControlResult + | ReleaseControlResponse ReleaseControlResult deriving (Eq, Show, Generic) instance ToJSON CollaborationResponse where - toJSON (ClaimProjectControlResultResponse claimResult) = over _Object (M.insert "type" "CLAIM_PROJECT_CONTROL_RESULT") $ toJSON claimResult - toJSON (ClearAllOfCollaboratorsControlResponse clearResult) = over _Object (M.insert "type" "CLEAR_ALL_OF_COLLABORATORS_CONTROL_RESULT") $ toJSON clearResult + toJSON (RequestProjectControlResultResponse claimResult) = over _Object (M.insert "type" "REQUEST_PROJECT_CONTROL_RESULT") $ toJSON claimResult + toJSON (ReleaseControlResponse releaseResult) = over _Object (M.insert "type" "RELEASE_CONTROL_RESULT") $ toJSON releaseResult type CollaborationSocketAPI = "v1" :> "collaboration" :> ReqBody '[JSON] CollaborationRequest :> Put '[JSON] CollaborationResponse diff --git a/shell.nix b/shell.nix index 1a534d4e51b3..d41ebfc49128 100644 --- a/shell.nix +++ b/shell.nix @@ -398,7 +398,7 @@ let cd $(${pkgs.git}/bin/git rev-parse --show-toplevel) PGLOCK_DIR="`pwd`/.pglock/" mkdir -p $PGLOCK_DIR - ${postgres}/bin/psql -h "$PGLOCK_DIR" -d utopia + ${postgres}/bin/psql -h "$PGLOCK_DIR" -d utopia "$@" '') ];