Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions src/components/LeftSidebar/LeftSidebar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,6 @@ describe('LeftSidebar.vue', () => {
const wrapper = mountComponent()

expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
expect(conversationsListMock).toHaveBeenCalled()

const conversationListItems = wrapper.findAllComponents({ name: 'Conversation' })
expect(conversationListItems).toHaveLength(conversationsList.length)

expect(wrapper.vm.searchText).toBe('')
expect(wrapper.vm.initialisedConversations).toBeFalsy()
Expand All @@ -146,9 +142,12 @@ describe('LeftSidebar.vue', () => {
await flushPromises()

expect(wrapper.vm.initialisedConversations).toBeTruthy()
expect(conversationListItems.at(0).props('item')).toStrictEqual(conversationsList[2])
expect(conversationListItems.at(1).props('item')).toStrictEqual(conversationsList[0])
expect(conversationListItems.at(2).props('item')).toStrictEqual(conversationsList[1])

const conversationListItems = wrapper.findAllComponents({ name: 'Conversation' })
expect(conversationListItems).toHaveLength(conversationsList.length)
expect(conversationListItems.at(0).props('item')).toStrictEqual(conversationsList[0])
expect(conversationListItems.at(1).props('item')).toStrictEqual(conversationsList[1])
expect(conversationListItems.at(2).props('item')).toStrictEqual(conversationsList[2])

expect(conversationsReceivedEvent).toHaveBeenCalledWith({
singleConversation: false,
Expand Down Expand Up @@ -304,7 +303,6 @@ describe('LeftSidebar.vue', () => {
const wrapper = mountComponent()

expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
expect(conversationsListMock).toHaveBeenCalled()

const searchBox = wrapper.findComponent({ name: 'SearchBox' })
expect(searchBox.exists()).toBeTruthy()
Expand Down
205 changes: 106 additions & 99 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,89 +113,98 @@
<template #list>
<li ref="container" class="left-sidebar__list" @scroll="debounceHandleScroll">
<ul class="scroller">
<NcListItem v-if="noMatchFound && searchText && canStartConversations"
:title="t('spreed', 'Create a new conversation')"
@click="createConversation(searchText)">
<template #icon>
<ChatPlus :size="30" />
</template>
<template #subtitle>
{{ searchText }}
</template>
</NcListItem>

<NcAppNavigationCaption :class="{'hidden-visually': !isSearching}"
:title="t('spreed', 'Conversations')" />
<Conversation v-for="item of conversationsList"
:key="item.id"
:ref="`conversation-${item.token}`"
:item="item" />
<template v-if="!initialisedConversations">
<LoadingPlaceholder type="conversations" />
<!-- Conversations List -->
<template v-if="!isSearching">
<NcAppNavigationCaption :title="t('spreed', 'Conversations')" class="hidden-visually" />
<Conversation v-for="item of filteredConversationsList"
:key="`conversation_${item.id}`"
:ref="`conversation-${item.token}`"
:item="item" />
<LoadingPlaceholder v-if="!initialisedConversations" type="conversations" />
<Hint v-else-if="filteredConversationsList.length === 0" :hint="t('spreed', 'No matches found')" />
</template>
<Hint v-else-if="noMatchFound"
:hint="t('spreed', 'No matches found')" />
<template v-if="isSearching">
<template v-if="!listedConversationsLoading && searchResultsListedConversations.length > 0">

<!-- Search results -->
<template v-else-if="isSearching">
<!-- Create a new conversation -->
<NcListItem v-if="searchResultsConversationList.length === 0 && canStartConversations"
:title="t('spreed', 'Create a new conversation')"
@click="createConversation(searchText)">
<template #icon>
<ChatPlus :size="30" />
</template>
<template #subtitle>
{{ searchText }}
</template>
</NcListItem>

<!-- Search results: user's conversations -->
<NcAppNavigationCaption :title="t('spreed', 'Conversations')" />
<Conversation v-for="item of searchResultsConversationList"
:key="`conversation_${item.id}`"
:ref="`conversation-${item.token}`"
:item="item" />
<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 :title="t('spreed', 'Open conversations')" />
<Conversation v-for="item of searchResultsListedConversations"
:key="item.id"
:key="`open-conversation_${item.id}`"
:item="item"
is-search-result />
</template>

<!-- Search results: users -->
<template v-if="searchResultsUsers.length !== 0">
<NcAppNavigationCaption :title="t('spreed', 'Users')" />
<NcListItem v-for="item of searchResultsUsers"
:key="item.id"
:key="`user_${item.id}`"
:title="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)"
:disable-menu="true" />
</template>
</NcListItem>
</template>
<template v-if="!showStartConversationsOptions">
<NcAppNavigationCaption v-if="searchResultsUsers.length === 0"
:title="t('spreed', 'Users')" />
<Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
<Hint v-else :hint="t('spreed', 'No matches found')" />
</template>
</template>
<template v-if="showStartConversationsOptions">
<template v-if="searchResultsGroups.length !== 0">
<NcAppNavigationCaption :title="t('spreed', 'Groups')" />
<NcListItem v-for="item of searchResultsGroups"
:key="item.id"
:title="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)"
:disable-menu="true" />
<ConversationIcon :item="iconData(item)" disable-menu />
</template>
</NcListItem>
</template>

<template v-if="searchResultsCircles.length !== 0">
<NcAppNavigationCaption :title="t('spreed', 'Circles')" />
<NcListItem v-for="item of searchResultsCircles"
:key="item.id"
:title="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)"
:disable-menu="true" />
</template>
</NcListItem>
<!-- Search results: new conversations -->
<template v-if="canStartConversations">
<!-- New conversations: Groups -->
<template v-if="searchResultsGroups.length !== 0">
<NcAppNavigationCaption :title="t('spreed', 'Groups')" />
<NcListItem v-for="item of searchResultsGroups"
:key="`group_${item.id}`"
:title="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)" disable-menu />
</template>
</NcListItem>
</template>

<!-- New conversations: Circles -->
<template v-if="searchResultsCircles.length !== 0">
<NcAppNavigationCaption :title="t('spreed', 'Circles')" />
<NcListItem v-for="item of searchResultsCircles"
:key="`circle_${item.id}`"
:title="item.label"
@click="createAndJoinConversation(item)">
<template #icon>
<ConversationIcon :item="iconData(item)" disable-menu />
</template>
</NcListItem>
</template>
</template>

<NcAppNavigationCaption v-if="sourcesWithoutResults"
:title="sourcesWithoutResultsList" />
<!-- Search results: no results (yet) -->
<NcAppNavigationCaption v-if="sourcesWithoutResults" :title="sourcesWithoutResultsList" />
<Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
<Hint v-else :hint="t('spreed', 'No search results')" />
</template>
</ul>
</li>

<NcButton v-if="!preventFindingUnread && unreadNum > 0"
class="unread-mention-button"
type="primary"
Expand Down Expand Up @@ -337,37 +346,42 @@ export default {

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

searchResultsConversationList() {
if (this.searchText !== '' || this.isFocused) {
const lowerSearchText = this.searchText.toLowerCase()
conversations = conversations.filter(conversation =>
return this.conversationsList.filter(conversation =>
conversation.displayName.toLowerCase().includes(lowerSearchText)
|| conversation.name.toLowerCase().includes(lowerSearchText)
|| conversation.name.toLowerCase().includes(lowerSearchText)
)
} else if (this.isFiltered === 'unread') {
conversations = conversations.filter(conversation => conversation.unreadMessages > 0)
} else if (this.isFiltered === 'mentions') {
conversations = conversations.filter(conversation => conversation.unreadMention || (conversation.unreadMessages > 0
} else {
return []
}
},

filteredConversationsList() {
if (this.isFocused) {
return this.conversationsList
}

if (this.isFiltered === 'unread') {
return this.conversationsList.filter(conversation => conversation.unreadMessages > 0)
}

if (this.isFiltered === 'mentions') {
return this.conversationsList.filter(conversation => conversation.unreadMention || (conversation.unreadMessages > 0
&& (conversation.type === CONVERSATION.TYPE.ONE_TO_ONE || conversation.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER)))
}

// FIXME: this modifies the original array,
// maybe should act on a copy or sort already within the store ?
return conversations.sort(this.sortConversations)
return this.conversationsList
},

isSearching() {
return this.searchText !== ''
},

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

showStartConversationsOptions() {
return this.isSearching && this.canStartConversations
},

sourcesWithoutResults() {
return !this.searchResultsUsers.length
|| !this.searchResultsGroups.length
Expand Down Expand Up @@ -601,14 +615,6 @@ export default {
emit('show-settings')
},

sortConversations(conversation1, conversation2) {
if (conversation1.isFavorite !== conversation2.isFavorite) {
return conversation1.isFavorite ? -1 : 1
}

return conversation2.lastActivity - conversation1.lastActivity
},

/**
* @param {object} [options] Options for conversation refreshing
* @param {string} [options.token] The conversation token that got update
Expand Down Expand Up @@ -784,7 +790,7 @@ export default {
}

.new-conversation {
position: relative;
position: relative;
display: flex;
padding: 8px 4px 8px 12px;
align-items: center;
Expand All @@ -793,17 +799,17 @@ export default {
border-bottom: 1px solid var(--color-placeholder-dark);
}

.filters {
position: absolute;
top : 8px;
right: 56px;
}

.actions {
position: absolute;
top: 8px;
right: 8px;
}
.filters {
position: absolute;
top : 8px;
right: 56px;
}

.actions {
position: absolute;
top: 8px;
right: 8px;
}
}

// Override vue overflow rules for <ul> elements within app-navigation
Expand All @@ -825,7 +831,7 @@ export default {
}

.conversations-search {
padding: 4px 0;
padding: 4px 0;
transition: all 0.15s ease;
z-index: 1;
// New conversation button width : 52 px
Expand Down Expand Up @@ -859,6 +865,7 @@ export default {
:deep(.input-field__clear-button) {
border-radius: var(--border-radius-pill) !important;
}

:deep(.app-navigation ul) {
padding: 0 !important;
}
Expand Down
22 changes: 18 additions & 4 deletions src/store/conversationsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,24 @@ const state = {

const getters = {
conversations: state => state.conversations,
conversationsList: state => Object.values(state.conversations).filter(conversation => {
// Filter out breakout rooms from left sidebar
return conversation.objectType !== 'room'
}),
/**
* List of all conversations sorted by isFavorite and lastActivity without breakout rooms
*
* @param {object} state state
* @return {object[]} sorted conversations list
*/
conversationsList: state => {
return Object.values(state.conversations)
// Filter out breakout rooms
.filter(conversation => conversation.objectType !== 'room')
// Sort by isFavorite and lastActivity
.sort((conversation1, conversation2) => {
if (conversation1.isFavorite !== conversation2.isFavorite) {
return conversation1.isFavorite ? -1 : 1
}
return conversation2.lastActivity - conversation1.lastActivity
})
},
/**
* Get a conversation providing its token
*
Expand Down
9 changes: 6 additions & 3 deletions src/store/conversationsStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ describe('conversationsStore', () => {

expect(fetchConversations).toHaveBeenCalledWith({ })
// conversationsList is actual to the response
expect(store.getters.conversationsList).toEqual([oldConversation, newConversation])
expect(store.getters.conversationsList).toEqual([newConversation, oldConversation])
// Only old conversation with new activity should be actually replaced with new objects
expect(store.state.conversationsStore.conversations[oldConversation.token]).toStrictEqual(oldConversation)
expect(store.state.conversationsStore.conversations[newConversation.token]).toStrictEqual(newConversation)
Expand Down Expand Up @@ -581,8 +581,11 @@ describe('conversationsStore', () => {
await store.dispatch('fetchConversations', { modifiedSince })

expect(fetchConversations).toHaveBeenCalledWith({ params: { modifiedSince } })
// conversationsList is actual to the response
expect(store.getters.conversationsList).toEqual([newConversation1, newConversation2])
// conversations are actual to the response
expect(store.state.conversationsStore.conversations).toEqual({
[newConversation1.token]: newConversation1,
[newConversation2.token]: newConversation2,
})
// Only old conversation with new activity should be actually replaced with new objects
expect(store.state.conversationsStore.conversations[oldConversation1.token]).toStrictEqual(oldConversation1)
expect(store.state.conversationsStore.conversations[oldConversation2.token]).toStrictEqual(newConversation2)
Expand Down