mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
The system did not detect the delivery errors earlier, resulting in some false positives. The user was not informed when an email failed to be delivered. While we do handle failure status in other channels, we were not able to capture the actual delivery status for the email channel. This pull request makes the following changes: - Updated the class EmailReplyWorker to use the deliver_now method instead of deliver_later. This change is made to raise any errors that may occur with the SMTP connection. The errors are then captured and sent to Sentry, and the email is marked as failed. Previously, we did not consider the case of retries in the email channel, so this feature is currently not functioning. So, I have disabled the retry option. We will address this in a follow-up ticket. - Downgraded the net-smtp gem to version 0.3.4. This change is made to avoid an argument error when using XOAUTH2. Fixes: https://linear.app/chatwoot/issue/CW-3032/argumenterror-wrong-authentication-type-xoauth2-argumenterror
759 lines
21 KiB
Vue
759 lines
21 KiB
Vue
<template>
|
|
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
|
|
<div :class="wrapClass">
|
|
<div
|
|
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
|
|
class="message-failed--alert"
|
|
>
|
|
<woot-button
|
|
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
|
size="tiny"
|
|
color-scheme="alert"
|
|
variant="clear"
|
|
icon="arrow-clockwise"
|
|
@click="retrySendMessage"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-tooltip.top-start="messageToolTip"
|
|
:class="bubbleClass"
|
|
@contextmenu="openContextMenu($event)"
|
|
>
|
|
<bubble-mail-head
|
|
:email-attributes="contentAttributes.email"
|
|
:cc="emailHeadAttributes.cc"
|
|
:bcc="emailHeadAttributes.bcc"
|
|
:is-incoming="isIncoming"
|
|
/>
|
|
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
|
|
<bubble-reply-to
|
|
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
|
|
:message="inReplyTo"
|
|
:message-type="data.message_type"
|
|
:parent-has-attachments="hasAttachments"
|
|
/>
|
|
<div v-if="isUnsupported">
|
|
<template v-if="isAFacebookInbox && isInstagram">
|
|
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
|
|
</template>
|
|
<template v-else-if="isAFacebookInbox">
|
|
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
|
|
</template>
|
|
<template v-else>
|
|
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
|
|
</template>
|
|
</div>
|
|
<bubble-text
|
|
v-else-if="data.content"
|
|
:message="message"
|
|
:is-email="isEmailContentType"
|
|
:display-quoted-button="displayQuotedButton"
|
|
/>
|
|
<bubble-integration
|
|
:message-id="data.id"
|
|
:content-attributes="contentAttributes"
|
|
:inbox-id="data.inbox_id"
|
|
/>
|
|
<span
|
|
v-if="isPending && hasAttachments"
|
|
class="chat-bubble has-attachment agent"
|
|
>
|
|
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
|
|
</span>
|
|
<div v-if="!isPending && hasAttachments">
|
|
<div v-for="attachment in attachments" :key="attachment.id">
|
|
<instagram-story
|
|
v-if="isAnInstagramStory"
|
|
:story-url="attachment.data_url"
|
|
@error="onMediaLoadError"
|
|
/>
|
|
<bubble-image-audio-video
|
|
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
|
:attachment="attachment"
|
|
@error="onMediaLoadError"
|
|
/>
|
|
<bubble-location
|
|
v-else-if="attachment.file_type === 'location'"
|
|
:latitude="attachment.coordinates_lat"
|
|
:longitude="attachment.coordinates_long"
|
|
:name="attachment.fallback_title"
|
|
/>
|
|
<bubble-contact
|
|
v-else-if="attachment.file_type === 'contact'"
|
|
:name="data.content"
|
|
:phone-number="attachment.fallback_title"
|
|
/>
|
|
<bubble-file v-else :url="attachment.data_url" />
|
|
</div>
|
|
</div>
|
|
<bubble-actions
|
|
:id="data.id"
|
|
:sender="data.sender"
|
|
:story-sender="storySender"
|
|
:external-error="externalError"
|
|
:story-id="`${storyId}`"
|
|
:is-a-tweet="isATweet"
|
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
|
:is-email="isEmailContentType"
|
|
:is-private="data.private"
|
|
:message-type="data.message_type"
|
|
:message-status="status"
|
|
:source-id="data.source_id"
|
|
:inbox-id="data.inbox_id"
|
|
:created-at="createdAt"
|
|
/>
|
|
</div>
|
|
<spinner v-if="isPending" size="tiny" />
|
|
<div
|
|
v-if="showAvatar"
|
|
v-tooltip.left="tooltipForSender"
|
|
class="sender--info"
|
|
>
|
|
<woot-thumbnail
|
|
:src="sender.thumbnail"
|
|
:username="senderNameForAvatar"
|
|
size="16px"
|
|
/>
|
|
<a
|
|
v-if="isATweet && isIncoming"
|
|
class="sender--available-name"
|
|
:href="twitterProfileLink"
|
|
target="_blank"
|
|
rel="noopener noreferrer nofollow"
|
|
>
|
|
{{ sender.name }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
|
<context-menu
|
|
v-if="isBubble && !isMessageDeleted"
|
|
:context-menu-position="contextMenuPosition"
|
|
:is-open="showContextMenu"
|
|
:enabled-options="contextMenuEnabledOptions"
|
|
:message="data"
|
|
@open="openContextMenu"
|
|
@close="closeContextMenu"
|
|
@replyTo="handleReplyTo"
|
|
/>
|
|
</div>
|
|
</li>
|
|
</template>
|
|
<script>
|
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
|
import BubbleActions from './bubble/Actions.vue';
|
|
import BubbleContact from './bubble/Contact.vue';
|
|
import BubbleFile from './bubble/File.vue';
|
|
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
|
import BubbleIntegration from './bubble/Integration.vue';
|
|
import BubbleLocation from './bubble/Location.vue';
|
|
import BubbleMailHead from './bubble/MailHead.vue';
|
|
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
|
import BubbleText from './bubble/Text.vue';
|
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
|
import InstagramStory from './bubble/InstagramStory.vue';
|
|
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
|
import Spinner from 'shared/components/Spinner.vue';
|
|
import alertMixin from 'shared/mixins/alertMixin';
|
|
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
|
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
|
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
|
|
|
|
export default {
|
|
components: {
|
|
BubbleActions,
|
|
BubbleContact,
|
|
BubbleFile,
|
|
BubbleImageAudioVideo,
|
|
BubbleIntegration,
|
|
BubbleLocation,
|
|
BubbleMailHead,
|
|
BubbleReplyTo,
|
|
BubbleText,
|
|
ContextMenu,
|
|
InstagramStory,
|
|
InstagramStoryReply,
|
|
Spinner,
|
|
},
|
|
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
|
props: {
|
|
data: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
isATweet: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isAFacebookInbox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isInstagram: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isAWhatsAppChannel: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isWebWidgetInbox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isAnEmailInbox: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
inboxSupportsReplyTo: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
inReplyTo: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
showContextMenu: false,
|
|
hasMediaLoadError: false,
|
|
contextMenuPosition: {},
|
|
showBackgroundHighlight: false,
|
|
};
|
|
},
|
|
computed: {
|
|
attachments() {
|
|
// Here it is used to get sender and created_at for each attachment
|
|
return this.data?.attachments.map(attachment => ({
|
|
...attachment,
|
|
sender: this.data.sender || {},
|
|
created_at: this.data.created_at || '',
|
|
}));
|
|
},
|
|
hasOneDayPassed() {
|
|
// Disable retry button if the message is failed and the message is older than 24 hours
|
|
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
|
|
},
|
|
shouldRenderMessage() {
|
|
return (
|
|
this.hasAttachments ||
|
|
this.data.content ||
|
|
this.isEmailContentType ||
|
|
this.isUnsupported ||
|
|
this.isAnIntegrationMessage
|
|
);
|
|
},
|
|
emailMessageContent() {
|
|
const {
|
|
html_content: { full: fullHTMLContent } = {},
|
|
text_content: { full: fullTextContent } = {},
|
|
} = this.contentAttributes.email || {};
|
|
return fullHTMLContent || fullTextContent || '';
|
|
},
|
|
displayQuotedButton() {
|
|
if (this.emailMessageContent.includes('<blockquote')) {
|
|
return true;
|
|
}
|
|
|
|
if (!this.isIncoming) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
message() {
|
|
// If the message is an email, emailMessageContent would be present
|
|
// In that case, we would use letter package to render the email
|
|
if (this.emailMessageContent && this.isIncoming) {
|
|
return this.emailMessageContent;
|
|
}
|
|
|
|
const botMessageContent = generateBotMessageContent(
|
|
this.contentType,
|
|
this.contentAttributes,
|
|
{
|
|
noResponseText: this.$t('CONVERSATION.NO_RESPONSE'),
|
|
csat: {
|
|
ratingTitle: this.$t('CONVERSATION.RATING_TITLE'),
|
|
feedbackTitle: this.$t('CONVERSATION.FEEDBACK_TITLE'),
|
|
},
|
|
}
|
|
);
|
|
|
|
if (this.contentType === 'input_csat') {
|
|
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE') + botMessageContent;
|
|
}
|
|
|
|
return (
|
|
this.formatMessage(
|
|
this.data.content,
|
|
this.isATweet,
|
|
this.data.private
|
|
) + botMessageContent
|
|
);
|
|
},
|
|
inReplyToMessageId() {
|
|
// Why not use the inReplyTo object directly?
|
|
// Glad you asked! The inReplyTo object may or may not be available
|
|
// depending on the current scroll position of the message list
|
|
// since old messages are only loaded when the user scrolls up
|
|
return this.data.content_attributes?.in_reply_to;
|
|
},
|
|
isAnInstagramStory() {
|
|
return this.contentAttributes.image_type === 'story_mention';
|
|
},
|
|
contextMenuEnabledOptions() {
|
|
return {
|
|
copy: this.hasText,
|
|
delete: this.hasText || this.hasAttachments,
|
|
cannedResponse: this.isOutgoing && this.hasText,
|
|
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
|
};
|
|
},
|
|
contentAttributes() {
|
|
return this.data.content_attributes || {};
|
|
},
|
|
externalError() {
|
|
return this.contentAttributes.external_error || '';
|
|
},
|
|
sender() {
|
|
return this.data.sender || {};
|
|
},
|
|
status() {
|
|
return this.data.status;
|
|
},
|
|
storySender() {
|
|
return this.contentAttributes.story_sender || null;
|
|
},
|
|
storyId() {
|
|
return this.contentAttributes.story_id || null;
|
|
},
|
|
storyUrl() {
|
|
return this.contentAttributes.story_url || null;
|
|
},
|
|
contentType() {
|
|
const {
|
|
data: { content_type: contentType },
|
|
} = this;
|
|
return contentType;
|
|
},
|
|
twitterProfileLink() {
|
|
const additionalAttributes = this.sender.additional_attributes || {};
|
|
const { screen_name: screenName } = additionalAttributes;
|
|
return `https://twitter.com/${screenName}`;
|
|
},
|
|
alignBubble() {
|
|
const { message_type: messageType } = this.data;
|
|
const isCentered = messageType === MESSAGE_TYPE.ACTIVITY;
|
|
const isLeftAligned = messageType === MESSAGE_TYPE.INCOMING;
|
|
const isRightAligned =
|
|
messageType === MESSAGE_TYPE.OUTGOING ||
|
|
messageType === MESSAGE_TYPE.TEMPLATE;
|
|
return {
|
|
center: isCentered,
|
|
left: isLeftAligned,
|
|
right: isRightAligned,
|
|
'has-context-menu': this.showContextMenu,
|
|
// this handles the offset required to align the context menu button
|
|
// extra alignment is required since a tweet message has a the user name and avatar below it
|
|
'has-tweet-menu': this.isATweet,
|
|
'has-bg': this.showBackgroundHighlight,
|
|
};
|
|
},
|
|
createdAt() {
|
|
return this.contentAttributes.external_created_at || this.data.created_at;
|
|
},
|
|
isBubble() {
|
|
return [0, 1, 3].includes(this.data.message_type);
|
|
},
|
|
isIncoming() {
|
|
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
|
},
|
|
isOutgoing() {
|
|
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
|
},
|
|
isTemplate() {
|
|
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
|
},
|
|
isAnIntegrationMessage() {
|
|
return this.contentType === 'integrations';
|
|
},
|
|
emailHeadAttributes() {
|
|
return {
|
|
email: this.contentAttributes.email,
|
|
cc: this.contentAttributes.cc_emails,
|
|
bcc: this.contentAttributes.bcc_emails,
|
|
};
|
|
},
|
|
hasAttachments() {
|
|
return !!(this.data.attachments && this.data.attachments.length > 0);
|
|
},
|
|
isMessageDeleted() {
|
|
return this.contentAttributes.deleted;
|
|
},
|
|
hasText() {
|
|
return !!this.data.content;
|
|
},
|
|
tooltipForSender() {
|
|
const name = this.senderNameForAvatar;
|
|
const { message_type: messageType } = this.data;
|
|
const showTooltip =
|
|
messageType === MESSAGE_TYPE.OUTGOING ||
|
|
messageType === MESSAGE_TYPE.TEMPLATE;
|
|
return showTooltip
|
|
? {
|
|
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
|
|
}
|
|
: false;
|
|
},
|
|
messageToolTip() {
|
|
if (this.isMessageDeleted) {
|
|
return false;
|
|
}
|
|
if (this.isFailed) {
|
|
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
|
|
}
|
|
return false;
|
|
},
|
|
wrapClass() {
|
|
return {
|
|
wrap: this.isBubble,
|
|
'activity-wrap': !this.isBubble,
|
|
'is-pending': this.isPending,
|
|
'is-failed': this.isFailed,
|
|
'is-email': this.isEmailContentType,
|
|
};
|
|
},
|
|
bubbleClass() {
|
|
return {
|
|
bubble: this.isBubble,
|
|
'is-private': this.data.private,
|
|
'is-unsupported': this.isUnsupported,
|
|
'is-image': this.hasMediaAttachment('image'),
|
|
'is-video': this.hasMediaAttachment('video'),
|
|
'is-text': this.hasText,
|
|
'is-from-bot': this.isSentByBot,
|
|
'is-failed': this.isFailed,
|
|
'is-email': this.isEmailContentType,
|
|
};
|
|
},
|
|
isUnsupported() {
|
|
return this.contentAttributes.is_unsupported ?? false;
|
|
},
|
|
isPending() {
|
|
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
|
},
|
|
isFailed() {
|
|
return this.data.status === MESSAGE_STATUS.FAILED;
|
|
},
|
|
isSentByBot() {
|
|
if (this.isPending || this.isFailed) return false;
|
|
return !this.sender.type || this.sender.type === 'agent_bot';
|
|
},
|
|
shouldShowContextMenu() {
|
|
return !(this.isFailed || this.isPending || this.isUnsupported);
|
|
},
|
|
showAvatar() {
|
|
if (this.isOutgoing || this.isTemplate) {
|
|
return true;
|
|
}
|
|
return this.isATweet && this.isIncoming && this.sender;
|
|
},
|
|
senderNameForAvatar() {
|
|
if (this.isOutgoing || this.isTemplate) {
|
|
const { name = this.$t('CONVERSATION.BOT') } = this.sender || {};
|
|
return name;
|
|
}
|
|
return '';
|
|
},
|
|
},
|
|
watch: {
|
|
data() {
|
|
this.hasMediaLoadError = false;
|
|
},
|
|
},
|
|
mounted() {
|
|
this.hasMediaLoadError = 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: {
|
|
isAttachmentImageVideoAudio(fileType) {
|
|
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
|
|
},
|
|
hasMediaAttachment(type) {
|
|
if (this.hasAttachments && this.data.attachments.length > 0) {
|
|
const { attachments = [{}] } = this.data;
|
|
const { file_type: fileType } = attachments[0];
|
|
return fileType === type && !this.hasMediaLoadError;
|
|
}
|
|
if (this.storyReply) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
handleContextMenuClick() {
|
|
this.showContextMenu = !this.showContextMenu;
|
|
},
|
|
async retrySendMessage() {
|
|
await this.$store.dispatch('sendMessageWithData', this.data);
|
|
},
|
|
onMediaLoadError() {
|
|
this.hasMediaLoadError = true;
|
|
},
|
|
openContextMenu(e) {
|
|
const shouldSkipContextMenu =
|
|
e.target?.classList.contains('skip-context-menu') ||
|
|
e.target?.tagName.toLowerCase() === 'a';
|
|
if (shouldSkipContextMenu || getSelection().toString()) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
if (e.type === 'contextmenu') {
|
|
this.$track(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
|
|
}
|
|
this.contextMenuPosition = {
|
|
x: e.pageX || e.clientX,
|
|
y: e.pageY || e.clientY,
|
|
};
|
|
this.showContextMenu = true;
|
|
},
|
|
closeContextMenu() {
|
|
this.showContextMenu = false;
|
|
this.contextMenuPosition = { x: null, y: null };
|
|
},
|
|
handleReplyTo() {
|
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
|
const { conversation_id: conversationId, id: replyTo } = this.data;
|
|
|
|
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
|
|
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
|
},
|
|
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>
|
|
<style lang="scss">
|
|
.wrap {
|
|
> .bubble {
|
|
@apply min-w-[128px];
|
|
|
|
&.is-unsupported {
|
|
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
|
|
|
|
.message-text--metadata .time {
|
|
@apply text-slate-400 dark:text-slate-300;
|
|
}
|
|
}
|
|
|
|
&.is-image,
|
|
&.is-video {
|
|
@apply p-0 overflow-hidden;
|
|
|
|
.image,
|
|
.video {
|
|
@apply max-w-[20rem] p-0.5;
|
|
|
|
> img,
|
|
> video {
|
|
@apply rounded-lg;
|
|
}
|
|
|
|
> video {
|
|
@apply h-full w-full object-cover;
|
|
}
|
|
}
|
|
|
|
.video {
|
|
@apply h-[11.25rem];
|
|
}
|
|
}
|
|
|
|
&.is-image.is-text > .message-text__wrap,
|
|
&.is-video.is-text > .message-text__wrap {
|
|
@apply max-w-[20rem] py-2 px-4;
|
|
}
|
|
|
|
&.is-private .file.message-text__wrap {
|
|
.file--icon {
|
|
@apply text-woot-400 dark:text-woot-400;
|
|
}
|
|
|
|
.text-block-title {
|
|
@apply text-slate-700 dark:text-slate-700;
|
|
}
|
|
|
|
.download.button {
|
|
@apply text-woot-400 dark:text-woot-400;
|
|
}
|
|
}
|
|
|
|
&.is-private.is-text > .message-text__wrap .link {
|
|
@apply text-woot-600 dark:text-woot-200;
|
|
}
|
|
|
|
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
|
|
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
|
|
}
|
|
|
|
&.is-from-bot {
|
|
@apply bg-violet-400 dark:bg-violet-400;
|
|
|
|
.message-text--metadata .time {
|
|
@apply text-violet-50 dark:text-violet-50;
|
|
}
|
|
|
|
&.is-private .message-text--metadata .time {
|
|
@apply text-slate-400 dark:text-slate-400;
|
|
}
|
|
}
|
|
|
|
&.is-failed {
|
|
@apply bg-red-200 dark:bg-red-200;
|
|
|
|
.message-text--metadata .time {
|
|
@apply text-red-50 dark:text-red-50;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.is-pending {
|
|
@apply relative opacity-80;
|
|
|
|
.spinner {
|
|
@apply absolute bottom-1 right-1;
|
|
}
|
|
|
|
> .is-image.is-text.bubble > .message-text__wrap {
|
|
@apply p-0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.wrap.is-email {
|
|
--bubble-max-width: 84% !important;
|
|
}
|
|
|
|
.sender--info {
|
|
@apply items-center text-black-700 dark:text-black-100 inline-flex py-1 px-0;
|
|
|
|
.sender--available-name {
|
|
@apply text-xs ml-1;
|
|
}
|
|
}
|
|
|
|
.message-failed--alert {
|
|
@apply text-red-900 dark:text-red-900 flex-grow text-right mt-1 mr-1 mb-0 ml-0;
|
|
}
|
|
|
|
li.left,
|
|
li.right {
|
|
@apply flex items-end;
|
|
}
|
|
|
|
li.left.has-tweet-menu .context-menu {
|
|
// this handles the offset required to align the context menu button
|
|
// extra alignment is required since a tweet message has a the user name and avatar below it
|
|
@apply mb-6;
|
|
}
|
|
|
|
li.has-bg {
|
|
@apply bg-woot-75 dark:bg-woot-600;
|
|
}
|
|
|
|
li.right .context-menu-wrap {
|
|
@apply ml-auto;
|
|
}
|
|
|
|
li.right {
|
|
@apply flex-row-reverse justify-end;
|
|
|
|
.wrap.is-pending {
|
|
@apply ml-auto;
|
|
}
|
|
|
|
.wrap.is-failed {
|
|
@apply flex items-end ml-auto;
|
|
}
|
|
}
|
|
|
|
.has-context-menu {
|
|
@apply bg-slate-50 dark:bg-slate-700;
|
|
}
|
|
|
|
.context-menu {
|
|
@apply relative;
|
|
}
|
|
|
|
/* Markdown styling */
|
|
|
|
.bubble .text-content {
|
|
p code {
|
|
@apply bg-slate-75 dark:bg-slate-700 inline-block leading-none rounded-sm p-1;
|
|
}
|
|
|
|
ol li {
|
|
@apply list-item list-decimal;
|
|
}
|
|
|
|
pre {
|
|
@apply bg-slate-75 dark:bg-slate-700 block border-slate-75 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-md p-2 mt-1 mb-2 leading-relaxed whitespace-pre-wrap;
|
|
|
|
code {
|
|
@apply bg-transparent text-slate-800 dark:text-slate-100 p-0;
|
|
}
|
|
}
|
|
|
|
blockquote {
|
|
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
|
|
|
p {
|
|
@apply text-slate-800 dark:text-slate-300;
|
|
}
|
|
}
|
|
}
|
|
|
|
.right .bubble .text-content {
|
|
p code {
|
|
@apply bg-woot-600 dark:bg-woot-600 text-white dark:text-white;
|
|
}
|
|
|
|
pre {
|
|
@apply bg-woot-800 dark:bg-woot-800 border-woot-700 dark:border-woot-700 text-white dark:text-white;
|
|
|
|
code {
|
|
@apply bg-transparent text-white dark:text-white;
|
|
}
|
|
}
|
|
|
|
blockquote {
|
|
@apply border-l-4 border-solid border-woot-400 dark:border-woot-400 text-white dark:text-white;
|
|
|
|
p {
|
|
@apply text-woot-75 dark:text-woot-75;
|
|
}
|
|
}
|
|
}
|
|
</style>
|