mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
feat: Allow agents/admins to copy the link to a message (#5912)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<li v-if="shouldRenderMessage" :class="alignBubble">
|
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
|
||||||
<div :class="wrapClass">
|
<div :class="wrapClass">
|
||||||
<div
|
<div
|
||||||
v-tooltip.top-start="messageToolTip"
|
v-tooltip.top-start="messageToolTip"
|
||||||
@@ -190,6 +190,7 @@ export default {
|
|||||||
showContextMenu: false,
|
showContextMenu: false,
|
||||||
hasImageError: false,
|
hasImageError: false,
|
||||||
contextMenuPosition: {},
|
contextMenuPosition: {},
|
||||||
|
showBackgroundHighlight: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -290,13 +291,13 @@ export default {
|
|||||||
const isRightAligned =
|
const isRightAligned =
|
||||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||||
messageType === MESSAGE_TYPE.TEMPLATE;
|
messageType === MESSAGE_TYPE.TEMPLATE;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
center: isCentered,
|
center: isCentered,
|
||||||
left: isLeftAligned,
|
left: isLeftAligned,
|
||||||
right: isRightAligned,
|
right: isRightAligned,
|
||||||
'has-context-menu': this.showContextMenu,
|
'has-context-menu': this.showContextMenu,
|
||||||
'has-tweet-menu': this.isATweet,
|
'has-tweet-menu': this.isATweet,
|
||||||
|
'has-bg': this.showBackgroundHighlight,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createdAt() {
|
createdAt() {
|
||||||
@@ -414,9 +415,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.hasImageError = false;
|
this.hasImageError = false;
|
||||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||||
|
this.setupHighlightTimer();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
bus.$off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
bus.$off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||||
|
clearTimeout(this.higlightTimeout);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hasMediaAttachment(type) {
|
hasMediaAttachment(type) {
|
||||||
@@ -458,6 +461,17 @@ export default {
|
|||||||
this.showContextMenu = false;
|
this.showContextMenu = false;
|
||||||
this.contextMenuPosition = { x: null, y: null };
|
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>
|
</script>
|
||||||
@@ -589,6 +603,10 @@ li.left.has-tweet-menu .context-menu {
|
|||||||
margin-bottom: var(--space-medium);
|
margin-bottom: var(--space-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.has-bg {
|
||||||
|
background: var(--w-75);
|
||||||
|
}
|
||||||
|
|
||||||
li.right .context-menu-wrap {
|
li.right .context-menu-wrap {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,11 +192,6 @@ export default {
|
|||||||
(!this.listLoadingStatus && this.isLoadingPrevious)
|
(!this.listLoadingStatus && this.isLoadingPrevious)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldLoadMoreChats() {
|
|
||||||
return !this.listLoadingStatus && !this.isLoadingPrevious;
|
|
||||||
},
|
|
||||||
|
|
||||||
conversationType() {
|
conversationType() {
|
||||||
const { additional_attributes: additionalAttributes } = this.currentChat;
|
const { additional_attributes: additionalAttributes } = this.currentChat;
|
||||||
const type = additionalAttributes ? additionalAttributes.type : '';
|
const type = additionalAttributes ? additionalAttributes.type : '';
|
||||||
@@ -302,8 +297,16 @@ export default {
|
|||||||
setSelectedTweet(tweetId) {
|
setSelectedTweet(tweetId) {
|
||||||
this.selectedTweetId = tweetId;
|
this.selectedTweetId = tweetId;
|
||||||
},
|
},
|
||||||
onScrollToMessage() {
|
onScrollToMessage({ messageId = '' } = {}) {
|
||||||
this.$nextTick(() => this.scrollToBottom());
|
this.$nextTick(() => {
|
||||||
|
const messageElement = document.getElementById('message' + messageId);
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
this.fetchPreviousMessages();
|
||||||
|
} else {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
this.makeMessagesRead();
|
this.makeMessagesRead();
|
||||||
},
|
},
|
||||||
showPopoutReplyBox() {
|
showPopoutReplyBox() {
|
||||||
@@ -354,34 +357,42 @@ export default {
|
|||||||
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
|
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleScroll(e) {
|
async fetchPreviousMessages(scrollTop = 0) {
|
||||||
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
|
|
||||||
this.setScrollParams();
|
this.setScrollParams();
|
||||||
|
const shouldLoadMoreMessages =
|
||||||
|
this.getMessages.dataFetched === true &&
|
||||||
|
!this.listLoadingStatus &&
|
||||||
|
!this.isLoadingPrevious;
|
||||||
|
|
||||||
const dataFetchCheck =
|
|
||||||
this.getMessages.dataFetched === true && this.shouldLoadMoreChats;
|
|
||||||
if (
|
if (
|
||||||
e.target.scrollTop < 100 &&
|
scrollTop < 100 &&
|
||||||
!this.isLoadingPrevious &&
|
!this.isLoadingPrevious &&
|
||||||
dataFetchCheck
|
shouldLoadMoreMessages
|
||||||
) {
|
) {
|
||||||
this.isLoadingPrevious = true;
|
this.isLoadingPrevious = true;
|
||||||
this.$store
|
try {
|
||||||
.dispatch('fetchPreviousMessages', {
|
await this.$store.dispatch('fetchPreviousMessages', {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
before: this.getMessages.messages[0].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() {
|
makeMessagesRead() {
|
||||||
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--w-75);
|
background-color: var(--w-75);
|
||||||
|
|
||||||
.submenu {
|
.submenu {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,8 @@
|
|||||||
"DELETE": "Delete",
|
"DELETE": "Delete",
|
||||||
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
|
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
|
||||||
"TRANSLATE": "Translate",
|
"TRANSLATE": "Translate",
|
||||||
|
"COPY_PERMALINK": "Copy link to the message",
|
||||||
|
"LINK_COPIED": "Message URL copied to the clipboard",
|
||||||
"DELETE_CONFIRMATION": {
|
"DELETE_CONFIRMATION": {
|
||||||
"TITLE": "Are you sure you want to delete this message?",
|
"TITLE": "Are you sure you want to delete this message?",
|
||||||
"MESSAGE": "You cannot undo this action",
|
"MESSAGE": "You cannot undo this action",
|
||||||
|
|||||||
@@ -62,7 +62,15 @@
|
|||||||
variant="icon"
|
variant="icon"
|
||||||
@click="handleTranslate"
|
@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
|
<menu-item
|
||||||
v-if="enabledOptions['cannedResponse']"
|
v-if="enabledOptions['cannedResponse']"
|
||||||
:option="{
|
:option="{
|
||||||
@@ -95,6 +103,7 @@ import { mixin as clickaway } from 'vue-clickaway';
|
|||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned';
|
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned';
|
||||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
|
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
|
||||||
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal';
|
import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal';
|
||||||
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
|
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
|
||||||
@@ -153,6 +162,21 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async handleCopy() {
|
||||||
await copyTextToClipboard(this.plainTextContent);
|
await copyTextToClipboard(this.plainTextContent);
|
||||||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||||
|
|||||||
@@ -60,10 +60,21 @@ export default {
|
|||||||
type: [String, Date, Number],
|
type: [String, Date, Number],
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
messageId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
navigateTo() {
|
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() {
|
createdAtTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return this.dynamicTime(this.createdAt);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
:account-id="accountId"
|
:account-id="accountId"
|
||||||
:inbox="message.inbox"
|
:inbox="message.inbox"
|
||||||
:created-at="message.created_at"
|
:created-at="message.created_at"
|
||||||
|
:message-id="message.id"
|
||||||
>
|
>
|
||||||
<message-content
|
<message-content
|
||||||
:author="getName(message)"
|
:author="getName(message)"
|
||||||
|
|||||||
@@ -163,9 +163,15 @@ export default {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$store.dispatch('setActiveChat', selectedConversation).then(() => {
|
const { messageId } = this.$route.query;
|
||||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
this.$store
|
||||||
});
|
.dispatch('setActiveChat', {
|
||||||
|
data: selectedConversation,
|
||||||
|
after: messageId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId });
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('clearSelectedState');
|
this.$store.dispatch('clearSelectedState');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.SET_CURRENT_CHAT_WINDOW, data);
|
||||||
commit(types.CLEAR_ALL_MESSAGES_LOADED);
|
commit(types.CLEAR_ALL_MESSAGES_LOADED);
|
||||||
|
|
||||||
if (data.dataFetched === undefined) {
|
if (data.dataFetched === undefined) {
|
||||||
try {
|
try {
|
||||||
await dispatch('fetchPreviousMessages', {
|
await dispatch('fetchPreviousMessages', {
|
||||||
conversationId: data.id,
|
after,
|
||||||
before: data.messages[0].id,
|
before: data.messages[0].id,
|
||||||
|
conversationId: data.id,
|
||||||
});
|
});
|
||||||
Vue.set(data, 'dataFetched', true);
|
Vue.set(data, 'dataFetched', true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user