app.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /*
  2. Copyright (c) 2019,2020 Farooq Karimi Zadeh <fkz@riseup.net>
  3. VM-IRC is under GNU LGPL 3 or at your option any later version and comes
  4. without any warranty from author(s).
  5. */
  6. const ad_txt = "VM-IRC, IRC client for KaiOS";
  7. // ^ Advertisement text :)
  8. const server_addr = "irc.freenode.net";
  9. var mynick = null;
  10. var sock = null;
  11. var channels = null;
  12. var firsttime = {};
  13. var quit = false;
  14. var hasfocus = ".left";
  15. var users_pane_visible = true;
  16. var lock = null;
  17. // ^ this is for keeping screen on till VM-IRC is running.
  18. // Thanks to perry the supreme app maker for the tip :D
  19. var tabs;
  20. var curr_tab;
  21. var hunt = "";
  22. var on_links_menu = false;
  23. var UTF8 = {
  24. encode: function(s){
  25. return unescape(encodeURIComponent(s));
  26. },
  27. decode: function(s){
  28. return decodeURIComponent(escape(s));
  29. }
  30. };
  31. function create_tab(name, with_right) {
  32. tabs.set(name, with_right?{".left": -1, ".right": -1}:{".left": -1});
  33. $("main").append(`<div id=${name}></div>`);
  34. let t;
  35. if (with_right) {
  36. t = '<div class="left"></div><div class="right"></div>';
  37. } else {
  38. t = '<div class="left"></div>';
  39. }
  40. $("#" + $.escapeSelector(name)).html(t);
  41. if (!with_right) {
  42. $("#" + $.escapeSelector(name)+">.left").css("flex-basis", "100%");
  43. }
  44. }
  45. function moveto(dir) {
  46. try{
  47. let tabs_array = array_of_tabs();
  48. if (tabs_array === []) {
  49. return;
  50. }
  51. $("#" + $.escapeSelector(tabs_array[curr_tab])).hide();
  52. if (dir === "left")
  53. curr_tab--;
  54. else
  55. curr_tab++;
  56. if (curr_tab < 0)
  57. curr_tab += tabs_array.length;
  58. curr_tab %= tabs_array.length;
  59. $("#" + $.escapeSelector(tabs_array[curr_tab])).show();
  60. $("h1").text(tabs_array[curr_tab]);
  61. if (!is_channel(tabs_array[curr_tab]))
  62. hasfocus = ".left";
  63. move2focus_msg();
  64. }catch(e){
  65. console.log(e);
  66. console.log(tabs_array);
  67. }
  68. }
  69. function is_channel(name) {
  70. return ["#", "&"].includes(name[0]);
  71. }
  72. function in_tabs(name) {
  73. for (let tab of tabs.keys()) {
  74. if (tab.toUpperCase() === name.toUpperCase()) {
  75. return tab;
  76. }
  77. }
  78. return false;
  79. }
  80. function process_cmd(s) {
  81. s = s.split(" ");
  82. s[0] = s[0].toUpperCase();
  83. let commands = {
  84. "JOIN": args => {
  85. if (is_channel(args[0])) {
  86. sock.send("JOIN " + args[0] + "\r\n");
  87. create_tab(args[0], true);
  88. }
  89. },
  90. "PART": args => {
  91. if (tabs[curr_tab] === server_addr)
  92. return;
  93. sock.send("PART " + tabs[curr_tab] + " :" + ad_txt + "\r\n");
  94. commands["CLOSE"]("");
  95. },
  96. "CLOSE": args => {
  97. if (tabs[curr_tab] === server_addr)
  98. return;
  99. if (is_channel(tabs[curr_tab])) {
  100. return commands["PART"]("");
  101. }
  102. $("main").remove("#" + $.escapeSelector(tabs[curr_tab]));
  103. tabs.pop(curr_tab);
  104. moveto("left");
  105. }
  106. }
  107. commands["J"] = commands["JOIN"];
  108. if (commands[s[0]]) {
  109. commands[s[0]](s.slice(1));
  110. }
  111. }
  112. function move2focus_msg() {
  113. let tabs_array = Array.from(tabs.entries());
  114. let selector = "#"+$.escapeSelector(tabs_array[curr_tab][0])+">.left>article";
  115. let focus_n = tabs.get(tabs_array[curr_tab][0])[hasfocus];
  116. if (focus_n === -1) {
  117. let elm = $(selector).last()[0];
  118. if (elm) elm.focus();
  119. } else {
  120. $(selector)[focus_n].focus();
  121. selector = selector.replace(">article", "");
  122. $(selector).scrollTo(":focus");
  123. }
  124. }
  125. function redify(s) {
  126. return `<span style="color: red">${s}</span>`;
  127. }
  128. function html_escape(s) {
  129. // CREDIT: https://stackoverflow.com/questions/5251520/
  130. return s.replace(
  131. /[^0-9A-Za-z ]/g,
  132. c => "&#" + c.charCodeAt(0) + ";"
  133. );
  134. }
  135. function array_of_tabs() {
  136. return Array.from(tabs.keys());
  137. }
  138. function links_menu(links) {
  139. on_links_menu = true;
  140. $("#menu").empty();
  141. $("main").hide();
  142. for (let link of links) {
  143. let _2add=`<article tabindex=-1><a href="${link}">${link}</a></article>`;
  144. $("#menu").append(_2add);
  145. }
  146. $("#menu").show();
  147. $("#menu > article").last()[0].focus();
  148. }
  149. function return_back() {
  150. on_links_menu = false;
  151. $("#menu").hide();
  152. $("main").show();
  153. move2focus_msg();
  154. }
  155. document.onkeydown = function (e) {
  156. switch (e.key) {
  157. case "Backspace":
  158. if (on_links_menu) {
  159. e.preventDefault();
  160. return_back();
  161. }
  162. break;
  163. case "Enter":
  164. if (on_links_menu) {
  165. var link = $("article:focus").find("a")[0].href;
  166. window.open(link);
  167. return_back();
  168. break;
  169. }
  170. var tab_name_es = $.escapeSelector(array_of_tabs()[curr_tab]);
  171. var links = $("#" + tab_name_es + ">.left>article:focus>a");
  172. if (links.length === 0) {
  173. alert("There is no link to be selected");
  174. } else {
  175. links_menu(links);
  176. }
  177. break;
  178. case "5":
  179. if (hasfocus === ".left"){
  180. hasfocus = ".right"; // -> users pane
  181. } else {
  182. hasfocus = ".left"; // -> messages pane
  183. }
  184. break;
  185. case "ArrowUp":
  186. if (on_links_menu) {
  187. var selector = "#menu>article";
  188. var curr_elm = $(selector + ":focus").index("article");
  189. if (curr_elm > 0) {
  190. curr_elm--;
  191. $(selector)[curr_elm].focus();
  192. }
  193. break;
  194. }
  195. var tabs_array = Array.from(tabs.entries());
  196. var curr_tab_name = tabs_array[curr_tab][0];
  197. var selector= "#"+$.escapeSelector(curr_tab_name)+">"+hasfocus;
  198. var curr_elm = tabs.get(curr_tab_name)[hasfocus];
  199. if (curr_elm === -1) {
  200. curr_elm = $(selector + ">article").length - 1;
  201. }
  202. if (curr_elm != 0) {
  203. curr_elm--;
  204. $(selector + ">article")[curr_elm].focus();
  205. }
  206. var new_values = tabs.get(curr_tab_name);
  207. new_values[hasfocus] = curr_elm;
  208. tabs.set(curr_tab_name, new_values);
  209. move2focus_msg();
  210. $(selector).scrollTo("-=37px");
  211. break;
  212. case "ArrowDown":
  213. if (on_links_menu) {
  214. var selector = "#menu>article";
  215. var curr_elm = $(selector + ":focus").index("article");
  216. if (curr_elm <= $(selector).length) {
  217. curr_elm++;
  218. $(selector)[curr_elm].focus();
  219. }
  220. break;
  221. }
  222. var tabs_array = Array.from(tabs.entries());
  223. var curr_tab_name = tabs_array[curr_tab][0];
  224. var selector= "#"+$.escapeSelector(curr_tab_name)+">"+hasfocus;
  225. var curr_elm = tabs.get(curr_tab_name)[hasfocus];
  226. if (curr_elm != -1) {
  227. curr_elm++;
  228. $(selector + ">article")[curr_elm].focus();
  229. }
  230. var new_values = tabs.get(curr_tab_name);
  231. if (curr_elm === $(selector + ">article").length - 1)
  232. curr_elm = -1;
  233. new_values[hasfocus] = curr_elm;
  234. tabs.set(curr_tab_name, new_values);
  235. move2focus_msg();
  236. $(selector).scrollTo("+=32px");
  237. break;
  238. case "ArrowLeft":
  239. moveto("left");
  240. break;
  241. case "ArrowRight":
  242. moveto("right");
  243. break;
  244. case "SoftLeft":
  245. if (sock == null || sock.readyState != "open"){
  246. alert("socket not ready!");
  247. break;
  248. }
  249. if (tabs[curr_tab] === server_addr) {
  250. alert("Please move to a channel");
  251. break;
  252. }
  253. var msg_text = prompt("Message");
  254. if (!msg_text)
  255. break;
  256. if (msg_text.startsWith("/")){
  257. process_cmd(msg_text.replace("/", ""));
  258. break;
  259. }
  260. var mynick_ = redify(mynick);
  261. var msg_to_add = `<article tabindex=-1 class="ownmsg">&lt;${mynick_}&gt;${msg_text}</article>`;
  262. selector = "#" +$.escapeSelector(tabs[curr_tab]) + ">.left";
  263. $(selector).append(msg_to_add);
  264. $(selector + ">article").last().linky();
  265. msg_text = UTF8.encode(msg_text);
  266. sock.send("PRIVMSG "+tabs[curr_tab]+ " :" + msg_text + "\r\n");
  267. move2focus_msg();
  268. break;
  269. case "SoftRight":
  270. if (sock != null && sock.readyState === "open"){
  271. quit = true;
  272. sock.send("QUIT :" + ad_txt + "\r\n");
  273. sock.close();
  274. }
  275. window.close();
  276. break;
  277. case "Call":
  278. try{
  279. var towhom = prompt("To whom?");
  280. if (towhom == null)
  281. break;
  282. var msg_text = prompt("Message to " + towhom);
  283. if (msg_text == null)
  284. break;
  285. let target_tab = in_tabs(towhom);
  286. if (!target_tab) {
  287. target_tab = towhom;
  288. create_tab(target_tab, false);
  289. }
  290. let selector = "#" + $.escapeSelector(target_tab) + ">.left";
  291. let txt2add = redify("&lt;" + mynick + "&gt;") + " " + msg_text;
  292. $(selector).append(`<article class="ownmsg">${txt2add}</article>`);
  293. while (array_of_tabs()[curr_tab] != target_tab) {
  294. moveto("right");
  295. }
  296. // TODO
  297. msg_text = UTF8.encode(msg_text);
  298. sock.send("PRIVMSG " + towhom + " :" + msg_text + "\r\n");
  299. move2focus_msg();
  300. }catch(e){console.log(e);}
  301. break;
  302. case "1":
  303. var current_size = Number($("body").css("fontSize").replace("px", ""));
  304. if (current_size === 0) break;
  305. current_size--;
  306. $("body").css("fontSize", current_size.toString() + "px");
  307. break;
  308. case "2":
  309. $("body").css("fontSize", "12px");
  310. break;
  311. case "3":
  312. var current_size = Number($("body").css("fontSize").replace("px", ""));
  313. if (current_size === "64") break;
  314. current_size++;
  315. $("body").css("fontSize", current_size.toString() + "px");
  316. break;
  317. case "*":
  318. var curr_tab_name = array_of_tabs()[curr_tab];
  319. if (is_channel(curr_tab_name)) {
  320. let selector = "#"+$.escapeSelector(curr_tab_name)+">.left";
  321. let width = $(selector).css("flex-basis");
  322. if (width === "75%") {
  323. $(selector).css("flex-basis", "100%");
  324. $(selector.replace(".left", ".right")).css("flex-basis", "0");
  325. hasfocus = ".left";
  326. } else {
  327. $(selector).css("flex-basis", "75%");
  328. $(selector.replace(".left", ".right")).css("flex-basis", "25%");
  329. }
  330. }
  331. break;
  332. case "#":
  333. if (lock === null){
  334. lock = window.navigator.requestWakeLock("screen");
  335. alert("Screen will not turn off");
  336. } else {
  337. lock.unlock();
  338. lock = null;
  339. alert("Screen will turn off");
  340. // TODO for this message and the previous one:
  341. // better message
  342. }
  343. }
  344. hunt += e.key;
  345. if (hunt === "768286") {
  346. // DO NOT TELL ANYONE ABOUT THIS!
  347. // It's an easter egg ;)
  348. alert("Farooq is a POTATO!");
  349. hunt = "";
  350. }
  351. if (hunt.length > 7) {
  352. hunt = "";
  353. }
  354. };
  355. function stort() {
  356. tabs = new Map();
  357. curr_tab = 0;
  358. try{
  359. // ^ DO NOT CHANGE NAME OF THIS FUNCTION!
  360. var def;
  361. $("body").css("fontSize", "12px");
  362. if (mynick === null){
  363. def = window.localStorage.getItem("nick");
  364. if (["null", null].includes(def)){
  365. mynick = prompt("Nick (max 9 chars)");
  366. }else{
  367. mynick = prompt("Nick (max 9 chars) or press OK for " + def);
  368. if (mynick === ""){
  369. mynick = def;
  370. }
  371. }
  372. }
  373. if (mynick === null) window.close();
  374. window.localStorage.setItem("nick", mynick);
  375. if (channels === null){
  376. def = window.localStorage.getItem("channels");
  377. if (["null", null].includes(def)){
  378. channels = prompt("Channels(space seperated)");
  379. }else{
  380. channels = prompt("Channels(space seperated) or press OK for " + def);
  381. if (channels === ""){
  382. channels = def;
  383. }
  384. }
  385. }
  386. if (channels === null) window.close();
  387. window.localStorage.setItem("channels", channels);
  388. sock = navigator.mozTCPSocket.open(server_addr,6697,{useSecureTransport: true});
  389. sock.onopen = function (e) {
  390. e.target.send("USER vm-irc vm-irc vm-irc :VM-IRC User\r\n");
  391. e.target.send("NICK " + mynick + "\r\n");
  392. tabs.clear();
  393. $("main").empty();
  394. create_tab(server_addr, false);
  395. for (let channel of channels.split(" ")) {
  396. if (is_channel(channel)) {
  397. process_cmd("JOIN " + channel);
  398. moveto("right");
  399. }
  400. }
  401. };
  402. sock.ondata = function (e) {
  403. if (typeof(e.data) !== "string")
  404. return;
  405. var msgs = e.data.split("\r\n");
  406. for (let msg of msgs){
  407. if (msg.startsWith("PING")){
  408. e.currentTarget.send("PONG " + msg.split(" ")[1] + "\r\n");
  409. continue;
  410. }
  411. msg = UTF8.decode(msg);
  412. var token = msg.split(" ", 3);
  413. var theirnick = token[0].split("!")[0].replace(":", "");
  414. if (token[1] === "PRIVMSG"){
  415. try{
  416. var target = token[2];
  417. var msg_text = msg.slice(msg.search(" :") + 2, msg.length);
  418. msg_text = html_escape(msg_text);
  419. if (document.visibilityState === "hidden"){
  420. if (window.Notification.permission === "granted"){
  421. if (mynick === target || msg_text.includes(mynick)){
  422. var vibrate_pattern = [200, 100, 200, 100, 200];
  423. new window.Notification(theirnick, {body: msg_text,
  424. vibrate:vibrate_pattern
  425. });
  426. }
  427. }
  428. }
  429. if (array_of_tabs().includes(target)) {
  430. theirnick = "&lt;" + theirnick + "&gt;";
  431. } else if (target === mynick) {
  432. // then it's a PM
  433. let target_tab = in_tabs(theirnick);
  434. if (!target_tab) {
  435. target_tab = theirnick;
  436. create_tab(theirnick, false);
  437. }
  438. let selector="#"+$.escapeSelector(target_tab)+">.left";
  439. theirnick = redify(`&lt;${theirnick}&gt;`);
  440. let text2add = `<article tabindex=-1>${theirnick}${msg_text}</article>`;
  441. $(selector).append(text2add);
  442. continue;
  443. }
  444. var text = redify(theirnick) + " " + msg_text + "<br>";
  445. if (msg_text.search(mynick) !== -1 || target === mynick)
  446. text = "<b>" + text + "<b>";
  447. text = "<article tabindex=-1>" + text + "</article>";
  448. let selector = "#" + $.escapeSelector(target) + ">.left";
  449. $(selector).append(text);
  450. $(selector + ">article").last().linky();
  451. }catch(e){console.log(e);}
  452. }
  453. if (token[1] === "NOTICE"){
  454. var msg_text = msg.slice(msg.search(" :") + 2, msg.length);
  455. msg_text = html_escape(msg_text);
  456. var text=`<article tabindex=-1>-${theirnick}- ${msg_text}</article>`;
  457. let selector = "#" + $.escapeSelector(server_addr)+">.left";
  458. $(selector).append(text);
  459. $(selector + ">article").last().linky();
  460. }
  461. let selector;
  462. if (token[1] === "JOIN"){
  463. if (theirnick === mynick) continue;
  464. selector = "#" + $.escapeSelector(token[2]) + " > .left";
  465. $(selector).append(`<div tabindex=-1>${theirnick} joined.</div>`);
  466. selector = "#" + $.escapeSelector(token[2]) + " > .right";
  467. $(selector).append(`<div name="${theirnick}">${theirnick}</div>`);
  468. }
  469. if (token[1] === "PART"){
  470. selector = "#" + $.escapeSelector(token[2]) + " > .left";
  471. $(selector).append(`<div tabindex=-1>${theirnick} left.</div>`);
  472. selector = selector.replace(".left", ".right");
  473. selector += ">div[name=" + theirnick + "]";
  474. $(selector).remove();
  475. }
  476. if (token[1] === "QUIT"){
  477. for (let key of tabs.keys()) {
  478. if (is_channel(key)) {
  479. let key_es = $.escapeSelector(key);
  480. let selector ="#"+key_es+">.right";
  481. let elm =$(selector+">div[name="+theirnick+"]");
  482. if (elm.length != 0) {
  483. elm.remove();
  484. $(selector.replace(".right", ".left")).append(`<div tabindex=-1>${theirnick} has quit.`);
  485. }
  486. }
  487. }
  488. }
  489. if (token[1] === "353"){ // Channel user list
  490. msg = msg.replace(/:/g, "").split(" ");
  491. let theirnicks = msg.slice(5).join(" ");
  492. let channel = msg[4];
  493. let selector_msg = "#" + $.escapeSelector(channel) + ">.left";
  494. let selector_user = "#" + $.escapeSelector(channel) + ">.right";
  495. if (!firsttime[channel]){
  496. $(selector_msg).append("Users: ");
  497. firsttime[channel] = true;
  498. }
  499. $(selector_msg).append(theirnicks + " ");
  500. // now the user list pane
  501. for (let anick of theirnicks.split(" ")){
  502. $(selector_user).append(`<div name="${anick}">${anick}</div>`);
  503. }
  504. }
  505. if (token[1] === "366"){
  506. firsttime[msg.split(" ")[4]] = true;
  507. }
  508. if (token[1] === "332"){ // Reply to TOPIC command
  509. let topic = msg.slice(msg.replace(":", ".").search(":") + 1, msg.length);
  510. topic = html_escape(topic);
  511. let channel = msg.split(" ")[3];
  512. selector = "#" + $.escapeSelector(channel) + ">.left";
  513. $(selector).append(`<article tabindex=-1>Topic: ${topic}</article>`);
  514. $(selector + ">article").last().linky();
  515. }
  516. if (token[1] === "433"){ // Nick name already in use
  517. mynick += "_";
  518. e.currentTarget.send(`NICK ${mynick}\r\n`);
  519. }
  520. if (token[1] === "NICK"){
  521. var newnick = token[2].replace(":", "");
  522. if (theirnick === mynick) {
  523. mynick = newnick;
  524. } else {
  525. let txt = `<article tabindex=-1>${theirnick} changed their nick to ${newnick}`;
  526. var curr_tab_name_es = $.escapeSelector(array_of_tabs()[curr_tab]);
  527. $("#" + curr_tab_name_es + ">.left").add(txt);
  528. $("#" + curr_tab_name_es + ">.right").remove(`article[name=${theirnick}]`);
  529. txt = `<article tabindex=-1>${newnick}</article>`;
  530. $("#" + curr_tab_name_es + ">.right").add(txt);
  531. }
  532. }
  533. move2focus_msg();
  534. }
  535. };
  536. sock.onclose = function (e) {
  537. if (!quit){
  538. stort();
  539. }
  540. };
  541. }catch(E){console.log(E);}
  542. }
  543. $(stort);