diff --git a/.travis.yml b/.travis.yml
index ce93082..e983a0f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -30,7 +30,7 @@ script:
# FIX (Tests): node_modules/@angular/core/src/render3/ng_dev_mode.d.ts(9,11): error TS2451: Cannot redeclare block-scoped variable 'ngDevMode'.
- rm -f node_modules/@angular/core/src/render3/ng_dev_mode.d.ts
- - ng test --code-coverage --watch=false
+ - ng test --sourcemaps=false --code-coverage --watch=false
after_success:
- ./node_modules/.bin/codecov
diff --git a/README.md b/README.md
index c9a98b3..3214a9b 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,7 @@ __Additional Settings__
* [linkfyEnabled]{boolean}: Transforms links within the messages to valid HTML links. Default is true.
* [audioEnabled]{boolean}: Enables audio notifications on received messages. Default is true.
* [audioSource]{string}: WAV source of the audio notification. Default is a RAW github WAV content from ng-chat repository.
+* [persistWindowsState]{boolean}: Saves the state of current open windows on the local storage. Default is true.
#### Implement your ChatAdapter:
diff --git a/demo/aspnetcore_signalr/package.json b/demo/aspnetcore_signalr/package.json
index 2435c44..66bf8ea 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.4",
+ "ng-chat": "1.0.5",
"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 c6dd7d2..b60284e 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.4"
+ "ng-chat": "1.0.5"
},
"devDependencies": {
"@angular/cli": "^1.6.4",
diff --git a/src/ng-chat/assets/ng-chat.component.default.css b/src/ng-chat/assets/ng-chat.component.default.css
index 3ca38e8..bb07b3d 100644
--- a/src/ng-chat/assets/ng-chat.component.default.css
+++ b/src/ng-chat/assets/ng-chat.component.default.css
@@ -67,6 +67,11 @@
max-width: 85%;
float:left;
}
+ .ng-chat-title > .ng-chat-user-status
+ {
+ float: left;
+ margin-left: 5px;
+ }
#ng-chat-search_friend
{
display: block;
@@ -129,27 +134,30 @@
#ng-chat-users li > .ng-chat-user-status
{
float: right;
- border-radius: 25px;
- width: 8px;
- height: 8px;
- margin-top:10px;
- }
- #ng-chat-users li > .ng-chat-user-status.online
- {
- background-color: #92A400;
- }
- #ng-chat-users li > .ng-chat-user-status.busy
- {
- background-color: #F91C1E;
- }
- #ng-chat-users li > .ng-chat-user-status.away
- {
- background-color: #F7D21B;
- }
- #ng-chat-users li > .ng-chat-user-status.offline
- {
- background-color: #BABABA;
}
+.ng-chat-user-status
+{
+ border-radius: 25px;
+ width: 8px;
+ height: 8px;
+ margin-top:10px;
+}
+.ng-chat-user-status.online
+{
+ background-color: #92A400;
+}
+.ng-chat-user-status.busy
+{
+ background-color: #F91C1E;
+}
+.ng-chat-user-status.away
+{
+ background-color: #F7D21B;
+}
+.ng-chat-user-status.offline
+{
+ background-color: #BABABA;
+}
.ng-chat-unread-messages-count
{
background-color: #E3E3E3;
@@ -242,4 +250,4 @@
border: 3px solid #E3E3E3;
margin-top: 0;
margin-bottom: 5px;
- }
\ No newline at end of file
+ }
diff --git a/src/ng-chat/ng-chat.component.html b/src/ng-chat/ng-chat.component.html
index ba20119..8aefcc0 100644
--- a/src/ng-chat/ng-chat.component.html
+++ b/src/ng-chat/ng-chat.component.html
@@ -23,6 +23,7 @@
{{window.chattingTo.displayName}}
+
0" class="ng-chat-unread-messages-count">{{unreadMessagesTotal(window)}}
X
@@ -32,6 +33,7 @@
{{window.chattingTo.displayName}}
+
0" class="ng-chat-unread-messages-count">{{unreadMessagesTotal(window)}}
X
diff --git a/src/ng-chat/ng-chat.component.ts b/src/ng-chat/ng-chat.component.ts
index c1b9a57..50806d6 100644
--- a/src/ng-chat/ng-chat.component.ts
+++ b/src/ng-chat/ng-chat.component.ts
@@ -58,12 +58,17 @@ export class NgChat implements OnInit {
@Input() // TODO: This might need a better content strategy
public audioSource: string = 'https://raw.githubusercontent.com/rpaschoal/ng-chat/master/src/ng-chat/assets/notification.wav';
+ @Input()
+ public persistWindowsState: boolean = true;
+
private audioFile: HTMLAudioElement;
public searchInput: string = '';
private users: User[];
+ private localStorageKey: string = "ng-chat-users";
+
get filteredUsers(): User[]
{
if (this.searchInput.length > 0){
@@ -125,13 +130,13 @@ export class NgChat implements OnInit {
// Loading current users list
if (this.pollFriendsList){
// Setting a long poll interval to update the friends list
- this.fetchFriendsList();
- setInterval(() => this.fetchFriendsList(), this.pollingInterval);
+ this.fetchFriendsList(true);
+ setInterval(() => this.fetchFriendsList(false), this.pollingInterval);
}
else
{
// Since polling was disabled, a friends list update mechanism will have to be implemented in the ChatAdapter.
- this.fetchFriendsList();
+ this.fetchFriendsList(true);
}
this.bufferAudioFile();
@@ -152,12 +157,17 @@ export class NgChat implements OnInit {
}
// Sends a request to load the friends list
- private fetchFriendsList(): void
+ private fetchFriendsList(isBootstrapping: boolean): void
{
this.adapter.listFriends()
.map((users: User[]) => {
this.users = users;
- }).subscribe();
+ }).subscribe(() => {
+ if (isBootstrapping)
+ {
+ this.restoreWindowsState();
+ }
+ });
}
// Updates the friends list via the event handler
@@ -221,6 +231,8 @@ export class NgChat implements OnInit {
this.windows.pop();
}
+ this.updateWindowsState(this.windows);
+
return [newChatWindow, true];
}
else
@@ -270,6 +282,45 @@ export class NgChat implements OnInit {
}
}
+ // Saves current windows state into local storage if persistence is enabled
+ private updateWindowsState(windows: Window[]): void
+ {
+ if (this.persistWindowsState)
+ {
+ let usersIds = windows.map((w) => {
+ return w.chattingTo.id;
+ });
+
+ localStorage.setItem(this.localStorageKey, JSON.stringify(usersIds));
+ }
+ }
+
+ private restoreWindowsState(): void
+ {
+ try
+ {
+ if (this.persistWindowsState)
+ {
+ let stringfiedUserIds = localStorage.getItem(this.localStorageKey);
+
+ if (stringfiedUserIds && stringfiedUserIds.length > 0)
+ {
+ let userIds = JSON.parse(stringfiedUserIds);
+
+ let usersToRestore = this.users.filter(u => userIds.indexOf(u.id) >= 0);
+
+ usersToRestore.forEach((user) => {
+ this.openChatWindow(user);
+ });
+ }
+ }
+ }
+ catch (ex)
+ {
+ console.log(`An error occurred while restoring ng-chat windows state. Details: ${ex}`);
+ }
+ }
+
// Returns the total unread messages from a chat window. TODO: Could use some Angular pipes in the future
unreadMessagesTotal(window: Window): string
{
@@ -321,6 +372,8 @@ export class NgChat implements OnInit {
let index = this.windows.indexOf(window);
this.windows.splice(index, 1);
+
+ this.updateWindowsState(this.windows);
}
// Toggle friends list visibility
diff --git a/src/package.json b/src/package.json
index 1c79934..0be4146 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,6 +1,6 @@
{
"name": "ng-chat",
- "version": "1.0.4",
+ "version": "1.0.5",
"peerDependencies": {
"@angular/common": "*",
"@angular/core": "*",
diff --git a/src/spec/ng-chat.component.spec.ts b/src/spec/ng-chat.component.spec.ts
index ac0ea4e..bb1826c 100644
--- a/src/spec/ng-chat.component.spec.ts
+++ b/src/spec/ng-chat.component.spec.ts
@@ -59,6 +59,10 @@ describe('NgChat', () => {
expect(this.subject.audioSource).not.toBeUndefined();
});
+ it('Persistent windows state must be enabled by default', () => {
+ expect(this.subject.persistWindowsState).toBeTruthy();
+ });
+
it('Exercise users filter', () => {
this.subject.users = [{
id: 1,
@@ -114,11 +118,29 @@ describe('NgChat', () => {
it('Must invoke adapter on fetchFriendsList', () => {
spyOn(MockableAdapter.prototype, 'listFriends').and.returnValue(Observable.of([]));
- this.subject.fetchFriendsList();
+ this.subject.fetchFriendsList(false);
expect(MockableAdapter.prototype.listFriends).toHaveBeenCalledTimes(1);
});
+ it('Must invoke restore windows state on fetchFriendsList when bootstrapping', () => {
+ spyOn(MockableAdapter.prototype, 'listFriends').and.returnValue(Observable.of([]));
+ spyOn(this.subject, 'restoreWindowsState');
+
+ this.subject.fetchFriendsList(true);
+
+ expect(this.subject.restoreWindowsState).toHaveBeenCalledTimes(1);
+ });
+
+ it('Must not invoke restore windows state on fetchFriendsList when not bootstrapping', () => {
+ spyOn(MockableAdapter.prototype, 'listFriends').and.returnValue(Observable.of([]));
+ spyOn(this.subject, 'restoreWindowsState');
+
+ this.subject.fetchFriendsList(false);
+
+ expect(this.subject.restoreWindowsState).not.toHaveBeenCalled();
+ });
+
it('Must update users property when onFriendsListChanged is invoked', () => {
expect(this.subject.users).toBeUndefined();
@@ -262,4 +284,88 @@ describe('NgChat', () => {
expect(this.subject.emitMessageSound).toHaveBeenCalledTimes(1);
});
+
+ it('Should not use local storage persistency if persistWindowsState is disabled', () => {
+ let windows = [new Window()];
+
+ this.subject.persistWindowsState = false;
+
+ spyOn(localStorage, 'setItem');
+
+ this.subject.updateWindowsState(windows);
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('Update windows state exercise', () => {
+ let persistedValue = null;
+
+ spyOn(localStorage, 'setItem').and.callFake((key, value) =>{
+ persistedValue = value;
+ });
+
+ let firstUser = new User();
+ let secondUser = new User();
+
+ firstUser.id = 88;
+ secondUser.id = 99;
+
+ let firstWindow = new Window();
+ let secondWindow = new Window();
+
+ firstWindow.chattingTo = firstUser;
+ secondWindow.chattingTo = secondUser;
+
+ let windows = [firstWindow, secondWindow];
+
+ this.subject.updateWindowsState(windows);
+
+ expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(persistedValue).toBe(JSON.stringify([88, 99]));
+ });
+
+ it('Should not restore windows state from local storage if persistWindowsState is disabled', () => {
+ this.subject.persistWindowsState = false;
+
+ spyOn(this.subject, 'openChatWindow');
+
+ this.subject.restoreWindowsState();
+
+ expect(this.subject.openChatWindow).not.toHaveBeenCalled();
+ });
+
+ it('Restore windows state exercise', () => {
+ let firstUser = new User();
+ let secondUser = new User();
+
+ firstUser.id = 88;
+ secondUser.id = 99;
+
+ localStorage.setItem(this.subject.localStorageKey, JSON.stringify([firstUser.id, secondUser.id]));
+
+ this.subject.users = [firstUser, secondUser];
+ let pushedUsers = [];
+
+ spyOn(this.subject, 'openChatWindow').and.callFake((user) => {
+ pushedUsers.push(user);
+ });
+
+ this.subject.restoreWindowsState();
+
+ expect(this.subject.openChatWindow).toHaveBeenCalledTimes(2);
+ expect(pushedUsers).not.toBeNull();
+ expect(pushedUsers.length).toBe(2);
+ expect(pushedUsers[0]).toBe(firstUser);
+ expect(pushedUsers[1]).toBe(secondUser);
+ });
+
+ it('Must invoke window state update when closing a chat window', () => {
+ this.subject.windows = [new Window()];
+
+ spyOn(this.subject, 'updateWindowsState');
+
+ let result = this.subject.onCloseChatWindow(this.subject.windows[0]);
+
+ expect(this.subject.updateWindowsState).toHaveBeenCalledTimes(1);
+ });
});