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.

222 lines
8.7 KiB

4 years ago
4 years ago
4 years ago
  1. import * as DOMPurify from 'dompurify';
  2. import * as log from 'loglevel';
  3. import { ChatMessageDTO } from "../dto/ChatMessageDTO";
  4. import { MessageCipherDTO } from "../dto/MessageCipherDTO";
  5. import { ChatModel } from "../model/ChatModel";
  6. import { Observer } from "../observe/Observer";
  7. import { EncryptionService } from "../service/EncryptionService";
  8. import { MarkDownService } from "../service/MarkDownService";
  9. import { JsonAPI } from "../singleton/JsonAPI";
  10. import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
  11. import { ChatViewDeps } from "./ChatViewDeps";
  12. import { fetchHandler } from "./FetchHandler";
  13. import { ObserverData } from '../observe/ObserverData';
  14. import { NotificationService } from '../service/NotificationService';
  15. export class ChatView implements Observer<ChatMessageViewModel> {
  16. private readonly _chatModel: ChatModel;
  17. private readonly _messageContainer: HTMLElement;
  18. private readonly _messageSendTemplate: Handlebars.TemplateDelegate<ChatMessageViewModel>;
  19. private readonly _messageReceiveTemplate: Handlebars.TemplateDelegate<ChatMessageViewModel>;
  20. private readonly _markdownService: MarkDownService;
  21. private readonly _encryptionService: EncryptionService;
  22. private readonly _notificationService: NotificationService;
  23. constructor(deps: ChatViewDeps) {
  24. this._messageContainer = deps.messageContainer;
  25. this._chatModel = deps.chatModel;
  26. this._messageSendTemplate = deps.messageSendTemplate;
  27. this._messageReceiveTemplate = deps.messageReceiveTemplate;
  28. this._markdownService = deps.markdownService;
  29. this._encryptionService = deps.encryptionService;
  30. this._notificationService = deps.notificationService;
  31. this._initEventListeners();
  32. $(document).ready(function () {
  33. $('#action_menu_btn').click(function () {
  34. $('.action_menu').toggle();
  35. });
  36. });
  37. this._chatMessagePageLoadAjax();
  38. }
  39. update(cd: ObserverData<ChatMessageViewModel>): void {
  40. log.info('ChatView: updating view');
  41. switch (cd.op) {
  42. case "clear": {
  43. $(this._messageContainer).html("");
  44. } break;
  45. case "new": {
  46. const rev: ChatMessageViewModel[] = Object.create(cd.data)
  47. // rev.reverse();
  48. let arr: string[] = [];
  49. rev.forEach((vm: ChatMessageViewModel) => {
  50. const vmTemp: ChatMessageViewModel = { ...vm };
  51. vmTemp.message = this._markdownService.render(vm.message);
  52. /** Very Important!!!
  53. * Sanitizing HTML before displaying on webpage to prevent XSS attacks!!
  54. */
  55. let rendered;
  56. if (vmTemp.fromUser == JsonAPI.principleName) {
  57. rendered = DOMPurify.sanitize(this._messageSendTemplate(vmTemp));
  58. }
  59. else {
  60. rendered = DOMPurify.sanitize(this._messageReceiveTemplate(vmTemp));
  61. }
  62. $(this._messageContainer).append(rendered);
  63. });
  64. $(this._messageContainer).stop().animate({
  65. scrollTop: $(this._messageContainer)[0].scrollHeight
  66. }, 1500);
  67. } break;
  68. default: {
  69. const rev: ChatMessageViewModel[] = Object.create(cd.data)
  70. rev.reverse();
  71. let arr: string[] = [];
  72. rev.forEach((vm: ChatMessageViewModel) => {
  73. const vmTemp: ChatMessageViewModel = { ...vm };
  74. vmTemp.message = this._markdownService.render(vm.message);
  75. /** Very Important!!!
  76. * Sanitizing HTML before displaying on webpage to prevent XSS attacks!!
  77. */
  78. let rendered;
  79. if (vmTemp.fromUser == JsonAPI.principleName) {
  80. rendered = DOMPurify.sanitize(this._messageSendTemplate(vmTemp));
  81. }
  82. else {
  83. rendered = DOMPurify.sanitize(this._messageReceiveTemplate(vmTemp));
  84. }
  85. $(this._messageContainer).prepend(rendered);
  86. });
  87. }
  88. }
  89. }
  90. private _initEventListeners(): void {
  91. this._addChatFormEL();
  92. }
  93. private _addChatFormEL() {
  94. const chatForm = document.getElementById('chatMessageForm') as HTMLSelectElement;
  95. if (chatForm == null) {
  96. log.error("Chat form is null");
  97. }
  98. else {
  99. chatForm.addEventListener('submit', (e) => this._createChatMessageDTO(e, chatForm))
  100. }
  101. }
  102. private _createChatMessageDTO(e: Event, chatForm: HTMLSelectElement): void {
  103. e.preventDefault();
  104. let contactName = JsonAPI.contactName;
  105. if (contactName == null) {
  106. log.error("Contact name is null");
  107. return;
  108. }
  109. if (!chatForm.checkValidity()) {
  110. log.error("Form is not valid");
  111. chatForm.classList.add('was-validated');
  112. return;
  113. }
  114. chatForm.classList.add('was-validated');
  115. const chatInput = document.getElementById('chatInput') as HTMLInputElement;
  116. const passphraseInput = document.getElementById('passphrase') as HTMLInputElement;
  117. if (chatInput.value == '' || chatInput.value == null) {
  118. this._notificationService.error("Please enter a message");
  119. log.error("Chat input is null.");
  120. return;
  121. }
  122. if (passphraseInput.value == '' || passphraseInput.value == null) {
  123. this._notificationService.error("Please enter a passphrase");
  124. log.error("Passphrase is null.");
  125. return;
  126. }
  127. const messageContent = chatInput.value;
  128. const msgTime = new Date();
  129. const context: ChatMessageViewModel = {
  130. fromUser: JsonAPI.principleName || "error",
  131. toUser: "",
  132. message: messageContent,
  133. messageTime: msgTime
  134. };
  135. this.update({data: new Array(context), op: "new"})
  136. let messageCipher: MessageCipherDTO = this._encryptionService.encrypt(passphraseInput.value, messageContent)
  137. let chatMessageDTO = {
  138. "fromUser": JsonAPI.principleName || "",
  139. "toUser": contactName,
  140. "messageCipher": messageCipher,
  141. messageTime: msgTime
  142. }
  143. this._sendMessageAJAX(chatMessageDTO);
  144. }
  145. private _sendMessageAJAX(chatMessageDTO: ChatMessageDTO): void {
  146. let headers = new Headers();
  147. // console.log("Token = " + btoa("hmm" + ":" + "hmm"))
  148. // headers.append('Accept','application/json')
  149. headers.append('Content-Type', 'application/json');
  150. // headers.append('Authorization', basicAuthToken);
  151. // @ts-ignore
  152. headers.append('X-AUTH-TOKEN', JsonAPI.authToken);
  153. fetch(JsonAPI.MESSAGE_POST, {
  154. method: 'POST',
  155. headers: headers,
  156. body: JSON.stringify(chatMessageDTO)
  157. })
  158. .then(response => {
  159. log.debug(response);
  160. return response.clone();
  161. })
  162. .then(response => fetchHandler(response, this._notificationService));
  163. }
  164. private _chatMessagePageLoadAjax() {
  165. this._messageContainer.addEventListener('scroll', (e) => {
  166. if ($(this._messageContainer).scrollTop() == 0 && $(this._messageContainer).html() != "") {
  167. let currentMsg = $('.msg:first');
  168. log.debug('Reached top')
  169. let passphrase: string;
  170. let passphraseInput = document.getElementById('passphrase') as HTMLInputElement;
  171. if (passphraseInput == null) {
  172. log.error('passphraseInput element reference is null');
  173. return;
  174. }
  175. passphrase = passphraseInput.value
  176. if (passphrase == '' || passphrase == null) {
  177. // alert('Please input passphrase')
  178. // alertify.error('Please enter a passphrase');
  179. log.error('passphrase is empty or null');
  180. return;
  181. }
  182. if (JsonAPI.contactName != null)
  183. this._chatModel.getMessages(JsonAPI.contactName, passphrase, null, "page").then(() => {
  184. if (currentMsg != null) {
  185. // log.debug(currentMsg.offset()!.top)
  186. $(this._messageContainer).scrollTop(currentMsg.position().top - $('.msg').position()!.top)
  187. }
  188. });
  189. }
  190. })
  191. }
  192. }