remove search from private-messages (#12044)

* remove search from private-messages + paged conversations + fixes

* remove autoSize call

* add conversation border at the top

* border-bottom under `Disable Private Messages` - revert border-bottom on conversation items
This commit is contained in:
negue
2020-04-20 16:32:54 +02:00
committed by GitHub
parent 0e36c1aa0f
commit cbcc7cd479
6 changed files with 135 additions and 160 deletions

View File

@@ -58,7 +58,6 @@
"vue-mugen-scroll": "^0.2.6", "vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"vue2-perfect-scrollbar": "^1.4.0",
"vuedraggable": "^2.23.1", "vuedraggable": "^2.23.1",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^4.42.1" "webpack": "^4.42.1"

View File

@@ -1,21 +1,23 @@
<template> <template>
<perfect-scrollbar <div
ref="container" ref="container"
class="container-fluid" class="container-fluid"
:class="{'disable-perfect-scroll': disablePerfectScroll}"
:options="psOptions"
> >
<div class="row loadmore"> <div class="row loadmore">
<div v-if="canLoadMore && !isLoading"> <div v-if="canLoadMore && !isLoading">
<div class="loadmore-divider-holder">
<div class="loadmore-divider"></div> <div class="loadmore-divider"></div>
</div>
<button <button
class="btn btn-secondary" class="btn btn-secondary"
@click="triggerLoad()" @click="triggerLoad()"
> >
{{ $t('loadEarlierMessages') }} {{ $t('loadEarlierMessages') }}
</button> </button>
<div class="loadmore-divider-holder">
<div class="loadmore-divider"></div> <div class="loadmore-divider"></div>
</div> </div>
</div>
<h2 <h2
v-show="isLoading" v-show="isLoading"
class="col-12 loading" class="col-12 loading"
@@ -30,11 +32,10 @@
:class="{ 'margin-right': user._id !== msg.uuid}" :class="{ 'margin-right': user._id !== msg.uuid}"
> >
<div <div
v-if="user._id !== msg.uuid"
class="d-flex flex-grow-1" class="d-flex flex-grow-1"
> >
<avatar <avatar
v-if="conversationOpponentUser" v-if="user._id !== msg.uuid && conversationOpponentUser"
class="avatar-left" class="avatar-left"
:member="conversationOpponentUser" :member="conversationOpponentUser"
:avatar-only="true" :avatar-only="true"
@@ -42,20 +43,10 @@
:hide-class-badge="true" :hide-class-badge="true"
@click.native="showMemberModal(msg.uuid)" @click.native="showMemberModal(msg.uuid)"
/> />
<div class="card card-right"> <div
<message-card class="card"
:msg="msg" :class="{'card-right': user._id !== msg.uuid, 'card-left': user._id === msg.uuid}"
@message-removed="messageRemoved" >
@show-member-modal="showMemberModal"
@message-card-mounted="itemWasMounted"
/>
</div>
</div>
<div
v-if="user._id === msg.uuid"
class="d-flex flex-grow-1"
>
<div class="card card-left">
<message-card <message-card
:msg="msg" :msg="msg"
@message-removed="messageRemoved" @message-removed="messageRemoved"
@@ -64,7 +55,7 @@
/> />
</div> </div>
<avatar <avatar
v-if="user" v-if="user && user._id === msg.uuid"
class="avatar-right" class="avatar-right"
:member="user" :member="user"
:avatar-only="true" :avatar-only="true"
@@ -74,16 +65,11 @@
/> />
</div> </div>
</div> </div>
</perfect-scrollbar> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~@/assets/scss/colors.scss'; @import '~@/assets/scss/colors.scss';
@import '~vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
.disable-perfect-scroll {
overflow-y: inherit !important;
}
.avatar { .avatar {
width: 170px; width: 170px;
@@ -162,6 +148,8 @@
.loadmore { .loadmore {
justify-content: center; justify-content: center;
margin-right: 12px; margin-right: 12px;
margin-top: 12px;
margin-bottom: 24px;
> div { > div {
display: flex; display: flex;
@@ -171,15 +159,11 @@
button { button {
text-align: center; text-align: center;
color: $gray-50; color: $gray-50;
margin-top: 12px;
margin-bottom: 24px;
} }
} }
} }
.loadmore-divider { .loadmore-divider-holder {
height: 1px;
background-color: $gray-500;
flex: 1; flex: 1;
margin-left: 24px; margin-left: 24px;
margin-right: 24px; margin-right: 24px;
@@ -189,6 +173,13 @@
} }
} }
.loadmore-divider {
height: 1px;
border-top: 1px $gray-500 solid;
width: 100%;
}
.loading { .loading {
padding-left: 1.5rem; padding-left: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -200,7 +191,6 @@
<script> <script>
import moment from 'moment'; import moment from 'moment';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { PerfectScrollbar } from 'vue2-perfect-scrollbar';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import Avatar from '../avatar'; import Avatar from '../avatar';
@@ -210,7 +200,6 @@ export default {
components: { components: {
Avatar, Avatar,
messageCard, messageCard,
PerfectScrollbar,
}, },
props: { props: {
chat: {}, chat: {},
@@ -248,15 +237,10 @@ export default {
messages () { messages () {
return this.chat; return this.chat;
}, },
psOptions () {
return {
suppressScrollX: true,
};
},
}, },
methods: { methods: {
async triggerLoad () { async triggerLoad () {
const container = this.$refs.container.$el; const { container } = this.$refs;
// get current offset // get current offset
this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight); this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight);
@@ -274,8 +258,9 @@ export default {
} }
}, },
displayDivider (message) { displayDivider (message) {
if (this.currentDayDividerDisplay !== moment(message.timestamp).day()) { const day = moment(message.timestamp).day();
this.currentDayDividerDisplay = moment(message.timestamp).day(); if (this.currentDayDividerDisplay !== day) {
this.currentDayDividerDisplay = day;
return true; return true;
} }
@@ -288,7 +273,7 @@ export default {
if (this.handleScrollBack) { if (this.handleScrollBack) {
this.handleScrollBack = false; this.handleScrollBack = false;
const container = this.$refs.container.$el; const { container } = this.$refs;
const offset = container.scrollHeight - container.clientHeight; const offset = container.scrollHeight - container.clientHeight;
const newOffset = offset + this.lastOffset; const newOffset = offset + this.lastOffset;

View File

@@ -19,7 +19,10 @@
<!-- placeholder --> <!-- placeholder -->
</div> </div>
</div> </div>
<div class="d-flex selected-conversion"> <div
v-if="selectedConversation && selectedConversation.key"
class="d-flex selected-conversion"
>
<router-link <router-link
:to="{'name': 'userProfile', 'params': {'userId': selectedConversation.key}}" :to="{'name': 'userProfile', 'params': {'userId': selectedConversation.key}}"
> >
@@ -49,13 +52,6 @@
@change="toggleOpt()" @change="toggleOpt()"
/> />
</div> </div>
<div class="search-section">
<b-form-input
v-model="search"
class="input-search"
:placeholder="$t('search')"
/>
</div>
<div <div
v-if="filtersConversations.length > 0" v-if="filtersConversations.length > 0"
class="conversations" class="conversations"
@@ -75,6 +71,13 @@
@click="selectConversation(conversation.key)" @click="selectConversation(conversation.key)"
/> />
</div> </div>
<button
class="btn btn-secondary"
v-if="canLoadMoreConversations"
@click="loadConversations()"
>
{{ $t('loadMore') }}
</button>
</div> </div>
<div class="messages-column d-flex flex-column align-items-center"> <div class="messages-column d-flex flex-column align-items-center">
<div <div
@@ -142,7 +145,7 @@
<h4>{{ disabledTexts.title }}</h4> <h4>{{ disabledTexts.title }}</h4>
<p>{{ disabledTexts.description }}</p> <p>{{ disabledTexts.description }}</p>
</div> </div>
<div> <div class="full-width">
<div <div
class="new-message-row d-flex align-items-center" class="new-message-row d-flex align-items-center"
> >
@@ -152,10 +155,8 @@
class="flex-fill" class="flex-fill"
:placeholder="$t('needsTextPlaceholder')" :placeholder="$t('needsTextPlaceholder')"
:maxlength="MAX_MESSAGE_LENGTH" :maxlength="MAX_MESSAGE_LENGTH"
:class="{'has-content': newMessage !== '', 'disabled': newMessageDisabled}" :class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
:style="{'--textarea-auto-height': textareaAutoHeight}"
@keyup.ctrl.enter="sendPrivateMessage()" @keyup.ctrl.enter="sendPrivateMessage()"
@input="autoSize()"
> >
</textarea> </textarea>
</div> </div>
@@ -293,20 +294,6 @@
margin-left: 12px; margin-left: 12px;
} }
.input-search {
background-repeat: no-repeat;
background-position: center left 16px;
background-size: 16px 16px;
background-image: url(~@/assets/svg/for-css/search_gray.svg) !important;
padding-left: 40px;
height: 40px;
}
.input-search::placeholder {
color: $gray-200 !important;
}
.selected-conversion { .selected-conversion {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -321,6 +308,8 @@
height: 44px; height: 44px;
background-color: $gray-600; background-color: $gray-600;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-bottom: 1px solid $gray-500;
} }
@@ -417,6 +406,10 @@
} }
} }
.full-width {
width: 100%;
}
.new-message-row { .new-message-row {
width: 100%; width: 100%;
padding-left: 1.5rem; padding-left: 1.5rem;
@@ -435,8 +428,12 @@
background-color: $gray-500; background-color: $gray-500;
} }
min-height: var(--textarea-auto-height, 40px); &.has-content {
--textarea-auto-height: 80px
}
max-height: var(--textarea-auto-height, 40px); max-height: var(--textarea-auto-height, 40px);
min-height: var(--textarea-auto-height, 40px);
} }
} }
@@ -502,11 +499,6 @@
padding: 0; padding: 0;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
.search-section {
padding: 1rem 1.5rem;
border-bottom: 1px solid $gray-500;
}
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
width: 280px; width: 280px;
} }
@@ -555,7 +547,6 @@
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import moment from 'moment'; import moment from 'moment';
import filter from 'lodash/filter';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import habiticaMarkdown from 'habitica-markdown'; import habiticaMarkdown from 'habitica-markdown';
@@ -574,7 +565,9 @@ import faceAvatar from '@/components/faceAvatar';
import Avatar from '@/components/avatar'; import Avatar from '@/components/avatar';
import { EVENTS } from '@/libs/events'; import { EVENTS } from '@/libs/events';
const MAX_TEXTAREA_HEIGHT = 80; // extract to a shared path
const CONVERSATIONS_PER_PAGE = 10;
const PM_PER_PAGE = 10;
export default { export default {
components: { components: {
@@ -597,19 +590,21 @@ export default {
messageIcon, messageIcon,
mail, mail,
}), }),
displayCreate: true,
selectedConversation: {},
search: '',
newMessage: '',
showPopover: false,
messages: [],
messagesByConversation: {}, // cache {uuid: []}
loadedConversations: [],
loaded: false, loaded: false,
messagesLoading: false, showPopover: false,
/* Conversation-specific data */
initiatedConversation: null, initiatedConversation: null,
updateConversationsCounter: 0, updateConversationsCounter: 0,
textareaAutoHeight: undefined, selectedConversation: {},
conversationPage: 0,
canLoadMoreConversations: false,
loadedConversations: [],
messagesByConversation: {}, // cache {uuid: []}
newMessage: '',
messages: [],
messagesLoading: false,
MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(), MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(),
}; };
}, },
@@ -724,13 +719,9 @@ export default {
// Vue-subscribe to changes // Vue-subscribe to changes
const subscribeToUpdate = this.updateConversationsCounter > -1; const subscribeToUpdate = this.updateConversationsCounter > -1;
const filtered = subscribeToUpdate && !this.search const filtered = subscribeToUpdate && this.conversations;
? this.conversations
/* eslint-disable max-len */ const ordered = orderBy(filtered, [o => o.date], ['desc']);
: filter(this.conversations, conversation => conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1);
const ordered = orderBy(filtered, [o => moment(o.date).toDate()], ['desc']);
return ordered; return ordered;
}, },
@@ -810,14 +801,25 @@ export default {
async reload () { async reload () {
this.loaded = false; this.loaded = false;
const conversationRes = await axios.get('/api/v4/inbox/conversations'); this.loadedConversations = [];
this.loadedConversations = conversationRes.data.data;
this.selectedConversation = {}; this.selectedConversation = {};
await this.loadConversations();
await this.$store.dispatch('user:markPrivMessagesRead'); await this.$store.dispatch('user:markPrivMessagesRead');
this.loaded = true; this.loaded = true;
}, },
async loadConversations () {
const query = ['/api/v4/inbox/conversations'];
query.push(`?page=${this.conversationPage}`);
this.conversationPage += 1;
const conversationRes = await axios.get(query.join(''));
const loadedConversations = conversationRes.data.data;
this.canLoadMoreConversations = loadedConversations.length === CONVERSATIONS_PER_PAGE;
this.loadedConversations.push(...loadedConversations);
},
messageRemoved (message) { messageRemoved (message) {
const messages = this.messagesByConversation[this.selectedConversation.key]; const messages = this.messagesByConversation[this.selectedConversation.key];
@@ -833,9 +835,6 @@ export default {
}; };
} }
}, },
toggleClick () {
this.displayCreate = !this.displayCreate;
},
toggleOpt () { toggleOpt () {
this.$store.dispatch('user:togglePrivateMessagesOpt'); this.$store.dispatch('user:togglePrivateMessagesOpt');
}, },
@@ -848,11 +847,7 @@ export default {
await this.loadMessages(); await this.loadMessages();
} }
Vue.nextTick(() => { this.scrollToBottom();
if (!this.$refs.chatscroll) return;
const chatscroll = this.$refs.chatscroll.$el;
chatscroll.scrollTop = chatscroll.scrollHeight;
});
}, },
sendPrivateMessage () { sendPrivateMessage () {
if (!this.newMessage) return; if (!this.newMessage) return;
@@ -890,11 +885,7 @@ export default {
this.selectedConversation.lastMessageText = this.newMessage; this.selectedConversation.lastMessageText = this.newMessage;
this.selectedConversation.date = new Date(); this.selectedConversation.date = new Date();
Vue.nextTick(() => { this.scrollToBottom();
if (!this.$refs.chatscroll) return;
const chatscroll = this.$refs.chatscroll.$el;
chatscroll.scrollTop = chatscroll.scrollHeight;
});
this.$store.dispatch('members:sendPrivateMessage', { this.$store.dispatch('members:sendPrivateMessage', {
toUserId: this.selectedConversation.key, toUserId: this.selectedConversation.key,
@@ -908,7 +899,13 @@ export default {
}); });
this.newMessage = ''; this.newMessage = '';
this.autoSize(); },
scrollToBottom () {
Vue.nextTick(() => {
if (!this.$refs.chatscroll) return;
const chatscroll = this.$refs.chatscroll.$el;
chatscroll.scrollTop = chatscroll.scrollHeight;
});
}, },
removeTags (html) { removeTags (html) {
const tmp = document.createElement('DIV'); const tmp = document.createElement('DIV');
@@ -943,31 +940,17 @@ export default {
const res = await axios.get(requestUrl); const res = await axios.get(requestUrl);
const loadedMessages = res.data.data; const loadedMessages = res.data.data;
/* eslint-disable max-len */
this.messagesByConversation[conversationKey] = this.messagesByConversation[conversationKey] || []; this.messagesByConversation[conversationKey] = this.messagesByConversation[conversationKey] || [];
/* eslint-disable max-len */
const loadedMessagesToAdd = loadedMessages const loadedMessagesToAdd = loadedMessages
.filter(m => this.messagesByConversation[conversationKey].findIndex(mI => mI.id === m.id) === -1); .filter(m => this.messagesByConversation[conversationKey].findIndex(mI => mI.id === m.id) === -1);
this.messagesByConversation[conversationKey].push(...loadedMessagesToAdd); this.messagesByConversation[conversationKey].push(...loadedMessagesToAdd);
// only show the load more Button if the max count was returned // only show the load more Button if the max count was returned
this.selectedConversation.canLoadMore = loadedMessages.length === 10; this.selectedConversation.canLoadMore = loadedMessages.length === PM_PER_PAGE;
this.messagesLoading = false; this.messagesLoading = false;
}, },
autoSize () {
const { textarea } = this.$refs;
// weird issue: browser only removing the scrollHeight / clientHeight per key event - 56-54-52
let { scrollHeight } = textarea;
if (this.newMessage === '') {
// reset height when the message was removed again
scrollHeight = 40;
}
if (scrollHeight > MAX_TEXTAREA_HEIGHT) {
scrollHeight = MAX_TEXTAREA_HEIGHT;
}
this.textareaAutoHeight = `${scrollHeight}px`;
},
selectFirstConversation () { selectFirstConversation () {
if (this.loadedConversations.length > 0) { if (this.loadedConversations.length > 0) {
this.selectConversation(this.loadedConversations[0].uuid, true); this.selectConversation(this.loadedConversations[0].uuid, true);

View File

@@ -81,6 +81,9 @@ api.clearMessages = {
* This is for API v4 which must not be used in third-party tools. * This is for API v4 which must not be used in third-party tools.
* For API v3, use "Get inbox messages for a user". * For API v3, use "Get inbox messages for a user".
* *
* @apiParam (Query) {Number} page (optional) Load the conversations of the selected Page
* - 10 conversations per Page
*
* @apiSuccess {Array} data An array of inbox conversations * @apiSuccess {Array} data An array of inbox conversations
* *
* @apiSuccessExample {json} Success-Response: * @apiSuccessExample {json} Success-Response:
@@ -104,8 +107,9 @@ api.conversations = {
url: '/inbox/conversations', url: '/inbox/conversations',
async handler (req, res) { async handler (req, res) {
const { user } = res.locals; const { user } = res.locals;
const { page } = req.query;
const result = await listConversations(user); const result = await listConversations(user, page);
res.respond(200, result); res.respond(200, result);
}, },
@@ -129,8 +133,7 @@ api.getInboxMessages = {
middlewares: [authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
const { user } = res.locals; const { user } = res.locals;
const { page } = req.query; const { page, conversation } = req.query;
const { conversation } = req.query;
const userInbox = await getUserInbox(user, { const userInbox = await getUserInbox(user, {
page, conversation, mapProps: true, page, conversation, mapProps: true,

View File

@@ -41,27 +41,36 @@ async function usersMapByConversations (users) {
return usersMap; return usersMap;
} }
export async function listConversations (owner) { const CONVERSATION_PER_PAGE = 10;
export async function listConversations (owner, page) {
const aggregateQuery = [
{
$match: {
ownerId: owner._id,
},
},
{
$group: {
_id: '$uuid',
user: { $last: '$user' },
username: { $last: '$username' },
timestamp: { $last: '$timestamp' },
text: { $last: '$text' },
count: { $sum: 1 },
},
},
{ $sort: { timestamp: -1 } }, // sort by latest message
];
if (page >= 0) {
aggregateQuery.push({ $skip: page * CONVERSATION_PER_PAGE });
aggregateQuery.push({ $limit: CONVERSATION_PER_PAGE });
}
// group messages by user owned by logged-in user // group messages by user owned by logged-in user
const query = Inbox const query = Inbox
.aggregate([ .aggregate(aggregateQuery);
{
$match: {
ownerId: owner._id,
},
},
{
$group: {
_id: '$uuid',
user: { $last: '$user' },
username: { $last: '$username' },
timestamp: { $last: '$timestamp' },
text: { $last: '$text' },
count: { $sum: 1 },
},
},
{ $sort: { timestamp: -1 } }, // sort by latest message
]);
const conversationsList = await query.exec(); const conversationsList = await query.exec();

View File

@@ -2,8 +2,6 @@ import { mapInboxMessage, inboxModel as Inbox } from '../../models/message';
import { getUserInfo, sendTxn as sendTxnEmail } from '../email'; // eslint-disable-line import/no-cycle import { getUserInfo, sendTxn as sendTxnEmail } from '../email'; // eslint-disable-line import/no-cycle
import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle
const PM_PER_PAGE = 10;
export async function sentMessage (sender, receiver, message, translate) { export async function sentMessage (sender, receiver, message, translate) {
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message }); const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
const senderName = getUserInfo(sender, ['name']).name; const senderName = getUserInfo(sender, ['name']).name;
@@ -33,17 +31,15 @@ export async function sentMessage (sender, receiver, message, translate) {
return messageSent; return messageSent;
} }
const PM_PER_PAGE = 10;
export async function getUserInbox (user, options = { const getUserInboxDefaultOptions = {
asArray: true, page: 0, conversation: null, mapProps: false, asArray: true, page: 0, conversation: null, mapProps: false,
}) { };
if (typeof options.asArray === 'undefined') {
options.asArray = true;
}
if (typeof options.mapProps === 'undefined') { export async function getUserInbox (user, optionParams = getUserInboxDefaultOptions) {
options.mapProps = false; // if not all properties are passed, fill the default values
} const options = { ...getUserInboxDefaultOptions, ...optionParams };
const findObj = { ownerId: user._id }; const findObj = { ownerId: user._id };
@@ -57,8 +53,8 @@ export async function getUserInbox (user, options = {
if (typeof options.page !== 'undefined') { if (typeof options.page !== 'undefined') {
query = query query = query
.limit(PM_PER_PAGE) .skip(PM_PER_PAGE * Number(options.page))
.skip(PM_PER_PAGE * Number(options.page)); .limit(PM_PER_PAGE);
} }
const messages = (await query.exec()).map(msg => { const messages = (await query.exec()).map(msg => {