Skip to content

Commit

Permalink
Merge pull request #13915 from nextcloud/feat/13450/force-password-fo…
Browse files Browse the repository at this point in the history
…r-public-conversations

Feat: add option to force password in public conversations
  • Loading branch information
DorraJaouad authored Dec 6, 2024
2 parents b74785b + 4af1bc0 commit de72cb6
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 105 deletions.
173 changes: 114 additions & 59 deletions src/components/ConversationSettings/LinkShareSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,60 @@
{{ t('spreed', 'Allow guests to join this conversation via link') }}
</NcCheckboxRadioSwitch>

<NcCheckboxRadioSwitch v-show="isSharedPublicly"
:checked="isPasswordProtectionChecked"
:disabled="isSaving"
type="switch"
aria-describedby="link_share_settings_password_hint"
@update:checked="togglePassword">
{{ t('spreed', 'Password protection') }}
</NcCheckboxRadioSwitch>

<form v-if="showPasswordField" class="password-form" @submit.prevent="handleSetNewPassword">
<NcPasswordField ref="passwordField"
:value.sync="password"
autocomplete="new-password"
check-password-strength
<template v-if="isSharedPublicly">
<NcCheckboxRadioSwitch v-if="!forcePasswordProtection"
:checked="isPasswordProtectionChecked"
:disabled="isSaving"
class="password-form__input-field"
label-visible
:label="t('spreed', 'Enter new password')"
@valid="isValid = true"
@invalid="isValid = false" />
<NcButton :disabled="isSaving || !isValid"
type="primary"
native-type="submit"
class="password-form__button">
<template #icon>
<ArrowRight />
</template>
{{ t('spreed', 'Save password') }}
</NcButton>
</form>
type="switch"
aria-describedby="link_share_settings_password_hint"
@update:checked="togglePassword">
{{ t('spreed', 'Password protection') }}
</NcCheckboxRadioSwitch>
<template v-else>
<p v-if="isPasswordProtectionChecked" class="app-settings-section__hint">
{{ t('spreed', 'This conversation is password-protected. Guests need password to join') }}
</p>
<NcNoteCard v-else-if="!isSaving"
type="warning">
{{ t('spreed', 'Password protection is needed for public conversations') }}
<NcButton class="warning__button" type="primary" @click="enforcePassword">
{{ t('spreed', 'Set a password') }}
</NcButton>
</NcNoteCard>
</template>

<form v-if="showPasswordField" class="password-form" @submit.prevent="handleSetNewPassword">
<NcPasswordField ref="passwordField"
:value.sync="password"
autocomplete="new-password"
check-password-strength
:disabled="isSaving"
class="password-form__input-field"
label-visible
:label="t('spreed', 'Enter new password')"
@valid="isValid = true"
@invalid="isValid = false" />
<NcButton :disabled="isSaving || !isValid"
type="primary"
native-type="submit"
class="password-form__button">
<template #icon>
<IconContentSaveOutline />
</template>
{{ t('spreed', 'Save password') }}
</NcButton>
<NcButton v-if="password"
type="tertiary"
:aria-label="t('spreed', 'Copy password')"
:title="t('spreed', 'Copy password')"
class="password-form__button"
@click="copyPassword">
<template #icon>
<IconContentCopy :size="16" />
</template>
</NcButton>
</form>
</template>
</template>

<p v-else-if="isSharedPublicly">
Expand All @@ -64,15 +88,15 @@
<NcButton ref="copyLinkButton"
@click="handleCopyLink">
<template #icon>
<ClipboardTextOutline />
<IconClipboardTextOutline />
</template>
{{ t('spreed', 'Copy conversation link') }}
{{ t('spreed', 'Copy link') }}
</NcButton>
<NcButton v-if="isSharedPublicly && canModerate"
:disabled="isSendingInvitations"
@click="handleResendInvitations">
<template #icon>
<Email />
<IconEmail />
</template>
{{ t('spreed', 'Resend invitations') }}
</NcButton>
Expand All @@ -82,18 +106,22 @@
</template>

<script>
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import ClipboardTextOutline from 'vue-material-design-icons/ClipboardTextOutline.vue'
import Email from 'vue-material-design-icons/Email.vue'
import IconClipboardTextOutline from 'vue-material-design-icons/ClipboardTextOutline.vue'
import IconContentCopy from 'vue-material-design-icons/ContentCopy.vue'
import IconContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
import IconEmail from 'vue-material-design-icons/Email.vue'

import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'

import { CONVERSATION } from '../../constants.js'
import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import generatePassword from '../../utils/generatePassword.ts'
import { copyConversationLinkToClipboard } from '../../utils/handleUrl.ts'

export default {
Expand All @@ -103,9 +131,12 @@ export default {
NcButton,
NcCheckboxRadioSwitch,
NcPasswordField,
ArrowRight,
ClipboardTextOutline,
Email,
NcNoteCard,
// Icons
IconClipboardTextOutline,
IconContentCopy,
IconContentSaveOutline,
IconEmail,
},

props: {
Expand Down Expand Up @@ -148,6 +179,14 @@ export default {
isPasswordProtectionChecked() {
return this.conversation.hasPassword || this.showPasswordField
},

forcePasswordProtection() {
return this.supportForcePasswordProtection && getTalkConfig(this.token, 'conversations', 'force-passwords')
},

supportForcePasswordProtection() {
return hasTalkFeature(this.token, 'conversation-creation-password')
},
},

methods: {
Expand All @@ -164,37 +203,34 @@ export default {
async toggleGuests() {
const allowGuests = this.conversation.type !== CONVERSATION.TYPE.PUBLIC
this.isSaving = true
await this.$store.dispatch('toggleGuests', { token: this.token, allowGuests })
if (this.forcePasswordProtection && allowGuests) {
await this.togglePassword(allowGuests)
await this.$store.dispatch('toggleGuests', { token: this.token, allowGuests, password: this.password })
} else {
if (!allowGuests) {
await this.togglePassword(false)
}
await this.$store.dispatch('toggleGuests', { token: this.token, allowGuests })
}
this.isSaving = false
},

async togglePassword(checked) {
if (checked) {
// Generate a random password
this.password = await generatePassword()
this.showPasswordField = true
await this.handlePasswordEnable()
this.$nextTick(() => {
this.$refs.passwordField.focus()
})
} else {
// disable the password protection for the current conversation
if (this.conversation.hasPassword) {
await this.setConversationPassword('')
}
this.password = ''
this.showPasswordField = false
await this.handlePasswordDisable()
this.isValid = true
}
},

async handlePasswordDisable() {
// disable the password protection for the current conversation
if (this.conversation.hasPassword) {
await this.setConversationPassword('')
}
this.password = ''
this.showPasswordField = false
this.isValid = true
},

async handlePasswordEnable() {
this.showPasswordField = true
},

async handleSetNewPassword() {
if (this.isValid) {
await this.setConversationPassword(this.password)
Expand All @@ -212,6 +248,21 @@ export default {
await this.$store.dispatch('resendInvitations', { token: this.token })
this.isSendingInvitations = false
},

async copyPassword() {
try {
await navigator.clipboard.writeText(this.password)
showSuccess(t('spreed', 'Password copied to clipboard'))
} catch (error) {
showError(t('spreed', 'Password could not be copied'))
}
},

async enforcePassword() {
// Turn on password protection and set a password
await this.togglePassword(true)
await this.$store.dispatch('toggleGuests', { token: this.token, allowGuests: true, password: this.password })
}
},
}
</script>
Expand All @@ -232,7 +283,7 @@ button > .material-design-icon {
gap: 8px;
align-items: flex-start;

&__input-field {
:deep(.input-field) {
width: 200px;
}

Expand All @@ -241,6 +292,10 @@ button > .material-design-icon {
}
}

.warning__button {
margin-top: var(--default-grid-baseline);
}

.app-settings-subsection__buttons {
display: flex;
gap: 8px;
Expand Down
46 changes: 24 additions & 22 deletions src/components/NewConversationDialog/NewConversationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,18 @@
</template>

<template #action>
<NcButton v-if="(error || isPublic) && !isLoading"
ref="closeButton"
type="tertiary"
@click="closeModal">
{{ t('spreed', 'Close') }}
</NcButton>
<NcButton v-if="!error && success && isPublic"
id="copy-link"
ref="copyLink"
type="secondary"
@click="onClickCopyLink">
{{ t('spreed', 'Copy conversation link') }}
{{ t('spreed', 'Copy link') }}
</NcButton>
<NcButton v-if="!error && success && isPublic && newConversation.hasPassword"
id="copy-password"
type="secondary"
@click="onClickCopyPassword">
{{ t('spreed', 'Copy password') }}
</NcButton>
</template>
</NcEmptyContent>
Expand All @@ -110,6 +110,7 @@ import { provide, ref } from 'vue'
import AlertCircle from 'vue-material-design-icons/AlertCircle.vue'
import Check from 'vue-material-design-icons/Check.vue'

import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
Expand All @@ -123,7 +124,6 @@ import LoadingComponent from '../LoadingComponent.vue'
import { useId } from '../../composables/useId.ts'
import { useIsInCall } from '../../composables/useIsInCall.js'
import { CONVERSATION } from '../../constants.js'
import { setConversationPassword } from '../../services/conversationsService.js'
import { addParticipant } from '../../services/participantsService.js'
import { copyConversationLinkToClipboard } from '../../utils/handleUrl.ts'

Expand Down Expand Up @@ -184,6 +184,7 @@ export default {
isLoading: true,
success: false,
error: false,
errorReason: '',
password: '',
listable: CONVERSATION.LISTABLE.NONE,
isAvatarEdited: false,
Expand Down Expand Up @@ -216,6 +217,9 @@ export default {
if (this.isLoading) {
return t('spreed', 'Creating the conversation …')
} else if (this.error) {
if (this.errorReason === 'password_required') {
return t('spreed', 'Error: A password is required to create the conversation.')
}
return t('spreed', 'Error while creating the conversation')
} else if (this.success && this.isPublic) {
return t('spreed', 'All set, the conversation "{conversationName}" was created.', { conversationName: this.conversationName })
Expand All @@ -226,22 +230,13 @@ export default {

watch: {
success(value) {
if (!value) {
if (!value || !this.isPublic) {
return
}
this.$nextTick(() => {
this.$refs.copyLink.$el.focus()
})
},

error(value) {
if (!value) {
return
}
this.$nextTick(() => {
this.$refs.closeButton.$el.focus()
})
},
},

expose: ['showModalForItem', 'showModal'],
Expand Down Expand Up @@ -298,15 +293,12 @@ export default {
this.newConversation.token = await this.$store.dispatch('createGroupConversation', {
conversationName: this.conversationName,
isPublic: this.isPublic,
password: this.password,
})

// Gather all secondary requests to run in parallel
const promises = []

if (this.isPublic && this.password && this.newConversation.hasPassword) {
promises.push(setConversationPassword(this.newConversation.token, this.password))
}

if (this.isAvatarEdited) {
promises.push(this.$refs.setupPage.$refs.conversationAvatar.saveAvatar())
}
Expand Down Expand Up @@ -334,6 +326,7 @@ export default {
console.error('Error creating new conversation: ', exception)
this.isLoading = false
this.error = true
this.errorReason = exception.message
// Stop the execution of the method on exceptions.
return
}
Expand Down Expand Up @@ -367,6 +360,15 @@ export default {
copyConversationLinkToClipboard(this.newConversation.token)
},

async onClickCopyPassword() {
try {
await navigator.clipboard.writeText(this.password)
showSuccess(t('spreed', 'Password copied to clipboard'))
} catch (error) {
showError(t('spreed', 'Password could not be copied'))
}
},

setIsPasswordValid(value) {
this.isPasswordValid = value
},
Expand Down
Loading

0 comments on commit de72cb6

Please sign in to comment.