зеркало из https://github.com/nextcloud/spreed.git
Merge pull request #2857 from nextcloud/feature/2546/improve-new-group-creation
Improve new group conversation feature
This commit is contained in:
Коммит
52add20363
|
@ -21,6 +21,7 @@
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Tooltip -->
|
||||
<Popover trigger="hover" placement="bottom">
|
||||
<Actions slot="trigger">
|
||||
<ActionButton
|
||||
|
@ -30,21 +31,27 @@
|
|||
</Actions>
|
||||
<p>{{ t('spreed','Create a new group conversation') }}</p>
|
||||
</Popover>
|
||||
<!-- New group form -->
|
||||
<Modal
|
||||
v-if="modal"
|
||||
size="full"
|
||||
@close="closeModal">
|
||||
<!-- Wrapper for content & navigation -->
|
||||
<div
|
||||
class="new-group-conversation talk-modal">
|
||||
<!-- Content -->
|
||||
<div
|
||||
class="new-group-conversation__content">
|
||||
<!-- First page -->
|
||||
<template
|
||||
v-if="page === 0">
|
||||
<SetConversationName
|
||||
v-model="conversationNameInput" />
|
||||
v-model="conversationNameInput"
|
||||
@clickEnter="handleEnter" />
|
||||
<SetConversationType
|
||||
v-model="isPublic"
|
||||
:conversation-name="conversationName" />
|
||||
<!-- Password protection -->
|
||||
<template v-if="isPublic">
|
||||
<input
|
||||
id="password-checkbox"
|
||||
|
@ -58,11 +65,13 @@
|
|||
v-model="password" />
|
||||
</template>
|
||||
</template>
|
||||
<!-- Second page -->
|
||||
<template v-if="page === 1">
|
||||
<SetContacts
|
||||
:conversation-name="conversationName"
|
||||
@updateSelectedParticipants="handleUpdateSelectedParticipants" />
|
||||
</template>
|
||||
<!-- Third page -->
|
||||
<template v-if="page === 2">
|
||||
<Confirmation
|
||||
:conversation-name="conversationName"
|
||||
|
@ -73,27 +82,32 @@
|
|||
:link-to-conversation="linkToConversation" />
|
||||
</template>
|
||||
</div>
|
||||
<!-- Navigation: different buttons with different actions and
|
||||
placement are rendered depending on the current page -->
|
||||
<div
|
||||
class="navigation">
|
||||
<!-- First page -->
|
||||
<button
|
||||
v-if="page===0"
|
||||
class="navigation__button-right primary"
|
||||
:disabled="disabled"
|
||||
@click="handleSetConversationName">
|
||||
{{ t('spreed', 'Add participants') }}
|
||||
</button>
|
||||
<!-- Second page -->
|
||||
<button
|
||||
v-if="page===1"
|
||||
class="navigation__button-left"
|
||||
@click="handleClickBack">
|
||||
{{ t('spreed', 'Back') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="page===0"
|
||||
class="navigation__button-right primary"
|
||||
:disabled="disabled"
|
||||
@click="handleClickForward">
|
||||
{{ t('spreed', 'Add participants') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="page===1"
|
||||
class="navigation__button-right primary"
|
||||
@click="handleCreateConversation">
|
||||
{{ t('spreed', 'Create conversation') }}
|
||||
</button>
|
||||
<!-- Third page -->
|
||||
<button
|
||||
v-if="page===2 && (error || isPublic)"
|
||||
class="navigation__button-right primary"
|
||||
|
@ -158,14 +172,17 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
// Trims whitespaces from the input string
|
||||
conversationName() {
|
||||
return this.conversationNameInput.trim()
|
||||
},
|
||||
// Generates the link to the current conversation
|
||||
linkToConversation() {
|
||||
if (this.token !== '') {
|
||||
return window.location.protocol + '//' + window.location.host + generateUrl('/call/' + this.token)
|
||||
} else return ''
|
||||
},
|
||||
// Controls the disabled/enabled state of the first page's button.
|
||||
disabled() {
|
||||
return this.conversationName === '' || (this.passwordProtect && this.password === '')
|
||||
},
|
||||
|
@ -175,7 +192,9 @@ export default {
|
|||
showModal() {
|
||||
this.modal = true
|
||||
},
|
||||
// Resets to the base state of the component
|
||||
/** Reinitialise the component to it's initial state. This is necessary
|
||||
* because once the component is mounted it's data would persist even if
|
||||
* the modal closes */
|
||||
closeModal() {
|
||||
this.modal = false
|
||||
this.page = 0
|
||||
|
@ -186,32 +205,25 @@ export default {
|
|||
this.selectedParticipants = []
|
||||
this.success = false
|
||||
this.error = false
|
||||
this.passwordProtect = false
|
||||
this.password = ''
|
||||
},
|
||||
handleSetConversationName(event) {
|
||||
/** Switch to page 2 */
|
||||
handleSetConversationName() {
|
||||
this.page = 1
|
||||
},
|
||||
|
||||
handleSetConversationType(event) {
|
||||
this.isPublic = event
|
||||
},
|
||||
|
||||
handleClickForward() {
|
||||
if (this.page === 0) {
|
||||
if (this.conversationName !== '') {
|
||||
this.page = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
/** Switch to page 1 from page 2 */
|
||||
handleClickBack() {
|
||||
this.page = 0
|
||||
},
|
||||
|
||||
handleUpdateSelectedParticipants(e) {
|
||||
console.debug(e)
|
||||
this.selectedParticipants = e
|
||||
/** Updates the selected participants array
|
||||
* @param {array} participants the participants array
|
||||
*/
|
||||
handleUpdateSelectedParticipants(participants) {
|
||||
this.selectedParticipants = participants
|
||||
},
|
||||
|
||||
/** Handles the creation of the group conversation, adds the seleced
|
||||
* participants to it and routes to it */
|
||||
async handleCreateConversation() {
|
||||
this.page = 2
|
||||
if (this.isPublic) {
|
||||
|
@ -257,14 +269,16 @@ export default {
|
|||
this.closeModal()
|
||||
}
|
||||
},
|
||||
|
||||
/** Creates a new private conversation, adds it to the store and sets
|
||||
* the local token value to the newly created conversation's token */
|
||||
async createPrivateConversation() {
|
||||
const response = await createPrivateConversation(this.conversationName)
|
||||
const conversation = response.data.ocs.data
|
||||
this.$store.dispatch('addConversation', conversation)
|
||||
this.token = conversation.token
|
||||
},
|
||||
|
||||
/** Creates a new public conversation, adds it to the store and sets
|
||||
* the local token value to the newly created conversation's token */
|
||||
async createPublicConversation() {
|
||||
const response = await createPublicConversation(this.conversationName)
|
||||
const conversation = response.data.ocs.data
|
||||
|
@ -282,6 +296,13 @@ export default {
|
|||
this.password = ''
|
||||
}
|
||||
},
|
||||
/** Handles the press of the enter key */
|
||||
handleEnter() {
|
||||
if (!this.disabled) {
|
||||
this.handleSetConversationName()
|
||||
this.page = 1
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<input
|
||||
ref="password"
|
||||
v-observe-visibility="visibilityChanged"
|
||||
type="text"
|
||||
type="password"
|
||||
:value="value"
|
||||
class="password-protect"
|
||||
:placeholder="t('spreed', 'Choose a password')"
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
<template>
|
||||
<div class="set-contacts">
|
||||
<!-- Search -->
|
||||
<div class="icon-search" />
|
||||
<input
|
||||
ref="setContacts"
|
||||
v-model="searchText"
|
||||
|
@ -29,23 +31,21 @@
|
|||
type="text"
|
||||
:placeholder="t('spreed', 'Search participants')"
|
||||
@input="handleInput">
|
||||
<template v-if="isSearching">
|
||||
<Caption
|
||||
:title="t('spreed', 'Select participants')" />
|
||||
<ParticipantsList
|
||||
:add-on-click="false"
|
||||
height="250px"
|
||||
:loading="contactsLoading"
|
||||
:no-results="noResults"
|
||||
:items="searchResults"
|
||||
@updateSelectedParticipants="handleUpdateSelectedParticipants" />
|
||||
</template>
|
||||
<template v-if="!isSearching">
|
||||
<div class="icon-contacts-dark set-contacts__icon" />
|
||||
<p class="set-contacts__hint">
|
||||
{{ t('spreed', 'Search participants') }}
|
||||
</p>
|
||||
</template>
|
||||
<!-- Loading state -->
|
||||
<Caption v-if="contactsLoading"
|
||||
:title="t('spreed', 'Loading contacts')" />
|
||||
<!-- List of possilbe participants -->
|
||||
<Caption v-if="!contactsLoading"
|
||||
:title="t('spreed', 'Select participants')" />
|
||||
<ParticipantsList
|
||||
:add-on-click="false"
|
||||
height="200px"
|
||||
:loading="contactsLoading"
|
||||
:no-results="noResults"
|
||||
:items="searchResults"
|
||||
:display-search-hint="!contactsLoading"
|
||||
@updateSelectedParticipants="handleUpdateSelectedParticipants"
|
||||
@clickSearchHint="focusInput" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -73,20 +73,20 @@ export default {
|
|||
return {
|
||||
searchText: '',
|
||||
searchResults: [],
|
||||
contactsLoading: false,
|
||||
// The loading state is true when the component is initialised as we perform a search for 'contacts'
|
||||
// with an empty screen as search text.
|
||||
contactsLoading: true,
|
||||
noResults: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isSearching() {
|
||||
return this.searchText !== ''
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
async mounted() {
|
||||
// Focus the input field of the current component.
|
||||
this.$refs.setContacts.focus()
|
||||
this.focusInput()
|
||||
// Perform a search with an empty string
|
||||
await this.fetchSearchResults()
|
||||
// Once the contacts are fetched, remove the spinner.
|
||||
this.contactsLoading = false
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -98,9 +98,7 @@ export default {
|
|||
},
|
||||
|
||||
debounceFetchSearchResults: debounce(function() {
|
||||
if (this.isSearching) {
|
||||
this.fetchSearchResults()
|
||||
}
|
||||
this.fetchSearchResults()
|
||||
}, 250),
|
||||
|
||||
async fetchSearchResults() {
|
||||
|
@ -116,16 +114,22 @@ export default {
|
|||
OCP.Toast.error(t('spreed', 'An error occurred while performing the search'))
|
||||
}
|
||||
},
|
||||
// Forward the event from the children to the parent
|
||||
/**
|
||||
* Forward the event from the children to the parent with thenew selected participants
|
||||
* @param {array} selectedParticipants the selected participants array
|
||||
*/
|
||||
handleUpdateSelectedParticipants(selectedParticipants) {
|
||||
this.$emit('updateSelectedParticipants', selectedParticipants)
|
||||
},
|
||||
visibilityChanged(isVisible) {
|
||||
if (isVisible) {
|
||||
// Focus the input field of the current component.
|
||||
this.$refs.setContacts.focus()
|
||||
this.focusInput()
|
||||
}
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.setContacts.focus()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -136,6 +140,8 @@ export default {
|
|||
&__input {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding-left: 28px;
|
||||
line-height: 34px;
|
||||
}
|
||||
&__icon {
|
||||
margin-top: 40px;
|
||||
|
@ -146,4 +152,9 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
:value="value"
|
||||
class="conversation-name"
|
||||
:placeholder="t('spreed', 'Conversation name')"
|
||||
@input="handleInput">
|
||||
@input="handleInput"
|
||||
@keydown.enter="handleKeydown">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -53,6 +54,10 @@ export default {
|
|||
this.$refs.conversationName.focus()
|
||||
}
|
||||
},
|
||||
// Forward the keydown event to the parent
|
||||
handleKeydown() {
|
||||
this.$emit('clickEnter')
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
@ -64,5 +69,7 @@ export default {
|
|||
.conversation-name {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 34px;
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
export default {
|
||||
name: 'SetConversationType',
|
||||
// The value of the checkbox
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
|
@ -42,6 +43,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
/** Emits the input event with the checked bulean as a value
|
||||
* @param {object} event The checkbox click event object.
|
||||
*/
|
||||
handleInput(event) {
|
||||
this.$emit('input', event.target.checked)
|
||||
},
|
||||
|
|
|
@ -237,6 +237,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
// Used to allow selecting participants in a search.
|
||||
handleClick() {
|
||||
if (this.isSearched) {
|
||||
this.$emit('clickParticipant', this.participant)
|
||||
|
|
|
@ -22,13 +22,27 @@
|
|||
<template>
|
||||
<div>
|
||||
<ul v-if="(!loading || addOnClick) && !noResults"
|
||||
:class="{'scrollable': scrollable }"
|
||||
:style="{'height': height}">
|
||||
:style="{'height': height}"
|
||||
:class="{'scrollable': scrollable }">
|
||||
<Participant
|
||||
v-for="participant in participants"
|
||||
:key="participant.userId"
|
||||
:participant="participant"
|
||||
@clickParticipant="handleClickParticipant" />
|
||||
<!-- 'search for more' empty content to display at the end of the
|
||||
participants list, this is useful in case the participants list is used
|
||||
to display the results of a search. Upon clicking on it, an event is
|
||||
emitted to the parent component in order to be able to focus on it's
|
||||
input field -->
|
||||
<li
|
||||
v-if="displaySearchHint"
|
||||
class="participants-list__hint"
|
||||
@click="handleClickHint">
|
||||
<div class="icon-contacts-dark set-contacts__icon" />
|
||||
<p>
|
||||
{{ t('spreed', 'Search for more contacts') }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<template v-if="loading">
|
||||
<template v-if="addOnClick">
|
||||
|
@ -106,6 +120,13 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Display 'search for more' empty content at the end of the list.
|
||||
*/
|
||||
displaySearchHint: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -180,6 +201,10 @@ export default {
|
|||
}
|
||||
|
||||
},
|
||||
handleClickHint() {
|
||||
this.$emit('clickSearchHint')
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -198,6 +223,14 @@ export default {
|
|||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
&__hint {
|
||||
margin: 20px 0;
|
||||
cursor: pointer;
|
||||
p {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
Загрузка…
Ссылка в новой задаче