main.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. (function() {
  2. 'use strict';
  3. var initialized = false;
  4. var i18n;
  5. // i18n key prefix for MUC ("muc.") or 1:1 chat ("chat.")
  6. var key_prefix;
  7. var display_data = null;
  8. // Return an array of platforms for this user agent, ranked from best
  9. // (first) to worst (last).
  10. function get_platforms() {
  11. var ua = navigator.userAgent;
  12. switch (true) {
  13. case (ua.indexOf("Windows") >= 0):
  14. return ["windows", "web"];
  15. case (ua.indexOf("Android") >= 0):
  16. case (ua.indexOf("CrOS") >= 0):
  17. return ["android", "web"];
  18. case (ua.indexOf("iPad") >= 0):
  19. case (ua.indexOf("iPhone") >= 0):
  20. return ["ios", "web"];
  21. case (ua.indexOf("Mac OS X") >= 0):
  22. case (ua.indexOf("Macintosh") >= 0):
  23. return ["macos", "web"];
  24. case (ua.indexOf("Linux") >= 0):
  25. return ["linux", "web"];
  26. case (true):
  27. }
  28. return ["web"];
  29. }
  30. function get_best_platform(client_info, platforms) {
  31. for(let platform of platforms) {
  32. if(client_info[platform]) {
  33. return platform;
  34. }
  35. }
  36. return null;
  37. }
  38. // Return the position of the first platform the client supports.
  39. // The idea is that platforms earlier in the list are preferred.
  40. function get_platform_rank(client_info, platforms) {
  41. for(let pos in platforms) {
  42. if(client_info[platforms[pos]]) {
  43. return pos;
  44. }
  45. }
  46. return null;
  47. }
  48. function get_client_link_element(client_id, client_info, platform) {
  49. let item = document.createElement("div");
  50. let link = document.createElement("a");
  51. let img = document.createElement("img");
  52. link.setAttribute("href", client_info[platform]);
  53. let logo_url = client_info.logo || ("assets/" + client_id + ".svg");
  54. img.setAttribute("src", logo_url);
  55. if(star_apps && star_apps.includes(client_id)) {
  56. let star_el = document.createElement("div");
  57. star_el.innerText = "\u2B50";
  58. star_el.classList.add("star");
  59. item.classList.add("starred");
  60. link.append(star_el);
  61. }
  62. link.append(img, client_info.title);
  63. item.append(link);
  64. item.classList.add("client-link");
  65. return item;
  66. }
  67. function show_clients(all_clients) {
  68. var list = document.getElementById('client_list');
  69. let platforms = get_platforms();
  70. let clients = [];
  71. // Filter to clients suitable for our platform
  72. for (var id in all_clients) {
  73. if(hidden_apps.includes(id)) {
  74. // Configured to never show this app
  75. continue;
  76. }
  77. let client_info = all_clients[id];
  78. for(let platform of platforms) {
  79. if(client_info[platform]) {
  80. clients.push(id);
  81. }
  82. }
  83. }
  84. // Sort the clients
  85. clients.sort(function (a, b) {
  86. let a_info = all_clients[a];
  87. let b_info = all_clients[b];
  88. // Prefer native clients
  89. let a_platform_rank = get_platform_rank(a_info, platforms);
  90. let b_platform_rank = get_platform_rank(b_info, platforms);
  91. if(a_platform_rank < b_platform_rank) {
  92. return -1;
  93. } else if(b_platform_rank < a_platform_rank) {
  94. return 1;
  95. }
  96. // Prefer starred clients
  97. if(star_apps && star_apps.length > 0) {
  98. let a_is_starred = star_apps.includes(a);
  99. let b_is_starred = star_apps.includes(b);
  100. if(a_is_starred && !b_is_starred) {
  101. return -1;
  102. } else if(b_is_starred && !a_is_starred) {
  103. return 1;
  104. }
  105. }
  106. // Sort lexically by title
  107. if(a_info.title < b_info.title) {
  108. return -1;
  109. } else if(b_info.title < a_info.title) {
  110. return 1;
  111. }
  112. return 0;
  113. });
  114. // Empty any existing content from the list element
  115. list.replaceChildren();
  116. // Generate links and add them to the list element
  117. for(var id of clients) {
  118. let platform = get_best_platform(all_clients[id], platforms);
  119. let el = get_client_link_element(id, all_clients[id], platform);
  120. list.append(el);
  121. }
  122. }
  123. function load_clients() {
  124. var request = new XMLHttpRequest();
  125. request.open('GET', "clients.json");
  126. request.onreadystatechange = function () {
  127. if (request.readyState === 4) {
  128. if (request.status === 200 || (isLocalFileRequest(url) && request.responseText.length > 0)) {
  129. let loaded_clients = JSON.parse(request.responseText);
  130. if(custom_apps) {
  131. for(let custom_app in custom_apps) {
  132. loaded_clients[custom_app] = custom_apps[custom_app];
  133. }
  134. }
  135. if(only_apps && only_apps.length > 0) {
  136. for(let id in loaded_clients) {
  137. if(!only_apps.includes(id)) {
  138. delete loaded_clients[id];
  139. }
  140. }
  141. }
  142. show_clients(loaded_clients);
  143. }
  144. }
  145. };
  146. request.send(null);
  147. }
  148. function load_hash() {
  149. key_prefix = "chat";
  150. var xmpp_uri = window.location.search || window.location.hash;
  151. xmpp_uri = decodeURIComponent(xmpp_uri.substring(xmpp_uri.indexOf('#') + 1, xmpp_uri.length));
  152. if (xmpp_uri.indexOf("xmpp:") === 0) {
  153. xmpp_uri = xmpp_uri.slice(5);
  154. }
  155. try {
  156. base_decoded = window.atob(xmpp_uri);
  157. if (base_decoded.search('@') >= 0)
  158. xmpp_uri = base_decoded;
  159. } catch (err) {
  160. // ignore error, JID wasn't base64 encoded
  161. }
  162. if (xmpp_uri.search("\\?join") >= 0) {
  163. key_prefix = "muc";
  164. } else if(xmpp_uri.search("\\?register") >= 0) {
  165. key_prefix = "register";
  166. }
  167. // TODO: proper error checking / display / Creation of invitations
  168. if (xmpp_uri.search("@") <= 0) return {xmpp_uri:xmpp_uri, xmpp_uri_encoded:xmpp_uri, name: xmpp_uri.split("?")[0]};
  169. var xmpp_params = {};
  170. var xmpp_uri_parts = xmpp_uri.split("?");
  171. if (xmpp_uri_parts.length > 1) {
  172. let parameter, parameters = xmpp_uri_parts[1].split(";")
  173. for (parameter of parameters) {
  174. let key_value = parameter.split("=")
  175. xmpp_params[key_value[0]] = key_value.length > 1 ? key_value[1] : "";
  176. }
  177. }
  178. const jid_parts = xmpp_uri_parts[0].split("@");
  179. const local_part = jid_parts[0];
  180. xmpp_params["name"] = local_part.charAt(0).toUpperCase() + local_part.slice(1);
  181. const domain_part = jid_parts[1];
  182. const jid_encoded = encodeURIComponent(local_part) + "@" + encodeURIComponent(domain_part)
  183. xmpp_uri_parts[0] = jid_encoded
  184. const xmpp_uri_encoded = xmpp_uri_parts.join("?")
  185. return {xmpp_uri: xmpp_uri, xmpp_uri_encoded: xmpp_uri_encoded, name: xmpp_params["name"]};
  186. }
  187. let fallbackLocale = "en";
  188. let requested_fallback_locale = false;
  189. function get_translated_string(key, data) {
  190. return new Promise(function (resolve, reject) {
  191. try {
  192. return resolve(i18n.text(key, data));
  193. } catch {
  194. if(i18n.hasLocale(fallbackLocale)) {
  195. return resolve(i18n.text(key, data, fallbackLocale));
  196. }
  197. i18n.once(I18nText.event.LOCALE_LOAD, function () {
  198. try {
  199. return resolve(i18n.text(key, data, fallbackLocale));
  200. } catch {
  201. return resolve("UNTRANSLATED[" + key + "]");
  202. }
  203. });
  204. if(!requested_fallback_locale) {
  205. i18n.loadLocale(fallbackLocale);
  206. requested_fallback_locale = true;
  207. }
  208. }
  209. });
  210. }
  211. function translate_ui() {
  212. // translation
  213. get_translated_string(key_prefix + '.title', display_data).then(function (text) {
  214. document.title = text;
  215. });
  216. let translatable_els = document.querySelectorAll("[data-i18n]");
  217. translatable_els.forEach(function (el) {
  218. let key = el.dataset.i18n;
  219. if(key.startsWith(".")) {
  220. key = key_prefix + key;
  221. }
  222. get_translated_string(key, display_data).then(function (text) {
  223. let target = el.dataset.i18nTarget || "innerText";
  224. if(target.startsWith("@")) {
  225. el.setAttribute(target.substr(1), text);
  226. } else {
  227. el[target] = text;
  228. }
  229. });
  230. });
  231. }
  232. function rehash() {
  233. let hash = window.location.search || window.location.hash;
  234. if(!hash || hash == "#") {
  235. // Input mode
  236. document.getElementById("display-uri").style.display = "none";
  237. document.getElementById("enter-uri").style.display = "block";
  238. initialize_uri_input();
  239. key_prefix = "create";
  240. } else {
  241. document.getElementById("display-uri").style.display = "block";
  242. document.getElementById("enter-uri").style.display = "none";
  243. display_data = load_hash();
  244. document.getElementById('button').href = "xmpp:" + display_data.xmpp_uri_encoded;
  245. document.getElementById('url_in').value = "xmpp:" + display_data.xmpp_uri;
  246. }
  247. translate_ui();
  248. }
  249. function createQR() {
  250. display_data = load_hash();
  251. new QRCode(document.getElementById("qrcode"), "xmpp:" + display_data.xmpp_uri_encoded);
  252. }
  253. function generate_link() {
  254. let input_el = document.getElementById("uri_input");
  255. let output_el = document.getElementById("generated-link");
  256. let is_muc_el = document.getElementById("is_muc");
  257. let input = input_el.value;
  258. var uri;
  259. if(!(input.indexOf("xmpp:") == 0)) {
  260. uri = "xmpp:" + input;
  261. if(is_muc_el.checked) {
  262. uri += "?join";
  263. }
  264. is_muc_el.disabled = false;
  265. } else {
  266. uri = decodeURIComponent(input);
  267. is_muc_el.disabled = true;
  268. is_muc_el.checked = uri.endsWith("?join");
  269. }
  270. let encoded_uri = uri.substr(5).split("@").map(encodeURIComponent).join("@");
  271. let link = document.location.origin + document.location.pathname + "#" + encoded_uri;
  272. output_el.href = link;
  273. output_el.innerText = link;
  274. }
  275. function copy_to_clipboard() {
  276. let link = document.getElementById("generated-link");
  277. let copy_result_el = document.getElementById("copy-result");
  278. Promise.resolve().then(function () {
  279. return navigator.clipboard.writeText(link.href);
  280. }).then(function () {
  281. get_translated_string('copy-success', {}).then(function (text) {
  282. copy_result_el.innerText = text;
  283. });
  284. }, function () {
  285. get_translated_string('copy-failure', {}).then(function (text) {
  286. copy_result_el.innerText = text;
  287. });
  288. }).finally(function () {
  289. copy_result_el.style.visibility = "visible";
  290. });
  291. }
  292. function initialize_uri_input() {
  293. document.getElementById("generate-link-btn").addEventListener("click", function () {
  294. generate_link();
  295. document.getElementById("display-link").style.display = "block";
  296. });
  297. document.getElementById("uri_input").addEventListener("input", generate_link);
  298. document.getElementById("uri_input").addEventListener("keyup", function(event) {
  299. event.preventDefault();
  300. if (event.keyCode === 13) {
  301. document.getElementById("generate-link-btn").click();
  302. }
  303. });
  304. document.getElementById("is_muc").addEventListener("change", generate_link);
  305. document.getElementById("copy-link").addEventListener("click", copy_to_clipboard);
  306. }
  307. function load_done() {
  308. if (initialized) return;
  309. initialized = true;
  310. // load i18n and perform translation
  311. i18n = new I18nText({path: 'lang'});
  312. i18n.once(I18nText.event.LOCALE_CHANGE, function (data) {
  313. rehash();
  314. });
  315. var preferredLocale, setLocale = false;
  316. for (preferredLocale of navigator.languages) {
  317. if (supportedLocales.includes(preferredLocale)) {
  318. i18n.setLocale(preferredLocale);
  319. setLocale = true;
  320. break;
  321. }
  322. }
  323. if (!setLocale) {
  324. i18n.setLocale(defaultLocale);
  325. }
  326. var rtlLangs = "ar, fa, he, ur"
  327. if (rtlLangs.includes(navigator.language)) {
  328. document.querySelector("body").dir = "rtl";
  329. }
  330. // functionality
  331. var ua = navigator.userAgent;
  332. load_clients();
  333. window.addEventListener("hashchange", rehash, false);
  334. document.getElementById("url_in").addEventListener("focus", function(event) {
  335. event.target.select();
  336. });
  337. }
  338. // Wait for the DOM to be ready
  339. document.addEventListener('DOMContentLoaded', load_done, false);
  340. document.onreadystatechange = function() {
  341. if (document.readyState === 'interactive') {
  342. load_done();
  343. }
  344. };
  345. var logo = document.createElement('img');
  346. logo.src = 'assets/xmpp.svg';
  347. logo.alt= 'XMPP logo';
  348. logo.width = 60;
  349. var link = document.createElement('a');
  350. link.href = 'https://xmpp.org/';
  351. link.append(logo)
  352. var brand = document.getElementById('xmpp');
  353. brand.append(link)
  354. })();