diff --git a/src/App.vue b/src/App.vue index b8457d26d27..d8d3dc66916 100644 --- a/src/App.vue +++ b/src/App.vue @@ -378,6 +378,10 @@ export default { } }) + EventBus.on('forbidden-route', (params) => { + this.$router.push({ name: 'forbidden' }) + }) + /** * Listens to the conversationsReceived globalevent, emitted by the conversationsList * component each time a new batch of conversations is received and processed in diff --git a/src/__mocks__/router.js b/src/__mocks__/router.js index 613e74aa4cc..f62163e43a8 100644 --- a/src/__mocks__/router.js +++ b/src/__mocks__/router.js @@ -24,6 +24,12 @@ export default new VueRouter({ component: Stub, props: true, }, + { + path: '/apps/spreed/forbidden', + name: 'forbidden', + component: Stub, + props: true, + }, { path: '/apps/spreed/duplicate-session', name: 'duplicatesession', diff --git a/src/components/ConversationSettings/BanSettings/BanSettings.vue b/src/components/ConversationSettings/BanSettings/BanSettings.vue new file mode 100644 index 00000000000..cc40d448728 --- /dev/null +++ b/src/components/ConversationSettings/BanSettings/BanSettings.vue @@ -0,0 +1,132 @@ + + + + + + + diff --git a/src/components/ConversationSettings/BanSettings/BannedItem.vue b/src/components/ConversationSettings/BanSettings/BannedItem.vue new file mode 100644 index 00000000000..cddd48399db --- /dev/null +++ b/src/components/ConversationSettings/BanSettings/BannedItem.vue @@ -0,0 +1,101 @@ + + + + + + + diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 3720cc789a5..782e9b86917 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -40,6 +40,7 @@ + @@ -100,6 +101,7 @@ import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDia import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import BanSettings from './BanSettings/BanSettings.vue' import BasicInfo from './BasicInfo.vue' import BotsSettings from './BotsSettings.vue' import BreakoutRoomsSettings from './BreakoutRoomsSettings.vue' @@ -123,6 +125,7 @@ export default { name: 'ConversationSettingsDialog', components: { + BanSettings, BasicInfo, BotsSettings, BreakoutRoomsSettings, @@ -180,6 +183,10 @@ export default { return (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) }, + supportBanV1() { + return hasTalkFeature(this.token, 'ban-v1') + }, + showMediaSettings() { return this.settingsStore.getShowMediaSettings(this.token) }, diff --git a/src/components/RightSidebar/Participants/Participant.spec.js b/src/components/RightSidebar/Participants/Participant.spec.js index 6dc3eaf8804..b69bad0e19f 100644 --- a/src/components/RightSidebar/Participants/Participant.spec.js +++ b/src/components/RightSidebar/Participants/Participant.spec.js @@ -14,7 +14,10 @@ import VideoIcon from 'vue-material-design-icons/Video.vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' +import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import Participant from './Participant.vue' import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue' @@ -107,7 +110,10 @@ describe('Participant.vue', () => { stubs: { NcActionButton, NcButton, + NcCheckboxRadioSwitch, NcDialog, + NcInputField, + NcTextField, }, directives: { tooltip: tooltipMock, @@ -639,6 +645,8 @@ describe('Participant.vue', () => { expect(removeAction).toHaveBeenCalledWith(expect.anything(), { token: 'current-token', attendeeId: 'alice-attendee-id', + banParticipant: false, + internalNote: '', }) } @@ -651,6 +659,56 @@ describe('Participant.vue', () => { expect(actionButton.exists()).toBe(false) } + /** + * @param {string} buttonText Label of the remove action to find + * @param {string} internalNote text of provided note + */ + async function testCanBan(buttonText = 'Remove participant', internalNote = 'test note') { + const wrapper = mountParticipant(participant) + const actionButton = findNcActionButton(wrapper, buttonText) + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + const dialog = wrapper.findComponent(NcDialog) + expect(dialog.exists()).toBeTruthy() + + const checkbox = dialog.findComponent(NcCheckboxRadioSwitch) + await checkbox.find('input').trigger('change') + + const input = dialog.findComponent(NcTextField) + expect(input.exists()).toBeTruthy() + input.find('input').setValue(internalNote) + await input.find('input').trigger('change') + + const button = findNcButton(dialog, 'Remove') + await button.find('button').trigger('click') + + expect(removeAction).toHaveBeenCalledWith(expect.anything(), { + token: 'current-token', + attendeeId: 'alice-attendee-id', + banParticipant: true, + internalNote + }) + } + + /** + * @param {string} buttonText Label of the remove action to find + */ + async function testCannotBan(buttonText = 'Remove participant') { + const wrapper = mountParticipant(participant) + const actionButton = findNcActionButton(wrapper, buttonText) + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + const dialog = wrapper.findComponent(NcDialog) + expect(dialog.exists()).toBeTruthy() + + const checkbox = dialog.findComponent(NcCheckboxRadioSwitch) + expect(checkbox.exists()).toBeFalsy() + } + test('allows a moderator to remove a moderator', async () => { conversation.participantType = PARTICIPANT.TYPE.MODERATOR participant.participantType = PARTICIPANT.TYPE.MODERATOR @@ -707,6 +765,30 @@ describe('Participant.vue', () => { conversation.participantType = PARTICIPANT.TYPE.USER await testCannotRemove() }) + + test('allows a moderator to ban a moderator', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.USER + await testCanBan() + }) + + test('allows a moderator to ban a guest', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.GUEST + await testCanBan() + }) + + test('does not allow a moderator to ban a moderator', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.MODERATOR + await testCannotBan() + }) + + test('does not allow a moderator to ban a group', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.actorType = ATTENDEE.ACTOR_TYPE.GROUPS + await testCannotBan('Remove group and members') + }) }) describe('dial-in PIN', () => { /** diff --git a/src/components/RightSidebar/Participants/Participant.vue b/src/components/RightSidebar/Participants/Participant.vue index 3d344ee2e25..921258c3c02 100644 --- a/src/components/RightSidebar/Participants/Participant.vue +++ b/src/components/RightSidebar/Participants/Participant.vue @@ -313,6 +313,17 @@ :name="removeParticipantLabel" :container="container">

{{ removeDialogMessage }}

+