Skip to content

Commit

Permalink
Merge pull request #226 from schlagmichdoch/parallelize_asset_loading
Browse files Browse the repository at this point in the history
Parallelize and speed up loading of deferred assets, refactor code, and fix `RTC_CONFIG` env var
  • Loading branch information
schlagmichdoch authored Dec 15, 2023
2 parents 9847fee + 6737dca commit 4f80ab4
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 99 deletions.
8 changes: 4 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,10 @@ <h1>PairDrop</h1>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/localization.js"></script>
<script src="scripts/persistent-storage.js"></script>
<script src="scripts/ui-main.js"></script>
<script src="scripts/main.js"></script>
<script src="scripts/localization.js" defer></script>
<script src="scripts/persistent-storage.js" defer></script>
<script src="scripts/ui-main.js" defer></script>
<script src="scripts/main.js" defer></script>
<!-- Sounds -->
<audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg">
Expand Down
142 changes: 91 additions & 51 deletions public/scripts/localization.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
class Localization {
constructor() {
Localization.$htmlRoot = document.querySelector('html');

Localization.defaultLocale = "en";
Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"];
Localization.supportedLocalesRtl = ["ar"];

Localization.translations = {};
Localization.defaultTranslations = {};
Localization.translationsDefaultLocale = {};

Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages);
Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);

let storedLanguageCode = localStorage.getItem('language_code');

Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
? storedLanguageCode
: Localization.systemLocale;
}

static isSupported(locale) {
static localeIsSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
}

static isRtlLanguage(locale) {
static localeIsRtl(locale) {
return Localization.supportedLocalesRtl.indexOf(locale) > -1;
}

static isCurrentLocaleRtl() {
return Localization.isRtlLanguage(Localization.locale);
static currentLocaleIsRtl() {
return Localization.localeIsRtl(Localization.locale);
}

static currentLocaleIsDefault() {
return Localization.locale === Localization.defaultLocale
}

static getSupportedOrDefault(locales) {
static getSupportedOrDefaultLocales(locales) {
// get generic locales not included in locales
// ["en-us", "de-CH", "fr"] --> ["en", "de"]
let localesGeneric = locales
.map(locale => locale.split("-")[0])
.filter(locale => locales.indexOf(locale) === -1);

return locales.find(Localization.isSupported)
|| localesGeneric.find(Localization.isSupported)
// If there is no perfect match for browser locales, try generic locales first before resorting to the default locale
return locales.find(Localization.localeIsSupported)
|| localesGeneric.find(Localization.localeIsSupported)
|| Localization.defaultLocale;
}

Expand All @@ -48,16 +57,14 @@ class Localization {
await Localization.setLocale(locale)
await Localization.translatePage();

const htmlRootNode = document.querySelector('html');

if (Localization.isRtlLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
if (Localization.localeIsRtl(locale)) {
Localization.$htmlRoot.setAttribute('dir', 'rtl');
}
else {
htmlRootNode.removeAttribute('dir');
Localization.$htmlRoot.removeAttribute('dir');
}

htmlRootNode.setAttribute('lang', locale);
Localization.$htmlRoot.setAttribute('lang', locale);


console.log("Page successfully translated",
Expand Down Expand Up @@ -111,75 +118,108 @@ class Localization {
const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" ");

for (let i in attrs) {
let attr = attrs[i];
attrs.forEach(attr => {
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
}
else {
element.setAttribute(attr, Localization.getTranslation(key, attr));
}
}
})
}

static getTranslation(key, attr = null, data = {}, useDefault = false) {
const keys = key.split(".");

let translationCandidates = useDefault
? Localization.defaultTranslations
: Localization.translations;

static getTranslationFromTranslationsObj(translationObj, key, attr) {
let translation;

try {
const keys = key.split(".");

for (let i = 0; i < keys.length - 1; i++) {
translationCandidates = translationCandidates[keys[i]]
// iterate into translation object until last layer
translationObj = translationObj[keys[i]]
}

let lastKey = keys[keys.length - 1];

if (attr) lastKey += "_" + attr;

translation = translationCandidates[lastKey];

for (let j in data) {
if (translation.includes(`{{${j}}}`)) {
translation = translation.replace(`{{${j}}}`, data[j]);
} else {
console.warn(`Translation for your language ${Localization.locale.toUpperCase()} misses at least one data placeholder:`, key, attr, data);
Localization.logHelpCallKey(key);
Localization.logHelpCall();
translation = "";
break;
}
}
translation = translationObj[lastKey];

} catch (e) {
console.error(e);
translation = "";
}

if (!translation) {
if (!useDefault) {
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
Localization.logHelpCallKey(key);
Localization.logHelpCall();
translation = this.getTranslation(key, attr, data, true);
throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`);
}

return translation;
}

static addDataToTranslation(translation, data) {
for (let j in data) {
if (!translation.includes(`{{${j}}}`)) {
throw new Error(`Translation misses data placeholder: ${j}`);
}
// Add data to translation
translation = translation.replace(`{{${j}}}`, data[j]);
}
return translation;
}

static getTranslation(key, attr = null, data = {}, useDefault = false) {
let translationObj = useDefault
? Localization.translationsDefaultLocale
: Localization.translations;

let translation;

try {
translation = Localization.getTranslationFromTranslationsObj(translationObj, key, attr);
translation = Localization.addDataToTranslation(translation, data);
}
catch (e) {
// Log warnings and help calls
console.warn(e);
Localization.logTranslationMissingOrBroken(key, attr, data, useDefault);
Localization.logHelpCallKey(key, attr);
Localization.logHelpCall();

if (useDefault || Localization.currentLocaleIsDefault()) {
// Is default locale already
// Use empty string as translation
translation = ""
}
else {
console.warn("Missing translation in default language:", key, attr);
Localization.logHelpCall();
// Is not default locale yet
// Get translation for default language with same arguments
console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`);
translation = this.getTranslation(key, attr, data, true);
}
}

return Localization.escapeHTML(translation);
}

static logTranslationMissingOrBroken(key, attr, data, useDefault) {
let usedLocale = useDefault
? Localization.defaultLocale.toUpperCase()
: Localization.locale.toUpperCase();

console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
}

static logHelpCall() {
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
}

static logHelpCallKey(key) {
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`);
static logHelpCallKey(key, attr) {
let locale = Localization.locale.toLowerCase();

let keyComplete = !attr || attr === "text"
? key
: `${key}_${attr}`;

console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
}

static escapeHTML(unsafeText) {
Expand Down
62 changes: 37 additions & 25 deletions public/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ class PairDrop {
await this.backgroundCanvas.fadeIn();

// Load deferred assets
console.log("Load deferred assets...");
await this.loadDeferredAssets();
console.log("Loading of deferred assets completed.");

console.log("Hydrate UI...");
await this.hydrate();
console.log("UI hydrated.");

// Evaluate url params as soon as ws is connected
console.log("Evaluate URL params as soon as websocket connection is established.");
Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
}

Expand Down Expand Up @@ -102,36 +105,40 @@ class PairDrop {
}
}

async loadDeferredAssets() {
console.log("Load deferred assets");
for (const url of this.deferredStyles) {
await this.loadAndApplyStylesheet(url);
}
for (const url of this.deferredScripts) {
await this.loadAndApplyScript(url);
}
loadDeferredAssets() {
const stylePromises = this.deferredStyles.map(url => this.loadAndApplyStylesheet(url));
const scriptPromises = this.deferredScripts.map(url => this.loadAndApplyScript(url));

return Promise.all([...stylePromises, ...scriptPromises]);
}

loadStyleSheet(url) {
return new Promise((resolve, reject) => {
let stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.rel = 'preload';
stylesheet.as = 'style';
stylesheet.href = url;
stylesheet.type = 'text/css';
stylesheet.onload = resolve;
stylesheet.onload = _ => {
stylesheet.onload = null;
stylesheet.rel = 'stylesheet';
resolve();
};
stylesheet.onerror = reject;

document.head.appendChild(stylesheet);
});
}

async loadAndApplyStylesheet(url) {
try {
await this.loadStyleSheet(url);
console.log(`Stylesheet loaded successfully: ${url}`);
} catch (error) {
console.error('Error loading stylesheet:', error);
}
loadAndApplyStylesheet(url) {
return new Promise( async (resolve) => {
try {
await this.loadStyleSheet(url);
console.log(`Stylesheet loaded successfully: ${url}`);
resolve();
} catch (error) {
console.error('Error loading stylesheet:', error);
}
});
}

loadScript(url) {
Expand All @@ -145,13 +152,16 @@ class PairDrop {
});
}

async loadAndApplyScript(url) {
try {
await this.loadScript(url);
console.log(`Script loaded successfully: ${url}`);
} catch (error) {
console.error('Error loading script:', error);
}
loadAndApplyScript(url) {
return new Promise( async (resolve) => {
try {
await this.loadScript(url);
console.log(`Script loaded successfully: ${url}`);
resolve();
} catch (error) {
console.error('Error loading script:', error);
}
});
}

async hydrate() {
Expand Down Expand Up @@ -223,6 +233,8 @@ class PairDrop {
// remove url params from url
const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);

console.log("URL params evaluated.");
}
}

Expand Down
2 changes: 1 addition & 1 deletion public/scripts/ui-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class HeaderUI {
this.$header.classList.remove('overflow-expanded');


const rtlLocale = Localization.isCurrentLocaleRtl();
const rtlLocale = Localization.currentLocaleIsRtl();
let icon;
const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])');

Expand Down
20 changes: 4 additions & 16 deletions public/scripts/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2420,14 +2420,9 @@ class Notifications {

_downloadNotification(files) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<files.length; i++) {
if (files[i].type.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let imagesOnly = files.every(file => file.type.split('/')[0] === 'image');
let title;

if (files.length === 1) {
title = `${files[0].name}`;
}
Expand All @@ -2452,15 +2447,8 @@ class Notifications {

_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}

let displayName = $(peerId).querySelector('.name').textContent
let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');
let displayName = $(peerId).querySelector('.name').textContent;

let descriptor;
if (request.header.length === 1) {
Expand Down
Loading

0 comments on commit 4f80ab4

Please sign in to comment.