Skip to content
25 changes: 17 additions & 8 deletions src/components/LeftSidebar/LeftSidebar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ describe('LeftSidebar.vue', () => {
// to prevent user status fetching
NcAvatar: true,
// to prevent complex dialog logic
NewGroupConversation: true,
NcActions: true,
NcModal: true,
},
})
}
Expand Down Expand Up @@ -611,26 +612,27 @@ describe('LeftSidebar.vue', () => {
})

test('shows group conversation dialog when clicking search result', async () => {
const eventHandler = jest.fn()
EventBus.$once('new-group-conversation-dialog', eventHandler)

const wrapper = await testSearch(SEARCH_TERM, [...groupsResults], [])

const resultsListItems = findNcListItems(wrapper, groupsResults.map(item => item.label))
expect(resultsListItems.exists()).toBeTruthy()
expect(resultsListItems).toHaveLength(groupsResults.length)

await resultsListItems.at(1).findAll('a').trigger('click')
expect(eventHandler).toHaveBeenCalledWith(groupsResults[1])
// Wait for the component to render
await wrapper.vm.$nextTick()
const ncModalComponent = wrapper.findComponent({ name: 'NcModal' })
expect(ncModalComponent.exists()).toBeTruthy()

const input = ncModalComponent.findComponent({ name: 'NcTextField', ref: 'conversationName' })
expect(input.props('value')).toBe(groupsResults[1].label)

// nothing created yet
expect(createOneToOneConversationAction).not.toHaveBeenCalled()
expect(addConversationAction).not.toHaveBeenCalled()
})

test('shows circles conversation dialog when clicking search result', async () => {
const eventHandler = jest.fn()
EventBus.$once('new-group-conversation-dialog', eventHandler)

const wrapper = await testSearch(SEARCH_TERM, [...circlesResults], [])

Expand All @@ -639,7 +641,13 @@ describe('LeftSidebar.vue', () => {
expect(resultsListItems).toHaveLength(circlesResults.length)

await resultsListItems.at(1).findAll('a').trigger('click')
expect(eventHandler).toHaveBeenCalledWith(circlesResults[1])

// Wait for the component to render
await wrapper.vm.$nextTick()
const ncModalComponent = wrapper.findComponent({ name: 'NcModal' })
expect(ncModalComponent.exists()).toBeTruthy()
const input = ncModalComponent.findComponent({ name: 'NcTextField', ref: 'conversationName' })
expect(input.props('value')).toBe(circlesResults[1].label)

// nothing created yet
expect(createOneToOneConversationAction).not.toHaveBeenCalled()
Expand Down Expand Up @@ -703,6 +711,7 @@ describe('LeftSidebar.vue', () => {
const wrapper = mountComponent()
const buttonEl = wrapper.findComponent({ name: 'NewGroupConversation' })
expect(buttonEl.exists()).toBeTruthy()

})
test('does not show new conversation button if user cannot start conversations', () => {
loadStateSettings.start_conversations = false
Expand Down
153 changes: 136 additions & 17 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,63 @@
<NcAppNavigation :aria-label="t('spreed', 'Conversation list')">
<div class="new-conversation"
:class="{ 'new-conversation--scrolled-down': !isScrolledToTop }">
<SearchBox v-model="searchText"
<SearchBox ref="searchbox"
v-model="searchText"
class="conversations-search"
:class="{'conversations-search--expanded': isFocused}"
:disabled="isFiltered"
:is-searching="isSearching"
@blur="setIsFocused(false)"
@focus="setIsFocused(true)"
@input="debounceFetchSearchResults"
@submit="onInputEnter"
@keydown.enter.native="handleEnter"
@abort-search="abortSearch" />
<NewGroupConversation v-if="canStartConversations" />

<!-- Options -->
<div class="options"
:class="{'hidden-visually': isFocused}">
<NcActions class="filter-actions"
:primary="isFiltered !== null">
<template #icon>
<FilterIcon :size="15" />
</template>
<NcActionButton close-after-click
class="filter-actions__button"
:primary="isFiltered === 'mentions'"
@click="handleFilter('mentions')">
<template #icon>
<AtIcon :size="20" />
</template>
{{ t('spreed','Filter unread mentions') }}
</NcActionButton>

<NcActionButton close-after-click
class="filter-actions__button"
:class="{'filter-actions__button--active': isFiltered === 'unread'}"
@click="handleFilter('unread')">
<template #icon>
<MessageBadge :size="20" />
</template>
{{ t('spreed','Filter unread messages') }}
</NcActionButton>

<NcActionButton v-if="isFiltered"
close-after-click
class="filter-actions__clearbutton"
@click="handleFilter(null)">
<template #icon>
<FilterRemoveIcon :size="20" />
</template>
{{ t('spreed', 'Clear filters') }}
</NcActionButton>
</NcActions>
<!-- New Conversation -->
<NewGroupConversation v-if="canStartConversations"
ref="newGroupConversation" />
</div>
</div>

<template #list>
<li ref="container" class="left-sidebar__list">
<ul ref="scroller"
Expand All @@ -46,7 +94,7 @@
<template v-if="!initialisedConversations">
<LoadingPlaceholder type="conversations" />
</template>
<Hint v-else-if="searchText && !conversationsList.length"
<Hint v-else-if="noMatchFound"
:hint="t('spreed', 'No matches')" />
<template v-if="isSearching">
<template v-if="!listedConversationsLoading && searchResultsListedConversations.length > 0">
Expand Down Expand Up @@ -132,10 +180,17 @@
<script>
import debounce from 'debounce'

import AtIcon from 'vue-material-design-icons/At.vue'
import FilterIcon from 'vue-material-design-icons/Filter.vue'
import FilterRemoveIcon from 'vue-material-design-icons/FilterRemove.vue'
import MessageBadge from 'vue-material-design-icons/MessageBadge.vue'

import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'

import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
Expand Down Expand Up @@ -173,6 +228,12 @@ export default {
LoadingPlaceholder,
NcListItem,
ConversationIcon,
NcActions,
NcActionButton,
AtIcon,
MessageBadge,
FilterIcon,
FilterRemoveIcon,
},

mixins: [
Expand Down Expand Up @@ -203,19 +264,32 @@ export default {
preventFindingUnread: false,
roomListModifiedBefore: 0,
forceFullRoomListRefreshAfterXLoops: 0,
isNewGroupConversationOpen: false,
isFocused: false,
isFiltered: null,
}
},

computed: {
conversationsList() {
let conversations = this.$store.getters.conversationsList

if (this.searchText !== '') {
const lowerSearchText = this.searchText.toLowerCase()
conversations = conversations.filter(conversation =>
conversation.displayName.toLowerCase().includes(lowerSearchText)
|| conversation.name.toLowerCase().includes(lowerSearchText)
)
switch (this.isFiltered) {
case ('unread'):
conversations = conversations.filter(conversation => conversation.unreadMessages > 0)
break
case ('mentions'):
conversations = conversations.filter(conversation => conversation.unreadMention || (conversation.unreadMessages > 0
&& (conversation.type === CONVERSATION.TYPE.ONE_TO_ONE || conversation.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER)))
break
default:
if (this.searchText !== '') {
const lowerSearchText = this.searchText.toLowerCase()
conversations = conversations.filter(conversation =>
conversation.displayName.toLowerCase().includes(lowerSearchText)
|| conversation.name.toLowerCase().includes(lowerSearchText)
)
break
}
}

// FIXME: this modifies the original array,
Expand All @@ -227,6 +301,10 @@ export default {
return this.searchText !== ''
},

noMatchFound() {
return (this.searchText || this.isFiltered) && !this.conversationsList.length
},

showStartConversationsOptions() {
return this.isSearching && this.canStartConversations
},
Expand Down Expand Up @@ -311,12 +389,18 @@ export default {
getFocusableList() {
return this.$el.querySelectorAll('li.acli_wrapper .acli')
},
setIsFocused(value) {
this.isFocused = value
},

handleFilter(filter) {
this.isFiltered = filter
},

focusCancel() {
return this.abortSearch()
},
isFocused() {
return this.isSearching
},

scrollBottomUnread() {
this.preventFindingUnread = true
this.$refs.container.scrollTo({
Expand All @@ -334,6 +418,10 @@ export default {
}
}, 250),

toggleNewGroupConversation(value) {
this.isNewGroupConversationOpen = value
},

async fetchPossibleConversations() {
this.contactsLoading = true

Expand Down Expand Up @@ -410,8 +498,8 @@ export default {
params: { token: conversation.token },
}).catch(err => console.debug(`Error while pushing the new conversation's route: ${err}`))
} else {
// For other types we start the conversation creation dialog
EventBus.$emit('new-group-conversation-dialog', item)
// For other types, show the modal directly
this.$refs.newGroupConversation.showModalForItem(item)
}
},

Expand Down Expand Up @@ -618,7 +706,6 @@ export default {
</script>

<style lang="scss" scoped>

@import '../../assets/variables';

.scroller {
Expand Down Expand Up @@ -654,11 +741,43 @@ export default {
}

.conversations-search {
flex-grow: 1;
transition: all 0.3s ease;
width: calc(65% - 8px);
z-index: 1;
position : absolute;

:deep(.input-field__input) {
border-radius: var(--border-radius-pill);
}
&--expanded {
width: 90%;
}

}

//FIXME : this should be changed once the disabled style for input is added
:deep(.input-field__input[disabled="disabled"]){
background-color: var(--color-background-dark);
}

.options{
position: relative;
transition: all 0.3s ease; //gets hidden once the search box is expanded
left : calc(65% + 4px);
display: flex;
}

.filter-actions__button--active{
background-color: var(--color-primary-element-light);
border-radius: 6px;
:deep(.action-button__longtext){
font-weight: bold;
}

}

.hidden-visually{
height:100%;
}

.settings-button {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ import {
createPrivateConversation,
setConversationPassword,
} from '../../../services/conversationsService.js'
import { EventBus } from '../../../services/EventBus.js'
import { addParticipant } from '../../../services/participantsService.js'

const NEW_CONVERSATION = {
Expand Down Expand Up @@ -267,14 +266,7 @@ export default {
}
},
},

mounted() {
EventBus.$on('new-group-conversation-dialog', this.showModalForItem)
},

destroyed() {
EventBus.$off('new-group-conversation-dialog', this.showModalForItem)
},
expose: ['showModalForItem'],

methods: {
showModal() {
Expand Down
8 changes: 7 additions & 1 deletion src/components/LeftSidebar/SearchBox/SearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
:value.sync="localValue"
:label="placeholderText"
:show-trailing-button="isSearching"
:disabled="disabled"
trailing-button-icon="close"
v-on="$listeners"
@trailing-button-click="abortSearch"
@keypress.enter="handleSubmit">
<Magnify :size="16" />
Expand All @@ -52,7 +54,7 @@ export default {
*/
placeholderText: {
type: String,
default: t('spreed', 'Search conversations or users'),
default: t('spreed', 'Search '),
},
/**
* The value of the input field, when receiving it as a prop the localValue
Expand All @@ -69,6 +71,10 @@ export default {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},

emits: ['update:value', 'input', 'submit', 'abort-search'],
Expand Down