diff --git a/demo/aspnetcore_signalr/package.json b/demo/aspnetcore_signalr/package.json
index 486b1e0..6249a5c 100644
--- a/demo/aspnetcore_signalr/package.json
+++ b/demo/aspnetcore_signalr/package.json
@@ -29,7 +29,7 @@
"bootstrap": "^3.3.7",
"core-js": "^2.4.1",
"jquery": "3.2.1",
- "ng-chat": "^1.0.8",
+ "ng-chat": "^1.0.9",
"ng2-loading-bar": "0.0.6",
"reflect-metadata": "^0.1.10",
"rxjs": "5.4.2",
diff --git a/demo/offline_bot/package.json b/demo/offline_bot/package.json
index ea353a6..0aeec94 100644
--- a/demo/offline_bot/package.json
+++ b/demo/offline_bot/package.json
@@ -24,7 +24,7 @@
"core-js": "^2.5.3",
"rxjs": "^5.5.6",
"zone.js": "^0.8.20",
- "ng-chat": "^1.0.8"
+ "ng-chat": "^1.0.9"
},
"devDependencies": {
"@angular/cli": "^1.6.4",
diff --git a/src/ng-chat/ng-chat.component.html b/src/ng-chat/ng-chat.component.html
index 10c35cc..c476c22 100644
--- a/src/ng-chat/ng-chat.component.html
+++ b/src/ng-chat/ng-chat.component.html
@@ -7,7 +7,7 @@
- -
+
-
diff --git a/src/ng-chat/ng-chat.component.ts b/src/ng-chat/ng-chat.component.ts
index 56e797a..beb6ca4 100644
--- a/src/ng-chat/ng-chat.component.ts
+++ b/src/ng-chat/ng-chat.component.ts
@@ -82,7 +82,10 @@ export class NgChat implements OnInit {
private users: User[];
- private localStorageKey: string = "ng-chat-users";
+ private get localStorageKey(): string
+ {
+ return `ng-chat-users-${this.userId}`; // Appending the user id so the state is unique per user in a computer.
+ };
get filteredUsers(): User[]
{
@@ -230,7 +233,7 @@ export class NgChat implements OnInit {
// Opens a new chat whindow. Takes care of available viewport
// Returns => [Window: Window object reference, boolean: Indicates if this window is a new chat window]
- private openChatWindow(user: User): [Window, boolean]
+ private openChatWindow(user: User, focusOnNewWindow: boolean = false): [Window, boolean]
{
// Is this window opened?
let openedWindow = this.windows.find(x => x.chattingTo.id == user.id);
@@ -265,6 +268,9 @@ export class NgChat implements OnInit {
}
this.updateWindowsState(this.windows);
+
+ if (focusOnNewWindow)
+ this.focusOnWindow(newChatWindow);
return [newChatWindow, true];
}
@@ -275,6 +281,23 @@ export class NgChat implements OnInit {
}
}
+ // Focus on the input element of the supplied window
+ private focusOnWindow(window: Window, callback: Function = () => {}) : void
+ {
+ let windowIndex = this.windows.indexOf(window);
+
+ if (windowIndex >= 0)
+ {
+ setTimeout(() => {
+ let messageInputToFocus = this.chatWindowInputs.toArray()[windowIndex];
+
+ messageInputToFocus.nativeElement.focus();
+
+ callback();
+ });
+ }
+ }
+
// Scrolls a chat window message flow to the bottom
private scrollChatWindowToBottom(window: Window): void
{
@@ -355,6 +378,21 @@ export class NgChat implements OnInit {
}
}
+ // Gets closest open window if any. Most recent opened has priority (Right)
+ private getClosestWindow(window: Window): Window | undefined
+ {
+ let index = this.windows.indexOf(window);
+
+ if (index > 0)
+ {
+ return this.windows[index - 1];
+ }
+ else if (index == 0 && this.windows.length > 1)
+ {
+ return this.windows[index + 1];
+ }
+ }
+
// Returns the total unread messages from a chat window. TODO: Could use some Angular pipes in the future
unreadMessagesTotal(window: Window): string
{
@@ -382,6 +420,7 @@ export class NgChat implements OnInit {
/* Monitors pressed keys on a chat window
- Dispatches a message when the ENTER key is pressed
- Tabs between windows on TAB or SHIFT + TAB
+ - Closes the current focused window on ESC
*/
onChatInputTyped(event: any, window: Window): void
{
@@ -420,6 +459,17 @@ export class NgChat implements OnInit {
messageInputToFocus.nativeElement.focus();
break;
+ case 27:
+ let closestWindow = this.getClosestWindow(window);
+
+ if (closestWindow)
+ {
+ this.focusOnWindow(closestWindow, () => { this.onCloseChatWindow(window); });
+ }
+ else
+ {
+ this.onCloseChatWindow(window);
+ }
}
}
diff --git a/src/package.json b/src/package.json
index 60828d3..1e0396b 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,6 +1,6 @@
{
"name": "ng-chat",
- "version": "1.0.8",
+ "version": "1.0.9",
"peerDependencies": {
"@angular/common": "*",
"@angular/core": "*",
diff --git a/src/spec/ng-chat.component.spec.ts b/src/spec/ng-chat.component.spec.ts
index 6868911..973070b 100644
--- a/src/spec/ng-chat.component.spec.ts
+++ b/src/spec/ng-chat.component.spec.ts
@@ -67,6 +67,10 @@ describe('NgChat', () => {
expect(this.subject.persistWindowsState).toBeTruthy();
});
+ it('Must use current user id as part of the localStorageKey identifier', () => {
+ expect(this.subject.localStorageKey).toBe(`ng-chat-users-${this.subject.userId}`);
+ });
+
it('Exercise users filter', () => {
this.subject.users = [{
id: 1,
@@ -200,6 +204,42 @@ describe('NgChat', () => {
expect(result[0].chattingTo.displayName).toEqual(user.displayName);
});
+ it('Must focus on the new window on a openChatWindow request when argument is supplied', () =>{
+ this.subject.historyEnabled = false;
+
+ let user: User = {
+ id: 999,
+ displayName: 'Test user',
+ status: 1,
+ avatar: ''
+ };
+
+ spyOn(this.subject, 'focusOnWindow');
+
+ let result = this.subject.openChatWindow(user, true);
+
+ expect(result).not.toBeUndefined();
+ expect(this.subject.focusOnWindow).toHaveBeenCalledTimes(1);
+ });
+
+ it('Must not focus on the new window on a openChatWindow request when argument is not supplied', () =>{
+ this.subject.historyEnabled = false;
+
+ let user: User = {
+ id: 999,
+ displayName: 'Test user',
+ status: 1,
+ avatar: ''
+ };
+
+ spyOn(this.subject, 'focusOnWindow');
+
+ let result = this.subject.openChatWindow(user);
+
+ expect(result).not.toBeUndefined();
+ expect(this.subject.focusOnWindow).not.toHaveBeenCalled();
+ });
+
it('Must load history from ChatAdapter when opening a window that is not yet opened', () =>{
let user: User = {
id: 999,
@@ -423,6 +463,65 @@ describe('NgChat', () => {
expect(sentMessage).toBeNull();
});
+ it('Must close the current window when the ESC key is pressed', () => {
+ let currentWindow = new Window();
+ let closedWindow: Window = null;
+ let event = {
+ keyCode: 27
+ };
+
+ spyOn(this.subject, 'onCloseChatWindow').and.callFake((window: Window) => {
+ closedWindow = window;
+ });
+
+ this.subject.onChatInputTyped(event, currentWindow);
+
+ expect(this.subject.onCloseChatWindow).toHaveBeenCalledTimes(1);
+ expect(closedWindow).not.toBeNull();
+ expect(closedWindow).toBe(currentWindow);
+ });
+
+ it('Must focus on closest window when the ESC key is pressed', () => {
+ let currentWindow = new Window();
+ let closestWindow = new Window();
+ let closedWindow: Window = null;
+ let event = {
+ keyCode: 27
+ };
+
+ spyOn(this.subject, 'onCloseChatWindow');
+
+ spyOn(this.subject, 'getClosestWindow').and.returnValue(closestWindow);
+
+ spyOn(this.subject, 'focusOnWindow').and.callFake((window: Window, callback: Function) => {
+ callback(); // This should invoke onCloseChatWindow
+ });;
+
+ this.subject.onChatInputTyped(event, currentWindow);
+
+ expect(this.subject.onCloseChatWindow).toHaveBeenCalledTimes(1);
+ expect(this.subject.getClosestWindow).toHaveBeenCalledTimes(1);
+ expect(this.subject.focusOnWindow).toHaveBeenCalledTimes(1);
+ });
+
+ it('Must not focus on closest window when the ESC key is pressed and there is no other window opened', () => {
+ let currentWindow = new Window();
+ let closedWindow: Window = null;
+ let event = {
+ keyCode: 27
+ };
+
+ spyOn(this.subject, 'onCloseChatWindow');
+ spyOn(this.subject, 'getClosestWindow').and.returnValue(undefined);
+ spyOn(this.subject, 'focusOnWindow');
+
+ this.subject.onChatInputTyped(event, currentWindow);
+
+ expect(this.subject.onCloseChatWindow).toHaveBeenCalledTimes(1);
+ expect(this.subject.getClosestWindow).toHaveBeenCalledTimes(1);
+ expect(this.subject.focusOnWindow).not.toHaveBeenCalled();
+ });
+
it('Must move to the next chat window when the TAB key is pressed', () => {
this.subject.windows = [
new Window(),
@@ -560,3 +659,100 @@ describe('NgChat', () => {
expect(this.subject.localization.statusDescription).not.toBe(this.subject.statusDescription);
});
});
+
+it('FocusOnWindow exercise', () => {
+ this.subject.windows = [
+ new Window(),
+ new Window(),
+ new Window()
+ ];
+
+ this.subject.chatWindowInputs = {
+ toArray: () => {}
+ };
+
+ let fakeChatInputs = [
+ {
+ nativeElement:
+ {
+ focus: () => {}
+ }
+ },
+ {
+ nativeElement:
+ {
+ focus: () => {}
+ }
+ },
+ {
+ nativeElement:
+ {
+ focus: () => {}
+ }
+ }
+ ];
+
+ spyOn(fakeChatInputs[1].nativeElement, 'focus');
+ spyOn(this.subject.chatWindowInputs, 'toArray').and.returnValue(fakeChatInputs);
+
+ spyOn(window, 'setTimeout').and.callFake((fn) => {
+ fn();
+ });
+
+ this.subject.focusOnWindow(this.subject.windows[1]);
+
+ expect(fakeChatInputs[1].nativeElement.focus).toHaveBeenCalledTimes(1);
+});
+
+it('Must not focus on native element if a window is not found to focus when focusOnWindow is invoked', () => {
+ this.subject.windows = [];
+
+ this.subject.chatWindowInputs = {
+ toArray: () => {}
+ };
+
+ this.subject.focusOnWindow(new Window());
+});
+
+it('GetClosestWindow must return next relative right chat window', () => {
+ this.subject.windows = [
+ new Window(),
+ new Window(),
+ new Window()
+ ];
+
+ let result = this.subject.getClosestWindow(this.subject.windows[2]);
+
+ expect(result).toBe(this.subject.windows[1]);
+});
+
+it('GetClosestWindow must return previous chat window on 0 based index', () => {
+ this.subject.windows = [
+ new Window(),
+ new Window(),
+ new Window()
+ ];
+
+ let result = this.subject.getClosestWindow(this.subject.windows[0]);
+
+ expect(result).toBe(this.subject.windows[1]);
+});
+
+it('GetClosestWindow must return undefined when there is only one chat window opened on the chat', () => {
+ this.subject.windows = [
+ new Window()
+ ];
+
+ let result = this.subject.getClosestWindow(this.subject.windows[0]);
+
+ expect(result).toBe(undefined);
+});
+
+it('GetClosestWindow must return undefined when there is no open chat window on the chat', () => {
+ this.subject.windows = [];
+
+ let result = this.subject.getClosestWindow(new Window());
+
+ expect(result).toBe(undefined);
+});
+