Source: theme/theme.js

import pageState from 'bitbucket/internal/model/page-state';
import * as clientStorage from 'bitbucket/internal/util/client-storage';
import { getFromProviderSync } from 'bitbucket/internal/util/feature-enabled';
import * as nav from 'bitbucket/util/navbuilder';
import * as server from 'bitbucket/util/server';

const COVERAGE_COLORS_AMOUNT = 10;
const COVERAGE_THEME = Object.freeze(
    [...new Array(COVERAGE_COLORS_AMOUNT)].map((item, index) => `COVERAGE${index}`)
);

const THEME_CONST = Object.freeze({
    localStorageKey: 'user-preferred-theme',
    defaultTheme: 'ORIGINAL',
    theme: {
        LIGHT: 'LIGHT',
        DARK: 'DARK',
        AUTO: 'AUTO',
        ORIGINAL: 'ORIGINAL',
        ...COVERAGE_THEME.reduce((result, key) => {
            result[key] = key;

            return result;
        }, {}),
    },
    atlaskit: {
        themeAttr: 'data-color-mode',
        dsCoverageAttr: 'ds-coverage',
        themes: {
            LIGHT: 'light',
            DARK: 'dark',
            AUTO: 'auto',
            ORIGINAL: '',
            ...COVERAGE_THEME.reduce((result, key) => {
                result[key] = 'dark';

                return result;
            }, {}),
        },
    },
    aui: {
        themeClass: {
            LIGHT: 'aui-theme-design-tokens',
            DARK: 'aui-theme-design-tokens',
            ...COVERAGE_THEME.reduce((result, key) => {
                result[key] = 'aui-theme-design-tokens';

                return result;
            }, {}),
        },
    },
    mediaListeners: {
        DARK: '(prefers-color-scheme: dark)',
        LIGHT: '(prefers-color-scheme: light)',
    },
});

let currentTheme = null;

/**
 * fetchThemefromAPI doescall to fetch theme from user preferences: GET latest/users/admin/settings
 * NOTE: this will be removed once we have plugin integrated for setting / getting theme preferences.
 *
 * @returns {Promise} - a promise that resolves with the result of the API call
 */

const fetchThemefromAPI = () => {
    const userId = pageState.getCurrentUser().id;

    return server.rest({
        url: nav.rest().users().addPathComponents(userId).addPathComponents('settings').build(),
        type: 'GET',
        statusCode: {
            '*': false,
        },
    });
};

/**
 * setThemeToAPI does an API call to set theme in user preferences: POST latest/users/admin/settings.
 * NOTE: this will be removed once we have plugin integrated for setting / getting theme preferences.
 * POST latest/users/admin/settings
 *
 * @returns {Promise} - a promise that resolves with the current theme
 */

const setThemeToAPI = (theme) => {
    const userId = pageState.getCurrentUser().id;

    return server.rest({
        url: nav.rest().users().addPathComponents(userId).addPathComponents('settings').build(),
        type: 'POST',
        data: {
            theme,
        },
        statusCode: {
            '*': false,
        },
    });
};

/**
 * Sets the required attributes by Atlaskit and AUI to turn ON/OFF their Dark themes
 *
 * @param {Object} theme - Any of the available themes in THEME_CONST.theme
 */
const setBitbucketTheme = (theme) => {
    // prevent setting the theme multiple times when in Light or dark mode
    if (theme !== THEME_CONST.theme.AUTO && currentTheme === theme) {
        return;
    }

    currentTheme = theme;

    const docElement = document.documentElement;
    const body = window.document.getElementsByTagName('body')[0];
    const isSystemInDarkTheme = window.matchMedia(THEME_CONST.mediaListeners.DARK).matches;

    if (theme === THEME_CONST.theme.AUTO) {
        theme = isSystemInDarkTheme ? THEME_CONST.theme.DARK : THEME_CONST.theme.LIGHT;
    }

    // required by AUI
    const auiClass = THEME_CONST.aui.themeClass[theme];

    Object.values(THEME_CONST.aui.themeClass).forEach((themeClass) => {
        body.classList.remove(themeClass);
    });

    if (auiClass) {
        body.classList.add(auiClass);
    }

    // required by Atlaskit
    const akTheme = THEME_CONST.atlaskit.themes[theme];

    docElement.removeAttribute(THEME_CONST.atlaskit.themeAttr);

    if (akTheme) {
        docElement.setAttribute(THEME_CONST.atlaskit.themeAttr, akTheme);
    }

    // coverage theming
    var coverageMatch = theme.match(/COVERAGE(\d)/);

    docElement.removeAttribute(THEME_CONST.atlaskit.dsCoverageAttr);

    if (coverageMatch) {
        docElement.setAttribute(THEME_CONST.atlaskit.dsCoverageAttr, coverageMatch[1]);
    }
};

/**
 * Reads the current theme from localStorage and update the attributes accordingly.
 * If the theme is not valid or undefined, it will set the default theme.
 *
 * We use a promise for 'update' due to the dependency to 'setUserPreferredTheme'.
 *
 * @returns {Promise} - a promise that resolves with the current theme
 */
const updateUserPreferredTheme = () => {
    const localstorageTheme = clientStorage.getItem(THEME_CONST.localStorageKey);

    if (localstorageTheme && THEME_CONST.theme[localstorageTheme]) {
        setBitbucketTheme(localstorageTheme);

        return Promise.resolve(localstorageTheme);
    } else {
        return fetchThemefromAPI().then(({ theme }) => {
            if (theme && THEME_CONST.theme[theme]) {
                clientStorage.setItem(THEME_CONST.localStorageKey, theme);
                setBitbucketTheme(theme);

                return Promise.resolve(theme);
            }

            return setUserPreferredTheme();
        });

        return setUserPreferredTheme();
    }
};

/**
 * Verifies if the module had initialize, if not it will call updateUserPreferredTheme,
 * to do it and once it resolves, it will resolve the current theme.
 *
 * We use a promise for 'get' to simulate a service call that will be implemented in
 * later releases.
 *
 * @returns {Promise} - a promise that resolves with the current theme
 */
const getUserPreferredTheme = () => {
    if (currentTheme) {
        return Promise.resolve(currentTheme);
    }

    return updateUserPreferredTheme();
};

/**
 * Sets a new theme if the theme passed inside data is valid, data is used to simulate
 * a REST call in case this functionality is moved to an API.
 *
 * We use a promise for 'set' to simulate a service call that will be implemented in
 * later releases.
 *
 * Currently it will store the selected theme in client storage.
 *
 * @returns {Promise} - a promise that resolves with the current theme
 */
const setUserPreferredTheme = (data) => {
    if (!data || !THEME_CONST.theme[data.theme]) {
        data = {
            theme: THEME_CONST.defaultTheme,
        };
    }

    return new Promise((resolve) => {
        setThemeToAPI(data.theme);
        setBitbucketTheme(data.theme);
        clientStorage.setItem(THEME_CONST.localStorageKey, data.theme);
        resolve(data.theme);
    });
};

/**
 * Initialize theme module and adds media listeners.
 */
const init = () => {
    if (getFromProviderSync('bitbucket.global.theme')) {
        Object.values(THEME_CONST.mediaListeners).forEach((mediaListeners) => {
            window.matchMedia(mediaListeners).addEventListener('change', updateUserPreferredTheme);
        });

        updateUserPreferredTheme();
    }
};

const removeLocalStorageKeys = () => {
    clientStorage.removeItem(THEME_CONST.localStorageKey);
};

define('@bitbucket/internal/feature/theme', Object.freeze({
    THEMES: THEME_CONST.theme,
    COVERAGE_THEME,
    getUserPreferredTheme,
    setUserPreferredTheme,
    removeLocalStorageKeys,
    init,
}));