feat: Ability to reply to specific tweets (#1117)

Ability to choose a specific tweet to reply to

Fixes #982
Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
Sojan Jose
2020-08-11 09:57:42 +05:30
committed by GitHub
parent a6a62d92bf
commit 4216d63311
23 changed files with 290 additions and 38 deletions

View File

@@ -23,11 +23,27 @@
</span>
</span>
<bubble-actions
:id="data.id"
:sender="data.sender"
:is-a-tweet="isATweet"
:is-email="isEmailContentType"
:readable-time="readableTime"
:is-private="data.private"
:message-type="data.message_type"
:readable-time="readableTime"
:source-id="data.source_id"
/>
</p>
<div v-if="isATweet && isIncoming && sender" class="sender--info">
<woot-thumbnail
:src="sender.thumbnail"
:username="sender.name"
size="16px"
/>
<div class="sender--available-name">
{{ sender.available_name || sender.name }}
</div>
</div>
</div>
</li>
</template>
@@ -40,6 +56,8 @@ import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import BubbleActions from './bubble/Actions';
import { MESSAGE_TYPE } from 'shared/constants/messageTypes';
export default {
components: {
BubbleActions,
@@ -53,6 +71,10 @@ export default {
type: Object,
required: true,
},
isATweet: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -61,7 +83,10 @@ export default {
},
computed: {
message() {
return this.formatMessage(this.data.content);
return this.formatMessage(this.data.content, this.isATweet);
},
sender() {
return this.data.sender || {};
},
contentType() {
const {
@@ -78,6 +103,9 @@ export default {
isBubble() {
return [0, 1, 3].includes(this.data.message_type);
},
isIncoming() {
return this.data.message_type === MESSAGE_TYPE.INCOMING;
},
hasAttachments() {
return !!(this.data.attachments && this.data.attachments.length > 0);
},
@@ -90,7 +118,7 @@ export default {
return false;
},
sentByMessage() {
const { sender } = this.data;
const { sender } = this;
return this.data.message_type === 1 && !this.isHovered && sender
? {
@@ -128,4 +156,15 @@ export default {
padding: 0;
}
}
.sender--info {
display: flex;
align-items: center;
padding: var(--space-smaller) 0;
.sender--available-name {
font-size: var(--font-size-mini);
margin-left: var(--space-smaller);
}
}
</style>

View File

@@ -5,7 +5,7 @@
:is-contact-panel-open="isContactPanelOpen"
@contactPanelToggle="onToggleContactPanel"
/>
<div v-if="!currentChat.can_reply" class="messenger-policy--banner">
<div v-if="!currentChat.can_reply" class="banner messenger-policy--banner">
<span>
{{ $t('CONVERSATION.CANNOT_REPLY') }}
<a
@@ -17,6 +17,23 @@
</a>
</span>
</div>
<div v-if="isATweet" class="banner">
<span v-if="!selectedTweetId">
{{ $t('CONVERSATION.LAST_INCOMING_TWEET') }}
</span>
<span v-else>
{{ $t('CONVERSATION.REPLYING_TO') }}
{{ selectedTweet }}
</span>
<button
v-if="selectedTweetId"
class="banner-close-button"
@click="removeTweetSelection"
>
<i v-tooltip="$t('CONVERSATION.REMOVE_SELECTION')" class="ion-close" />
</button>
</div>
<ul class="conversation-panel">
<transition name="slide-up">
<li class="spinner--container">
@@ -27,6 +44,7 @@
v-for="message in getReadMessages"
:key="message.id"
:data="message"
:is-a-tweet="isATweet"
/>
<li v-show="getUnreadCount != 0" class="unread--toast">
<span>
@@ -37,6 +55,7 @@
v-for="message in getUnReadMessages"
:key="message.id"
:data="message"
:is-a-tweet="isATweet"
/>
</ul>
<div class="conversation-footer">
@@ -52,6 +71,7 @@
</div>
<ReplyBox
:conversation-id="currentChat.id"
:in-reply-to="selectedTweetId"
@scrollToMessage="scrollToBottom"
/>
</div>
@@ -59,7 +79,6 @@
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import ConversationHeader from './ConversationHeader';
@@ -67,6 +86,7 @@ import ReplyBox from './ReplyBox';
import Message from './Message';
import conversationMixin from '../../../mixins/conversations';
import { getTypingUsersText } from '../../../helper/commons';
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
components: {
@@ -93,6 +113,7 @@ export default {
isLoadingPrevious: true,
heightBeforeLoad: null,
conversationPanel: null,
selectedTweetId: null,
};
},
@@ -151,6 +172,36 @@ export default {
shouldLoadMoreChats() {
return !this.listLoadingStatus && !this.isLoadingPrevious;
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
return type || '';
},
isATweet() {
return this.conversationType === 'tweet';
},
selectedTweet() {
if (this.selectedTweetId) {
const { messages = [] } = this.getMessages;
const [selectedMessage = {}] = messages.filter(
message => message.id === this.selectedTweetId
);
return selectedMessage.content || '';
}
return '';
},
},
watch: {
currentChat(newChat, oldChat) {
if (newChat.id === oldChat.id) {
return;
}
this.selectedTweetId = null;
},
},
created() {
@@ -158,6 +209,10 @@ export default {
setTimeout(() => this.scrollToBottom(), 0);
this.makeMessagesRead();
});
bus.$on(BUS_EVENTS.SET_TWEET_REPLY, selectedTweetId => {
this.selectedTweetId = selectedTweetId;
});
},
mounted() {
@@ -220,23 +275,37 @@ export default {
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},
removeTweetSelection() {
this.selectedTweetId = null;
},
},
};
</script>
<style scoped lang="scss">
.messenger-policy--banner {
background: var(--r-400);
.banner {
background: var(--b-500);
color: var(--white);
font-size: var(--font-size-mini);
padding: var(--space-slab) var(--space-normal);
text-align: center;
position: relative;
a {
text-decoration: underline;
color: var(--white);
font-size: var(--font-size-mini);
}
&.messenger-policy--banner {
background: var(--r-400);
}
.banner-close-button {
cursor: pointer;
margin-left: var(--space--two);
color: var(--white);
}
}
.spinner--container {

View File

@@ -46,14 +46,14 @@
}}</a>
</li>
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
<a href="#" @click="setPrivateReplyMode">{{
$t('CONVERSATION.REPLYBOX.PRIVATE_NOTE')
}}</a>
<a href="#" @click="setPrivateReplyMode">
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
</a>
</li>
<li v-if="message.length" class="tabs-title message-length">
<a :class="{ 'message-error': isMessageLengthReachingThreshold }">{{
characterCountIndicator
}}</a>
<a :class="{ 'message-error': isMessageLengthReachingThreshold }">
{{ characterCountIndicator }}
</a>
</li>
</ul>
<button
@@ -106,6 +106,12 @@ export default {
ResizableTextArea,
},
mixins: [clickaway, inboxMixin],
props: {
inReplyTo: {
type: [String, Number],
default: '',
},
},
data() {
return {
message: '',
@@ -248,11 +254,15 @@ export default {
if (!this.showCannedResponsesList) {
this.clearMessage();
try {
await this.$store.dispatch('sendMessage', {
const messagePayload = {
conversationId: this.currentChat.id,
message: newMessage,
private: this.isPrivate,
});
};
if (this.inReplyTo) {
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
}
await this.$store.dispatch('sendMessage', messagePayload);
this.$emit('scrollToMessage');
} catch (error) {
// Error

View File

@@ -13,12 +13,32 @@
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
/>
<i
v-if="isATweet && isIncoming"
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
class="icon ion-reply cursor-pointer"
@click="onTweetReply"
/>
<a :href="linkToTweet" target="_blank" rel="noopener noreferrer nofollow">
<i
v-if="isATweet && isIncoming"
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
class="icon ion-android-open cursor-pointer"
/>
</a>
</div>
</template>
<script>
import { MESSAGE_TYPE } from 'shared/constants/messageTypes';
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
props: {
sender: {
type: Object,
default: () => ({}),
},
readableTime: {
type: String,
default: '',
@@ -31,6 +51,41 @@ export default {
type: Boolean,
default: true,
},
isATweet: {
type: Boolean,
default: true,
},
messageType: {
type: Number,
default: 1,
},
sourceId: {
type: String,
default: '',
},
id: {
type: [String, Number],
default: '',
},
},
computed: {
isIncoming() {
return MESSAGE_TYPE.INCOMING === this.messageType;
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
this.sender || {};
return additionalAttributes?.screen_name || '';
},
linkToTweet() {
const { screenName, sourceId } = this;
return `https://twitter.com/${screenName}/status/${sourceId}`;
},
},
methods: {
onTweetReply() {
bus.$emit(BUS_EVENTS.SET_TWEET_REPLY, this.id);
},
},
};
</script>
@@ -65,6 +120,13 @@ export default {
i {
line-height: 1.4;
padding-right: var(--space-small);
padding-left: var(--space-small);
color: var(--s-900);
}
a {
color: var(--s-900);
}
}