mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 02:32:29 +00:00
feat: Inbox list API integration (#8825)
* feat: Inbox view * feat: Bind real values * chore: code cleanup * feat: add observer * fix: Inbox icon * chore: more code cleanup * chore: Replace conversation id * chore: Minor fix * chore: Hide from side bar * chore: Fix eslint * chore: Minor fix * fix: dark mode color * chore: Minor fix * feat: Add description for each notification types * chore: remove commented code * Update InboxList.vue * Update InboxView.vue * chore: fix specs * fix: specs * Update InboxView.vue --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
|
||||
@@ -90,9 +90,6 @@
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
},
|
||||
"INBOX_PAGE": {
|
||||
"HEADER": "Inbox"
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"OFFLINE": "Offline"
|
||||
|
||||
17
app/javascript/dashboard/i18n/locale/en/inbox.json
Normal file
17
app/javascript/dashboard/i18n/locale/en/inbox.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"INBOX": {
|
||||
"LIST": {
|
||||
"TITLE": "Inbox",
|
||||
"LOADING": "Fetching notifications",
|
||||
"EOF": "All notifications loaded 🎉",
|
||||
"404": "There are no active notifications in this group."
|
||||
},
|
||||
"TYPES": {
|
||||
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
|
||||
"CONVERSATION_CREATION": "New conversation created",
|
||||
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import settings from './settings.json';
|
||||
import signup from './signup.json';
|
||||
import teamsSettings from './teamsSettings.json';
|
||||
import whatsappTemplates from './whatsappTemplates.json';
|
||||
import inbox from './inbox.json';
|
||||
|
||||
export default {
|
||||
...advancedFilters,
|
||||
@@ -62,4 +63,5 @@ export default {
|
||||
...signup,
|
||||
...teamsSettings,
|
||||
...whatsappTemplates,
|
||||
...inbox,
|
||||
};
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
|
||||
"SWITCH": "Switch",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"INBOX": "Inbox",
|
||||
"ALL_CONVERSATIONS": "All Conversations",
|
||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||
"PARTICIPATING_CONVERSATIONS": "Participating",
|
||||
|
||||
@@ -52,19 +52,77 @@ describe('#dateFormat', () => {
|
||||
});
|
||||
|
||||
describe('#shortTimestamp', () => {
|
||||
it('returns correct value', () => {
|
||||
// Test cases when withAgo is false or not provided
|
||||
it('returns correct value without ago', () => {
|
||||
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
|
||||
'now'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp(' minute ago')).toEqual('m');
|
||||
expect(TimeMixin.methods.shortTimestamp(' minutes ago')).toEqual('m');
|
||||
expect(TimeMixin.methods.shortTimestamp(' hour ago')).toEqual('h');
|
||||
expect(TimeMixin.methods.shortTimestamp(' hours ago')).toEqual('h');
|
||||
expect(TimeMixin.methods.shortTimestamp(' day ago')).toEqual('d');
|
||||
expect(TimeMixin.methods.shortTimestamp(' days ago')).toEqual('d');
|
||||
expect(TimeMixin.methods.shortTimestamp(' month ago')).toEqual('mo');
|
||||
expect(TimeMixin.methods.shortTimestamp(' months ago')).toEqual('mo');
|
||||
expect(TimeMixin.methods.shortTimestamp(' year ago')).toEqual('y');
|
||||
expect(TimeMixin.methods.shortTimestamp(' years ago')).toEqual('y');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago')).toEqual('1m');
|
||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago')).toEqual('12m');
|
||||
expect(TimeMixin.methods.shortTimestamp('a minute ago')).toEqual('1m');
|
||||
expect(TimeMixin.methods.shortTimestamp('an hour ago')).toEqual('1h');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago')).toEqual('1h');
|
||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago')).toEqual('2h');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 day ago')).toEqual('1d');
|
||||
expect(TimeMixin.methods.shortTimestamp('a day ago')).toEqual('1d');
|
||||
expect(TimeMixin.methods.shortTimestamp('3 days ago')).toEqual('3d');
|
||||
expect(TimeMixin.methods.shortTimestamp('a month ago')).toEqual('1mo');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 month ago')).toEqual('1mo');
|
||||
expect(TimeMixin.methods.shortTimestamp('2 months ago')).toEqual('2mo');
|
||||
expect(TimeMixin.methods.shortTimestamp('a year ago')).toEqual('1y');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 year ago')).toEqual('1y');
|
||||
expect(TimeMixin.methods.shortTimestamp('4 years ago')).toEqual('4y');
|
||||
});
|
||||
|
||||
// Test cases when withAgo is true
|
||||
it('returns correct value with ago', () => {
|
||||
expect(
|
||||
TimeMixin.methods.shortTimestamp('less than a minute ago', true)
|
||||
).toEqual('now');
|
||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago', true)).toEqual(
|
||||
'1m ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago', true)).toEqual(
|
||||
'12m ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('a minute ago', true)).toEqual(
|
||||
'1m ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('an hour ago', true)).toEqual(
|
||||
'1h ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago', true)).toEqual(
|
||||
'1h ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago', true)).toEqual(
|
||||
'2h ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('1 day ago', true)).toEqual(
|
||||
'1d ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('a day ago', true)).toEqual(
|
||||
'1d ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('3 days ago', true)).toEqual(
|
||||
'3d ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('a month ago', true)).toEqual(
|
||||
'1mo ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('1 month ago', true)).toEqual(
|
||||
'1mo ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('2 months ago', true)).toEqual(
|
||||
'2mo ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('a year ago', true)).toEqual(
|
||||
'1y ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('1 year ago', true)).toEqual(
|
||||
'1y ago'
|
||||
);
|
||||
expect(TimeMixin.methods.shortTimestamp('4 years ago', true)).toEqual(
|
||||
'4y ago'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,25 +28,36 @@ export default {
|
||||
const unixTime = fromUnixTime(time);
|
||||
return format(unixTime, dateFormat);
|
||||
},
|
||||
shortTimestamp(time) {
|
||||
shortTimestamp(time, withAgo = false) {
|
||||
// This function takes a time string and converts it to a short time string
|
||||
// with the following format: 1m, 1h, 1d, 1mo, 1y
|
||||
// The function also takes an optional boolean parameter withAgo
|
||||
// which will add the word "ago" to the end of the time string
|
||||
const suffix = withAgo ? ' ago' : '';
|
||||
const timeMappings = {
|
||||
'less than a minute ago': 'now',
|
||||
'a minute ago': `1m${suffix}`,
|
||||
'an hour ago': `1h${suffix}`,
|
||||
'a day ago': `1d${suffix}`,
|
||||
'a month ago': `1mo${suffix}`,
|
||||
'a year ago': `1y${suffix}`,
|
||||
};
|
||||
// Check if the time string is one of the specific cases
|
||||
if (timeMappings[time]) {
|
||||
return timeMappings[time];
|
||||
}
|
||||
const convertToShortTime = time
|
||||
.replace(/about|over|almost|/g, '')
|
||||
.replace('less than a minute ago', 'now')
|
||||
.replace(' minute ago', 'm')
|
||||
.replace(' minutes ago', 'm')
|
||||
.replace('a minute ago', 'm')
|
||||
.replace('an hour ago', 'h')
|
||||
.replace(' hour ago', 'h')
|
||||
.replace(' hours ago', 'h')
|
||||
.replace(' day ago', 'd')
|
||||
.replace('a day ago', 'd')
|
||||
.replace(' days ago', 'd')
|
||||
.replace('a month ago', 'mo')
|
||||
.replace(' months ago', 'mo')
|
||||
.replace(' month ago', 'mo')
|
||||
.replace('a year ago', 'y')
|
||||
.replace(' year ago', 'y')
|
||||
.replace(' years ago', 'y');
|
||||
.replace(' minute ago', `m${suffix}`)
|
||||
.replace(' minutes ago', `m${suffix}`)
|
||||
.replace(' hour ago', `h${suffix}`)
|
||||
.replace(' hours ago', `h${suffix}`)
|
||||
.replace(' day ago', `d${suffix}`)
|
||||
.replace(' days ago', `d${suffix}`)
|
||||
.replace(' month ago', `mo${suffix}`)
|
||||
.replace(' months ago', `mo${suffix}`)
|
||||
.replace(' year ago', `y${suffix}`)
|
||||
.replace(' years ago', `y${suffix}`);
|
||||
return convertToShortTime;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
const ConversationView = () => import('./ConversationView');
|
||||
const InboxView = () => import('../inbox/InboxView.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox'),
|
||||
name: 'inbox',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: InboxView,
|
||||
props: () => {},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/dashboard'),
|
||||
name: 'home',
|
||||
|
||||
@@ -5,7 +5,6 @@ import { routes as contactRoutes } from './contacts/routes';
|
||||
import { routes as notificationRoutes } from './notifications/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
import { routes as inboxRoutes } from './inbox/routes';
|
||||
|
||||
const AppContainer = () => import('./Dashboard.vue');
|
||||
const Suspended = () => import('./suspended/Index.vue');
|
||||
@@ -22,7 +21,6 @@ export default {
|
||||
...contactRoutes,
|
||||
...searchRoutes,
|
||||
...notificationRoutes,
|
||||
...inboxRoutes,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<script setup>
|
||||
// import { defineProps } from 'vue';
|
||||
|
||||
import PriorityIcon from './components/PriorityIcon.vue';
|
||||
import StatusIcon from './components/StatusIcon.vue';
|
||||
import InboxNameAndId from './components/InboxNameAndId.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
// const props = defineProps({
|
||||
// notificationItem: {
|
||||
// type: Object,
|
||||
// default: () => {},
|
||||
// },
|
||||
// });
|
||||
|
||||
const assigneeMeta = {
|
||||
thumbnail: '',
|
||||
name: 'Michael Johnson',
|
||||
};
|
||||
const { thumbnail, name } = assigneeMeta || {};
|
||||
const status = 'open';
|
||||
const priority = 'high';
|
||||
const inboxTypeMessage = 'Mentioned by Michael';
|
||||
const inboxMessage = 'What is the best way to get started?';
|
||||
const inbox = {
|
||||
inbox_id: 16787,
|
||||
inbox_name: 'Chatwoot Support',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex max-w-[360px] flex-col pl-5 pr-3 gap-2.5 py-3 w-full bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-500 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
>
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div
|
||||
class="absolute -left-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
/>
|
||||
<InboxNameAndId :inbox="inbox" />
|
||||
<div class="flex gap-2">
|
||||
<PriorityIcon :priority="priority" />
|
||||
<StatusIcon :status="status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<div class="flex gap-1.5 items-center max-w-[80%]">
|
||||
<Thumbnail
|
||||
v-if="assigneeMeta"
|
||||
:src="thumbnail"
|
||||
:username="name"
|
||||
size="20px"
|
||||
/>
|
||||
<div class="flex min-w-0">
|
||||
<span
|
||||
class="font-medium text-slate-800 dark:text-slate-100 text-xs overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{ inboxTypeMessage }}<span v-if="inboxTypeMessage">:</span>
|
||||
<span class="font-normal">{{ inboxMessage }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="font-medium text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
||||
>
|
||||
10h ago
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
103
app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
Normal file
103
app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import InboxCard from './components/InboxCard.vue';
|
||||
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||
export default {
|
||||
components: {
|
||||
InboxCard,
|
||||
IntersectionObserver,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
infiniteLoaderOptions: {
|
||||
root: this.$refs.notificationList,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
meta: 'notifications/getMeta',
|
||||
records: 'notifications/getNotifications',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
}),
|
||||
showEndOfList() {
|
||||
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('notifications/clear');
|
||||
this.$store.dispatch('notifications/index', { page: 1 });
|
||||
},
|
||||
methods: {
|
||||
openConversation(notification) {
|
||||
const {
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor: { id: conversationId },
|
||||
notification_type: notificationType,
|
||||
} = notification;
|
||||
|
||||
this.$track(ACCOUNT_EVENTS.OPEN_CONVERSATION_VIA_NOTIFICATION, {
|
||||
notificationType,
|
||||
});
|
||||
this.$store.dispatch('notifications/read', {
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
});
|
||||
this.$router.push(
|
||||
`/app/accounts/${this.accountId}/conversations/${conversationId}`
|
||||
);
|
||||
},
|
||||
onMarkAllDoneClick() {
|
||||
this.$track(ACCOUNT_EVENTS.MARK_AS_READ_NOTIFICATIONS);
|
||||
this.$store.dispatch('notifications/readAll');
|
||||
},
|
||||
loadMoreNotifications() {
|
||||
if (this.uiFlags.isAllNotificationsLoaded) return;
|
||||
this.$store.dispatch('notifications/index', { page: this.page + 1 });
|
||||
this.page += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col min-w-[360px] w-full max-w-[360px] h-full ltr:border-r border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<div
|
||||
class="flex text-xl w-full pl-5 pr-3 py-2 h-14 items-center font-medium text-slate-900 dark:text-slate-25 border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
{{ $t('INBOX.LIST.TITLE') }}
|
||||
</div>
|
||||
<div
|
||||
ref="notificationList"
|
||||
class="flex flex-col w-full h-full overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<inbox-card
|
||||
v-for="notificationItem in records"
|
||||
:key="notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
/>
|
||||
<div v-if="uiFlags.isFetching" class="text-center">
|
||||
<span class="spinner mt-4 mb-4" />
|
||||
</div>
|
||||
<p
|
||||
v-if="showEndOfList"
|
||||
class="text-center text-slate-300 dark:text-slate-400 p-4"
|
||||
>
|
||||
{{ $t('INBOX.LIST.EOF') }}
|
||||
</p>
|
||||
<intersection-observer
|
||||
v-if="!showEndOfList && !uiFlags.isFetching"
|
||||
:options="infiniteLoaderOptions"
|
||||
@observed="loadMoreNotifications"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import InboxList from './InboxList.vue';
|
||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxList,
|
||||
InboxItemHeader,
|
||||
ConversationBox,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
notifications: 'notifications/getNotifications',
|
||||
}),
|
||||
isInboxViewEnabled() {
|
||||
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
|
||||
this.currentAccountId,
|
||||
FEATURE_FLAGS.INBOX_VIEW
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Open inbox view if inbox view feature is enabled, else redirect to dashboard
|
||||
// TODO: Remove this code once inbox view feature is enabled for all accounts
|
||||
if (!this.isInboxViewEnabled) {
|
||||
this.$router.push({
|
||||
name: 'home',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<section class="flex w-full h-full bg-white dark:bg-slate-900">
|
||||
<InboxList />
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<InboxItemHeader :total-length="28" :current-index="1" />
|
||||
<ConversationBox class="h-full" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-col pl-5 pr-3 gap-2.5 py-3 w-full bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
@click="openConversation(notificationItem)"
|
||||
>
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div
|
||||
v-if="isUnread"
|
||||
class="absolute -left-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
/>
|
||||
<InboxNameAndId :inbox="inbox" :conversation-id="primaryActor.id" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<PriorityIcon :priority="primaryActor.priority" />
|
||||
<StatusIcon :status="primaryActor.status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<div class="flex gap-1.5 items-center max-w-[calc(100%-70px)]">
|
||||
<Thumbnail
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
:username="assigneeMeta.name"
|
||||
size="20px"
|
||||
/>
|
||||
<div class="flex min-w-0">
|
||||
<span
|
||||
class="font-medium text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<span class="font-normal text-sm">
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="font-medium max-w-[60px] text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
||||
>
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import PriorityIcon from './PriorityIcon.vue';
|
||||
import StatusIcon from './StatusIcon.vue';
|
||||
import InboxNameAndId from './InboxNameAndId.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
export default {
|
||||
components: {
|
||||
PriorityIcon,
|
||||
StatusIcon,
|
||||
InboxNameAndId,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
primaryActor() {
|
||||
return this.notificationItem?.primary_actor;
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](
|
||||
this.primaryActor.inbox_id
|
||||
);
|
||||
},
|
||||
isUnread() {
|
||||
return !this.notificationItem?.read_at;
|
||||
},
|
||||
meta() {
|
||||
return this.primaryActor?.meta;
|
||||
},
|
||||
assigneeMeta() {
|
||||
return this.meta?.assignee;
|
||||
},
|
||||
pushTitle() {
|
||||
return this.$t(
|
||||
`INBOX.TYPES.${this.notificationItem.notification_type.toUpperCase()}`
|
||||
);
|
||||
},
|
||||
lastActivityAt() {
|
||||
const dynamicTime = this.dynamicTime(
|
||||
this.notificationItem?.last_activity_at
|
||||
);
|
||||
return this.shortTimestamp(dynamicTime, true);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openConversation(notification) {
|
||||
this.$emit('open-conversation', notification);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,33 +1,34 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import PaginationButton from './PaginationButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
totalLength: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
export default {
|
||||
components: {
|
||||
PaginationButton,
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
props: {
|
||||
totalLength: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSnooze() {},
|
||||
onDelete() {},
|
||||
},
|
||||
});
|
||||
|
||||
const onSnooze = () => {
|
||||
// TODO: Implement snooze
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
// TODO: Implement delete
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex gap-2 py-2 pl-4 pr-2 justify-between items-center w-full border-b border-slate-200 dark:border-slate-500"
|
||||
class="flex gap-2 py-2 pl-4 h-14 pr-2 justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<pagination-button
|
||||
:total-length="props.totalLength"
|
||||
:current-index="props.currentIndex"
|
||||
:total-length="totalLength"
|
||||
:current-index="currentIndex"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<woot-button
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
<script>
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
export default {
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
conversationId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { inbox } = props;
|
||||
const { inbox_id: inboxId, inbox_name: inboxName } = inbox;
|
||||
computed: {
|
||||
inboxIcon() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center rounded-[4px] border border-slate-100 dark:border-slate-600 divide-x divide-slate-100 dark:divide-slate-600 bg-none"
|
||||
class="inline-flex items-center rounded-[4px] border border-slate-100 dark:border-slate-700/50 divide-x divide-slate-100 dark:divide-slate-700/50 bg-none"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 py-0.5 px-1.5">
|
||||
<div v-if="inbox" class="flex items-center gap-0.5 py-0.5 px-1.5">
|
||||
<fluent-icon
|
||||
class="text-slate-600 dark:text-slate-300"
|
||||
icon="globe-desktop"
|
||||
:icon="inboxIcon"
|
||||
size="14"
|
||||
/>
|
||||
<span class="font-medium text-slate-600 dark:text-slate-300 text-xs">
|
||||
{{ inboxName }}
|
||||
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center py-0.5 px-1.5">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-300 text-xs">
|
||||
{{ inboxId }}
|
||||
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
|
||||
{{ conversationId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<h1>Inbox View</h1>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@@ -1,20 +1,25 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import { CONVERSATION_PRIORITY } from 'shared/constants/messages';
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
priority: {
|
||||
type: String,
|
||||
default: '',
|
||||
export default {
|
||||
props: {
|
||||
priority: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_PRIORITY,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center rounded-md">
|
||||
<!-- High Priority -->
|
||||
<svg
|
||||
v-if="props.priority === CONVERSATION_PRIORITY.HIGH"
|
||||
v-if="priority === CONVERSATION_PRIORITY.HIGH"
|
||||
class="h-4 w-4"
|
||||
width="24"
|
||||
height="24"
|
||||
@@ -29,7 +34,7 @@ const props = defineProps({
|
||||
|
||||
<!-- Low Priority -->
|
||||
<svg
|
||||
v-if="props.priority === CONVERSATION_PRIORITY.LOW"
|
||||
v-if="priority === CONVERSATION_PRIORITY.LOW"
|
||||
class="h-4 w-4"
|
||||
width="24"
|
||||
height="24"
|
||||
@@ -44,7 +49,7 @@ const props = defineProps({
|
||||
|
||||
<!-- Medium Priority -->
|
||||
<svg
|
||||
v-if="props.priority === CONVERSATION_PRIORITY.MEDIUM"
|
||||
v-if="priority === CONVERSATION_PRIORITY.MEDIUM"
|
||||
class="h-4 w-4"
|
||||
width="24"
|
||||
height="24"
|
||||
@@ -59,7 +64,7 @@ const props = defineProps({
|
||||
|
||||
<!-- Urgent Priority -->
|
||||
<svg
|
||||
v-if="props.priority === CONVERSATION_PRIORITY.URGENT"
|
||||
v-if="priority === CONVERSATION_PRIORITY.URGENT"
|
||||
class="h-4 w-4"
|
||||
width="24"
|
||||
height="24"
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
<script setup>
|
||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center rounded-md">
|
||||
<!-- Pending -->
|
||||
<svg
|
||||
v-if="props.status === CONVERSATION_STATUS.PENDING"
|
||||
v-if="status === CONVERSATION_STATUS.PENDING"
|
||||
class="h-3.5 w-3.5"
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -29,7 +17,7 @@ const props = defineProps({
|
||||
</svg>
|
||||
<!-- Open -->
|
||||
<svg
|
||||
v-if="props.status === CONVERSATION_STATUS.OPEN"
|
||||
v-if="status === CONVERSATION_STATUS.OPEN"
|
||||
class="h-3.5 w-3.5"
|
||||
width="19"
|
||||
height="19"
|
||||
@@ -45,7 +33,7 @@ const props = defineProps({
|
||||
|
||||
<!-- Snoozed -->
|
||||
<svg
|
||||
v-if="props.status === CONVERSATION_STATUS.SNOOZED"
|
||||
v-if="status === CONVERSATION_STATUS.SNOOZED"
|
||||
class="h-3.5 w-3.5"
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -61,7 +49,7 @@ const props = defineProps({
|
||||
|
||||
<!-- Resolved -->
|
||||
<svg
|
||||
v-if="props.status === CONVERSATION_STATUS.RESOLVED"
|
||||
v-if="status === CONVERSATION_STATUS.RESOLVED"
|
||||
class="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -78,3 +66,21 @@ const props = defineProps({
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_STATUS,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
const SettingsWrapper = () => import('../settings/Wrapper.vue');
|
||||
const InboxView = () => import('./components/InboxView.vue');
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox'),
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'INBOX_PAGE.HEADER',
|
||||
icon: 'alert',
|
||||
showNewButton: false,
|
||||
showSidemenuIcon: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'inbox_index',
|
||||
component: InboxView,
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -5,7 +5,6 @@ import dashboard from './dashboard/dashboard.routes';
|
||||
import store from '../store';
|
||||
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
||||
import AnalyticsHelper from '../helper/AnalyticsHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const routes = [...dashboard.routes];
|
||||
|
||||
@@ -42,16 +41,6 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
|
||||
return '/app/login';
|
||||
}
|
||||
|
||||
// Open inbox view if inbox view feature is enabled, else redirect to dashboard
|
||||
// TODO: Remove this code once inbox view feature is enabled for all accounts
|
||||
const isInboxViewEnabled = store.getters['accounts/isFeatureEnabledGlobally'](
|
||||
user.account_id,
|
||||
FEATURE_FLAGS.INBOX_VIEW
|
||||
);
|
||||
if (to.name === 'inbox_index' && !isInboxViewEnabled) {
|
||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||
}
|
||||
|
||||
if (!to.name) {
|
||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,24 @@ export const actions = {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
index: async ({ commit }, { page = 1 } = {}) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
data: { payload, meta },
|
||||
},
|
||||
} = await NotificationsAPI.get(page);
|
||||
commit(types.SET_NOTIFICATIONS, payload);
|
||||
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
if (payload.length < 15) {
|
||||
commit(types.SET_ALL_NOTIFICATIONS_LOADED);
|
||||
}
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
unReadCount: async ({ commit } = {}) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true });
|
||||
try {
|
||||
@@ -59,4 +77,7 @@ export const actions = {
|
||||
deleteNotification({ commit }, data) {
|
||||
commit(types.DELETE_NOTIFICATION, data);
|
||||
},
|
||||
clear({ commit }) {
|
||||
commit(types.CLEAR_NOTIFICATIONS);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ const state = {
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isUpdatingUnreadCount: false,
|
||||
isAllNotificationsLoaded: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const mutations = {
|
||||
},
|
||||
[types.CLEAR_NOTIFICATIONS]: $state => {
|
||||
Vue.set($state, 'records', {});
|
||||
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', false);
|
||||
},
|
||||
[types.SET_NOTIFICATIONS_META]: ($state, data) => {
|
||||
const {
|
||||
@@ -61,4 +62,7 @@ export const mutations = {
|
||||
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||
Vue.set($state.meta, 'count', count);
|
||||
},
|
||||
[types.SET_ALL_NOTIFICATIONS_LOADED]: $state => {
|
||||
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', true);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,6 +39,38 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#index', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
payload: [{ id: 1 }],
|
||||
meta: { count: 3, current_page: 1, unread_count: 2 },
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.index({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_NOTIFICATIONS, [{ id: 1 }]],
|
||||
[
|
||||
types.SET_NOTIFICATIONS_META,
|
||||
{ count: 3, current_page: 1, unread_count: 2 },
|
||||
],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||
[types.SET_ALL_NOTIFICATIONS_LOADED],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.index({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unReadCount', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: 1 });
|
||||
@@ -107,4 +139,11 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('sends correct actions', async () => {
|
||||
await actions.clear({ commit });
|
||||
expect(commit.mock.calls).toEqual([[types.CLEAR_NOTIFICATIONS]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,10 @@ describe('#mutations', () => {
|
||||
|
||||
describe('#CLEAR_NOTIFICATIONS', () => {
|
||||
it('clear notifications', () => {
|
||||
const state = { records: { 1: { id: 1 } } };
|
||||
const state = {
|
||||
records: { 1: { id: 1 } },
|
||||
uiFlags: { isAllNotificationsLoaded: true },
|
||||
};
|
||||
mutations[types.CLEAR_NOTIFICATIONS](state);
|
||||
expect(state.records).toEqual({});
|
||||
});
|
||||
@@ -141,4 +144,12 @@ describe('#mutations', () => {
|
||||
expect(state.meta.count).toEqual(232);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_NOTIFICATIONS_LOADED', () => {
|
||||
it('set all notifications loaded', () => {
|
||||
const state = { uiFlags: { isAllNotificationsLoaded: false } };
|
||||
mutations[types.SET_ALL_NOTIFICATIONS_LOADED](state);
|
||||
expect(state.uiFlags).toEqual({ isAllNotificationsLoaded: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||
SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED',
|
||||
|
||||
// Contact Conversation
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
"location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z",
|
||||
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
||||
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
||||
"mail-inbox-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5h-11.5ZM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25v-3.25Zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Z",
|
||||
"mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z",
|
||||
"mail-unread-outline": "M16 6.5H5.25a1.75 1.75 0 0 0-1.744 1.606l-.004.1L11 12.153l6.03-3.174a3.489 3.489 0 0 0 2.97.985v6.786a3.25 3.25 0 0 1-3.066 3.245L16.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-8.5a3.25 3.25 0 0 1 3.066-3.245L5.25 5h11.087A3.487 3.487 0 0 0 16 6.5Zm2.5 3.399-7.15 3.765a.75.75 0 0 1-.603.042l-.096-.042L3.5 9.9v6.85a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.899ZM19.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z",
|
||||
"mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z",
|
||||
|
||||
Reference in New Issue
Block a user