Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added conditional check to only initialize messenger instance once #19

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

marcel-matchory
Copy link

When installing the plugin according to the docs to a Vue SPA, the messenger instance is mounted once per component. This results in tens or even hundreds of embedded tawk scripts and eventually messes with app performance. This tiny change makes sure the messenger instance is only embedded once.

Additionally, we changed the tawk messenger property on Vue components to hold the tawk messenger instance instead of the Vue instance, which was probably a bug, anyway.

@Radiergummi
Copy link

@jaoaustero would you be able to review this PR anytime soon? We have to continuously run off a fork right now :(

@Radiergummi
Copy link

Okay, I've resorted to creating the Vue plugin directly in our application and removed this package. If it helps anyone; I've written it in TypeScript:

src/plugins/tawk/index.ts
import { TawkMessenger, type TawkMessengerOptions } from '@/plugins/tawk/messenger';
import type { PluginFunction } from 'vue';

type TawkPluginOptions = TawkMessengerOptions;

export const tawkPlugin: PluginFunction<TawkPluginOptions> = function tawkPlugin( Vue, options ) {
    const { propertyId, widgetId } = options ?? {};

    if ( !propertyId ) {
        throw new Error( 'Missing required property ID' );
    }

    if ( !widgetId ) {
        throw new Error( 'Missing required widget ID' );
    }

    Vue.mixin( {
        mounted() {
            if ( !Vue.prototype.$tawkMessenger ) {
                Vue.prototype.$tawkMessenger = new TawkMessenger(
                    new Vue(),
                    options ?? {
                        propertyId,
                        widgetId,
                    },
                );
            }
        },
    } );
};
src/plugins/tawk/messenger.ts
import { loadScript } from '@/plugins/tawk/loader';
import type { TawkVisitorData } from '@main/plugins/tawk/types';
import type { CombinedVueInstance } from 'vue/types/vue';

export class TawkMessenger {
    public readonly propertyId: string;
    public readonly embedId: string | undefined;
    public readonly widgetId: string;
    public readonly customStyle: Record<string, string>;
    public readonly basePath: string;

    readonly #root: TawkRoot;

    constructor(
        root: TawkRoot,
        {
            basePath = 'tawk.to',
            customStyle = {},
            embedId = undefined,
            propertyId,
            widgetId,
        }: TawkMessengerOptions,
    ) {
        this.#root = root;
        this.basePath = basePath;
        this.customStyle = customStyle;
        this.embedId = embedId;
        this.propertyId = propertyId;
        this.widgetId = widgetId;

        this.#load();
    }

    get widget() {
        return this.#root;
    }

    #load() {
        if ( !self || !self.document ) {
            return;
        }

        self.Tawk_API = self.Tawk_API || {};
        self.Tawk_LoadStart = new Date();

        loadScript( {
            propertyId: this.propertyId,
            widgetId: this.widgetId,
            embedId: this.embedId,
            basePath: this.basePath,
        } );

        if ( this.customStyle && typeof this.customStyle === 'object' ) {
            self.Tawk_API.customStyle = this.customStyle;
        }

        this.#mapActions();
        this.#mapGetters();
        this.#mapListeners();
        this.#mapSetters();
    }

    /**
     * API for calling an action on the widget
     */
    #mapActions() {
        this.#root.maximize = () => self.Tawk_API.maximize();
        this.#root.minimize = () => self.Tawk_API.minimize();
        this.#root.toggle = () => self.Tawk_API.toggle();
        this.#root.popup = () => self.Tawk_API.popup();
        this.#root.showWidget = () => self.Tawk_API.showWidget();
        this.#root.hideWidget = () => self.Tawk_API.hideWidget();
        this.#root.toggleVisibility = () => self.Tawk_API.toggleVisibility();
        this.#root.endChat = () => self.Tawk_API.endChat();
    }

    /**
     * API for returning a data
     */
    #mapGetters() {
        this.#root.getWindowType = () => self.Tawk_API.getWindowType();
        this.#root.getStatus = () => self.Tawk_API.getStatus();
        this.#root.isChatMaximized = () => self.Tawk_API.isChatMaximized();
        this.#root.isChatMinimized = () => self.Tawk_API.isChatMinimized();
        this.#root.isChatHidden = () => self.Tawk_API.isChatHidden();
        this.#root.isChatOngoing = () => self.Tawk_API.isChatOngoing();
        this.#root.isVisitorEngaged = () => self.Tawk_API.isVisitorEngaged();
        this.#root.onLoaded = () => self.Tawk_API.onLoaded;
        this.#root.onBeforeLoaded = () => self.Tawk_API.onBeforeLoaded;
        this.#root.widgetPosition = () => self.Tawk_API.widgetPosition();
    }

    /**
     * API for listening an event emitting inside the widget
     */
    #mapListeners() {
        self.addEventListener( 'tawkLoad', () => this.#root.$emit( 'load' ) );
        self.addEventListener( 'tawkStatusChange', ( { detail } ) =>
            this.#root.$emit( 'statusChange', detail ),
        );
        self.addEventListener( 'tawkBeforeLoad', () => this.#root.$emit( 'beforeLoad' ) );
        self.addEventListener( 'tawkChatMaximized', () => this.#root.$emit( 'chatMaximized' ) );
        self.addEventListener( 'tawkChatMinimized', () => this.#root.$emit( 'chatMinimized' ) );
        self.addEventListener( 'tawkChatHidden', () => this.#root.$emit( 'chatHidden' ) );
        self.addEventListener( 'tawkChatStarted', () => this.#root.$emit( 'chatStarted' ) );
        self.addEventListener( 'tawkChatEnded', () => this.#root.$emit( 'chatEnded' ) );
        self.addEventListener( 'tawkPrechatSubmit', ( { detail } ) =>
            this.#root.$emit( 'prechatSubmit', detail ),
        );
        self.addEventListener( 'tawkOfflineSubmit', ( { detail } ) =>
            this.#root.$emit( 'offlineSubmit', detail ),
        );
        self.addEventListener( 'tawkChatMessageVisitor', ( { detail } ) =>
            this.#root.$emit( 'chatMessageVisitor', detail ),
        );
        self.addEventListener( 'tawkChatMessageAgent', ( { detail } ) =>
            this.#root.$emit( 'chatMessageAgent', detail ),
        );
        self.addEventListener( 'tawkChatMessageSystem', ( { detail } ) =>
            this.#root.$emit( 'chatMessageSystem', detail ),
        );
        self.addEventListener( 'tawkAgentJoinChat', ( { detail } ) =>
            this.#root.$emit( 'agentJoinChat', detail ),
        );
        self.addEventListener( 'tawkAgentLeaveChat', ( { detail } ) =>
            this.#root.$emit( 'agentLeaveChat', detail ),
        );
        self.addEventListener( 'tawkChatSatisfaction', ( { detail } ) =>
            this.#root.$emit( 'chatSatisfaction', detail ),
        );
        self.addEventListener( 'tawkVisitorNameChanged', ( { detail } ) =>
            this.#root.$emit( 'visitorNameChanged', detail ),
        );
        self.addEventListener( 'tawkFileUpload', ( { detail } ) =>
            this.#root.$emit( 'fileUpload', detail ),
        );
        self.addEventListener( 'tawkTagsUpdated', ( { detail } ) =>
            this.#root.$emit( 'tagsUpdated', detail ),
        );
        self.addEventListener( 'tawkUnreadCountChanged', ( { detail } ) =>
            this.#root.$emit( 'unreadCountChanged', detail ),
        );
    }

    /**
     * API for setting a data on the widget
     */
    #mapSetters() {
        this.#root.visitor = ( data: TawkVisitorData ) => ( self.Tawk_API.visitor = data );
        this.#root.setAttributes = (
            attributes: Record<string, unknown>,
            callback?: ( error?: Error ) => void,
        ) => self.Tawk_API.setAttributes( attributes, callback );
        this.#root.addEvent = (
            event: string,
            metadata: Record<string, unknown>,
            callback?: ( error?: Error ) => void,
        ) => self.Tawk_API.addEvent( event, metadata, callback );
        this.#root.addTags = ( tags: string[], callback?: ( error?: Error ) => void ) =>
            self.Tawk_API.addTags( tags, callback );
        this.#root.removeTags = ( tags: string[], callback?: ( error?: Error ) => void ) =>
            self.Tawk_API.removeTags( tags, callback );
    }
}

export type TawkMessengerOptions = {
    propertyId: string;
    widgetId: string;
    embedId?: string;
    customStyle?: Record<string, string>;
    basePath?: string;
};

type TawkRoot = CombinedVueInstance<
    Vue,
    {
        maximize: () => void;
        minimize: () => void;
        toggle: () => void;
        popup: () => void;
        showWidget: () => void;
        hideWidget: () => void;
        toggleVisibility: () => void;
        endChat: () => void;
        getWindowType: () => string;
        getStatus: () => string;
        isChatMaximized: () => boolean;
        isChatMinimized: () => boolean;
        isChatHidden: () => boolean;
        isChatOngoing: () => boolean;
        isVisitorEngaged: () => boolean;
        onLoaded: () => void;
        onBeforeLoaded: () => void;
        widgetPosition: () => string;
        visitor: ( data: TawkVisitorData ) => void;
        setAttributes: (
            attributes: Record<string, unknown>,
            callback?: ( error?: Error ) => void,
        ) => void;
        addEvent: (
            event: string,
            metadata: Record<string, unknown>,
            callback?: ( error?: Error ) => void,
        ) => void;
        addTags: ( tags: string[], callback?: ( error?: Error ) => void ) => void;
        removeTags: ( tags: string[], callback?: ( error?: Error ) => void ) => void;
    },
    unknown,
    unknown,
    unknown,
    unknown
>;
src/plugins/tawk/loader.ts
export function loadScript( { propertyId = '', widgetId = '', embedId = '', basePath = 'tawk.to' } ) {
    if ( embedId.length ) {
        if ( !document.getElementById( embedId ) ) {
            const element = document.createElement( 'div' );
            element.id = embedId;

            document.body.appendChild( element );
        }

        window.Tawk_API.embedded = embedId;
    }

    const script = document.createElement( 'script' );
    script.async = true;
    script.defer = true;
    script.src = `https://embed.${basePath}/${propertyId}/${widgetId}`;

    document.head.appendChild( script );
}
src/plugins/tawk/types.ts
export type TawkStatus = 'online' | 'away' | 'offline';
export type TawkVisitorData = { name: string; email: string; hash?: string; };
export type TawkAgentData = { name: string; position: string; image: string; id: string; };
export type TawkOfflineFormData = {
    name: string;
    email: string;
    message: string;
    questions: [];
};
export type TawkAddEvent = {
    ( event: string, metadata: Record<string, unknown>, callback?: ( error?: Error ) => unknown ): void;
    ( event: string, callback?: ( error?: Error ) => unknown ): void;
};
export type TawkApi = {
    onLoad?: () => unknown;
    onStatusChange?: ( status: TawkStatus ) => unknown;
    onBeforeLoad?: () => unknown;
    onChatMaximized?: () => unknown;
    onChatMinimized?: () => unknown;
    onChatHidden?: () => unknown;
    onChatStarted?: () => unknown;
    onChatEnded?: () => unknown;
    onPrechatSubmit?: ( data: Record<string, unknown> ) => unknown;
    onOfflineSubmit?: ( data: TawkOfflineFormData ) => unknown;
    onChatMessageVisitor?: ( message?: string ) => unknown;
    onChatMessageAgent?: ( message?: string ) => unknown;
    onChatMessageSystem?: ( message?: string ) => unknown;
    onAgentJoinChat?: ( agent: TawkAgentData ) => unknown;
    onAgentLeaveChat?: ( agent: Pick<TawkAgentData, 'name' | 'id'> ) => unknown;
    onChatSatisfaction?: ( satisfaction: number ) => unknown;
    onVisitorNameChanged?: ( visitorName: string ) => unknown;
    onFileUpload?: ( link: string ) => unknown;
    onTagsUpdated?: ( data: unknown ) => unknown;
    onUnreadCountChanged?: ( count: number ) => unknown;
    visitor?: TawkVisitorData;
    customStyle?: Record<string, string>;
    embedded?: string;

    /**
     * This option allows you to disable the auto start connection of the
     * chat widget.
     *
     * You have the option to disable autoStart after the widget loads on your
     * website. By default, this is set to true. When autoStart is disabled, the
     * widget will be hidden by default on load.
     */
    autoload?: boolean;

    readonly maximize: () => void;
    readonly minimize: () => void;
    readonly toggle: () => void;
    readonly popup: () => void;
    readonly getWindowType: () => 'inline' | 'embed';
    readonly showWidget: () => void;
    readonly hideWidget: () => void;
    readonly toggleVisibility: () => void;
    readonly getStatus: () => TawkStatus;
    readonly isChatMaximized: () => boolean;
    readonly isChatMinimized: () => boolean;
    readonly isChatHidden: () => boolean;
    readonly isChatOngoing: () => boolean;
    readonly isVisitorEngaged: () => boolean;
    readonly onLoaded: () => boolean | undefined;
    readonly onBeforeLoaded: () => boolean | undefined;
    readonly widgetPosition: () => string;
    readonly endChat: () => void;
    readonly setAttributes: (
        attributes: Record<string, unknown>,
        callback?: ( error?: Error ) => unknown,
    ) => void;
    readonly addEvent: TawkAddEvent;
    readonly addTags: ( tags: string[], callback?: ( error?: Error ) => unknown ) => void;
    readonly removeTags: ( tags: string[], callback?: ( error?: Error ) => unknown ) => void;
};
export type TawkEventMap = {
    tawkLoad: void;
    tawkStatusChange: TawkStatus;
    tawkBeforeLoad: void;
    tawkChatMaximized: void;
    tawkChatMinimized: void;
    tawkChatHidden: void;
    tawkChatStarted: void;
    tawkChatEnded: void;
    tawkPrechatSubmit: Record<string, unknown>;
    tawkOfflineSubmit: TawkOfflineFormData;
    tawkChatMessageVisitor: string;
    tawkChatMessageAgent: string;
    tawkChatMessageSystem: string;
    tawkAgentJoinChat: TawkAgentData;
    tawkAgentLeaveChat: Pick<TawkAgentData, 'name' | 'id'>;
    tawkChatSatisfaction: number;
    tawkVisitorNameChanged: string;
    tawkFileUpload: string;
    tawkTagsUpdated: string[];
    tawkUnreadCountChanged: number;
};
src/env.d.ts
import type { TawkApi, TawkEventMap } from '@/plugins/tawk/types';

declare global
{
    interface Window
    {
        Tawk_API: TawkApi;
        Tawk_LoadStart: Date | undefined;

        addEventListener<K extends keyof TawkEventMap>(
            type: K,
            listener: ( this: Window, ev: CustomEvent<TawkEventMap[K]> ) => unknown,
            options?: boolean | AddEventListenerOptions,
        ): void;

        removeEventListener<K extends keyof TawkEventMap>(
            type: K,
            listener: ( this: Window, ev: CustomEvent<TawkEventMap[K]> ) => unknown,
            options?: boolean | EventListenerOptions,
        ): void;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants