feat: Allow agents/admins to copy the link to a message (#5912)

This commit is contained in:
Pranav Raj S
2023-03-26 22:58:42 -07:00
committed by GitHub
parent 1e8881577a
commit 6000028f64
9 changed files with 107 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
<template>
<li v-if="shouldRenderMessage" :class="alignBubble">
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
<div :class="wrapClass">
<div
v-tooltip.top-start="messageToolTip"
@@ -190,6 +190,7 @@ export default {
showContextMenu: false,
hasImageError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
};
},
computed: {
@@ -290,13 +291,13 @@ export default {
const isRightAligned =
messageType === MESSAGE_TYPE.OUTGOING ||
messageType === MESSAGE_TYPE.TEMPLATE;
return {
center: isCentered,
left: isLeftAligned,
right: isRightAligned,
'has-context-menu': this.showContextMenu,
'has-tweet-menu': this.isATweet,
'has-bg': this.showBackgroundHighlight,
};
},
createdAt() {
@@ -414,9 +415,11 @@ export default {
mounted() {
this.hasImageError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
beforeDestroy() {
bus.$off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
clearTimeout(this.higlightTimeout);
},
methods: {
hasMediaAttachment(type) {
@@ -458,6 +461,17 @@ export default {
this.showContextMenu = false;
this.contextMenuPosition = { x: null, y: null };
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
return;
}
this.showBackgroundHighlight = true;
const HIGHLIGHT_TIMER = 1000;
this.higlightTimeout = setTimeout(() => {
this.showBackgroundHighlight = false;
}, HIGHLIGHT_TIMER);
},
},
};
</script>
@@ -589,6 +603,10 @@ li.left.has-tweet-menu .context-menu {
margin-bottom: var(--space-medium);
}
li.has-bg {
background: var(--w-75);
}
li.right .context-menu-wrap {
margin-left: auto;
}

View File

@@ -192,11 +192,6 @@ export default {
(!this.listLoadingStatus && this.isLoadingPrevious)
);
},
shouldLoadMoreChats() {
return !this.listLoadingStatus && !this.isLoadingPrevious;
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
@@ -302,8 +297,16 @@ export default {
setSelectedTweet(tweetId) {
this.selectedTweetId = tweetId;
},
onScrollToMessage() {
this.$nextTick(() => this.scrollToBottom());
onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => {
const messageElement = document.getElementById('message' + messageId);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
this.fetchPreviousMessages();
} else {
this.scrollToBottom();
}
});
this.makeMessagesRead();
},
showPopoutReplyBox() {
@@ -354,34 +357,42 @@ export default {
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
},
handleScroll(e) {
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
async fetchPreviousMessages(scrollTop = 0) {
this.setScrollParams();
const shouldLoadMoreMessages =
this.getMessages.dataFetched === true &&
!this.listLoadingStatus &&
!this.isLoadingPrevious;
const dataFetchCheck =
this.getMessages.dataFetched === true && this.shouldLoadMoreChats;
if (
e.target.scrollTop < 100 &&
scrollTop < 100 &&
!this.isLoadingPrevious &&
dataFetchCheck
shouldLoadMoreMessages
) {
this.isLoadingPrevious = true;
this.$store
.dispatch('fetchPreviousMessages', {
try {
await this.$store.dispatch('fetchPreviousMessages', {
conversationId: this.currentChat.id,
before: this.getMessages.messages[0].id,
})
.then(() => {
const heightDifference =
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
this.conversationPanel.scrollTop =
this.scrollTopBeforeLoad + heightDifference;
this.isLoadingPrevious = false;
this.setScrollParams();
});
const heightDifference =
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
this.conversationPanel.scrollTop =
this.scrollTopBeforeLoad + heightDifference;
this.setScrollParams();
} catch (error) {
// Ignore Error
} finally {
this.isLoadingPrevious = false;
}
}
},
handleScroll(e) {
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
this.fetchPreviousMessages(e.target.scrollTop);
},
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},

View File

@@ -67,6 +67,7 @@ export default {
&:hover {
background-color: var(--w-75);
.submenu {
display: block;
}

View File

@@ -167,6 +167,8 @@
"DELETE": "Delete",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
"COPY_PERMALINK": "Copy link to the message",
"LINK_COPIED": "Message URL copied to the clipboard",
"DELETE_CONFIRMATION": {
"TITLE": "Are you sure you want to delete this message?",
"MESSAGE": "You cannot undo this action",

View File

@@ -62,7 +62,15 @@
variant="icon"
@click="handleTranslate"
/>
<hr v-if="enabledOptions['cannedResponse']" />
<hr />
<menu-item
:option="{
icon: 'link',
label: this.$t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
}"
variant="icon"
@click="copyLinkToMessage"
/>
<menu-item
v-if="enabledOptions['cannedResponse']"
:option="{
@@ -95,6 +103,7 @@ import { mixin as clickaway } from 'vue-clickaway';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal';
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
@@ -153,6 +162,21 @@ export default {
},
},
methods: {
async copyLinkToMessage() {
const fullConversationURL =
window.chatwootConfig.hostURL +
frontendURL(
conversationUrl({
id: this.conversationId,
accountId: this.currentAccountId,
})
);
await copyTextToClipboard(
`${fullConversationURL}?messageId=${this.messageId}`
);
this.showAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
this.handleClose();
},
async handleCopy() {
await copyTextToClipboard(this.plainTextContent);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));

View File

@@ -60,10 +60,21 @@ export default {
type: [String, Date, Number],
default: '',
},
messageId: {
type: Number,
default: 0,
},
},
computed: {
navigateTo() {
return frontendURL(`accounts/${this.accountId}/conversations/${this.id}`);
const params = {};
if (this.messageId) {
params.messageId = this.messageId;
}
return frontendURL(
`accounts/${this.accountId}/conversations/${this.id}`,
params
);
},
createdAtTime() {
return this.dynamicTime(this.createdAt);

View File

@@ -11,6 +11,7 @@
:account-id="accountId"
:inbox="message.inbox"
:created-at="message.created_at"
:message-id="message.id"
>
<message-content
:author="getName(message)"

View File

@@ -163,9 +163,15 @@ export default {
) {
return;
}
this.$store.dispatch('setActiveChat', selectedConversation).then(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
});
const { messageId } = this.$route.query;
this.$store
.dispatch('setActiveChat', {
data: selectedConversation,
after: messageId,
})
.then(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId });
});
} else {
this.$store.dispatch('clearSelectedState');
}

View File

@@ -86,15 +86,15 @@ const actions = {
}
},
async setActiveChat({ commit, dispatch }, data) {
async setActiveChat({ commit, dispatch }, { data, after }) {
commit(types.SET_CURRENT_CHAT_WINDOW, data);
commit(types.CLEAR_ALL_MESSAGES_LOADED);
if (data.dataFetched === undefined) {
try {
await dispatch('fetchPreviousMessages', {
conversationId: data.id,
after,
before: data.messages[0].id,
conversationId: data.id,
});
Vue.set(data, 'dataFetched', true);
} catch (error) {