Browse Source

Chat area is cleared when changing user

Also frontend code has een refactored significantly
master
Rohan Sircar 4 years ago
parent
commit
f34a5524fd
  1. 1
      chatto/src/main/java/org/ros/chatto/config/DataSourceConfig.java
  2. 1
      chatto/src/main/java/org/ros/chatto/service/DBInitializerService.java
  3. 2
      chatto/src/main/javascript/package.json
  4. 134
      chatto/src/main/javascript/ts/src/main.ts
  5. 4
      chatto/src/main/javascript/ts/src/model/AbstractModel.ts
  6. 126
      chatto/src/main/javascript/ts/src/model/ChatModel.ts
  7. 54
      chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts
  8. 8
      chatto/src/main/javascript/ts/src/model/FetchErrorHandler.ts
  9. 82
      chatto/src/main/javascript/ts/src/model/UserModel.ts
  10. 9
      chatto/src/main/javascript/ts/src/observe/Observable.ts
  11. 5
      chatto/src/main/javascript/ts/src/observe/Observer.ts
  12. 4
      chatto/src/main/javascript/ts/src/observe/ObserverData.ts
  13. 2
      chatto/src/main/javascript/ts/src/view/AbstractView.ts
  14. 135
      chatto/src/main/javascript/ts/src/view/ChatView.ts
  15. 3
      chatto/src/main/javascript/ts/src/view/ChatViewDeps.ts
  16. 4
      chatto/src/main/javascript/ts/src/view/FetchHandler.ts
  17. 42
      chatto/src/main/javascript/ts/src/view/UserView.ts

1
chatto/src/main/java/org/ros/chatto/config/DataSourceConfig.java

@ -9,7 +9,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Configuration

1
chatto/src/main/java/org/ros/chatto/service/DBInitializerService.java

@ -3,7 +3,6 @@ package org.ros.chatto.service;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.persistence.EntityManager;

2
chatto/src/main/javascript/package.json

@ -45,6 +45,6 @@
"alertifyjs": "global:alertify"
},
"scripts": {
"watch": "watchify ts/src/main.ts -p [ tsify --target es2017 --noImplicitAny ] --debug -o ../resources/static/js/bundle.js"
"watch": "watchify ts/src/main.ts -p [ tsify --target ES6 --noImplicitAny ] --debug -o ../resources/static/js/bundle.js"
}
}

134
chatto/src/main/javascript/ts/src/main.ts

@ -1,51 +1,58 @@
import { Controller } from "./controller/AbstractController";
import { UserModel } from "./model/UserModel"
import { Model } from "./model/AbstractModel";
import { View } from "./view/AbstractView";
import { UserView } from "./view/UserView";
import { UserController } from "./controller/UserController";
import { ModelFactory } from "./model/ModelFactory";
import { ActiveUserViewModel } from "./viewmodel/ActiveUserViewModel";
import { ChatMessageViewModel } from "./viewmodel/ChatMessageViewModel";
import { Builder } from "builder-pattern";
import * as Handlebars from "handlebars";
import { ChatModel } from "./model/ChatModel";
import { ChatView } from "./view/ChatView";
import { ChatController } from "./controller/ChatController";
import { JsonAPI } from "./singleton/JsonAPI";
import * as log from 'loglevel';
import { EncryptionService } from "./service/EncryptionService";
import { SJCLEncryptionService } from "./service/SJCLEncryptionService";
import { MessageCipherDTO } from "./dto/MessageCipherDTO";
import { SearchService } from "./service/SearchService";
import { ChatController } from "./controller/ChatController";
import { UserController } from "./controller/UserController";
import { ChatModel } from "./model/ChatModel";
import { ChatModelHelper } from "./model/ChatModelHelper";
import { UserModel } from "./model/UserModel";
import { AlertifyNotificationService } from "./service/AlertifyNotificationService";
import { EncryptionServiceFactory } from "./service/EncryptionServiceFactory";
import { FuseSearchService } from "./service/FuseSearchService";
import { ChatMessageDTO } from "./dto/ChatMessageDTO";
import { MarkDownItMarkDownService } from "./service/MarkDownItMarkDownService";
import { NotificationService } from "./service/NotificationService";
import { AlertifyNotificationService } from "./service/AlertifyNotificationService";
import { Builder } from "builder-pattern";
// import "./SprintfTest.d.ts"
// import { sprintf } from "sprintf-js";
// import sprintf = require('sprintf-js');
import { SearchService } from "./service/SearchService";
import { TemplateFactory } from "./template/TemplateFactory";
import { UserViewDeps } from "./view/UserViewDeps";
import { ChatView } from "./view/ChatView";
import { ChatViewDeps } from "./view/ChatViewDeps";
import { MarkDownItMarkDownService } from "./service/MarkDownItMarkDownService";
import { Sprintf } from "./singleton/Sprintf";
import { EncryptionServiceFactory } from "./service/EncryptionServiceFactory";
import { UserView } from "./view/UserView";
import { UserViewDeps } from "./view/UserViewDeps";
import { ActiveUserViewModel } from "./viewmodel/ActiveUserViewModel";
log.setLevel("TRACE");
const usersListElement = document.getElementById('contacts-box');
const userSearchButton = document.getElementById('user-search');
const userSearchInputElement = document.getElementById('user-search-term') as HTMLInputElement;
const userSearchCancelButton = document.getElementById('user-search-cancel');
const chatArea = document.getElementById('chat-area-new');
const activeUserSearchService: SearchService<ActiveUserViewModel> = new FuseSearchService(["userName"]);
const ns: NotificationService = new AlertifyNotificationService();
const encryptionService = EncryptionServiceFactory.getEncryptionService();
log.setLevel("TRACE")
const chatModel = new ChatModel();
const userModel = new UserModel();
// const userModel = ModelFactory.createModel("USER");
const chatModelHelper = new ChatModelHelper(encryptionService, ns);
const chatModel = new ChatModel(chatModelHelper);
const cvDeps: ChatViewDeps = {
chatModel: chatModel,
// @ts-ignore: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.
messageContainer: chatArea,
messageSendTemplate: TemplateFactory.getTemplate('msg_container_send_template'),
messageReceiveTemplate: TemplateFactory.getTemplate('msg_container_template'),
markdownService: new MarkDownItMarkDownService,
encryptionService: encryptionService,
notificationService: ns
}
const chatView = new ChatView(cvDeps);
chatModel.attach(chatView);
const chatController = new ChatController(chatModel, chatView);
const userModel = new UserModel(ns);
const uvDeps: UserViewDeps = {
model: userModel,
chatModel: chatModel,
@ -61,82 +68,21 @@ const uvDeps: UserViewDeps = {
userContactOfflineTemplate: TemplateFactory.getTemplate('user-contact-offline-template')
}
const userView = new UserView(uvDeps);
// console.log(userBox);
userModel.attach(userView);
// userView.model
const userController = new UserController(userModel, userView);
// userController.test();
// userModel.someBusinessMethod(activeUsersMock);
log.info("hello");
const chatArea = document.getElementById('chat-area-new');
// @ts-ignore: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.
const cvDeps: ChatViewDeps = {
chatModel: chatModel,
// @ts-ignore: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.
messageContainer: chatArea,
messageSendTemplate: TemplateFactory.getTemplate('msg_container_send_template'),
messageReceiveTemplate: TemplateFactory.getTemplate('msg_container_template'),
markdownService: new MarkDownItMarkDownService,
encryptionService: EncryptionServiceFactory.getEncryptionService()
}
const chatView = new ChatView(cvDeps);
chatModel.attach(chatView);
const chatController = new ChatController(chatModel, chatView);
function someFunc(vm: ActiveUserViewModel): void {
// log.info(vm);
// logger.info(vm)
}
userController.getActiveUsers();
log.info("test");
// someFunc(activeUserViewModelMock);
// @ts-ignore: Object is possibly 'null'.
var source = document.getElementById("msg_container_template").innerHTML;
var msgContainerTemplate = Handlebars.compile(source);
JsonAPI.ACTIVE_USERS_GET + 'aef';
const encryptionService: EncryptionService = EncryptionServiceFactory.getEncryptionService();
let messageCipherDTO: MessageCipherDTO = encryptionService.encrypt("password", "data");
console.log(encryptionService.decrypt("password", messageCipherDTO));
async function func(): Promise<void> {
const text = await encryptionService.decryptAsPromise("password", messageCipherDTO)
log.debug(text);
}
func();
Handlebars.registerHelper('avatar', function () {
return '<div class="img_cont_msg"> <img src="https://static.turbosquid.com/Preview/001292/481/WV/_D.jpg" class="rounded-circle user_img_msg"> </div>';
});
const testList: ChatMessageDTO[] = [];
// @ts-ignore
console.log()
log.info(Sprintf("test sprintf"))
// log.info(sprintf2.sprintf("test sprintf"))
const ns: NotificationService = new AlertifyNotificationService();
ns.success("Welcome");
// ns.errorWithDelay("Hmm very long error notif", 10);
const ss = FuseSearchService.getInstance<ActiveUserViewModel>([""]);
const test = Builder<UserViewDeps>().build();

4
chatto/src/main/javascript/ts/src/model/AbstractModel.ts

@ -1,7 +1,7 @@
import { Subject } from "../observe/Observable";
import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel";
export interface Model extends Subject{
export interface Model extends Subject<any> {
someBusinessMethod(data: Object): void;
}

126
chatto/src/main/javascript/ts/src/model/ChatModel.ts

@ -1,114 +1,126 @@
import { Subject } from "../observe/Observable";
import { Model } from "./AbstractModel";
import { Observer } from "../observe/Observer";
import { fetchErrorHandler } from "./FetchErrorHandler";
import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel";
import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import { JsonAPI } from "../singleton/JsonAPI";
import log = require('loglevel');
import { EncryptionService } from "../service/EncryptionService";
import { SJCLEncryptionService } from "../service/SJCLEncryptionService";
import { ChatMessageDTO } from "../dto/ChatMessageDTO";
import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import { ChatModelHelper } from "./ChatModelHelper";
import log = require('loglevel');
import { ObserverData } from "../observe/ObserverData";
export class ChatModel implements Subject {
interface Params {
userName: string,
data: ChatMessageViewModel[],
op: string
}
export class ChatModel implements Subject<ChatMessageViewModel> {
/**
* @type {Observer[]} List of subscribers. In real life, the list of
* subscribers can be stored more comprehensively (categorized by event
* type, etc.).
*/
private readonly _observers: Observer[] = [];
private state: ChatMessageViewModel[] | null;
private readonly _observers: Observer<ChatMessageViewModel>[] = [];
private readonly _messagePageMap: Map<string, number>;
private readonly _messagesMap: Map<string, ChatMessageViewModel[]>;
private readonly _chatModelHelper: ChatModelHelper;
constructor() {
this.state = null;
constructor(chatModelHelper: ChatModelHelper) {
this._messagePageMap = new Map();
this._messagesMap = new Map();
this._chatModelHelper = chatModelHelper;
}
/**
* The subscription management methods.
*/
public attach(observer: Observer): void {
console.log('Subject: Attached an observer.');
public attach(observer: Observer<ChatMessageViewModel>): void {
log.info('Subject: Attached an observer.');
this._observers.push(observer);
}
public detach(observer: Observer): void {
public detach(observer: Observer<ChatMessageViewModel>): void {
const observerIndex = this._observers.indexOf(observer);
this._observers.splice(observerIndex, 1);
console.log('Subject: Detached an observer.');
log.info('Subject: Detached an observer.');
}
private storeUserMessages(username: string, messages: ChatMessageViewModel[]) {
this._messagesMap.set(username, messages);
private storeUserMessages(username: string, messages: ChatMessageViewModel[], op: string) {
switch (op) {
case "clear": this._messagesMap.set(username, []);
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;
default: new Error("Invalid option");
}
}
private getStoredUserMessages(username: string): ChatMessageViewModel[] {
return this._messagesMap.get(username)!;
let temp = this._messagesMap.get(username);
if (temp == null)
return [];
else {
return temp;
}
}
/**
* Trigger an update in each subscriber.
*/
public notify(userName: string): void {
console.log('Subject: Notifying observers...');
for (const observer of this._observers) {
observer.update(this._messagesMap.get(userName));
public notify(p: Params): void {
log.info('Subject: Notifying observers...');
switch (p.op) {
case "clear": {
const od: ObserverData<ChatMessageViewModel> = { data: [], op: "clear" }
for (const observer of this._observers) {
observer.update(od);
}
} break;
case "new": {
const od: ObserverData<ChatMessageViewModel> = { data: p.data, op: p.op }
for (const observer of this._observers) {
observer.update(od);
}
} break;
case "page": {
const od: ObserverData<ChatMessageViewModel> = { data: p.data, op: p.op }
for (const observer of this._observers) {
observer.update(od);
}
} break;
default: { log.error("error") }
}
}
public someBusinessMethod(chatMessageList: ChatMessageViewModel[]): void {
this.state = chatMessageList;
console.log(`Subject: My state has just changed`);
console.log(chatMessageList);
this.notify("some user");
}
public async getMessages(contactName: string, passphrase: string, lastMessageTime: string | null): Promise<ChatMessageViewModel[]> {
public clear(): void {
this._messagePageMap.set(JsonAPI.contactName!, 0);
this.storeUserMessages(JsonAPI.contactName!, [], "clear");
this.notify({ userName: "", data: [], op: "clear" })
}
public async getMessages(contactName: string, passphrase: string, lastMessageTime: string | null, op: string): Promise<ChatMessageViewModel[]> {
if (this._messagePageMap.get(contactName) == null)
this._messagePageMap.set(contactName, 0);
// else {
// log.debug('page number before = ' + this._messagePageMap.get(contactName)!)
// this._messagePageMap.set(contactName, this._messagePageMap.get(contactName)! + 1);
// log.debug('page number after = ' + this._messagePageMap.get(contactName)!)
// }
const pageNumber = this._messagePageMap.get(contactName)
const cVMs = await ChatModelHelper.getMessages(contactName, passphrase, pageNumber!, lastMessageTime);
const cVMs = await this._chatModelHelper.getMessages(contactName, passphrase, pageNumber!, lastMessageTime, op);
if (cVMs != null) {
log.info('Subject: My state has just changed')
// this._messagesMap.set(userName, cVMs);
const existingMessages = this.getStoredUserMessages(contactName);
log.debug('existing message:')
log.debug(existingMessages);
log.debug('new messages:')
log.debug(cVMs);
if (existingMessages != null) {
// existingMessages.forEach(function (elem) {
// cVMs.push(elem);
// })
const newArr = cVMs.concat(existingMessages)
// log.debug(newArr);
this.storeUserMessages(contactName, cVMs);
// this.storeUserMessages(contactName, cVMs);
}
else {
this.storeUserMessages(contactName, cVMs);
}
JsonAPI.contactName = contactName;
this.notify(contactName);
this.storeUserMessages(contactName, cVMs, op);
this.notify({ userName: contactName, data: cVMs, op: op });
}
else {
log.error('Messages were null');
}
if(this._messagePageMap.get(contactName) == 0)
{
log.info(cVMs[cVMs.length - 1].messageTime)
}
if (cVMs.length != 0) {
// log.debug('page number before = ' + this._messagePageMap.get(contactName)!)
this._messagePageMap.set(contactName, this._messagePageMap.get(contactName)! + 1);
// log.debug('page number after = ' + this._messagePageMap.get(contactName)!)
}
return cVMs;

54
chatto/src/main/javascript/ts/src/model/ChatModelHelper.ts

@ -1,30 +1,38 @@
import * as log from "loglevel";
import { ChatMessageDTO } from "../dto/ChatMessageDTO";
import { EncryptionService } from "../service/EncryptionService";
import { EncryptionServiceFactory } from "../service/EncryptionServiceFactory";
import { NotificationService } from "../service/NotificationService";
import { JsonAPI } from "../singleton/JsonAPI";
import { Sprintf } from "../singleton/Sprintf";
import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import { fetchErrorHandler } from "./FetchErrorHandler";
export class ChatModelHelper {
private static readonly _encryptionService: EncryptionService = EncryptionServiceFactory.getEncryptionService();
private readonly _encryptionService: EncryptionService;
private readonly _notificationService: NotificationService;
public static async getMessages(userName: string, passphrase: string, page: number | null, lastMessageTime: string | null): Promise<ChatMessageViewModel[]> {
constructor(encryptionService: EncryptionService, notificationService: NotificationService) {
this._encryptionService = encryptionService;
this._notificationService = notificationService;
}
public async getMessages(userName: string, passphrase: string, page: number | null, lastMessageTime: string | null, op: string): Promise<ChatMessageViewModel[]> {
switch (lastMessageTime) {
case null: {
const data: ChatMessageDTO[] = await this.getPaginatedMessagesAjax(userName, page!);
const cVMs = Promise.all(data.map(vm => this.toChatMessageVMAsync(vm, passphrase)).reverse());
const data: ChatMessageDTO[] = await this._getPaginatedMessagesAjax(userName, page!);
const cVMs = Promise.all(data.map(vm => this._toChatMessageVMAsync(vm, passphrase)).reverse());
return cVMs;
}
default: {
const data: ChatMessageDTO[] = await this.getNewMessagesAjax(userName, lastMessageTime);
return data.map(vm => this.toChatMessageVM(vm, passphrase));
const data: ChatMessageDTO[] = await this._getNewMessagesAjax(userName, lastMessageTime);
return data.map(vm => this._toChatMessageVM(vm, passphrase));
}
}
}
private static async toChatMessageVMAsync(chatMessageDTO: ChatMessageDTO, passphrase: string): Promise<ChatMessageViewModel> {
private async _toChatMessageVMAsync(chatMessageDTO: ChatMessageDTO, passphrase: string): Promise<ChatMessageViewModel> {
const vm = new ChatMessageViewModel();
vm.fromUser = chatMessageDTO.fromUser;
vm.toUser = chatMessageDTO.toUser;
@ -34,7 +42,7 @@ export class ChatModelHelper {
return vm;
}
private static toChatMessageVM(chatMessageDTO: ChatMessageDTO, passphrase: string): ChatMessageViewModel {
private _toChatMessageVM(chatMessageDTO: ChatMessageDTO, passphrase: string): ChatMessageViewModel {
const vm = new ChatMessageViewModel();
vm.fromUser = chatMessageDTO.fromUser;
vm.toUser = chatMessageDTO.toUser;
@ -44,7 +52,7 @@ export class ChatModelHelper {
return vm;
}
private static async getAllMessagesAjax(toUser: string): Promise<any> {
private async _getAllMessagesAjax(toUser: string): Promise<any> {
const headers = new Headers();
if (JsonAPI.authToken == null) {
log.error("authToken null");
@ -52,51 +60,41 @@ export class ChatModelHelper {
};
headers.append('X-AUTH-TOKEN', JsonAPI.authToken);
const url = Sprintf(JsonAPI.CHAT_MESSAGES_GET, toUser);
// const url = Sprintf(JsonAPI.CHAT_MESSAGE_PAGE_GET, toUser,1,5);
log.debug(url)
// const response = await fetch(`${JsonAPI.CHAT_MESSAGES_GET}/${toUser}`, {
// method: 'GET',
// headers: headers
// });
const response = await fetch(url, {
method: 'GET',
headers: headers
});
console.log(response.clone());
if (fetchErrorHandler(response.clone())) {
log.debug(response.clone());
if (fetchErrorHandler(response.clone(), this._notificationService)) {
return null;
}
const data: Promise<any> = await response.json();
return data;
}
private static async getPaginatedMessagesAjax(toUser: string, page: number): Promise<any> {
private async _getPaginatedMessagesAjax(toUser: string, page: number): Promise<any> {
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);
const url = Sprintf(JsonAPI.CHAT_MESSAGE_PAGE_GET, toUser, page, 5);
log.debug(url)
// const response = await fetch(`${JsonAPI.CHAT_MESSAGES_GET}/${toUser}`, {
// method: 'GET',
// headers: headers
// });
const response = await fetch(url, {
method: 'GET',
headers: headers
});
console.log(response.clone());
if (fetchErrorHandler(response.clone())) {
log.debug(response.clone());
if (fetchErrorHandler(response.clone(), this._notificationService)) {
return null;
}
const data: Promise<any> = await response.json();
return data;
}
private static async getNewMessagesAjax(toUser: string, lastMessageTimeStamp: string): Promise<any> {
private async _getNewMessagesAjax(toUser: string, lastMessageTimeStamp: string): Promise<any> {
const headers = new Headers();
if (JsonAPI.authToken == null) {
log.error("authToken null");
@ -107,8 +105,8 @@ export class ChatModelHelper {
method: 'GET',
headers: headers
});
console.log(response.clone());
if (fetchErrorHandler(response.clone())) {
log.debug(response.clone());
if (fetchErrorHandler(response.clone(), this._notificationService)) {
return null;
}
const data: Promise<any> = await response.json();

8
chatto/src/main/javascript/ts/src/model/FetchErrorHandler.ts

@ -1,17 +1,19 @@
import log = require("loglevel");
import { NotificationService } from "../service/NotificationService";
import { Sprintf } from "../singleton/Sprintf";
// import { sprintf } from "sprintf-js";
///<reference path="../SprintfTest.d.ts" />
// import sprintf = require('sprintf-js').sprintf;
export function fetchErrorHandler(response: Response) {
export function fetchErrorHandler(response: Response, ns: NotificationService) {
// alertify.success('Current position : ' + alertify.get('notifier', 'position'));
if (!response.ok) {
return response.text().catch(err => {
// the status was not ok and there is no json body
// throw new Error(response.statusText);
// window.alert(sprintf('Some error occured. Http code is %s', response.status));
// alertify.error(sprintf('Some error occured. Http code is %s', response.status));
ns.error(Sprintf('Some error occured. Http code is %s', response.status));
// @ts-ignore
log.error(sprintf('Some error occured. Http code is %s', response.status));
log.error();
@ -20,7 +22,7 @@ export function fetchErrorHandler(response: Response) {
// the status was not ok but there is a json body
// throw new Error(json.error.message); // example error message returned by a REST API
// window.alert(sprintf('Error: %s (Http code %s)', json, response.status));
// alertify.error(sprintf('Some error occured. Http code is %s', response.status));
ns.error(Sprintf('Some error occured. Http code is %s', response.status));
// @ts-ignore
log.error(sprintf('Some error occured. Http code is %s', response.status));
log.error(json);

82
chatto/src/main/javascript/ts/src/model/UserModel.ts

@ -1,64 +1,66 @@
import * as log from "loglevel";
import { Subject } from "../observe/Observable";
import { Model } from "./AbstractModel";
import { Observer } from "../observe/Observer";
import { fetchErrorHandler } from "./FetchErrorHandler";
import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel";
import { JsonAPI } from "../singleton/JsonAPI";
import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import * as log from "loglevel";
import { ActiveUserViewModel } from "../viewmodel/ActiveUserViewModel";
import { fetchErrorHandler } from "./FetchErrorHandler";
import { NotificationService } from "../service/NotificationService";
export class UserModel implements Subject {
export class UserModel implements Subject<ActiveUserViewModel> {
/**
* @type {Observer[]} List of subscribers. In real life, the list of
* subscribers can be stored more comprehensively (categorized by event
* type, etc.).
*/
private readonly observers: Observer[] = [];
private readonly observers: Observer<ActiveUserViewModel>[] = [];
private _activeUsersList: ActiveUserViewModel[] | undefined;
// @ts-ignore: Cannot find name 'hostAddress'.
constructor() { }
private _activeUsersList: ActiveUserViewModel[];
private readonly _notificationService: NotificationService;
/**
* Getter activeUsersList
* @return {ActiveUserViewModel[] }
*/
public get activeUsersList(): ActiveUserViewModel[] | undefined {
return this._activeUsersList;
constructor(notificationService: NotificationService) {
this._activeUsersList = [];
this._notificationService = notificationService;
}
/**
* Getter activeUsersList
* @return {ActiveUserViewModel[] }
*/
public get activeUsersList(): ActiveUserViewModel[] {
return this._activeUsersList;
}
/**
* The subscription management methods.
*/
public attach(observer: Observer): void {
console.log('Subject: Attached an observer.');
public attach(observer: Observer<ActiveUserViewModel>): void {
log.info('Subject: Attached an observer.');
this.observers.push(observer);
}
public detach(observer: Observer): void {
public detach(observer: Observer<ActiveUserViewModel>): void {
const observerIndex = this.observers.indexOf(observer);
this.observers.splice(observerIndex, 1);
console.log('Subject: Detached an observer.');
log.info('Subject: Detached an observer.');
}
/**
* Trigger an update in each subscriber.
*/
public notify(): void {
console.log('Subject: Notifying observers...');
log.info('Subject: Notifying observers...');
for (const observer of this.observers) {
observer.update(this._activeUsersList);
observer.update({ data: this._activeUsersList!, op: "" });
}
}
public someBusinessMethod(activeuserList: ActiveUserViewModel[]): void {
this._activeUsersList = activeuserList;
this.helperMethod();
console.log(`Subject: My state has just changed`);
console.log(activeuserList);
log.info(`Subject: My state has just changed`);
log.trace(activeuserList);
this.notify();
}
@ -66,31 +68,31 @@ export class UserModel implements Subject {
* getActiveUsers
*/
public getActiveUsers(): void {
if(JsonAPI.authToken!= null){
this.getActiveUsersAjax(JsonAPI.authToken)
.then(data => {
// // activeUsers = data;
// sessionStorage.setItem('activeUsers', JSON.stringify(data));
// console.log(sessionStorage.getItem('activeUsers'));
console.log(`Subject: received ajax active users`);
this._activeUsersList = data;
this.notify();
})
if (JsonAPI.authToken != null) {
this._getActiveUsersAjax(JsonAPI.authToken)
.then(data => {
// // activeUsers = data;
// sessionStorage.setItem('activeUsers', JSON.stringify(data));
// log.trace(sessionStorage.getItem('activeUsers'));
log.info(`Subject: received ajax active users`);
this._activeUsersList = data;
this.notify();
})
}
else {
log.error('Auth token is null');
}
}
async getActiveUsersAjax(authToken: string): Promise<any> {
private async _getActiveUsersAjax(authToken: string): Promise<any> {
let headers = new Headers();
headers.append('X-AUTH-TOKEN', authToken);
let response = await fetch(JsonAPI.ACTIVE_USERS_GET, {
method: 'GET',
headers: headers
});
console.log(response.clone());
if (fetchErrorHandler(response.clone())) {
log.debug(response.clone());
if (fetchErrorHandler(response.clone(), this._notificationService)) {
return null;
}
let data = await response.json();

9
chatto/src/main/javascript/ts/src/observe/Observable.ts

@ -1,17 +1,18 @@
import { Observer } from "./Observer";
import { ObserverData } from "./ObserverData";
/**
* The Subject interface declares a set of methods for managing subscribers.
*/
export interface Subject {
export interface Subject<T> {
// Attach an observer to the subject.
attach(observer: Observer): void;
attach(observer: Observer<T>): void;
// Detach an observer from the subject.
detach(observer: Observer): void;
detach(observer: Observer<T>): void;
// Notify all observers about an event.
notify(param: any | null): void;
notify(param: any): void;
}
/**

5
chatto/src/main/javascript/ts/src/observe/Observer.ts

@ -1,8 +1,9 @@
import { Subject } from "./Observable";
import { ObserverData } from "./ObserverData";
export interface Observer {
export interface Observer<T> {
// Receive update from subject.
update(data: any): void;
update(data: ObserverData<T>): void;
}
// /**

4
chatto/src/main/javascript/ts/src/observe/ObserverData.ts

@ -0,0 +1,4 @@
export interface ObserverData<T> {
data: Array<T>
op: string
}

2
chatto/src/main/javascript/ts/src/view/AbstractView.ts

@ -2,7 +2,7 @@
import { Model } from "../model/AbstractModel";
import { Controller } from "../controller/AbstractController";
import { Observer } from "../observe/Observer";
export interface View extends Observer {
export interface View extends Observer<any> {
readonly model: Model,
readonly element: any
}

135
chatto/src/main/javascript/ts/src/view/ChatView.ts

@ -10,14 +10,17 @@ import { JsonAPI } from "../singleton/JsonAPI";
import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import { ChatViewDeps } from "./ChatViewDeps";
import { fetchHandler } from "./FetchHandler";
import { ObserverData } from '../observe/ObserverData';
import { NotificationService } from '../service/NotificationService';
export class ChatView implements Observer {
export class ChatView implements Observer<ChatMessageViewModel> {
private readonly _chatModel: ChatModel;
private readonly _messageContainer: HTMLElement;
private readonly _messageSendTemplate: Handlebars.TemplateDelegate<ChatMessageViewModel>;
private readonly _messageReceiveTemplate: Handlebars.TemplateDelegate<ChatMessageViewModel>;
private readonly _markdownService: MarkDownService;
private readonly _encryptionService: EncryptionService;
private readonly _notificationService: NotificationService;
constructor(deps: ChatViewDeps) {
@ -27,7 +30,8 @@ export class ChatView implements Observer {
this._messageReceiveTemplate = deps.messageReceiveTemplate;
this._markdownService = deps.markdownService;
this._encryptionService = deps.encryptionService;
this.addEventListeners();
this._notificationService = deps.notificationService;
this._initEventListeners();
$(document).ready(function () {
$('#action_menu_btn').click(function () {
@ -35,67 +39,83 @@ export class ChatView implements Observer {
});
});
this.chatMessagePageLoadAjax();
this._chatMessagePageLoadAjax();
}
update(data: ChatMessageViewModel[]): void {
update(cd: ObserverData<ChatMessageViewModel>): void {
log.info('ChatView: updating view');
// let html: string = "";
// let currentMsg = $('.msg:first');
// this._messageContainer.innerHTML = "";
const rev: ChatMessageViewModel[] = Object.create(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));
switch (cd.op) {
case "clear": {
$(this._messageContainer).html("");
} break;
case "new": {
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).prepend(rendered);
});
$(this._messageContainer).stop().animate({
scrollTop: $(this._messageContainer)[0].scrollHeight
}, 1500);
} break;
default: {
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).prepend(rendered);
});
}
else {
rendered = DOMPurify.sanitize(this._messageReceiveTemplate(vmTemp));
}
$(this._messageContainer).prepend(rendered);
// if (currentMsg.position() != null) {
// $(this._messageContainer).scrollTop(currentMsg.position().top)
// }
// log.debug(vm)
// log.debug(vmTemp)
// html += this._messageSendTemplate(vm);
});
// if (currentMsg.position() != null) {
// $(this._messageContainer).scrollTop(currentMsg.position().top)
// }
}
// html = DOMPurify.sanitize(md.render(html));
// this._element.innerHTML = html;
// log.debug(this._element.innerHTML);
}
private addEventListeners(): void {
this.addChatFormEL();
private _initEventListeners(): void {
this._addChatFormEL();
}
private addChatFormEL() {
private _addChatFormEL() {
const chatForm = document.getElementById('chatMessageForm') as HTMLSelectElement;
if (chatForm == null) {
log.error("Chat form is null");
}
else {
chatForm.addEventListener('submit', (e) => this.createChatMessageDTO(e, chatForm))
chatForm.addEventListener('submit', (e) => this._createChatMessageDTO(e, chatForm))
}
}
private createChatMessageDTO(e: Event, chatForm: HTMLSelectElement): void {
private _createChatMessageDTO(e: Event, chatForm: HTMLSelectElement): void {
e.preventDefault();
let contactName = JsonAPI.contactName;
@ -106,7 +126,7 @@ export class ChatView implements Observer {
}
if (!chatForm.checkValidity()) {
console.log("error");
log.error("Form is not valid");
chatForm.classList.add('was-validated');
return;
}
@ -117,18 +137,24 @@ export class ChatView implements Observer {
const passphraseInput = document.getElementById('passphrase') as HTMLInputElement;
if (chatInput.value == '' || chatInput.value == null) {
this._notificationService.error("Please enter a message");
log.error("Chat input is null.");
return;
}
if (passphraseInput.value == '' || passphraseInput.value == null) {
log.error("Chat input is null.");
this._notificationService.error("Please enter a passphrase");
log.error("Passphrase is null.");
return;
}
// @ts-ignore
const messageContent = chatInput.value;
const context = { fromUser: JsonAPI.principleName, toUser: "", message: this._markdownService.render(messageContent), messageTime: new Date().toLocaleString() };
const context = {
fromUser: JsonAPI.principleName, toUser: "",
message: this._markdownService.render(messageContent),
messageTime: new Date().toLocaleString()
};
// @ts-ignore
const msgContainer: string = this._messageSendTemplate(context);
$(this._messageContainer).append(DOMPurify.sanitize(msgContainer));
@ -143,10 +169,10 @@ export class ChatView implements Observer {
// "messageTime": null
}
// @ts-ignore
this.sendMessageAJAX(chatMessageDTO);
this._sendMessageAJAX(chatMessageDTO);
}
private sendMessageAJAX(chatMessageDTO: ChatMessageDTO): void {
private _sendMessageAJAX(chatMessageDTO: ChatMessageDTO): void {
let headers = new Headers();
// console.log("Token = " + btoa("hmm" + ":" + "hmm"))
@ -164,16 +190,15 @@ export class ChatView implements Observer {
log.debug(response);
return response.clone();
})
.then(response => fetchHandler(response));
.then(response => fetchHandler(response, this._notificationService));
}
chatMessagePageLoadAjax() {
private _chatMessagePageLoadAjax() {
this._messageContainer.addEventListener('scroll', (e) => {
if ($(this._messageContainer).scrollTop() == 0) {
if ($(this._messageContainer).scrollTop() == 0 && $(this._messageContainer).html() != "") {
let currentMsg = $('.msg:first');
log.debug('Reached top')
// @ts-ignore
let passphrase: string;
let passphraseInput = document.getElementById('passphrase') as HTMLInputElement;
@ -189,9 +214,11 @@ export class ChatView implements Observer {
return;
}
if (JsonAPI.contactName != null)
this._chatModel.getMessages(JsonAPI.contactName, passphrase, null).then(() => {
log.debug(currentMsg.offset()!.top)
$(this._messageContainer).scrollTop(currentMsg.position()!.top - $('.msg').position()!.top)
this._chatModel.getMessages(JsonAPI.contactName, passphrase, null, "page").then(() => {
if (currentMsg != null) {
// log.debug(currentMsg.offset()!.top)
$(this._messageContainer).scrollTop(currentMsg.position().top - $('.msg').position()!.top)
}
});
}
})

3
chatto/src/main/javascript/ts/src/view/ChatViewDeps.ts

@ -2,6 +2,7 @@ import { ChatMessageViewModel } from "../viewmodel/ChatMessageViewModel";
import { ChatModel } from "../model/ChatModel";
import { MarkDownService } from "../service/MarkDownService";
import { EncryptionService } from "../service/EncryptionService";
import { NotificationService } from "../service/NotificationService";
export interface ChatViewDeps {
chatModel: ChatModel;
@ -10,5 +11,5 @@ export interface ChatViewDeps {
messageReceiveTemplate: Handlebars.TemplateDelegate<ChatMessageViewModel>;
markdownService: MarkDownService;
encryptionService: EncryptionService;
notificationService: NotificationService
}

4
chatto/src/main/javascript/ts/src/view/FetchHandler.ts

@ -1,9 +1,7 @@
import { NotificationService } from "../service/NotificationService";
import { AlertifyNotificationService } from "../service/AlertifyNotificationService";
import { Sprintf } from "../singleton/Sprintf";
export function fetchHandler(response: any) {
const ns: NotificationService = new AlertifyNotificationService();
export function fetchHandler(response: Response, ns: NotificationService) {
if (response.ok) {
return response.json().then((json: any) => {
// the status was ok and there is a json body

42
chatto/src/main/javascript/ts/src/view/UserView.ts

@ -6,8 +6,10 @@ import * as DOMPurify from "dompurify";
import { SearchService } from "../service/SearchService";
import { UserModel } from "../model/UserModel";
import { UserViewDeps } from "./UserViewDeps";
import { ObserverData } from "../observe/ObserverData";
import { JsonAPI } from "../singleton/JsonAPI";
export class UserView implements Observer {
export class UserView implements Observer<ActiveUserViewModel> {
private readonly _model: UserModel;
@ -31,35 +33,35 @@ export class UserView implements Observer {
this._userContactOnlineTemplate = deps.userContactOnlineTemplate;
this._userContactOfflineTemplate = deps.userContactOfflineTemplate;
this.addSearchEventListeners();
this._addSearchEventListeners();
}
update(data: ActiveUserViewModel[]): void {
update(d: ObserverData<ActiveUserViewModel>): void {
let html: string = "";
data.forEach((element: ActiveUserViewModel) => {
d.data.forEach((element: ActiveUserViewModel) => {
element.online ? html += this._userContactOnlineTemplate(element) : html += this._userContactOfflineTemplate(element);
});
$(this._usersListElement).html(DOMPurify.sanitize(html));
this.addUserCallBacks();
console.log(this._usersListElement.innerHTML);
this._addUserCallBacks();
}
private addSearchEventListeners(): void {
this.addSearchButtonEL();
this.addSearchCancelEL();
this.addSearchInputEL();
private _addSearchEventListeners(): void {
this._addSearchButtonEL();
this._addSearchCancelEL();
this._addSearchInputEL();
}
private addUserCallBacks(): void {
private _addUserCallBacks(): void {
let userBoxes = document.getElementsByClassName('user-box');
for (let i = 0; i < userBoxes.length; i++) {
let userBox = userBoxes[i];
userBoxes[i].addEventListener('click', this.userCallBack.bind(this, userBox));
userBoxes[i].addEventListener('click', this._userCallBack.bind(this, userBox));
}
}
private userCallBack(el: Element): void {
private _userCallBack(el: Element): void {
this._chatModel.clear();
let current = document.getElementsByClassName('user-box active');
let passphrase: string = '';
@ -104,13 +106,14 @@ export class UserView implements Observer {
// console.log(this.getElementsByClassName('to-user-span'));
let elem = el.getElementsByClassName('to-user-span')[0] as HTMLElement;
let userName = elem.innerText;
JsonAPI.contactName = userName;
// @ts-ignore: Object is possibly 'null'.
document.getElementById('user-name-span').innerText = userName;
this._chatModel.getMessages(userName, passphrase, null);
this._chatModel.getMessages(userName, passphrase, null, "new");
el.className += " active";
}
private addSearchButtonEL() {
private _addSearchButtonEL() {
this._userSearchButton.addEventListener('submit', (e) => {
e.preventDefault();
// log.trace(temp);
@ -124,12 +127,12 @@ export class UserView implements Observer {
return;
}
let searchResult = this._searchService.search(list, searchTerm);
this.update(searchResult);
this.update({data: searchResult, op: ""});
log.debug(searchResult);
})
}
private addSearchInputEL() {
private _addSearchInputEL() {
this._userSearchInputElement.addEventListener('input', (e) => {
e.preventDefault();
if (this._userSearchInputElement.value.length < 2) {
@ -139,14 +142,13 @@ export class UserView implements Observer {
})
}
private addSearchCancelEL() {
private _addSearchCancelEL() {
this._userSearchCancelButton.addEventListener('click', (e) => {
e.preventDefault();
this._userSearchInputElement.value = "";
this._userSearchCancelButton.hidden = true;
let list = this._model.activeUsersList;
// @ts-ignore
this.update(list)
this.update({data: list, op: ""})
})
}
Loading…
Cancel
Save