Camamba Chat Tweaks

tweaks layout of the chat

  1. // ==UserScript==
  2. // @name Camamba Chat Tweaks
  3. // @namespace dannysaurus.camamba
  4. // @version 0.5.14
  5. // @description tweaks layout of the chat
  6. // @license MIT License
  7. //
  8. // @include https://www.camamba.com/chat/
  9. // @include https://www.de.camamba.com/chat/
  10. //
  11. // @connect camamba.com
  12. // @grant GM_xmlhttpRequest
  13. //
  14. // @require https://greasyfork.org/scripts/405143-simplecache/code/SimpleCache.js
  15. // @require https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1106047
  16. // @require https://greasyfork.org/scripts/391854-enum/code/Enum.js
  17. // @require https://greasyfork.org/scripts/405699-camamba-user/code/Camamba%20User.js
  18. //
  19. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
  20. //
  21. // @require https://greasyfork.org/scripts/423722-camamba-chat-helpers-library/code/Camamba%20Chat%20Helpers%20Library.js?version=960246
  22. // @require https://greasyfork.org/scripts/423662-camamba-chat-settings/code/Camamba%20Chat%20Settings.js?version=913122
  23. // @require https://greasyfork.org/scripts/423665-camamba-hook-into-onmessage/code/Camamba%20Hook%20Into%20OnMessage.js?version=1180072
  24. //
  25. // @grant GM.getValue
  26. // @grant GM.setValue
  27. // ==/UserScript==
  28.  
  29. // https://greasyfork.org/de/scripts/419077-camamba-chat-tweaks
  30.  
  31. /* jslint esnext: true */
  32. /* globals knownUsers, me */
  33. (function () {
  34. 'use strict';
  35. // --- initial sizes ---
  36. const SIZES = {
  37. FONT_EM: {
  38. userList: 1.2,
  39. chatBox: 1.8,
  40. },
  41. WIDTH_EM: {
  42. sidebarLeft: 10,
  43. sidebarRight: 14,
  44. },
  45. };
  46.  
  47. // --- HTML Selector Helpers ---
  48. const SELECTORS = {
  49. ID: {
  50. // original
  51. userList: 'userList',
  52. chatBox: 'chatBox',
  53. chatInput: 'chatInput',
  54. chatWindow: 'chatWindow',
  55. mediaContainer1: 'mediaContainer1',
  56. mediaContainer2: 'mediaContainer2',
  57. mediaContainer3: 'mediaContainer3',
  58. mediaContainer4: 'mediaContainer4',
  59. mediaContainer5: 'mediaContainer5',
  60. mediaContainer6: 'mediaContainer6',
  61. mediaContainer7: 'mediaContainer7',
  62. mediaContainer8: 'mediaContainer8',
  63.  
  64. // script
  65. cbCamslots: 'cb-camslots',
  66. spinnerUserlistFont: 'spinner-userlist-font',
  67. spinnerChatFont: 'spinner-chat-font',
  68. unamePermaInput: 'uname-perma-input',
  69. cbPrivateConvo: 'cb-privateConvo',
  70. },
  71. CLASS: {
  72. noTextSelect: 'noTextSelect',
  73. borderBox: 'borderBox',
  74. camBox: 'camBox'
  75. }
  76. };
  77.  
  78. const containers = (() => {
  79. let userList, chatBox, sidebars, camslots;
  80.  
  81. return {
  82. get userList() {
  83. if (typeof userList === "undefined") {
  84. userList = document.getElementById(SELECTORS.ID.userList);
  85. }
  86. return userList;
  87. },
  88.  
  89. get chatBox() {
  90. if (typeof chatBox === "undefined") {
  91. chatBox = document.getElementById(SELECTORS.ID.chatBox);
  92. }
  93. return chatBox;
  94. },
  95.  
  96. get sidebars() {
  97. if (typeof sidebars === "undefined") {
  98. sidebars = document.getElementById(SELECTORS.ID.chatWindow).querySelectorAll(`.${SELECTORS.CLASS.noTextSelect}`);
  99. }
  100. return sidebars;
  101. },
  102.  
  103. get sidebarLeft() {
  104. return this.sidebars[0];
  105. },
  106.  
  107. get sidebarTop() {
  108. return this.sidebars[1];
  109. },
  110.  
  111. get sidebarRight() {
  112. return this.sidebars[2];
  113. },
  114.  
  115. get camslots() {
  116. if (typeof camslots === "undefined") {
  117. const parentContainers = [
  118. SELECTORS.ID.mediaContainer1,
  119. SELECTORS.ID.mediaContainer2,
  120. SELECTORS.ID.mediaContainer3,
  121. SELECTORS.ID.mediaContainer4,
  122. SELECTORS.ID.mediaContainer5,
  123. SELECTORS.ID.mediaContainer6,
  124. SELECTORS.ID.mediaContainer7,
  125. SELECTORS.ID.mediaContainer8,
  126. ]
  127. .map(id => document.getElementById(id))
  128. .filter(el => el !== null)
  129. .map(el => el.parentNode);
  130.  
  131. camslots = [...new Set(parentContainers)];
  132. }
  133. return camslots;
  134. }
  135. };
  136. })();
  137.  
  138. const layoutPatcher = new class {
  139. constructor() {
  140. this.historyCamslotsRemoved = [];
  141. }
  142.  
  143. patchSizes() {
  144. // this.setWidthOfSidebarLeft(`${SIZES.WIDTH_EM.sidebarLeft}em`);
  145. this.setWidthOfSidebarRight(`${SIZES.WIDTH_EM.sidebarRight}em`);
  146. return this;
  147. }
  148.  
  149. setFontSizeOfUserList(fontSize) {
  150. containers.userList.style.fontSize = fontSize;
  151. return this;
  152. }
  153.  
  154. setFontSizeOfChat(fontSize) {
  155. containers.chatBox.style.fontSize = fontSize;
  156. return this;
  157. }
  158.  
  159. setWidthOfSidebarLeft(width) {
  160. containers.sidebarLeft.style.width = width;
  161. return this;
  162. }
  163.  
  164. setWidthOfSidebarRight(width) {
  165. containers.sidebarLeft.style.width = width;
  166. return this;
  167. }
  168.  
  169. showCamslots() {
  170. for (let i = 0; i < this.historyCamslotsRemoved.length; i++) {
  171. const { parent, index, element } = this.historyCamslotsRemoved.pop();
  172. parent.insertBefore(element, parent.children[index]);
  173. }
  174. return this;
  175. }
  176.  
  177. hideCamslots() {
  178. for (let element of containers.camslots) {
  179. const parent = element.parentNode;
  180. if (parent) {
  181. let index = Array.from(parent.children).indexOf(element);
  182. parent.removeChild(element);
  183.  
  184. this.historyCamslotsRemoved.push({ parent, index, element });
  185. }
  186. }
  187. return this;
  188. }
  189. }();
  190.  
  191.  
  192. const controls = (() => {
  193. // --- HTML Create Element Helpers ---
  194. const createInput = ({
  195. id,
  196. parentElement = null,
  197. type = 'text',
  198. defaultValue = '',
  199. labelText = null,
  200. onValueChange = null,
  201. propertyNameValue = 'value',
  202. eventNameValueChange = 'input',
  203. }) => {
  204. const div = document.createElement('div');
  205.  
  206. const input = div.appendChild(document.createElement('input'));
  207. input.type = type;
  208. input.id = id;
  209. input.style.backgroundColor = 'rgba(39,62,77,1)';
  210.  
  211. if (labelText) {
  212. const label = div.appendChild(document.createElement('label'));
  213. label.htmlFor = id;
  214. label.appendChild(document.createTextNode(labelText));
  215. }
  216.  
  217. if (onValueChange) {
  218. let oldValue;
  219.  
  220. input.addEventListener(eventNameValueChange, () => {
  221. const newValue = input[propertyNameValue];
  222. if (oldValue !== newValue) {
  223. oldValue = newValue;
  224.  
  225. onValueChange(newValue);
  226. }
  227. });
  228. }
  229.  
  230. if (parentElement) {
  231. parentElement.appendChild(div);
  232. }
  233. return input;
  234. };
  235.  
  236. const createInputPersistent = ({
  237. id,
  238. parentElement = null,
  239. type = 'text',
  240. defaultValue = '',
  241. labelText = null,
  242. onValueChange = null,
  243. propertyNameValue = 'value',
  244. eventNameValueChange = 'input',
  245. }) => {
  246. const input = createInput({
  247. parentElement, type, id, defaultValue, labelText, propertyNameValue, eventNameValueChange,
  248. onValueChange: value => {
  249. GM.setValue(id, value);
  250. if (onValueChange) {
  251. onValueChange(value);
  252. }
  253. }
  254. });
  255.  
  256. input.setValue = value => {
  257. GM.setValue(id, value);
  258. input[propertyNameValue] = value;
  259. onValueChange(value);
  260. };
  261.  
  262. input.updateValue = () => GM.getValue(id, defaultValue).then(value => {
  263. input[propertyNameValue] = value;
  264. if (onValueChange) {
  265. onValueChange(value);
  266. }
  267. });
  268.  
  269. return input;
  270. };
  271.  
  272. const createCheckbox = ({
  273. id,
  274. parentElement = null,
  275. initialChecked = false,
  276. labelText = null,
  277. onValueChange = null,
  278. }) => {
  279. const checkbox = createInputPersistent({
  280. parentElement, id, labelText, onValueChange,
  281. defaultValue: !!initialChecked,
  282. type: 'checkbox',
  283. propertyNameValue: 'checked',
  284. eventNameValueChange: 'click',
  285. });
  286. return checkbox;
  287. };
  288.  
  289. const createSpinner = ({
  290. id, min, max, step,
  291. parentElement = null,
  292. defaultValue = 0,
  293. labelText = null,
  294. onValueChange = null,
  295. }) => {
  296. const spinner = createInputPersistent({
  297. parentElement, id, defaultValue, labelText, onValueChange,
  298. type: 'number',
  299. });
  300. spinner.min = min;
  301. spinner.max = max;
  302. spinner.step = step;
  303.  
  304. const buttonDec = spinner.parentNode.insertBefore(document.createElement('button'), spinner);
  305. buttonDec.type = 'button';
  306. buttonDec.innerHTML = '-';
  307. buttonDec.addEventListener('click', () => {
  308. spinner.stepDown();
  309. spinner.setValue(spinner.value);
  310. });
  311.  
  312. const buttonInc = spinner.parentNode.insertBefore(document.createElement('button'), spinner.nextSibling);
  313. buttonInc.type = 'button';
  314. buttonInc.innerHTML = '+';
  315. buttonInc.addEventListener('click', () => {
  316. spinner.stepUp();
  317. spinner.setValue(spinner.value);
  318. });
  319.  
  320. return spinner;
  321. };
  322.  
  323. const sidebarLeftCenter = containers.sidebarLeft.children[1];
  324. sidebarLeftCenter.innerHTML = "";
  325. const container = sidebarLeftCenter.appendChild(document.createElement('div'));
  326.  
  327. // checkbox camslots on/off
  328. const cbCamslots = createCheckbox({
  329. parentElement: container,
  330. id: SELECTORS.ID.cbCamslots,
  331. initialChecked: true,
  332. labelText: 'camslots',
  333. onValueChange: value => {
  334. if (value) {
  335. layoutPatcher.showCamslots();
  336. } else {
  337. layoutPatcher.hideCamslots();
  338. }
  339. },
  340. });
  341.  
  342. // spinner userlist font
  343. const spinnerUserlistFont = createSpinner({
  344. parentElement: container,
  345. id: SELECTORS.ID.spinnerUserlistFont,
  346. defaultValue: SIZES.FONT_EM.userList,
  347. min: 1.0,
  348. max: 3.2,
  349. step: 0.1,
  350. labelText: 'users',
  351. onValueChange: value => {
  352. const fontSize = `${value}em`;
  353. layoutPatcher.setFontSizeOfUserList(fontSize);
  354. },
  355. });
  356.  
  357. // spinner chat font
  358. const spinnerChatFont = createSpinner({
  359. parentElement: container,
  360. id: SELECTORS.ID.spinnerChatFont,
  361. defaultValue: SIZES.FONT_EM.chatBox,
  362. min: 1.0,
  363. max: 5.5,
  364. step: 0.1,
  365. labelText: 'chat',
  366. onValueChange: value => {
  367. const fontSize = `${value}em`;
  368. layoutPatcher.setFontSizeOfChat(fontSize);
  369. },
  370. });
  371.  
  372. const buttonKickFromCam = container.appendChild(document.createElement('button'));
  373. buttonKickFromCam.type = 'button';
  374. buttonKickFromCam.innerHTML = 'Kick from cam';
  375. buttonKickFromCam.addEventListener('click', () => {
  376. knownUsers.bySelected().stopViewing();
  377. });
  378.  
  379. if (me.admin) {
  380. const labelUnamePerma = container.appendChild(document.createElement('label'));
  381. labelUnamePerma.type = 'text';
  382. labelUnamePerma.for = "uname-perma";
  383. labelUnamePerma.innerHTML = 'Username Perma';
  384.  
  385. const inputUnamePerma = container.appendChild(document.createElement('input'));
  386. inputUnamePerma.type = 'text';
  387. inputUnamePerma.id = SELECTORS.ID.unamePermaInput;
  388. inputUnamePerma.name = 'uname-perma';
  389.  
  390. const buttonPerma = container.appendChild(document.createElement('button'));
  391. buttonPerma.type = 'button';
  392. buttonPerma.innerHTML = 'perma';
  393. buttonPerma.addEventListener('click', () => {
  394. const unamePerma = document.getElementById(SELECTORS.ID.unamePermaInput).value;
  395. if (unamePerma) {
  396. knownUsers.addExact(unamePerma).then(() => knownUsers.byName(unamePerma).banPermaFast(""));
  397. } else {
  398. knownUsers.bySelected().ban("You are permanently banned from Camamba. Please do not create any additional accounts!", 24, { isPublic: true, isPerma: true, suppressBanLog: false });
  399. }
  400. });
  401. }
  402.  
  403. const isGerman = location.hostname === "www.de.camamba.com";
  404.  
  405.  
  406. let oldPrivateHandler = null;
  407.  
  408. // checkbox camslots on/off
  409. const cbPrivateConvo = createCheckbox({
  410. parentElement: container,
  411. id: SELECTORS.ID.cbPrivateConvo,
  412. initialChecked: true,
  413. labelText: isGerman ? 'PN ablehnen ohne Freundschaft' : 'PM denie withouth friendship',
  414. onValueChange: (value) => {
  415. if (value) {
  416. if (!oldPrivateHandler && onMessageHandlers.private) {
  417. oldPrivateHandler = onMessageHandlers.private;
  418. console.log("Alter Handler gesichert.", oldPrivateHandler.toString());
  419. }
  420. /**
  421. * @param {{ id: number }} data
  422. * @return {boolean} - true if further handling is required, false if action is fully handled
  423. */
  424. onMessageHandlers.private = (data) => {
  425. if (!data.id) {
  426. return true;
  427. }
  428. const user = knownUsers[data.id];
  429. if (!user) {
  430. console.log(`Unknown user with id ${data.id} requesting ${"privConvo"}.`);
  431. return false;
  432. }
  433.  
  434. if (!user.friend) {
  435. wsSend({ command: "control", target: data.id, request: "privReject" });
  436. console.log(`PN von ${user.name} abgelehnt`)
  437. return false;
  438. }
  439. console.log(`PN von ${user.name} erlaubt`)
  440. console.log("Alter Handler ausgeführt.")
  441. if (typeof oldPrivateHandler === 'function') {
  442. return oldPrivateHandler(data);
  443. }
  444. return true;
  445. };
  446. } else {
  447. if (oldPrivateHandler) {
  448. onMessageHandlers.private = oldPrivateHandler;
  449. console.log("Alter Handler wiederhergestellt.")
  450. }
  451. }
  452. },
  453. });
  454.  
  455. return {
  456. cbCamslots,
  457. spinnerUserlistFont,
  458. spinnerChatFont,
  459. cbPrivateConvo,
  460. };
  461. })();
  462.  
  463. const wait = async (ms) => new Promise(res => setTimeout(res, ms));
  464. (async () => {
  465. // wait until websocket has been connected
  466. while (typeof initSettings !== 'function') {
  467. await wait(100);
  468. }
  469.  
  470. const original = initSettings;
  471. initSettings = () => {
  472. original();
  473.  
  474. // Breite von Userliste anpassen
  475. layoutPatcher.patchSizes();
  476.  
  477. // weiterere Einstellungen überschreiben, bzw übernehmen
  478. for (let control of [controls.cbCamslots, controls.spinnerUserlistFont, controls.spinnerChatFont, controls.cbPrivateConvo]) {
  479. control.updateValue();
  480. }
  481. };
  482. })();
  483.  
  484. (async () => {
  485. let lastBanData = { userId: 0, text: '', time: 0, isPerma: false };
  486.  
  487. while (typeof adminExec !== 'function') {
  488. await wait(100);
  489. }
  490.  
  491. adminExec();
  492.  
  493. if (currentAdminAction == "ban") {
  494. let userId, text, time, isPerma;
  495.  
  496. text = byId('adminMessageInput').value;
  497. if (!text || text.length <= 3 && byId('adminMessageSelect').selectedIndex) {
  498. text = adminMessages[currentAdminAction][byId('adminMessageSelect').value];
  499. }
  500.  
  501. userId = currentAdminTarget;
  502. time = parseInt(byId('banTime').value);
  503. isPerma = byId('permaBan') && byId('permaBan').checked;
  504.  
  505. if (userId && text > 3 && time) {
  506. lastBanData = { userId, text, time, isPerma };
  507. }
  508. }
  509. })();
  510.  
  511.  
  512. (async () => {
  513. while (document.getElementById(SELECTORS.ID.chatInput) === null) {
  514. await wait(100);
  515. }
  516. document.getElementById(SELECTORS.ID.chatInput).setAttribute('autoComplete', 'on');
  517. })();
  518.  
  519. console.log("running camamba chat tweaks")
  520. })();