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,
}));