A self hosted chat application with end-to-end encrypted messaging.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

205 lines
7.3 KiB

4 years ago
4 years ago
  1. import { Observer } from "../observe/Observer";
  2. import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel";
  3. import { ChatModel } from "../model/ChatModel";
  4. import log = require("loglevel");
  5. import * as DOMPurify from "dompurify";
  6. import { SearchService } from "../service/SearchService";
  7. import { UserModel } from "../model/UserModel";
  8. import { UserViewDeps } from "./UserViewDeps";
  9. import { ObserverData } from "../observe/ObserverData";
  10. import { JsonAPI } from "../singleton/JsonAPI";
  11. import { NotificationService } from "../service/NotificationService";
  12. import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
  13. export class UserView implements Observer<ActiveUserViewModel> {
  14. private readonly _model: UserModel;
  15. private readonly _chatModel: ChatModel;
  16. private readonly _usersListElement: HTMLElement;
  17. private readonly _userSearchInputElement: HTMLInputElement;
  18. private readonly _userSearchButton: HTMLElement;
  19. private readonly _userSearchCancelButton: HTMLElement;
  20. private readonly _searchService: SearchService<ActiveUserViewModel>;
  21. private readonly _userContactOnlineTemplate: Handlebars.TemplateDelegate<
  22. ActiveUserViewModel
  23. >;
  24. private readonly _userContactOfflineTemplate: Handlebars.TemplateDelegate<
  25. ActiveUserViewModel
  26. >;
  27. private readonly _notificationService: NotificationService;
  28. private _newMessagesLoop: any;
  29. constructor(deps: UserViewDeps) {
  30. this._model = deps.model;
  31. this._chatModel = deps.chatModel;
  32. this._usersListElement = deps.usersListElement;
  33. this._userSearchInputElement = deps.userSearchInputElement;
  34. this._userSearchButton = deps.userSearchButton;
  35. this._userSearchCancelButton = deps.userSearchCancelButton;
  36. this._searchService = deps.searchService;
  37. this._userContactOnlineTemplate = deps.userContactOnlineTemplate;
  38. this._userContactOfflineTemplate = deps.userContactOfflineTemplate;
  39. this._notificationService = deps.notificationService;
  40. this._addSearchEventListeners();
  41. }
  42. update(d: ObserverData<ActiveUserViewModel>): void {
  43. let html: string = "";
  44. d.data.forEach((element: ActiveUserViewModel) => {
  45. element.online
  46. ? (html += this._userContactOnlineTemplate(element))
  47. : (html += this._userContactOfflineTemplate(element));
  48. });
  49. $(this._usersListElement).html(DOMPurify.sanitize(html));
  50. this._addUserCallBacks();
  51. }
  52. private _addSearchEventListeners(): void {
  53. this._addSearchButtonEL();
  54. this._addSearchCancelEL();
  55. this._addSearchInputEL();
  56. }
  57. private _addUserCallBacks(): void {
  58. let userBoxes = document.getElementsByClassName("user-box");
  59. Array.from(userBoxes).forEach((ub: Element) =>
  60. ub.addEventListener("click", this._userCallBack.bind(this, ub))
  61. );
  62. }
  63. private _userCallBack(el: Element): void {
  64. this._chatModel.clear();
  65. clearInterval(this._newMessagesLoop);
  66. let current = document.getElementsByClassName("user-box active");
  67. if (current.length > 0) {
  68. current[0].className = current[0].className.replace(" active", "");
  69. }
  70. // Add the active class to the current/clicked button
  71. else if (current.length == 0) {
  72. // @ts-ignore: Object is possibly 'null'.
  73. document.getElementById("no-user-selected").hidden = true;
  74. // @ts-ignore: Object is possibly 'null'.
  75. document.getElementById("chat-card").hidden = false;
  76. }
  77. // console.log(this.getElementsByClassName('to-user-span'));
  78. let elem = el.getElementsByClassName("to-user-span")[0] as HTMLElement;
  79. let userName = elem.innerText;
  80. JsonAPI.contactName = userName;
  81. // @ts-ignore: Object is possibly 'null'.
  82. document.getElementById("user-name-span").innerText = userName;
  83. let currentUser = this._model.activeUsersList.find((vm) => vm.userName === userName) || new ActiveUserViewModel();
  84. currentUser.userName = userName;
  85. if (!currentUser?.passphrase) {
  86. this._notificationService.passphrasePrompt(
  87. async (result) => {
  88. if (result) {
  89. const valid = await this._chatModel.isPassphraseValid(result, currentUser.userName!);
  90. if (!valid) {
  91. bootbox.alert("Some error occured. Please check your password");
  92. log.error("invalid password");
  93. return;
  94. }
  95. currentUser.unlocked = true;
  96. currentUser.passphrase = result;
  97. const chatMessages: ChatMessageViewModel[] = await this._chatModel.getMessages(currentUser, "new");
  98. this._model.activeUsersList
  99. .filter((v) => v.userName == currentUser!.userName)
  100. .forEach((v) => {
  101. v.passphrase = result;
  102. v.unlocked = true;
  103. if (chatMessages.length > 0) {
  104. v.lastMessageTime = new Date(
  105. chatMessages[chatMessages.length - 1].messageTime
  106. );
  107. const lastMessageText = (v.lastMessageText =
  108. chatMessages[chatMessages.length - 1].message);
  109. if (lastMessageText.length > 15) {
  110. v.lastMessageText =
  111. chatMessages[chatMessages.length - 1].message.slice(
  112. 0,
  113. 15
  114. ) + "...";
  115. }
  116. }
  117. });
  118. this._promptHandler(currentUser);
  119. }
  120. }
  121. );
  122. }
  123. else {
  124. this._chatModel.getMessages(currentUser, "new");
  125. }
  126. el.className += " active";
  127. if (currentUser.unlocked && currentUser.lastMessageTime) {
  128. this._newMessagesLoop = setInterval(
  129. this._chatModel.getMessages.bind(this._chatModel, currentUser, "update"),
  130. 10_000
  131. );
  132. this._model.notify();
  133. }
  134. }
  135. private _promptHandler(vm: ActiveUserViewModel) {
  136. // vms.filter(v => v.userName == vm.userName).map(v => v.userName = vm.userName)
  137. // log.debug(vms);
  138. if (vm.lastMessageTime) {
  139. this._newMessagesLoop = setInterval(
  140. this._chatModel.getMessages.bind(this._chatModel, vm, "update"),
  141. 10_000
  142. );
  143. this._model.notify();
  144. }
  145. }
  146. private _addSearchButtonEL() {
  147. this._userSearchButton.addEventListener("submit", (e) => {
  148. e.preventDefault();
  149. // log.trace(temp);
  150. const searchTerm = this._userSearchInputElement.value;
  151. if (searchTerm.length > 0) {
  152. log.debug("search term value = " + searchTerm);
  153. const list = this._model.activeUsersList;
  154. log.debug("active users");
  155. log.debug(list);
  156. if (!list) {
  157. log.error("Users list is null");
  158. return;
  159. }
  160. let searchResult = this._searchService.search(list, searchTerm);
  161. this.update({ data: searchResult, op: "" });
  162. log.debug(searchResult);
  163. }
  164. else {
  165. this._notificationService.error("Please enter a name")
  166. }
  167. });
  168. }
  169. private _addSearchInputEL() {
  170. this._userSearchInputElement.addEventListener("input", (e) => {
  171. e.preventDefault();
  172. if (this._userSearchInputElement.value.length < 2) {
  173. this._userSearchCancelButton.hidden = false;
  174. }
  175. });
  176. }
  177. private _addSearchCancelEL() {
  178. this._userSearchCancelButton.addEventListener("click", (e) => {
  179. e.preventDefault();
  180. this._userSearchInputElement.value = "";
  181. this._userSearchCancelButton.hidden = true;
  182. let list = this._model.activeUsersList;
  183. this.update({ data: list, op: "" });
  184. });
  185. }
  186. }