Skip to content

Commit

Permalink
feat: handle leave conversation via dialog
Browse files Browse the repository at this point in the history
Signed-off-by: Maksim Sukharev <[email protected]>
  • Loading branch information
Antreesy committed Sep 24, 2024
1 parent 7d68fa5 commit 456af8b
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 111 deletions.
61 changes: 57 additions & 4 deletions src/components/ConversationSettings/DangerZone.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,26 @@
<p class="danger-zone__hint">
{{ t('spreed', 'Once a conversation is left, to rejoin a closed conversation, an invite is needed. An open conversation can be rejoined at any time.') }}
</p>
<NcButton type="warning" @click="leaveConversation">
<NcButton type="warning" @click="toggleShowLeaveConversationDialog">
{{ t('spreed', 'Leave conversation') }}
</NcButton>
<NcDialog class="danger-zone__dialog"
:open.sync="isLeaveConversationDialogOpen"
:name="t('spreed','Leave conversation')"
:message="leaveConversationDialogMessage"
container=".danger-zone">
<template #actions>
<NcButton v-if="supportsArchive && !conversation.isArchived" type="secondary" @click="toggleArchiveConversation">
{{ t('spreed', 'Archive instead') }}
</NcButton>
<NcButton type="tertiary" @click="toggleShowLeaveConversationDialog">
{{ t('spreed', 'No') }}
</NcButton>
<NcButton type="warning" @click="leaveConversation">
{{ t('spreed', 'Yes') }}
</NcButton>
</template>
</NcDialog>
</div>
<div v-if="canDeleteConversation" class="app-settings-subsection">
<h4 class="app-settings-section__subtitle">
Expand Down Expand Up @@ -76,6 +93,8 @@
</template>

<script>
import { ref } from 'vue'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
Expand All @@ -84,6 +103,10 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
const supportsArchive = hasTalkFeature('local', 'archived-conversations')
export default {
name: 'DangerZone',
components: {
Expand All @@ -109,10 +132,16 @@ export default {
},
},
data() {
setup() {
const isLeaveConversationDialogOpen = ref(false)
const isDeleteConversationDialogOpen = ref(false)
const isDeleteChatDialogOpen = ref(false)
return {
isDeleteConversationDialogOpen: false,
isDeleteChatDialogOpen: false,
supportsArchive,
isLeaveConversationDialogOpen,
isDeleteConversationDialogOpen,
isDeleteChatDialogOpen,
}
},
Expand All @@ -125,6 +154,13 @@ export default {
return this.conversation.token
},
leaveConversationDialogMessage() {
return t('spreed', 'Do you really want to leave "{displayName}"?', this.conversation, undefined, {
escape: false,
sanitize: false,
})
},
deleteConversationDialogMessage() {
return t('spreed', 'Do you really want to delete "{displayName}"?', this.conversation, undefined, {
escape: false,
Expand All @@ -146,10 +182,23 @@ export default {
hideConversationSettings() {
emit('hide-conversation-settings')
},
/**
* Deletes the current user from the conversation.
*/
async toggleArchiveConversation() {
this.isLeaveConversationDialogOpen = false
await this.$store.dispatch('toggleArchive', this.conversation)
this.hideConversationSettings()
},
/**
* Deletes the current user from the conversation.
*/
async leaveConversation() {
this.isLeaveConversationDialogOpen = false
try {
await this.$store.dispatch('removeCurrentUserFromConversation', { token: this.token })
this.hideConversationSettings()
Expand Down Expand Up @@ -198,6 +247,10 @@ export default {
}
},
toggleShowLeaveConversationDialog() {
this.isLeaveConversationDialogOpen = !this.isLeaveConversationDialogOpen
},
toggleShowDeleteConversationDialog() {
this.isDeleteConversationDialogOpen = !this.isDeleteConversationDialogOpen
},
Expand Down
191 changes: 100 additions & 91 deletions src/components/LeftSidebar/ConversationsList/Conversation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { showSuccess, showError } from '@nextcloud/dialogs'

import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'

import Conversation from './Conversation.vue'
Expand Down Expand Up @@ -66,6 +65,7 @@ describe('Conversation.vue', () => {
type: CONVERSATION.TYPE.GROUP,
displayName: 'conversation one',
isFavorite: false,
isArchived: false,
lastMessage: {
actorId: 'user-id-alice',
actorDisplayName: 'Alice Wonderland',
Expand Down Expand Up @@ -357,17 +357,63 @@ describe('Conversation.vue', () => {
return findNcActionButton(el, actionName)
}

/**
* @param {string} actionName The name of the action to shallow
* @param {number} buttonsAmount The amount of buttons to be shown in dialog
*/
async function shallowMountAndOpenDialog(actionName, buttonsAmount) {
const wrapper = shallowMount(Conversation, {
localVue,
store,
mocks: {
$router,
},
stubs: {
NcActionButton,
NcButton,
},
propsData: {
isSearchResult: false,
item,
},
})
const el = wrapper.findComponent({ name: 'NcListItem' })

const action = findNcActionButton(el, actionName)
expect(action.exists()).toBeTruthy()

// Act 1 : click on the button from the menu
await action.find('button').trigger('click')

// Assert 1
const dialog = wrapper.findComponent({ name: 'NcDialog' })
expect(dialog.exists).toBeTruthy()
const buttons = dialog.findAllComponents({ name: 'NcButton' })
expect(buttons.exists()).toBeTruthy()
expect(buttons).toHaveLength(buttonsAmount)

return buttons
}

describe('leaving conversation', () => {
test('leaves conversation', async () => {
const actionHandler = jest.fn()
testStoreConfig.modules.participantsStore.actions.removeCurrentUserFromConversation = actionHandler
let actionHandler

beforeEach(() => {
leaveConversation.mockResolvedValue()
const action = shallowMountAndGetAction('Leave conversation')
expect(action.exists()).toBe(true)
actionHandler = jest.fn().mockResolvedValueOnce()
testStoreConfig.modules.participantsStore.actions.removeCurrentUserFromConversation = actionHandler
testStoreConfig.modules.conversationsStore.actions.toggleArchive = actionHandler
store = new Vuex.Store(testStoreConfig)
})

await action.find('button').trigger('click')
await flushPromises()
test('leaves conversation when confirmed', async () => {
// Arrange
const buttons = await shallowMountAndOpenDialog('Leave conversation', 3)

// Act: click on the 'confirm' button
await buttons.at(2).find('button').trigger('click')

// Assert
expect(actionHandler).toHaveBeenCalledWith(expect.anything(), { token: TOKEN })
})

Expand All @@ -379,116 +425,79 @@ describe('Conversation.vue', () => {
})

test('errors with notification when a new moderator is required before leaving', async () => {
const actionHandler = jest.fn().mockRejectedValueOnce({
response: {
status: 400,
},
})
// Arrange
actionHandler = jest.fn().mockRejectedValueOnce({ response: { status: 400 } })
testStoreConfig.modules.participantsStore.actions.removeCurrentUserFromConversation = actionHandler
store = new Vuex.Store(testStoreConfig)

const action = shallowMountAndGetAction('Leave conversation')
expect(action.exists()).toBe(true)
const buttons = await shallowMountAndOpenDialog('Leave conversation', 3)

action.find('button').trigger('click')
// Act: click on the 'confirm' button
await buttons.at(2).find('button').trigger('click')
await flushPromises()

// Assert
expect(actionHandler).toHaveBeenCalledWith(expect.anything(), { token: TOKEN })
expect(showError).toHaveBeenCalledWith(expect.stringContaining('promote'))
})

test('does not leave conversation when not confirmed', async () => {
// Arrange
const buttons = await shallowMountAndOpenDialog('Leave conversation', 3)

// Act: click on the 'decline' button
await buttons.at(1).find('button').trigger('click')

// Assert
expect(actionHandler).not.toHaveBeenCalled()
})

test('archives conversation when selected', async () => {
// Arrange
const buttons = await shallowMountAndOpenDialog('Leave conversation', 3)

// Act: click on the 'archive' button
await buttons.at(0).find('button').trigger('click')

// Assert
expect(actionHandler).toHaveBeenCalledWith(expect.anything(), item)
})
})

describe('deleting conversation', () => {
test('deletes conversation when confirmed', async () => {
// Arrange
const actionHandler = jest.fn().mockResolvedValueOnce()
const updateTokenAction = jest.fn()
let actionHandler
let updateTokenAction

beforeEach(() => {
actionHandler = jest.fn().mockResolvedValueOnce()
updateTokenAction = jest.fn()
testStoreConfig.modules.conversationsStore.actions.deleteConversationFromServer = actionHandler
testStoreConfig.modules.tokenStore.getters.getToken = jest.fn().mockReturnValue(() => 'another-token')
testStoreConfig.modules.tokenStore.actions.updateToken = updateTokenAction
store = new Vuex.Store(testStoreConfig)
})

test('deletes conversation when confirmed', async () => {
// Arrange
const buttons = await shallowMountAndOpenDialog('Delete conversation', 2)

const wrapper = shallowMount(Conversation, {
localVue,
store: new Vuex.Store(testStoreConfig),
mocks: {
$router,
},
stubs: {
NcActionButton,
NcDialog,
NcButton,
},
propsData: {
isSearchResult: false,
item,
},
})
const el = wrapper.findComponent({ name: 'NcListItem' })

const action = findNcActionButton(el, 'Delete conversation')
expect(action.exists()).toBe(true)

// Act 1 : click on the button from the menu
await action.find('button').trigger('click')

// Assert 1
const dialog = wrapper.findComponent({ name: 'NcDialog' })
expect(dialog.exists).toBeTruthy()
const buttons = dialog.findAllComponents({ name: 'NcButton' })
expect(buttons.exists()).toBeTruthy()
expect(buttons).toHaveLength(2)

// Act 2 : click on the confirm button
// Act: click on the 'confirm' button
await buttons.at(1).find('button').trigger('click')

// Assert 2
// Assert
expect(actionHandler).toHaveBeenCalledWith(expect.anything(), { token: TOKEN })
expect($router.push).not.toHaveBeenCalled()
expect(updateTokenAction).not.toHaveBeenCalled()
})

test('does not delete conversation when not confirmed', async () => {
// Arrange
const actionHandler = jest.fn().mockResolvedValueOnce()
const updateTokenAction = jest.fn()
testStoreConfig.modules.conversationsStore.actions.deleteConversationFromServer = actionHandler
testStoreConfig.modules.tokenStore.getters.getToken = jest.fn().mockReturnValue(() => 'another-token')
testStoreConfig.modules.tokenStore.actions.updateToken = updateTokenAction
const buttons = await shallowMountAndOpenDialog('Delete conversation', 2)

const wrapper = shallowMount(Conversation, {
localVue,
store: new Vuex.Store(testStoreConfig),
mocks: {
$router,
},
stubs: {
NcActionButton,
NcDialog,
NcButton,
},
propsData: {
isSearchResult: false,
item,
},
})
const el = wrapper.findComponent({ name: 'NcListItem' })

const action = findNcActionButton(el, 'Delete conversation')
expect(action.exists()).toBe(true)

// Act 1 : click on the button from the menu
await action.find('button').trigger('click')

// Assert 1
const dialog = wrapper.findComponent({ name: 'NcDialog' })
expect(dialog.exists).toBeTruthy()
const buttons = dialog.findAllComponents({ name: 'NcButton' })
expect(buttons.exists()).toBeTruthy()
expect(buttons).toHaveLength(2)

// Act 2 : click on the confirm button
// Act: click on the 'decline' button
await buttons.at(0).find('button').trigger('click')

// Assert 2
// Assert
expect(actionHandler).not.toHaveBeenCalled()
expect($router.push).not.toHaveBeenCalled()
expect(updateTokenAction).not.toHaveBeenCalled()
Expand Down
Loading

0 comments on commit 456af8b

Please sign in to comment.