Browse Source

Merge pull request #11893 from nextcloud/feat/11632/separate-new-conv

feat(LeftSidebar): rearrange conversation fast creating options
pull/11941/head
Maksim Sukharev 2 years ago
committed by GitHub
parent
commit
2a80deea95
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/components/LeftSidebar/CallPhoneDialog/CallPhoneDialog.vue
  2. 93
      src/components/LeftSidebar/LeftSidebar.vue
  3. 6
      src/components/NewConversationDialog/NewConversationContactsPage.vue
  4. 2
      src/components/RightSidebar/Participants/Participant.vue
  5. 41
      src/components/RightSidebar/Participants/ParticipantsTab.vue
  6. 47
      src/composables/useArrowNavigation.js

2
src/components/LeftSidebar/CallPhoneDialog/CallPhoneDialog.vue

@ -67,9 +67,9 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import DialpadPanel from '../../UIShared/DialpadPanel.vue'
import LoadingComponent from '../../LoadingComponent.vue'
import SelectPhoneNumber from '../../SelectPhoneNumber.vue'
import DialpadPanel from '../../UIShared/DialpadPanel.vue'
import { CONVERSATION, PARTICIPANT } from '../../../constants.js'
import { callSIPDialOut } from '../../../services/callsService.js'

93
src/components/LeftSidebar/LeftSidebar.vue

@ -182,28 +182,28 @@
<!-- Search results -->
<ul v-else class="h-100 scroller">
<!-- Search results: user's conversations -->
<NcAppNavigationCaption :name="t('spreed', 'Conversations')" />
<Conversation v-for="item of searchResultsConversationList"
:key="`conversation_${item.id}`"
:ref="`conversation-${item.token}`"
:item="item"
@click="abortSearch" />
<Hint v-if="searchResultsConversationList.length === 0" :hint="t('spreed', 'No matches found')" />
<!-- Create a new conversation -->
<NcListItem v-if="canStartConversations"
:name="t('spreed', 'Create a new conversation')"
:name="searchText"
data-nav-id="conversation_create_new"
@click="createConversation(searchText)">
<template #icon>
<ChatPlus :size="30" />
<ChatPlus :size="44" />
</template>
<template #subname>
{{ searchText }}
{{ t('spreed', 'New group conversation') }}
</template>
</NcListItem>
<!-- Search results: user's conversations -->
<NcAppNavigationCaption :name="t('spreed', 'Conversations')" />
<Conversation v-for="item of searchResultsConversationList"
:key="`conversation_${item.id}`"
:ref="`conversation-${item.token}`"
:item="item"
@click="abortSearch" />
<Hint v-if="searchResultsConversationList.length === 0" :hint="t('spreed', 'No matches found')" />
<!-- Search results: listed (open) conversations -->
<template v-if="!listedConversationsLoading && searchResultsListedConversations.length !== 0">
<NcAppNavigationCaption :name="t('spreed', 'Open conversations')" />
@ -223,7 +223,10 @@
:name="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)" />
<AvatarWrapper v-bind="iconData(item)" />
</template>
<template #subname>
{{ t('spreed', 'New private conversation') }}
</template>
</NcListItem>
</template>
@ -241,6 +244,9 @@
<template #icon>
<ConversationIcon :item="iconData(item)" />
</template>
<template #subname>
{{ t('spreed', 'New group conversation') }}
</template>
</NcListItem>
</template>
@ -255,14 +261,36 @@
<template #icon>
<ConversationIcon :item="iconData(item)" />
</template>
<template #subname>
{{ t('spreed', 'New group conversation') }}
</template>
</NcListItem>
</template>
<!-- New conversations: Federated users -->
<template v-if="searchResultsFederated.length !== 0">
<NcAppNavigationCaption :name="t('spreed', 'Federated users')" />
<NcListItem v-for="item of searchResultsFederated"
:key="`federated_${item.id}`"
:data-nav-id="`federated_${item.id}`"
:name="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<AvatarWrapper v-bind="iconData(item)" />
</template>
<template #subname>
{{ t('spreed', 'New group conversation') }}
</template>
</NcListItem>
</template>
</template>
<!-- Search results: no results (yet) -->
<NcAppNavigationCaption v-if="sourcesWithoutResults" :name="sourcesWithoutResultsList" />
<Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
<Hint v-else :hint="t('spreed', 'No search results')" />
<template v-if="sourcesWithoutResults">
<NcAppNavigationCaption :name="sourcesWithoutResultsList" />
<Hint :hint="t('spreed', 'No search results')" />
</template>
<Hint v-else-if="contactsLoading" :hint="t('spreed', 'Loading')" />
</ul>
</li>
</template>
@ -316,10 +344,11 @@ import Conversation from './ConversationsList/Conversation.vue'
import ConversationsListVirtual from './ConversationsList/ConversationsListVirtual.vue'
import InvitationHandler from './InvitationHandler.vue'
import OpenConversationsList from './OpenConversationsList/OpenConversationsList.vue'
import SearchBox from '../UIShared/SearchBox.vue'
import AvatarWrapper from '../AvatarWrapper/AvatarWrapper.vue'
import ConversationIcon from '../ConversationIcon.vue'
import Hint from '../UIShared/Hint.vue'
import NewConversationDialog from '../NewConversationDialog/NewConversationDialog.vue'
import Hint from '../UIShared/Hint.vue'
import SearchBox from '../UIShared/SearchBox.vue'
import TransitionWrapper from '../UIShared/TransitionWrapper.vue'
import { useArrowNavigation } from '../../composables/useArrowNavigation.js'
@ -349,6 +378,7 @@ export default {
name: 'LeftSidebar',
components: {
AvatarWrapper,
CallPhoneDialog,
InvitationHandler,
NcAppNavigation,
@ -389,7 +419,7 @@ export default {
const federationStore = useFederationStore()
const talkHashStore = useTalkHashStore()
const { initializeNavigation, resetNavigation } = useArrowNavigation(leftSidebar, searchBox, '.list-item')
const { initializeNavigation, resetNavigation } = useArrowNavigation(leftSidebar, searchBox)
const isMobile = useIsMobile()
return {
@ -413,6 +443,7 @@ export default {
searchResultsUsers: [],
searchResultsGroups: [],
searchResultsCircles: [],
searchResultsFederated: [],
searchResultsListedConversations: [],
contactsLoading: false,
listedConversationsLoading: false,
@ -703,6 +734,10 @@ export default {
})
this.searchResultsGroups = this.searchResults.filter((match) => match.source === ATTENDEE.ACTOR_TYPE.GROUPS)
this.searchResultsCircles = this.searchResults.filter((match) => match.source === ATTENDEE.ACTOR_TYPE.CIRCLES)
this.searchResultsFederated = this.searchResults.filter((match) => match.source === ATTENDEE.ACTOR_TYPE.REMOTES)
.map((item) => {
return { ...item, source: ATTENDEE.ACTOR_TYPE.FEDERATED_USERS }
})
this.contactsLoading = false
} catch (exception) {
if (CancelableRequest.isCancel(exception)) {
@ -960,11 +995,15 @@ export default {
},
iconData(item) {
if (item.source === ATTENDEE.ACTOR_TYPE.USERS) {
if (item.source === ATTENDEE.ACTOR_TYPE.USERS
|| item.source === ATTENDEE.ACTOR_TYPE.FEDERATED_USERS) {
return {
type: CONVERSATION.TYPE.ONE_TO_ONE,
displayName: item.label,
name: item.id,
id: item.id,
name: item.label,
source: item.source,
disableMenu: true,
token: 'new',
showUserStatus: true,
}
}
return {
@ -1074,6 +1113,14 @@ export default {
padding: 0 !important;
}
:deep(.app-navigation-caption):not(:first-child) {
margin-top: 12px !important;
}
:deep(.app-navigation-caption__name) {
margin: 0 !important;
}
:deep(.list-item) {
overflow: hidden;
outline-offset: -2px;

6
src/components/NewConversationDialog/NewConversationContactsPage.vue

@ -83,10 +83,10 @@ import { showError } from '@nextcloud/dialogs'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import ContactSelectionBubble from '../UIShared/ContactSelectionBubble.vue'
import DialpadPanel from '../UIShared/DialpadPanel.vue'
import ParticipantSearchResults from '../RightSidebar/Participants/ParticipantsSearchResults.vue'
import SelectPhoneNumber from '../SelectPhoneNumber.vue'
import ContactSelectionBubble from '../UIShared/ContactSelectionBubble.vue'
import DialpadPanel from '../UIShared/DialpadPanel.vue'
import TransitionWrapper from '../UIShared/TransitionWrapper.vue'
import { useArrowNavigation } from '../../composables/useArrowNavigation.js'
@ -131,7 +131,7 @@ export default {
const wrapper = ref(null)
const setContacts = ref(null)
const { initializeNavigation, resetNavigation } = useArrowNavigation(wrapper, setContacts, '.participant-row')
const { initializeNavigation, resetNavigation } = useArrowNavigation(wrapper, setContacts)
return {
initializeNavigation,

2
src/components/RightSidebar/Participants/Participant.vue

@ -31,7 +31,7 @@
'selected': isSelected }"
:aria-label="participantAriaLabel"
:role="isSearched ? 'listitem' : undefined"
:tabindex="isSearched ? 0 : undefined"
:tabindex="0"
v-on="isSearched ? { click: handleClick, 'keydown.enter': handleClick } : {}"
@keydown.enter="handleClick">
<!-- Participant's avatar -->

41
src/components/RightSidebar/Participants/ParticipantsTab.vue

@ -20,9 +20,10 @@
-->
<template>
<div class="wrapper">
<div ref="wrapper" class="wrapper">
<div class="search-form">
<SearchBox v-if="canSearch"
ref="searchBox"
class="search-form__input"
:value.sync="searchText"
:is-focused.sync="isFocused"
@ -46,15 +47,17 @@
:participants="participants"
:loading="!participantsInitialised" />
<div v-else class="scroller">
<div v-else class="scroller h-100">
<NcAppNavigationCaption v-if="canAdd" :name="t('spreed', 'Participants')" />
<ParticipantsList v-if="filteredParticipants.length"
class="known-results"
:items="filteredParticipants"
:loading="!participantsInitialised" />
<Hint v-else :hint="t('spreed', 'No search results')" />
<ParticipantsSearchResults v-if="canAdd"
class="search-results"
:search-results="searchResults"
:contacts-loading="contactsLoading"
:no-results="noResults"
@ -66,7 +69,7 @@
<script>
import debounce from 'debounce'
import { toRefs } from 'vue'
import { ref, toRefs } from 'vue'
import { getCapabilities } from '@nextcloud/capabilities'
import { showError } from '@nextcloud/dialogs'
@ -77,11 +80,12 @@ import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigati
import ParticipantsList from './ParticipantsList.vue'
import ParticipantsListVirtual from './ParticipantsListVirtual.vue'
import ParticipantsSearchResults from './ParticipantsSearchResults.vue'
import SelectPhoneNumber from '../../SelectPhoneNumber.vue'
import DialpadPanel from '../../UIShared/DialpadPanel.vue'
import Hint from '../../UIShared/Hint.vue'
import SearchBox from '../../UIShared/SearchBox.vue'
import SelectPhoneNumber from '../../SelectPhoneNumber.vue'
import { useArrowNavigation } from '../../../composables/useArrowNavigation.js'
import { useGetParticipants } from '../../../composables/useGetParticipants.js'
import { useIsInCall } from '../../../composables/useIsInCall.js'
import { useSortParticipants } from '../../../composables/useSortParticipants.js'
@ -125,12 +129,20 @@ export default {
},
setup(props) {
const wrapper = ref(null)
const searchBox = ref(null)
const { isActive } = toRefs(props)
const { sortParticipants } = useSortParticipants()
const isInCall = useIsInCall()
const { cancelableGetParticipants } = useGetParticipants(isActive, false)
const { initializeNavigation, resetNavigation } = useArrowNavigation(wrapper, searchBox)
return {
initializeNavigation,
resetNavigation,
wrapper,
searchBox,
sortParticipants,
isInCall,
cancelableGetParticipants,
@ -253,7 +265,7 @@ export default {
if (!this.isSearching) {
return
}
this.resetNavigation()
try {
this.cancelSearchPossibleConversations('canceled')
const { request, cancel } = CancelableRequest(searchPossibleConversations)
@ -266,6 +278,9 @@ export default {
this.searchResults = response?.data?.ocs?.data || []
this.contactsLoading = false
this.$nextTick(() => {
this.initializeNavigation()
})
} catch (exception) {
if (CancelableRequest.isCancel(exception)) {
return
@ -356,6 +371,14 @@ export default {
overflow-y: auto;
}
.known-results {
padding: 0 2px;
}
.search-results {
margin-top: 12px; // compensate margin before first header inside
}
/** TODO: fix these in the nextcloud-vue library **/
:deep(.app-sidebar-header__menu) {
@ -369,6 +392,14 @@ export default {
right: 6px !important;
}
:deep(.app-navigation-caption):not(:first-child) {
margin-top: 12px !important;
}
:deep(.app-navigation-caption__name) {
margin: 0 !important;
}
/*
* The field will fully overlap the top of the sidebar content so
* that elements will scroll behind it

47
src/composables/useArrowNavigation.js

@ -21,11 +21,12 @@
import { computed, onMounted, ref, unref } from 'vue'
/* selector to check, if item element or its children are focusable */
const focusableCondition = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
/**
* Mount navigation according to https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
* Item elements should have:
* - specific valid CSS selector (tag, class or another attribute)
* - unique "data-nav-id" attribute (on element or its parent, if it's not possible to pass it through the wrapper)
* Item elements should have unique "data-nav-id" attribute
*
* Controls:
* - ArrowDown or ArrowUp keys - to move through the itemElements list
@ -36,11 +37,10 @@ import { computed, onMounted, ref, unref } from 'vue'
*
* @param {import('vue').Ref | HTMLElement} listElementRef component ref to mount navigation
* @param {import('vue').Ref} defaultElementRef component ref to return focus to // Vue component
* @param {string} selector native selector of elements to look for
* @param {object} options navigation options
* @param {boolean} [options.confirmEnter=false] flag to confirm Enter click
*/
export function useArrowNavigation(listElementRef, defaultElementRef, selector, options = { confirmEnter: false }) {
export function useArrowNavigation(listElementRef, defaultElementRef, options = { confirmEnter: false }) {
const listRef = ref(null)
const defaultRef = ref(null)
@ -50,17 +50,29 @@ export function useArrowNavigation(listElementRef, defaultElementRef, selector,
*/
const itemElements = ref([])
const itemElementsIdMap = computed(() => itemElements.value.map(item => {
return item.getAttribute('data-nav-id') || item.parentElement.getAttribute('data-nav-id')
return item.getAttribute('data-nav-id')
}))
const itemSelector = ref(selector)
const focusedIndex = ref(null)
const isConfirmationEnabled = ref(null)
const lookupNavId = (element) => {
if (element.hasAttribute('data-nav-id')) {
return element.getAttribute('data-nav-id')
}
// Find parent element with data-nav-id attribute
let parentElement = element.parentNode
while (parentElement && parentElement !== document.body) {
if (parentElement.hasAttribute('data-nav-id')) {
return parentElement.getAttribute('data-nav-id')
}
parentElement = parentElement.parentNode
}
}
// Set focused index according to selected element
const handleFocusEvent = (event) => {
const newIndex = itemElementsIdMap.value.indexOf(event.target?.getAttribute('data-nav-id'))
const newIndex = itemElementsIdMap.value.indexOf(lookupNavId(event.target))
// Quit if triggered by arrow navigation as already handled
// or if using Tab key to navigate, and going through NcActions
if (focusedIndex.value !== newIndex && newIndex !== -1) {
@ -104,7 +116,7 @@ export function useArrowNavigation(listElementRef, defaultElementRef, selector,
* Put a listener for focus/blur events on navigation area
*/
function initializeNavigation() {
itemElements.value = Array.from(listRef.value?.querySelectorAll(itemSelector.value))
itemElements.value = Array.from(listRef.value?.querySelectorAll('[data-nav-id]'))
focusedIndex.value = null
listRef.value?.addEventListener('focus', handleFocusEvent, true)
@ -125,11 +137,22 @@ export function useArrowNavigation(listElementRef, defaultElementRef, selector,
/**
* Focus natively the DOM element specified by index
*
* @param {object} index the item index
* @param {number} index the item index
*/
function nativelyFocusElement(index) {
focusedIndex.value = index
itemElements.value[index].focus()
const itemElement = itemElements.value[index]
if (itemElement.matches(focusableCondition)) {
itemElement.focus()
return
}
try {
itemElement.querySelector(focusableCondition).focus()
} catch (e) {
console.warn('Nav element does not have any focusable children')
}
}
/**

Loading…
Cancel
Save