app.js 20 KB

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