mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-25 22:09:36 +00:00
Merge pull request #731 from abhinavxd/fix/inbox-card-alignment
align inbox channel card headings
This commit is contained in:
+13
-11
@@ -191,27 +191,29 @@ func campaignWithinHours(app *App, c proactive.Campaign, now time.Time, loc *tim
|
||||
if err := json.Unmarshal(hours.Hours, &working); err != nil {
|
||||
return false, campaignInputError(app)
|
||||
}
|
||||
previousDate := local.AddDate(0, 0, -1)
|
||||
previous := working[previousDate.Weekday().String()]
|
||||
for _, holiday := range holidays {
|
||||
if holiday.Date == local.Format(time.DateOnly) {
|
||||
return false, nil
|
||||
}
|
||||
if holiday.Date == previousDate.Format(time.DateOnly) {
|
||||
previous = bhmodels.WorkingHours{}
|
||||
}
|
||||
}
|
||||
day, ok := working[local.Weekday().String()]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
day := working[local.Weekday().String()]
|
||||
clock := local.Format("15:04")
|
||||
return withinWorkingHours(clock, day), nil
|
||||
return withinWorkingHours(clock, day, previous), nil
|
||||
}
|
||||
|
||||
func withinWorkingHours(clock string, day bhmodels.WorkingHours) bool {
|
||||
if day.Open == day.Close {
|
||||
return false
|
||||
func withinWorkingHours(clock string, day, previous bhmodels.WorkingHours) bool {
|
||||
if day.Open < day.Close && clock >= day.Open && clock < day.Close {
|
||||
return true
|
||||
}
|
||||
if day.Open < day.Close {
|
||||
return clock >= day.Open && clock < day.Close
|
||||
if day.Open > day.Close && clock >= day.Open {
|
||||
return true
|
||||
}
|
||||
return clock >= day.Open || clock < day.Close
|
||||
return previous.Open > previous.Close && clock < previous.Close
|
||||
}
|
||||
|
||||
func campaignSender(app *App, id int) (umodels.User, error) {
|
||||
|
||||
@@ -4,33 +4,94 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
bh "github.com/abhinavxd/libredesk/internal/business_hours"
|
||||
bhmodels "github.com/abhinavxd/libredesk/internal/business_hours/models"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat/proactive"
|
||||
"github.com/abhinavxd/libredesk/internal/testutil"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/volatiletech/null/v9"
|
||||
)
|
||||
|
||||
func TestWithinWorkingHours(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clock string
|
||||
day bhmodels.WorkingHours
|
||||
want bool
|
||||
name string
|
||||
clock string
|
||||
day bhmodels.WorkingHours
|
||||
previous bhmodels.WorkingHours
|
||||
want bool
|
||||
}{
|
||||
{name: "normal open", clock: "09:00", day: bhmodels.WorkingHours{Open: "09:00", Close: "17:00"}, want: true},
|
||||
{name: "normal close", clock: "17:00", day: bhmodels.WorkingHours{Open: "09:00", Close: "17:00"}},
|
||||
{name: "overnight evening", clock: "23:00", day: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}, want: true},
|
||||
{name: "overnight morning", clock: "01:00", day: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}, want: true},
|
||||
{name: "overnight morning", clock: "01:00", previous: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}, want: true},
|
||||
{name: "overnight midnight", clock: "00:00", previous: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}, want: true},
|
||||
{name: "overnight close", clock: "02:00", previous: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}},
|
||||
{name: "today overnight has not opened", clock: "01:00", day: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}},
|
||||
{name: "previous daytime does not carry over", clock: "01:00", previous: bhmodels.WorkingHours{Open: "09:00", Close: "17:00"}},
|
||||
{name: "today daytime and previous overnight", clock: "01:00", day: bhmodels.WorkingHours{Open: "09:00", Close: "17:00"}, previous: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}, want: true},
|
||||
{name: "overnight closed", clock: "12:00", day: bhmodels.WorkingHours{Open: "22:00", Close: "02:00"}},
|
||||
{name: "same time closed", clock: "09:00", day: bhmodels.WorkingHours{Open: "09:00", Close: "09:00"}},
|
||||
{name: "no schedule", clock: "01:00"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := withinWorkingHours(tc.clock, tc.day); got != tc.want {
|
||||
if got := withinWorkingHours(tc.clock, tc.day, tc.previous); got != tc.want {
|
||||
t.Fatalf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCampaignWithinHours(t *testing.T) {
|
||||
db := testutil.NewDB(t, "widget_campaign_hours")
|
||||
i18n := testutil.NewI18n(t)
|
||||
manager, err := bh.New(bh.Opts{DB: db, I18n: i18n})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hours, err := manager.Create("Overnight", null.String{} /* description */, false /* isAlwaysOpen */, types.JSONText(`{"Friday":{"open":"22:00","close":"02:00"},"Sunday":{"open":"22:00","close":"02:00"},"Monday":{"open":"09:00","close":"17:00"}}`), types.JSONText(`[{"date":"2026-10-31"},{"date":"2026-11-06"},{"date":"2026-11-08"}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := &App{businessHours: manager, i18n: i18n}
|
||||
campaign := proactive.Campaign{BusinessHours: "inside", BusinessHoursID: hours.ID}
|
||||
loc, err := time.LoadLocation("Asia/Kolkata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
now string
|
||||
want bool
|
||||
}{
|
||||
{name: "Friday opening", now: "2026-10-23T16:30:00Z", want: true},
|
||||
{name: "Saturday early morning without Saturday hours", now: "2026-10-23T19:30:00Z", want: true},
|
||||
{name: "Saturday closing", now: "2026-10-23T20:30:00Z"},
|
||||
{name: "Friday early morning before opening", now: "2026-10-22T19:30:00Z"},
|
||||
{name: "Sunday overnight carries into Monday", now: "2026-10-25T19:30:00Z", want: true},
|
||||
{name: "holiday blocks previous overnight hours", now: "2026-10-30T19:30:00Z"},
|
||||
{name: "holiday cancels evening shift", now: "2026-11-06T17:30:00Z"},
|
||||
{name: "holiday shift cannot carry into midnight", now: "2026-11-06T18:30:00Z"},
|
||||
{name: "holiday shift cannot carry into an unscheduled day", now: "2026-11-06T19:30:00Z"},
|
||||
{name: "holiday shift cannot carry into a scheduled day", now: "2026-11-08T19:30:00Z"},
|
||||
{name: "day after holiday opens at its own hours", now: "2026-11-09T03:30:00Z", want: true},
|
||||
{name: "next week overnight shift remains open", now: "2026-11-13T19:30:00Z", want: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
now, err := time.Parse(time.RFC3339, tc.now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := campaignWithinHours(app, campaign, now, loc)
|
||||
if err != nil || got != tc.want {
|
||||
t.Fatalf("got %v, %v, want %v", got, err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCampaignCooldown(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-full w-full min-w-0 flex-col items-stretch whitespace-normal bg-card p-4 text-left font-normal text-card-foreground"
|
||||
class="h-full w-full min-w-0 flex-col items-stretch justify-start whitespace-normal bg-card p-4 text-left font-normal text-card-foreground"
|
||||
@click="handleClick">
|
||||
<span class="mb-2 flex items-center">
|
||||
<img v-if="typeof icon === 'string'" :src="icon" alt="" class="w-6 h-6 mr-2" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="sticky bottom-0 bg-background border-t border-border px-4 py-3 mt-auto">
|
||||
<div class="sticky bottom-0 bg-card border-t border-border px-4 py-3 mt-auto">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-muted-foreground tabular-nums">
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
<template>
|
||||
<table class="min-w-full table-fixed divide-y divide-border">
|
||||
<thead class="bg-muted">
|
||||
<tr>
|
||||
<th
|
||||
v-for="(header, index) in headers"
|
||||
:key="index"
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
|
||||
>
|
||||
{{ header }}
|
||||
</th>
|
||||
<th v-if="showDelete" scope="col" class="relative px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-background divide-y divide-border">
|
||||
<!-- Loading State -->
|
||||
<template v-if="loading">
|
||||
<tr v-for="i in skeletonRows" :key="`skeleton-${i}`" class="hover:bg-accent">
|
||||
<td
|
||||
v-for="(header, index) in headers"
|
||||
:key="`skeleton-cell-${index}`"
|
||||
class="px-6 py-3 text-sm font-medium text-foreground whitespace-normal break-words"
|
||||
>
|
||||
<Skeleton class="h-4 w-[85%]" />
|
||||
</td>
|
||||
<td v-if="showDelete" class="px-6 py-4 text-sm text-muted-foreground">
|
||||
<Skeleton class="h-8 w-8 rounded-md" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- No Results State -->
|
||||
<template v-else-if="data.length === 0">
|
||||
<div class="relative overflow-auto rounded-lg border border-border bg-card shadow-sm">
|
||||
<table class="min-w-full table-fixed divide-y divide-border">
|
||||
<thead class="bg-muted/40">
|
||||
<tr>
|
||||
<td :colspan="headers.length + (showDelete ? 1 : 0)" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-md text-muted-foreground">
|
||||
{{
|
||||
$t('globals.messages.noResultsFound')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Data Rows -->
|
||||
<template v-else>
|
||||
<tr v-for="(item, index) in data" :key="index" class="hover:bg-accent">
|
||||
<td
|
||||
v-for="key in keys"
|
||||
:key="key"
|
||||
class="p-4 text-sm text-foreground whitespace-normal break-words"
|
||||
<th
|
||||
v-for="(header, index) in headers"
|
||||
:key="index"
|
||||
scope="col"
|
||||
class="h-11 px-4 text-left text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
<slot :name="`cell-${key}`" :value="item[key]" :item="item">
|
||||
{{ item[key] }}
|
||||
</slot>
|
||||
</td>
|
||||
<td v-if="showDelete" class="px-6 py-4 text-sm text-muted-foreground">
|
||||
<Button size="xs" variant="ghost" @click.prevent="deleteItem(item)">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</td>
|
||||
{{ header }}
|
||||
</th>
|
||||
<th v-if="showDelete" scope="col" class="relative px-6 py-3"></th>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody class="bg-card divide-y divide-border">
|
||||
<!-- Loading State -->
|
||||
<template v-if="loading">
|
||||
<tr v-for="i in skeletonRows" :key="`skeleton-${i}`" class="hover:bg-accent">
|
||||
<td
|
||||
v-for="(header, index) in headers"
|
||||
:key="`skeleton-cell-${index}`"
|
||||
class="px-4 py-3 text-sm text-foreground whitespace-normal break-words"
|
||||
>
|
||||
<Skeleton class="h-4 w-[85%]" />
|
||||
</td>
|
||||
<td v-if="showDelete" class="px-6 py-4 text-sm text-muted-foreground">
|
||||
<Skeleton class="h-8 w-8 rounded-md" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- No Results State -->
|
||||
<template v-else-if="data.length === 0">
|
||||
<tr>
|
||||
<td :colspan="headers.length + (showDelete ? 1 : 0)" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-md text-muted-foreground">
|
||||
{{ $t('globals.messages.noResultsFound') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Data Rows -->
|
||||
<template v-else>
|
||||
<tr v-for="(item, index) in data" :key="index" class="hover:bg-accent">
|
||||
<td
|
||||
v-for="key in keys"
|
||||
:key="key"
|
||||
class="px-4 py-3 text-sm text-foreground whitespace-normal break-words"
|
||||
>
|
||||
<slot :name="`cell-${key}`" :value="item[key]" :item="item">
|
||||
{{ item[key] }}
|
||||
</slot>
|
||||
</td>
|
||||
<td v-if="showDelete" class="px-6 py-4 text-sm text-muted-foreground">
|
||||
<Button type="button" size="xs" variant="ghost" @click.prevent="deleteItem(item)">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</FormField>
|
||||
</FormGrid>
|
||||
</fieldset>
|
||||
<div class="sticky bottom-0 z-10 flex border-t border-border bg-card py-3">
|
||||
<div class="flex border-t border-border pt-4">
|
||||
<Button type="submit" :isLoading="formLoading">{{ submitLabel }}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -780,7 +780,7 @@
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div class="sticky bottom-0 z-10 !mt-0 border-t bg-background py-4">
|
||||
<div class="border-t border-border pt-4">
|
||||
<Button type="submit" :is-loading="isLoading" :disabled="isLoading">
|
||||
{{ submitLabel }}
|
||||
</Button>
|
||||
|
||||
@@ -1729,13 +1729,19 @@ const jsApiSnippet = computed(() => {
|
||||
return `window.Libredesk.show();
|
||||
window.Libredesk.hide();
|
||||
window.Libredesk.toggle();
|
||||
window.Libredesk.isVisible();
|
||||
|
||||
window.Libredesk.setLanguage('en-US');
|
||||
window.Libredesk.trackEvent('checkout_started');
|
||||
|
||||
window.Libredesk.setUser('SIGNED_JWT_TOKEN');
|
||||
window.Libredesk.logout();
|
||||
|
||||
window.Libredesk.onShow(function() {});
|
||||
window.Libredesk.onHide(function() {});
|
||||
window.Libredesk.onUnreadCountChange(function(count) {});`
|
||||
window.Libredesk.onUnreadCountChange(function(count) {});
|
||||
|
||||
window.Libredesk.destroy();`
|
||||
})
|
||||
|
||||
function defaultBranding(theme) {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<div class="rounded-t-xl bg-background text-foreground">
|
||||
<div class="text-foreground">
|
||||
<!-- Fullscreen editor -->
|
||||
<Dialog :open="isEditorFullscreen" @update:open="isEditorFullscreen = false">
|
||||
<DialogContent
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
<!-- Main Editor non-fullscreen -->
|
||||
<div
|
||||
class="bg-background text-card-foreground box m-2 px-2 pt-2 flex flex-col relative"
|
||||
class="bg-card text-card-foreground box m-2 px-2 pt-2 flex flex-col relative"
|
||||
:class="{ '!bg-private': messageType === 'private_note', 'ai-generating': isGenerating }"
|
||||
v-if="!isCollapsed && !isEditorFullscreen"
|
||||
>
|
||||
@@ -787,14 +787,4 @@ watch(
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// Media files and macro state are restored per draft by the draft manager; resetting here would race ahead of the save and drop them.
|
||||
watch(
|
||||
() => conversationStore.current?.uuid,
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
activeContentRef()?.focus()
|
||||
}, 100)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
:message-type="messageType"
|
||||
:placeholder="isCramped ? t('globals.terms.typeMessage') : t('editor.hint.full')"
|
||||
:insertContent="insertContent"
|
||||
:autoFocus="true"
|
||||
:autoFocus="false"
|
||||
:disabled="isDraftLoading"
|
||||
:enableMentions="messageType === 'private_note'"
|
||||
:enableConversationReferences="messageType === 'private_note'"
|
||||
@@ -502,14 +502,20 @@ watch(
|
||||
} else if (newType === 'private_note') {
|
||||
macroStore.setCurrentView('adding_private_note')
|
||||
}
|
||||
// Focus editor on tab change
|
||||
setTimeout(() => {
|
||||
editorRef.value?.focus()
|
||||
}, 50)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
[messageType, () => props.isDraftLoading, () => conversationStore.current?.uuid],
|
||||
async () => {
|
||||
await nextTick()
|
||||
if (props.isDraftLoading || document.activeElement?.matches('input, textarea')) return
|
||||
editorRef.value?.focus()
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
// Expose focus method for parent components
|
||||
const focus = () => {
|
||||
editorRef.value?.focus()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="sticky bottom-0 mt-auto bg-background pb-4 pt-3">
|
||||
<div class="sticky bottom-0 mt-auto bg-card pb-4 pt-3">
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 rounded-md border border-border px-4 py-3 shadow-sm"
|
||||
>
|
||||
|
||||
@@ -11,18 +11,14 @@
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tabs v-model="previewUserType">
|
||||
<AdminToolbar>
|
||||
<template #left>
|
||||
<TabsList class="w-max justify-start h-8 p-0.5">
|
||||
<TabsTrigger value="visitors" class="text-xs">
|
||||
{{ $t('admin.inbox.livechat.userSettings.visitors') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="users" class="text-xs">
|
||||
{{ $t('globals.terms.users') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</template>
|
||||
</AdminToolbar>
|
||||
<TabsList class="w-max justify-start h-8 p-0.5">
|
||||
<TabsTrigger value="visitors" class="text-xs">
|
||||
{{ $t('admin.inbox.livechat.userSettings.visitors') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="users" class="text-xs">
|
||||
{{ $t('globals.terms.users') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<PreviewThemeToggle v-model="previewTheme" />
|
||||
</div>
|
||||
@@ -84,7 +80,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminToolbar from '@main/layouts/admin/AdminToolbar.vue'
|
||||
import { ref, provide } from 'vue'
|
||||
import AdminSplitLayout from '@/layouts/admin/AdminSplitLayout.vue'
|
||||
import LivechatWidgetPreview from '@/features/admin/inbox/LivechatWidgetPreview.vue'
|
||||
|
||||
@@ -214,7 +214,7 @@ const setupParentMessageListeners = () => {
|
||||
widgetStore.navigateToHome()
|
||||
signalWidgetLoaded()
|
||||
} else if (event.data.type === 'CAMPAIGN_CONTEXT') {
|
||||
await proactive.next(event.data.context)
|
||||
await proactive.next(event.data.context, event.data.pageGeneration)
|
||||
} else if (event.data.type === 'CAMPAIGN_EVENT') {
|
||||
await proactive.event(event.data.event, event.data.id)
|
||||
} else if (event.data.type === 'PAGE_VISIT') {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useProactiveStore = defineStore('proactive', () => {
|
||||
const abandon = () => {
|
||||
pending.value = null
|
||||
}
|
||||
const next = async (context) => {
|
||||
const next = async (context, pageGeneration) => {
|
||||
if (!browserKey || busy || invitation.value || pending.value || !widget.config.has_campaigns || widget.isOpen || chat.getConversations.some(item => item.unread_message_count > 0)) return
|
||||
busy = true
|
||||
const request = generation
|
||||
@@ -40,7 +40,7 @@ export const useProactiveStore = defineStore('proactive', () => {
|
||||
if (request !== generation || !response.data.data || widget.isOpen) return
|
||||
const delivery = response.data.data
|
||||
invitation.value = delivery
|
||||
postToParent({ type: 'SHOW_CAMPAIGN', delivery })
|
||||
postToParent({ type: 'SHOW_CAMPAIGN', delivery, url: context.url, pageGeneration })
|
||||
} catch { return false }
|
||||
finally { if (request === generation) busy = false }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import api from '@widget/api/index.js'
|
||||
import { useProactiveStore } from './proactive.js'
|
||||
|
||||
vi.mock('@widget/api/index.js', () => ({ default: { nextCampaign: vi.fn(), campaignEvent: vi.fn() } }))
|
||||
vi.mock('@widget/store/widget.js', () => ({ useWidgetStore: () => ({ config: { has_campaigns: true }, isOpen: false }) }))
|
||||
vi.mock('@widget/store/chat.js', () => ({ useChatStore: () => ({ getConversations: [] }) }))
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
window.history.replaceState({}, '', '/?parent_origin=https://example.com')
|
||||
})
|
||||
|
||||
describe('campaign delivery', () => {
|
||||
test('sends the requested page URL with a delayed response', async () => {
|
||||
const store = useProactiveStore()
|
||||
store.setBrowserKey('00000000-0000-4000-8000-000000000001')
|
||||
store.setSessionKey('00000000-0000-4000-8000-000000000002')
|
||||
let resolve
|
||||
api.nextCampaign.mockImplementationOnce(() => new Promise(done => { resolve = done }))
|
||||
const post = vi.spyOn(window.parent, 'postMessage').mockImplementation(() => {})
|
||||
const delivery = { id: 'delivery' }
|
||||
|
||||
const request = store.next({ url: 'https://example.com/pricing' }, 7 /* pageGeneration */)
|
||||
resolve({ data: { data: delivery } })
|
||||
await request
|
||||
|
||||
expect(post).toHaveBeenCalledWith({
|
||||
type: 'SHOW_CAMPAIGN', delivery, url: 'https://example.com/pricing', pageGeneration: 7
|
||||
}, 'https://example.com')
|
||||
await store.event('dropped', delivery.id)
|
||||
expect(store.invitation).toBeNull()
|
||||
expect(api.campaignEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
// @vitest-environment jsdom
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(import.meta.dirname, '../../../../static/widget.js'), 'utf8')
|
||||
let widget
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval', 'setTimeout', 'clearTimeout', 'performance'] })
|
||||
window.history.replaceState({}, '', '/pricing')
|
||||
delete window.__libredeskWidgetLoaded
|
||||
window.eval(source)
|
||||
vi.spyOn(window.Libredesk.prototype, 'init').mockImplementation(() => {})
|
||||
widget = new window.Libredesk({ baseURL: window.location.origin, inboxID: 'test' })
|
||||
vi.spyOn(widget, 'renderPreviews').mockImplementation(() => { widget.previewHost = {} })
|
||||
vi.spyOn(widget, 'postToIframe').mockImplementation(() => {})
|
||||
Object.defineProperty(document, 'hidden', { configurable: true, value: false })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
widget.stopPageTracking()
|
||||
vi.restoreAllMocks()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('campaign navigation', () => {
|
||||
const deliver = (url, pageGeneration = widget.campaignPageGeneration) => {
|
||||
const contentWindow = {}
|
||||
widget.iframe = { contentWindow }
|
||||
widget.handleMessage({
|
||||
source: contentWindow,
|
||||
origin: window.location.origin,
|
||||
data: { type: 'SHOW_CAMPAIGN', delivery: { id: 'pricing-message' }, url, pageGeneration }
|
||||
})
|
||||
}
|
||||
|
||||
test('drops a delayed response after navigation before the next timer tick', () => {
|
||||
const requestedURL = window.location.href
|
||||
window.history.pushState({}, '', '/checkout')
|
||||
|
||||
deliver(requestedURL)
|
||||
|
||||
expect(widget.campaignData).toBeNull()
|
||||
expect(widget.renderPreviews).not.toHaveBeenCalled()
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_EVENT', event: 'dropped', id: 'pricing-message' })
|
||||
})
|
||||
|
||||
test('shows responses for the current page', () => {
|
||||
deliver(window.location.href)
|
||||
|
||||
expect(widget.campaignData).toEqual({ id: 'pricing-message' })
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_EVENT', event: 'displayed', id: 'pricing-message' })
|
||||
})
|
||||
|
||||
test.each(['pushState', 'replaceState'])('rejects earlier visits after returning to the same URL with %s', method => {
|
||||
widget.startPageTracking()
|
||||
const requestedURL = window.location.href
|
||||
const requestedGeneration = widget.campaignPageGeneration
|
||||
for (let visit = 0; visit < 2; visit++) {
|
||||
window.history[method]({}, '', '/checkout')
|
||||
window.history[method]({}, '', '/pricing')
|
||||
}
|
||||
widget.renderPreviews.mockClear()
|
||||
|
||||
deliver(requestedURL, requestedGeneration)
|
||||
|
||||
expect(widget.campaignData).toBeNull()
|
||||
expect(widget.renderPreviews).not.toHaveBeenCalled()
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_EVENT', event: 'dropped', id: 'pricing-message' })
|
||||
|
||||
deliver(window.location.href)
|
||||
|
||||
expect(widget.campaignData).toEqual({ id: 'pricing-message' })
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_EVENT', event: 'displayed', id: 'pricing-message' })
|
||||
})
|
||||
|
||||
test.each([false, true])('preserves an immediate event with page tracking enabled: %s', pageTracking => {
|
||||
if (pageTracking) widget.startPageTracking()
|
||||
widget.campaignDelay = 3
|
||||
widget.startCampaignTracking()
|
||||
widget.trackEvent('old-page')
|
||||
vi.advanceTimersByTime(60000)
|
||||
widget.postToIframe.mockClear()
|
||||
|
||||
window.history.pushState({}, '', '/checkout')
|
||||
widget.trackEvent('checkout-viewed')
|
||||
vi.advanceTimersByTime(3000)
|
||||
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_CONTEXT', pageGeneration: widget.campaignPageGeneration, context: {
|
||||
url: window.location.href, mobile: widget.isMobile, active_seconds: 3, event: 'checkout-viewed'
|
||||
} })
|
||||
})
|
||||
|
||||
test('clears old page events and visible messages on navigation', () => {
|
||||
widget.startPageTracking()
|
||||
widget.startCampaignTracking()
|
||||
widget.trackEvent('pricing-viewed')
|
||||
widget.campaignData = { id: 'pricing-message' }
|
||||
|
||||
window.history.pushState({}, '', '/checkout')
|
||||
vi.advanceTimersByTime(2000)
|
||||
|
||||
expect(widget.campaignData).toBeNull()
|
||||
expect(widget.postToIframe).toHaveBeenCalledWith({ type: 'CAMPAIGN_CONTEXT', pageGeneration: widget.campaignPageGeneration, context: {
|
||||
url: window.location.href, mobile: widget.isMobile, active_seconds: 2, event: ''
|
||||
} })
|
||||
})
|
||||
})
|
||||
@@ -351,7 +351,11 @@ describe('Conversation lifecycle', () => {
|
||||
|
||||
for (const field of ['CC', 'BCC']) {
|
||||
it(`keeps ${field} open and focused while replacing an address`, () => {
|
||||
cy.intercept('GET', '**/api/v1/drafts', (req) => {
|
||||
req.continue((res) => res.setDelay(1000))
|
||||
}).as('loadDrafts')
|
||||
openConversation(ccConversationUuid)
|
||||
cy.wait('@loadDrafts')
|
||||
cy.get('.tiptap.ProseMirror[contenteditable="true"]').should('be.focused')
|
||||
if (field === 'BCC') {
|
||||
cy.contains('button', /^BCC$/).click()
|
||||
|
||||
@@ -431,7 +431,7 @@ describe('Email inbox form', () => {
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('fits a narrow viewport and keeps the save action visible', () => {
|
||||
it('fits a narrow viewport and keeps the save action reachable', () => {
|
||||
cy.viewport(390, 844)
|
||||
cy.visit(editPath)
|
||||
field('aliases[0].email').should('be.visible')
|
||||
@@ -446,7 +446,7 @@ describe('Email inbox form', () => {
|
||||
cy.get('form').should(($form) => {
|
||||
expect($form[0].scrollWidth).to.be.at.most($form[0].clientWidth)
|
||||
})
|
||||
cy.get('form button[type="submit"]').should('be.visible')
|
||||
cy.get('form button[type="submit"]').scrollIntoView().should('be.visible')
|
||||
cy.screenshot('email-inbox-mobile-connection', { capture: 'viewport' })
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
:class="['w-full justify-between px-3', buttonClass]"
|
||||
:class="['w-full justify-between bg-transparent px-3', buttonClass]"
|
||||
>
|
||||
<span class="flex min-w-0 flex-1 items-center text-left">
|
||||
<slot name="selected" :selected="selectedItem">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
class="w-full justify-between font-normal"
|
||||
class="w-full justify-between bg-transparent font-normal"
|
||||
:aria-expanded="open"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { TagsInputRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { cn } from '../../../lib/utils'
|
||||
import { cn } from '@shared-ui/lib/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Array, required: false },
|
||||
@@ -39,7 +39,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-3 py-1.5 text-sm',
|
||||
'flex flex-wrap gap-2 items-center rounded-md border border-input bg-transparent px-3 py-1.5 text-sm',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -128,7 +128,7 @@ func TestAutomationNotifyUsesSystemLayout(t *testing.T) {
|
||||
}
|
||||
for _, want := range []string{
|
||||
htmltemplate.HTMLEscapeString(message),
|
||||
"<html>",
|
||||
"<!doctype html>",
|
||||
"</html>",
|
||||
"support.example.com/images/logo.png",
|
||||
"Powered by",
|
||||
|
||||
@@ -265,7 +265,7 @@ func TestRenderNotificationEmailUsesSystemLayout(t *testing.T) {
|
||||
if strings.Contains(got, "<script>") || !strings.Contains(got, "{{ .Author.FirstName }}") {
|
||||
t.Fatal("message content was interpreted as HTML or template code")
|
||||
}
|
||||
if !strings.Contains(got, "<html>") || !strings.Contains(got, "</html>") || !strings.Contains(got, "support.example.com/images/logo.png") {
|
||||
if !strings.Contains(got, "<!doctype html>") || !strings.Contains(got, "</html>") || !strings.Contains(got, "support.example.com/images/logo.png") {
|
||||
t.Fatal("notification lost the system layout")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "header" }}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
@@ -10,7 +10,7 @@
|
||||
body {
|
||||
background-color: #f4f4f5;
|
||||
font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
color: #09090b;
|
||||
@@ -20,24 +20,32 @@
|
||||
|
||||
.wrap {
|
||||
background-color: #ffffff;
|
||||
padding: 40px;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e4e7;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 28px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-wrap: anywhere;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
padding: 24px 0 8px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
@@ -50,33 +58,29 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #18181b;
|
||||
color: #fafafa !important;
|
||||
display: inline-block;
|
||||
border-radius: 6px;
|
||||
padding: 12px 32px;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #09090b;
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 14px;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
@@ -90,6 +94,16 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
@@ -100,42 +114,51 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.wrap {
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
padding: 16px;
|
||||
.email-shell {
|
||||
padding: 16px 12px !important;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
padding: 28px 20px;
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 12px 20px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color: #f4f4f5;">
|
||||
<div class="gutter"> </div>
|
||||
<div class="wrap">
|
||||
<div class="header">
|
||||
{{ if ne LogoURL "" }}
|
||||
<img src="{{ LogoURL }}" alt="{{ SiteName }}" style="max-height: 25px; width: auto;">
|
||||
{{ else }}
|
||||
<img src="{{ RootURL }}/images/logo.png" alt="{{ SiteName }}" style="max-height: 25px; width: auto;">
|
||||
{{ end }}
|
||||
</div>
|
||||
<body style="margin: 0; background-color: #f4f4f5; font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #09090b;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="width: 100%; background-color: #f4f4f5;">
|
||||
<tr>
|
||||
<td class="email-shell" align="center" style="padding: 32px 16px;">
|
||||
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 560px; table-layout: fixed;">
|
||||
<tr>
|
||||
<td class="wrap" style="padding: 28px; background-color: #ffffff; border: 1px solid #e4e4e7; border-radius: 8px; text-align: left;">
|
||||
<div class="header" style="padding-bottom: 16px; margin-bottom: 20px; border-bottom: 1px solid #e4e4e7; line-height: 1;">
|
||||
{{ if ne LogoURL "" }}
|
||||
<img src="{{ LogoURL }}" alt="{{ SiteName }}" height="24" style="display: block; height: 24px; width: auto; max-width: 100%; border: 0;">
|
||||
{{ else }}
|
||||
<img src="{{ RootURL }}/images/logo.png" alt="{{ SiteName }}" height="24" style="display: block; height: 24px; width: auto; max-width: 100%; border: 0;">
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="content" style="overflow-wrap: anywhere; word-wrap: break-word;">
|
||||
{{ end }}
|
||||
|
||||
{{ define "footer" }}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span>Powered by <a href="https://libredesk.io/" target="_blank">libredesk</a></span>
|
||||
</div>
|
||||
<div class="gutter"> </div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="footer" align="center" style="padding-top: 16px; text-align: center; font-size: 12px; line-height: 18px; color: #71717a;">
|
||||
<span>Powered by <a href="https://libredesk.io/" target="_blank" style="color: #71717a; text-decoration: none;">libredesk</a></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
<p>We received a request to reset your password. Click the button below to create a new one:</p>
|
||||
|
||||
<div style="text-align: center; margin: 28px 0;">
|
||||
<div style="margin: 24px 0;">
|
||||
<a href="{{ RootURL }}/set-password?token={{ .ResetToken }}" class="button"
|
||||
style="background-color: #18181b; color: #fafafa; display: inline-block; border-radius: 6px; padding: 12px 32px; text-decoration: none; font-weight: 500; font-size: 14px;">
|
||||
Reset Password
|
||||
style="background-color: #18181b; color: #fafafa; display: inline-block; border-radius: 6px; padding: 10px 20px; line-height: 20px; text-decoration: none; font-weight: 500; font-size: 14px;">
|
||||
Reset password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
|
||||
<p>Click the button below to set your password and get started:</p>
|
||||
|
||||
<div style="text-align: center; margin: 28px 0;">
|
||||
<div style="margin: 24px 0;">
|
||||
<a href="{{ RootURL }}/set-password?token={{ .ResetToken }}" class="button"
|
||||
style="background-color: #18181b; color: #fafafa; display: inline-block; border-radius: 6px; padding: 12px 32px; text-decoration: none; font-weight: 500; font-size: 14px;">
|
||||
Set Your Password
|
||||
style="background-color: #18181b; color: #fafafa; display: inline-block; border-radius: 6px; padding: 10px 20px; line-height: 20px; text-decoration: none; font-weight: 500; font-size: 14px;">
|
||||
Set your password
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
+28
-21
@@ -56,8 +56,12 @@
|
||||
this.campaignData = null;
|
||||
this.campaignActiveSeconds = 0;
|
||||
this.campaignURL = location.href;
|
||||
this.campaignPageGeneration = 0;
|
||||
this.campaignEvent = "";
|
||||
this.campaignDelay = 0;
|
||||
this.campaignElapsed = 0;
|
||||
this.campaignAsked = false;
|
||||
this.campaignGap = this.CAMPAIGN_POLL_MIN;
|
||||
this.campaignBrowserKey = this.getCookie(this.getCookieName('campaign')) || this.randomKey();
|
||||
this.setCookie(this.getCookieName('campaign'), this.campaignBrowserKey);
|
||||
this.campaignSessionKey = this.getCookie(this.getCookieName('campaign-session')) || this.randomKey();
|
||||
@@ -490,7 +494,7 @@
|
||||
|
||||
switch (event.data.type) {
|
||||
case 'SHOW_CAMPAIGN':
|
||||
if (this.isChatVisible || this.unreadCount > 0 || this.hideLauncher) {
|
||||
if (event.data.url !== location.href || event.data.pageGeneration !== this.campaignPageGeneration || this.isChatVisible || this.unreadCount > 0 || this.hideLauncher) {
|
||||
this.dropCampaign(event.data.delivery.id);
|
||||
break;
|
||||
}
|
||||
@@ -757,6 +761,7 @@
|
||||
const url = window.location.href;
|
||||
if (url === self._lastPageURL) return;
|
||||
self._lastPageURL = url;
|
||||
self.resetCampaignPage();
|
||||
// Defer to let SPA frameworks update document.title after route change.
|
||||
setTimeout(() => { self.sendPageInfo(); }, 100);
|
||||
};
|
||||
@@ -918,39 +923,41 @@
|
||||
this.previewHost = host;
|
||||
}
|
||||
|
||||
resetCampaignPage () {
|
||||
if (location.href === this.campaignURL) return;
|
||||
this.campaignURL = location.href;
|
||||
this.campaignPageGeneration++;
|
||||
this.campaignActiveSeconds = 0;
|
||||
this.campaignEvent = '';
|
||||
this.campaignElapsed = 0;
|
||||
this.campaignAsked = false;
|
||||
this.campaignGap = Math.max(this.CAMPAIGN_POLL_MIN, this.campaignDelay);
|
||||
if (this.campaignData) this.dropCampaign(this.campaignData.id);
|
||||
this.renderPreviews();
|
||||
}
|
||||
|
||||
startCampaignTracking () {
|
||||
if (this.campaignInterval) return;
|
||||
let last = performance.now();
|
||||
let elapsed = 0;
|
||||
let asked = false;
|
||||
const firstGap = () => Math.max(this.CAMPAIGN_POLL_MIN, this.campaignDelay);
|
||||
let gap = firstGap();
|
||||
this.campaignGap = Math.max(this.CAMPAIGN_POLL_MIN, this.campaignDelay);
|
||||
this.campaignInterval = setInterval(() => {
|
||||
const now = performance.now();
|
||||
const delta = Math.min((now - last) / 1000, 2);
|
||||
last = now;
|
||||
if (location.href !== this.campaignURL) {
|
||||
this.campaignURL = location.href;
|
||||
this.campaignActiveSeconds = 0;
|
||||
this.campaignEvent = '';
|
||||
elapsed = 0;
|
||||
asked = false;
|
||||
gap = firstGap();
|
||||
if (this.campaignData) this.dropCampaign(this.campaignData.id);
|
||||
this.renderPreviews();
|
||||
}
|
||||
this.resetCampaignPage();
|
||||
if (document.hidden || this.isChatVisible || this.hideLauncher) return;
|
||||
this.campaignActiveSeconds += delta;
|
||||
elapsed += delta;
|
||||
if (elapsed < gap || this.unreadCount > 0 || this.campaignData) return;
|
||||
elapsed = 0;
|
||||
gap = asked ? Math.min(gap * 2, this.CAMPAIGN_POLL_MAX) : this.CAMPAIGN_POLL_MIN;
|
||||
asked = true;
|
||||
this.postToIframe({ type: 'CAMPAIGN_CONTEXT', context: { url: location.href, mobile: this.isMobile, active_seconds: Math.floor(this.campaignActiveSeconds), event: this.campaignEvent } });
|
||||
this.campaignElapsed += delta;
|
||||
if (this.campaignElapsed < this.campaignGap || this.unreadCount > 0 || this.campaignData) return;
|
||||
this.campaignElapsed = 0;
|
||||
this.campaignGap = this.campaignAsked ? Math.min(this.campaignGap * 2, this.CAMPAIGN_POLL_MAX) : this.CAMPAIGN_POLL_MIN;
|
||||
this.campaignAsked = true;
|
||||
this.postToIframe({ type: 'CAMPAIGN_CONTEXT', pageGeneration: this.campaignPageGeneration, context: { url: location.href, mobile: this.isMobile, active_seconds: Math.floor(this.campaignActiveSeconds), event: this.campaignEvent } });
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
trackEvent (name) {
|
||||
this.resetCampaignPage();
|
||||
if (typeof name === 'string' && name.length <= 128) this.campaignEvent = name;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user