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.

243 lines
7.5 KiB

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. import { UserModel } from "../model/UserModel";
  16. export class ChatView implements Observer<ChatMessageViewModel> {
  17. private readonly _chatModel: ChatModel;
  18. private readonly _messageContainer: HTMLElement;
  19. private readonly _messageSendTemplate: Handlebars.TemplateDelegate<
  20. ChatMessageViewModel
  21. >;
  22. private readonly _messageReceiveTemplate: Handlebars.TemplateDelegate<
  23. ChatMessageViewModel
  24. >;
  25. private readonly _markdownService: MarkDownService;
  26. private readonly _encryptionService: EncryptionService;
  27. private readonly _notificationService: NotificationService;
  28. private readonly _userModel: UserModel;
  29. constructor(deps: ChatViewDeps) {
  30. this._messageContainer = deps.messageContainer;
  31. this._chatModel = deps.chatModel;
  32. this._messageSendTemplate = deps.messageSendTemplate;
  33. this._messageReceiveTemplate = deps.messageReceiveTemplate;
  34. this._markdownService = deps.markdownService;
  35. this._encryptionService = deps.encryptionService;
  36. this._notificationService = deps.notificationService;
  37. this._userModel = deps.userModel;
  38. this._initEventListeners();
  39. $(document).ready(function () {
  40. $("#action_menu_btn").click(function () {
  41. $(".action_menu").toggle();
  42. });
  43. });
  44. this._chatMessagePageLoadAjax();
  45. }
  46. update(cd: ObserverData<ChatMessageViewModel>): void {
  47. log.info("ChatView: updating view");
  48. switch (cd.op) {
  49. case "clear":
  50. {
  51. $(this._messageContainer).html("");
  52. }
  53. break;
  54. case "new":
  55. {
  56. cd.data.forEach((vm: ChatMessageViewModel) => {
  57. let rendered = this.renderMessage(vm);
  58. $(this._messageContainer).append(rendered);
  59. });
  60. $(this._messageContainer)
  61. .stop()
  62. .animate(
  63. {
  64. scrollTop: $(this._messageContainer)[0].scrollHeight,
  65. },
  66. 1500
  67. );
  68. }
  69. break;
  70. case "update":
  71. {
  72. cd.data.forEach((vm: ChatMessageViewModel) => {
  73. let rendered = this.renderMessage(vm);
  74. $(this._messageContainer).append(rendered);
  75. });
  76. // if (cd.data.length > 0) {
  77. // this._userModel.notify();
  78. // }
  79. $(this._messageContainer)
  80. .stop()
  81. .animate(
  82. {
  83. scrollTop: $(this._messageContainer)[0].scrollHeight,
  84. },
  85. 1500
  86. );
  87. }
  88. break;
  89. case "page":
  90. {
  91. {
  92. const rev: ChatMessageViewModel[] = Object.create(cd.data);
  93. rev.reverse();
  94. rev.forEach((vm: ChatMessageViewModel) => {
  95. let rendered = this.renderMessage(vm);
  96. $(this._messageContainer).prepend(rendered);
  97. });
  98. }
  99. }
  100. break;
  101. default:
  102. new Error("Invalid option");
  103. }
  104. }
  105. private renderMessage(vm: ChatMessageViewModel): string {
  106. const vmTemp: ChatMessageViewModel = { ...vm };
  107. vmTemp.message = this._markdownService.render(vm.message);
  108. switch (vmTemp.fromUser) {
  109. case JsonAPI.principleName:
  110. return DOMPurify.sanitize(this._messageSendTemplate(vmTemp));
  111. default:
  112. return DOMPurify.sanitize(this._messageReceiveTemplate(vmTemp));
  113. }
  114. }
  115. private _initEventListeners(): void {
  116. this._addChatFormEL();
  117. }
  118. private _addChatFormEL() {
  119. const chatForm = document.getElementById(
  120. "chatMessageForm"
  121. ) as HTMLSelectElement;
  122. if (chatForm == null) {
  123. log.error("Chat form is null");
  124. } else {
  125. chatForm.addEventListener("submit", (e) =>
  126. this._createChatMessageDTO(e, chatForm)
  127. );
  128. }
  129. }
  130. private _createChatMessageDTO(e: Event, chatForm: HTMLSelectElement): void {
  131. e.preventDefault();
  132. let contactName = JsonAPI.contactName;
  133. if (!chatForm.checkValidity()) {
  134. log.error("Form is not valid");
  135. chatForm.classList.add("was-validated");
  136. return;
  137. }
  138. chatForm.classList.add("was-validated");
  139. const chatInput = document.getElementById("chatInput") as HTMLInputElement;
  140. const vm = this._userModel.activeUsersList.find(
  141. (u) => u.userName == JsonAPI.contactName
  142. );
  143. // new Date().
  144. vm!.lastMessageTime = new Date();
  145. const passphrase = vm?.passphrase;
  146. if (chatInput.value == "") {
  147. this._notificationService.error("Please enter a message");
  148. return;
  149. }
  150. // if (passphraseInput.value == '' || passphraseInput.value == null) {
  151. // this._notificationService.error("Please enter a passphrase");
  152. // log.error("Passphrase is null.");
  153. // return;
  154. // }
  155. const messageContent = chatInput.value;
  156. vm!.lastMessageText = messageContent.slice(0, 15) + "...";
  157. const msgTime = new Date();
  158. const context: ChatMessageViewModel = {
  159. fromUser: JsonAPI.principleName,
  160. toUser: contactName,
  161. message: messageContent,
  162. messageTime: msgTime,
  163. };
  164. this.update({ data: new Array(context), op: "new" });
  165. this._userModel.updateLastActive(contactName, msgTime)
  166. this._userModel.notify();
  167. let messageCipher: MessageCipherDTO = this._encryptionService.encrypt(
  168. passphrase!,
  169. messageContent
  170. );
  171. let chatMessageDTO = {
  172. fromUser: JsonAPI.principleName,
  173. toUser: contactName,
  174. messageCipher: messageCipher,
  175. messageTime: msgTime.toISOString(),
  176. };
  177. this._sendMessageAJAX(chatMessageDTO);
  178. }
  179. private _sendMessageAJAX(chatMessageDTO: any): void {
  180. let headers = new Headers();
  181. // console.log("Token = " + btoa("hmm" + ":" + "hmm"))
  182. // headers.append('Accept','application/json')
  183. headers.append("Content-Type", "application/json");
  184. // headers.append('Authorization', basicAuthToken);
  185. // @ts-ignore
  186. headers.append("X-AUTH-TOKEN", JsonAPI.authToken);
  187. fetch(JsonAPI.MESSAGE_POST, {
  188. method: "POST",
  189. headers: headers,
  190. body: JSON.stringify(chatMessageDTO),
  191. })
  192. .then((response) => {
  193. log.debug(response);
  194. return response.clone();
  195. })
  196. .then((response) => fetchHandler(response, this._notificationService));
  197. }
  198. private _chatMessagePageLoadAjax() {
  199. this._messageContainer.addEventListener("scroll", (e) => {
  200. if (
  201. $(this._messageContainer).scrollTop() == 0 &&
  202. $(this._messageContainer).html() != ""
  203. ) {
  204. let currentMsg = $(".msg:first");
  205. log.debug("Reached top");
  206. const vm = this._userModel.activeUsersList.find(
  207. (u) => u.userName == JsonAPI.contactName
  208. );
  209. this._chatModel.getMessages(vm!, "page").then(() => {
  210. if (currentMsg != null) {
  211. // log.debug(currentMsg.offset()!.top)
  212. $(this._messageContainer).scrollTop(
  213. currentMsg.position().top - $(".msg").position()!.top
  214. );
  215. }
  216. });
  217. }
  218. });
  219. }
  220. }