diff --git a/chatto/src/main/javascript/ts/src/model/ChatModel.ts b/chatto/src/main/javascript/ts/src/model/ChatModel.ts index 6b082a8..abff836 100644 --- a/chatto/src/main/javascript/ts/src/model/ChatModel.ts +++ b/chatto/src/main/javascript/ts/src/model/ChatModel.ts @@ -5,6 +5,7 @@ import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel"; import { ChatModelHelper } from "./ChatModelHelper"; import log = require('loglevel'); import { ObserverData } from "../observe/ObserverData"; +import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel"; interface Params { userName: string, @@ -44,10 +45,11 @@ export class ChatModel implements Subject { private storeUserMessages(username: string, messages: ChatMessageViewModel[], op: string) { switch (op) { - case "clear": this._messagesMap.set(username, []); + case "clear": this._messagesMap.set(username, []); break; case "page": this._messagesMap.set(username, messages.concat(this.getStoredUserMessages(username))); break; // case "page": this._messagesMap.set(username, messages); case "new": this._messagesMap.set(username, this.getStoredUserMessages(username).concat(messages)); break; + case "update": this._messagesMap.set(username, this.getStoredUserMessages(username).concat(messages)); break; default: new Error("Invalid option"); } @@ -86,6 +88,12 @@ export class ChatModel implements Subject { observer.update(od); } } break; + case "update": { + const od: ObserverData = { data: p.data, op: p.op } + for (const observer of this._observers) { + observer.update(od); + } + } break; default: { log.error("error") } } } @@ -100,28 +108,31 @@ export class ChatModel implements Subject { this.notify({ userName: "", data: [], op: "clear" }) } - public async getMessages(contactName: string, passphrase: string, lastMessageTime: string | null, op: string): Promise { - if (this._messagePageMap.get(contactName) == null) - this._messagePageMap.set(contactName, 0); + public async getMessages(vm: ActiveUserViewModel, op: string): Promise { + if (this._messagePageMap.get(vm.userName!) == null) + this._messagePageMap.set(vm.userName!, 0); - const pageNumber = this._messagePageMap.get(contactName) - const cVMs = await this._chatModelHelper.getMessages(contactName, passphrase, pageNumber!, lastMessageTime, op); + const pageNumber = this._messagePageMap.get(vm.userName!) + const cVMs = await this._chatModelHelper.getMessages(vm.userName!, vm.passphrase, pageNumber!, vm.lastMessageTime!, op); if (cVMs != null) { log.info('Subject: My state has just changed') - const existingMessages = this.getStoredUserMessages(contactName); + const existingMessages = this.getStoredUserMessages(vm.userName!); log.debug('existing message:') log.debug(existingMessages); log.debug('new messages:') log.debug(cVMs); - this.storeUserMessages(contactName, cVMs, op); - this.notify({ userName: contactName, data: cVMs, op: op }); + this.storeUserMessages(vm.userName!, cVMs, op); + this.notify({ userName: vm.userName!, data: cVMs, op: op }); } else { log.error('Messages were null'); } - if (cVMs.length != 0) { - this._messagePageMap.set(contactName, this._messagePageMap.get(contactName)! + 1); + if (cVMs.length != 0 && op == "update") vm.lastMessageTime = cVMs[cVMs.length - 1].messageTime; + + if (cVMs.length != 0 && op != "update") { + + this._messagePageMap.set(vm.userName!, this._messagePageMap.get(vm.userName!)! + 1); } return cVMs; diff --git a/chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts b/chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts index 7eebf3b..e3b03ea 100644 --- a/chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts +++ b/chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts @@ -18,23 +18,32 @@ export class ChatModelHelper { } - public async getMessages(userName: string, passphrase: string, page: number | null, lastMessageTime: string | null, op: string): Promise { - switch (lastMessageTime) { - case null: { - const data: ChatMessageDTO[] = await this._getPaginatedMessagesAjax(userName, page!); - const cVMs = Promise.all(data.map(vm => this._toChatMessageVMAsync(vm, passphrase)).reverse()); - return cVMs; + public async getMessages(userName: string, passphrase: string, page: number | null, lastMessageTime: Date | null, op: string): Promise { + switch (op) { + case "page": { + const data: ChatMessageDTO[] = await this._getPaginatedMessagesAjax(userName, page!); + const cVMs = Promise.all(data.map(vm => this._toChatMessageVMAsync(vm, passphrase)).reverse()); + return cVMs; } - default: { + case "new": { + const data: ChatMessageDTO[] = await this._getPaginatedMessagesAjax(userName, page!); + const cVMs = Promise.all(data.map(vm => this._toChatMessageVMAsync(vm, passphrase)).reverse()); + return cVMs; + } + case "update": { const data: ChatMessageDTO[] = await this._getNewMessagesAjax(userName, lastMessageTime!); return data.map(vm => this._toChatMessageVM(vm, passphrase)); } + default: { + log.error("Invalid operation"); + return Array(); + } } } public async isPassphraseValid(passphrase: string, userName: string): Promise { const messages: ChatMessageDTO[] = await this._getPaginatedMessagesAjax(userName, 0); - if(messages.length === 0) return true; + if (messages.length === 0) return true; try { this._encryptionService.decrypt(passphrase, messages[0].messageCipher) } catch (error) { @@ -102,17 +111,30 @@ export class ChatModelHelper { return null; } const data: Promise = await response.json(); - return data; + function func(data: any) { + const d1 = data.map((d: any) => { + if (d.messageTime == null) + return null; + + d.messageTime = new Date(d.messageTime) + return d; + }); + return d1; + } + const data2 = func(data) + return data2; } - private async _getNewMessagesAjax(toUser: string, lastMessageTimeStamp: string): Promise { + private async _getMessagesAjax(toUser: string, lastMessageTimeStamp: Date): Promise { const headers = new Headers(); if (JsonAPI.authToken == null) { log.error("authToken null"); return; }; headers.append('X-AUTH-TOKEN', JsonAPI.authToken); - const response = await fetch(`${JsonAPI.CHAT_MESSAGES_GET}/${toUser}/${lastMessageTimeStamp}`, { + // const url = Sprintf(JsonAPI.CHAT_MESSAGES_GET, toUser, page, 5); + // log.debug(url) + const response = await fetch(`/api/chat/get/messages/${toUser}`, { method: 'GET', headers: headers }); @@ -121,6 +143,52 @@ export class ChatModelHelper { return null; } const data: Promise = await response.json(); - return data; + function func(data: any) { + const d1 = data.map((d: any) => { + if (d.messageTime == null) + return null; + + d.messageTime = new Date(d.messageTime) + return d; + }); + return d1; + } + const data2 = func(data) + return data2; + } + + private async _getNewMessagesAjax(toUser: string, lastMessageTimeStamp: Date): Promise { + const headers = new Headers(); + if (JsonAPI.authToken == null) { + log.error("authToken null"); + return; + }; + headers.append('X-AUTH-TOKEN', JsonAPI.authToken); + // const url = Sprintf(JsonAPI.CHAT_MESSAGES_GET, toUser, page, 5); + // log.debug(url) + log.debug(lastMessageTimeStamp); + log.debug(lastMessageTimeStamp.toISOString()) + const response = await fetch(`/api/chat/get/messages/${toUser}/${lastMessageTimeStamp.toISOString()}`, { + method: 'GET', + headers: headers + }); + log.debug(response.clone()); + if (fetchErrorHandler(response.clone(), this._notificationService)) { + return null; + } + const data: Promise = await response.json(); + function func(data: any) { + const d1 = data.map((d: any) => { + if (d.messageTime == null) + return null; + + d.messageTime = new Date(d.messageTime) + return d; + }); + return d1; + } + const data2 = func(data) + log.debug("LOOK HERE NOW ", data, data2) + return data2; } } \ No newline at end of file diff --git a/chatto/src/main/javascript/ts/src/service/AlertifyNotificationService.ts b/chatto/src/main/javascript/ts/src/service/AlertifyNotificationService.ts index cf92d56..eca65d3 100644 --- a/chatto/src/main/javascript/ts/src/service/AlertifyNotificationService.ts +++ b/chatto/src/main/javascript/ts/src/service/AlertifyNotificationService.ts @@ -4,6 +4,7 @@ import * as alertify from "alertifyjs"; import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel"; import log = require("loglevel"); import bootbox = require("bootbox") +import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel"; export class AlertifyNotificationService implements NotificationService { private readonly _alertify = alertify; @@ -28,9 +29,8 @@ export class AlertifyNotificationService implements NotificationService { message(message: string): void { this._alertify.message(message); } - passphrasePrompt(vm: ActiveUserViewModel, vms: ActiveUserViewModel[], cb1: (contactName: string, passphrase: string, - lastMessageTime: string | null, op: string) => void, - cb2: () => any, cb3: (...x: any) => any): void { + passphrasePrompt(vm: ActiveUserViewModel, vms: ActiveUserViewModel[], cb1: (v: ActiveUserViewModel, op: string) => ChatMessageViewModel[], + cb2: (...x: any) => any, cb3: (...x: any) => any): void { // alertify.myprompt || alertify.dialog('myprompt', function () { @@ -100,10 +100,19 @@ export class AlertifyNotificationService implements NotificationService { log.error("invalid password"); return; } - cb1(vm.userName!, result, null, "new"); vm.unlocked = true - vms.filter(v => v.userName == vm.userName).map(v => { v.passphrase = result; v.unlocked = true }) - cb2(); + vm.passphrase = result; + const chatMessages: ChatMessageViewModel[] = await cb1(vm, "new"); + log.debug("here", chatMessages) + + vms.filter(v => v.userName == vm.userName).map(v => { + v.passphrase = result; + v.unlocked = true; + v.lastMessageTime = new Date(chatMessages[chatMessages.length - 1].messageTime); + }) + log.debug("last message time = ", chatMessages[chatMessages.length - 1].messageTime) + vm.lastMessageTime = new Date(chatMessages[chatMessages.length - 1].messageTime); + cb2(vm, vms); log.debug(vm) log.debug(vms) } @@ -111,7 +120,11 @@ export class AlertifyNotificationService implements NotificationService { }); } else { - cb1(vm.userName!, vm.passphrase, null, "new"); + cb1(vm, "new"); } } -} \ No newline at end of file +} + +// log.debug("LOOK HERE",vm.lastMessageTime) +// setInterval(this._chatModel.getMessages.bind(this._chatModel, +// vm.userName!, vm.passphrase, vm.lastMessageTime!, "new"), 2000); \ No newline at end of file diff --git a/chatto/src/main/javascript/ts/src/view/ChatView.ts b/chatto/src/main/javascript/ts/src/view/ChatView.ts index 7dd4269..aec8137 100644 --- a/chatto/src/main/javascript/ts/src/view/ChatView.ts +++ b/chatto/src/main/javascript/ts/src/view/ChatView.ts @@ -76,6 +76,30 @@ export class ChatView implements Observer { scrollTop: $(this._messageContainer)[0].scrollHeight }, 1500); } break; + case "update": { + const rev: ChatMessageViewModel[] = Object.create(cd.data) + // rev.reverse(); + let arr: string[] = []; + rev.forEach((vm: ChatMessageViewModel) => { + const vmTemp: ChatMessageViewModel = { ...vm }; + vmTemp.message = this._markdownService.render(vm.message); + /** Very Important!!! + * Sanitizing HTML before displaying on webpage to prevent XSS attacks!! + */ + let rendered; + if (vmTemp.fromUser == JsonAPI.principleName) { + rendered = DOMPurify.sanitize(this._messageSendTemplate(vmTemp)); + + } + else { + rendered = DOMPurify.sanitize(this._messageReceiveTemplate(vmTemp)); + } + $(this._messageContainer).append(rendered); + }); + // $(this._messageContainer).stop().animate({ + // scrollTop: $(this._messageContainer)[0].scrollHeight + // }, 1500); + } break; default: { const rev: ChatMessageViewModel[] = Object.create(cd.data) rev.reverse(); @@ -137,8 +161,10 @@ export class ChatView implements Observer { const chatInput = document.getElementById('chatInput') as HTMLInputElement; - const passphraseInput = document.getElementById('passphrase') as HTMLInputElement; - const passphrase = this._userModel.activeUsersList.find(u => u.userName == JsonAPI.contactName)?.passphrase + const vm = this._userModel.activeUsersList.find(u => u.userName == JsonAPI.contactName); + // new Date(). + vm!.lastMessageTime = new Date(); + const passphrase = vm?.passphrase if (chatInput.value == '' || chatInput.value == null) { this._notificationService.error("Please enter a message"); @@ -155,13 +181,13 @@ export class ChatView implements Observer { const messageContent = chatInput.value; const msgTime = new Date(); const context: ChatMessageViewModel = { - fromUser: JsonAPI.principleName || "error", + fromUser: JsonAPI.principleName || "error", toUser: "", message: messageContent, messageTime: msgTime }; - this.update({data: new Array(context), op: "new"}) + this.update({ data: new Array(context), op: "new" }) let messageCipher: MessageCipherDTO = this._encryptionService.encrypt(passphrase!, messageContent) let chatMessageDTO = { @@ -200,9 +226,10 @@ export class ChatView implements Observer { if ($(this._messageContainer).scrollTop() == 0 && $(this._messageContainer).html() != "") { let currentMsg = $('.msg:first'); log.debug('Reached top') - let ab = this._userModel.activeUsersList.find(u => u.userName == JsonAPI.contactName) + const vm = this._userModel.activeUsersList.find(u => u.userName == JsonAPI.contactName) if (JsonAPI.contactName != null) - this._chatModel.getMessages(JsonAPI.contactName, ab!.passphrase, null, "page").then(() => { + + this._chatModel.getMessages(vm!, "page").then(() => { if (currentMsg != null) { // log.debug(currentMsg.offset()!.top) $(this._messageContainer).scrollTop(currentMsg.position().top - $('.msg').position()!.top) diff --git a/chatto/src/main/javascript/ts/src/view/UserView.ts b/chatto/src/main/javascript/ts/src/view/UserView.ts index ec5f75a..a4df4a6 100644 --- a/chatto/src/main/javascript/ts/src/view/UserView.ts +++ b/chatto/src/main/javascript/ts/src/view/UserView.ts @@ -23,6 +23,7 @@ export class UserView implements Observer { private readonly _userContactOnlineTemplate: Handlebars.TemplateDelegate; private readonly _userContactOfflineTemplate: Handlebars.TemplateDelegate; private readonly _notificationService: NotificationService; + private _newMessagesLoop: any; constructor(deps: UserViewDeps) { this._model = deps.model; @@ -65,6 +66,7 @@ export class UserView implements Observer { private _userCallBack(el: Element): void { this._chatModel.clear(); + clearInterval(this._newMessagesLoop); let current = document.getElementsByClassName('user-box active'); let passphrase: string = ''; @@ -122,12 +124,19 @@ export class UserView implements Observer { this._promptHandler.bind(this), this._chatModel.isPassphraseValid.bind(this._chatModel)); // this._chatModel.getMessages(userName, vm.passphrase, null, "new"); el.className += " active"; + log.debug("loop", this._newMessagesLoop) + if (this._newMessagesLoop != null) { + this._newMessagesLoop = setInterval(this._chatModel.getMessages.bind(this._chatModel, + vm, "update"), 2000); + } } private _promptHandler(vm: ActiveUserViewModel, vms: ActiveUserViewModel[]) { // vms.filter(v => v.userName == vm.userName).map(v => v.userName = vm.userName) log.debug(vms) this._model.notify(); + this._newMessagesLoop = setInterval(this._chatModel.getMessages.bind(this._chatModel, + vm, "update"), 2000); } private _addSearchButtonEL() { diff --git a/chatto/src/main/javascript/ts/src/viewmodel/ActiveUserViewModel.ts b/chatto/src/main/javascript/ts/src/viewmodel/ActiveUserViewModel.ts index 555a62e..34cfb65 100644 --- a/chatto/src/main/javascript/ts/src/viewmodel/ActiveUserViewModel.ts +++ b/chatto/src/main/javascript/ts/src/viewmodel/ActiveUserViewModel.ts @@ -4,4 +4,5 @@ export class ActiveUserViewModel { unlocked: boolean = false; passphrase: string = ""; lastActive: Date | undefined; + lastMessageTime: Date | undefined; } \ No newline at end of file