Skip to content
This repository was archived by the owner on Nov 18, 2024. It is now read-only.

Commit ee6e9fb

Browse files
committed
Use local storage for conversation history
1 parent d19498e commit ee6e9fb

File tree

5 files changed

+148
-57
lines changed

5 files changed

+148
-57
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ module.exports = {
1313
},
1414
},
1515
},
16+
setupFiles: ['jest-localstorage-mock'],
1617
};

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"documentation": "^12.0.2",
2525
"husky": "^3.0.0",
2626
"jest": "^24.8.0",
27+
"jest-localstorage-mock": "^2.4.0",
2728
"mock-socket": "^9.0.0",
2829
"prettier": "^1.14.2",
2930
"pretty-quick": "^1.6.0",

src/index.js

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,41 @@ const getDefaultPayload = () => ({
1414
},
1515
});
1616

17-
const store = {
17+
export const chatHistory = {
18+
get: () => {
19+
const history = localStorage.getItem('TwylaWidget__chat_history');
20+
21+
let parsedHistory;
22+
23+
try {
24+
parsedHistory = JSON.parse(history);
25+
} catch {
26+
chatHistory.clean();
27+
return [];
28+
}
29+
30+
if (!Array.isArray(parsedHistory)) {
31+
chatHistory.clean();
32+
return [];
33+
}
34+
35+
return parsedHistory;
36+
},
37+
38+
set: value => localStorage.setItem('TwylaWidget__chat_history', JSON.stringify(value)),
39+
40+
clean: () => chatHistory.set([]),
41+
42+
push: value => {
43+
const history = chatHistory.get();
44+
45+
history.push(value);
46+
47+
chatHistory.set(history);
48+
},
49+
};
50+
51+
export const store = {
1852
configuration: {
1953
apiKey: undefined,
2054
hookURL: undefined,
@@ -30,6 +64,7 @@ const store = {
3064
},
3165

3266
messageQueue: [],
67+
history: undefined,
3368

3469
connected: false,
3570
onConnectionChangeCallback: f => f,
@@ -85,22 +120,24 @@ export const notificationsChannelURLFromHookURL = hookURL => {
85120
const handleIncoming = event => {
86121
const data = JSON.parse(event.data);
87122

88-
// init response
89-
// identified by presence of user_id_cookie
90-
if (data.user_id_cookie) {
123+
const isInitResponse = data.user_id_cookie;
124+
const isErrorResponse = data.error;
125+
126+
if (isInitResponse) {
91127
if (!store.userId || store.userId !== data.user_id_cookie) {
92128
store.userId = data.user_id_cookie;
93129
Cookies.set(COOKIE_NAME, store.userId);
94130

131+
chatHistory.clean();
132+
95133
if (store.promises.getUserId) {
96134
store.promises.getUserId.resolve(store.userId);
97135
store.promises.getUserId = undefined;
98136
}
99137
}
100138

101-
// if messages were queued then they need to be appended after history
102139
if (store.promises.init.resolve) {
103-
const history = cleanHistory(data.history);
140+
const history = cleanHistory(chatHistory.get());
104141

105142
store.messageQueue.forEach(message => {
106143
if (message !== CONVERSATION_STARTER) {
@@ -110,16 +147,17 @@ const handleIncoming = event => {
110147

111148
getBotName()
112149
.then(response => {
113-
// before getBotName resolves,
114-
// if clearSession was called and init will be undefined
115-
if (store.promises.init.resolve) {
116-
store.promises.init.resolve({
117-
botName: response.name,
118-
history,
119-
});
120-
121-
clearInitPromise();
150+
// if clearSession was called before getBotName resolves
151+
if (!store.promises.init.resolve) {
152+
return;
122153
}
154+
155+
store.promises.init.resolve({
156+
botName: response.name,
157+
history,
158+
});
159+
160+
clearInitPromise();
123161
})
124162
.catch(error => {
125163
handleError('Get metadata error:', error);
@@ -135,24 +173,35 @@ const handleIncoming = event => {
135173

136174
if (store.messageQueue.length) {
137175
const toPost = [...store.messageQueue];
138-
store.messageQueue = [];
139176

140177
toPost.forEach(send);
178+
179+
store.messageQueue = [];
141180
}
142-
} else if (data.error) {
181+
} else if (isErrorResponse) {
143182
store.userId = null;
144183
} else {
145184
const isMessageJSON = isJSON(data.emission);
185+
let textFromBot;
186+
146187
if (isMessageJSON.result) {
147188
const template = isMessageJSON.json;
189+
148190
if (template.template_type === TemplateTypes.FB_MESSENGER_BUTTON) {
149-
store.onMessage(template.payload.text);
191+
textFromBot = template.payload.text;
192+
193+
store.onMessage(textFromBot);
150194
} else if (template.template_type === TemplateTypes.FB_MESSENGER_QUICK_REPLY) {
151-
store.onMessage(template.text);
195+
textFromBot = template.text;
196+
197+
store.onMessage(textFromBot);
152198
}
153199
}
154200

155-
store.onMessage(data.emission);
201+
textFromBot = data.emission;
202+
203+
store.onMessage(textFromBot);
204+
chatHistory.push({ made_by: 'chatbot', content: textFromBot });
156205
}
157206
};
158207

@@ -244,6 +293,8 @@ API.send = message => {
244293
userId: store.userId,
245294
payload: store.payload,
246295
});
296+
297+
chatHistory.push({ made_by: 'user', content: message });
247298
} else {
248299
if (store.messageQueue.length === 1 && store.messageQueue[0] === CONVERSATION_STARTER) {
249300
store.messageQueue = [];
@@ -381,12 +432,14 @@ API.getBotName = (hookURL = store.configuration.hookURL, apiKey = store.configur
381432
API.endSession = () => {
382433
// toggle session flag first so socket doesn't reconnect
383434
store.inSession = false;
435+
384436
if (store.socket) store.socket.close();
385437

386438
store.onMessage = f => f;
387439
store.promises.getUserId = null;
388440
store.messageQueue = [];
389441
store.onConnectionChangeCallback = f => f;
442+
390443
clearInitPromise();
391444
};
392445

@@ -396,11 +449,14 @@ API.endSession = () => {
396449
*/
397450
API.clearSession = () => {
398451
API.endSession();
452+
399453
store.userId = null;
400454
store.configuration = {};
401455
store.notificationsChannelURL = null;
402456
store.payload = getDefaultPayload();
457+
403458
Cookies.remove(COOKIE_NAME);
459+
chatHistory.clean();
404460
};
405461

406462
export { version } from '../package.json';

src/tests/api.test.js

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,23 @@ import {
1313
setConversationLogging,
1414
} from '../index';
1515
import { WebSocket, Server } from 'mock-socket';
16+
import { store, chatHistory } from '../index';
1617
import { CONVERSATION_STARTER } from '../constants';
1718

1819
global.fetch = jest.fn().mockImplementation(url => {
1920
let responseJSON = f => f;
21+
2022
if (url === 'https://api.demo.twyla.io/widget-hook/massive-dynamics/templates?key=fakeApiKey') {
2123
responseJSON = () => ({ name: 'Templates' });
2224
}
25+
2326
return new Promise(resolve => {
2427
resolve({ ok: true, json: responseJSON });
2528
});
2629
});
2730

2831
global.WebSocket = WebSocket;
2932

30-
describe('API error test', () => {
31-
const consoleError = console.error;
32-
33-
beforeEach(() => {
34-
console.error = jest.fn();
35-
});
36-
37-
afterEach(() => {
38-
console.error = consoleError;
39-
});
40-
41-
test('Rejects init for invalid Hook URL', done => {
42-
const errorConfiguration = {
43-
apiKey: 'fakeApiKey',
44-
hookURL: '',
45-
};
46-
47-
init(errorConfiguration).then(
48-
f => f,
49-
error => {
50-
expect(error).toEqual('Invalid hook URL');
51-
expect(console.error).toHaveBeenCalled();
52-
done();
53-
}
54-
);
55-
});
56-
});
57-
5833
describe('API test', () => {
5934
let mockServer;
6035
let socketRef;
@@ -123,15 +98,14 @@ describe('API test', () => {
12398
socket.on('message', data => {
12499
expect(data).toEqual(
125100
JSON.stringify({
126-
user_id_cookie: null,
101+
user_id_cookie: store.userId,
127102
api_key: configuration.apiKey,
128103
})
129104
);
130105

131106
socket.send(
132107
JSON.stringify({
133108
user_id_cookie: fakeCookie,
134-
history: [{ made_by: 'user', content: 'a' }, { made_by: 'chatbot', content: 'b' }],
135109
})
136110
);
137111
});
@@ -141,19 +115,18 @@ describe('API test', () => {
141115
afterEach(() => {
142116
mockServer.stop();
143117

144-
clearSession(1000);
118+
clearSession();
145119
});
146120

147-
test('promise params', done => {
121+
test('promise params, new session', done => {
148122
const queuedMsg = 'queuedMessage';
149123

124+
chatHistory.push({ content: 'a', made_by: 'user' });
125+
chatHistory.push({ content: 'b', made_by: 'chatbot' });
126+
150127
init(configuration, onMessage).then(({ botName, history }) => {
151128
expect(botName).toEqual('Templates');
152-
expect(history).toEqual([
153-
{ made_by: 'user', content: 'a' },
154-
{ made_by: 'chatbot', content: 'b' },
155-
{ content: 'queuedMessage', made_by: 'user' },
156-
]);
129+
expect(history).toEqual([{ content: queuedMsg, made_by: 'user' }]);
157130

158131
expect(global.fetch.mock.calls.length).toEqual(2);
159132
expect(global.fetch.mock.calls[1][0]).toEqual(configuration.hookURL);
@@ -165,6 +138,32 @@ describe('API test', () => {
165138
send(queuedMsg);
166139
});
167140

141+
test('promise params, existing session', done => {
142+
const queuedMsg = 'queuedMessage';
143+
144+
store.userId = fakeCookie;
145+
146+
chatHistory.push({ content: 'a', made_by: 'user' });
147+
chatHistory.push({ content: 'b', made_by: 'chatbot' });
148+
149+
init(configuration, onMessage).then(({ botName, history }) => {
150+
expect(botName).toEqual('Templates');
151+
expect(history).toEqual([
152+
{ content: 'a', made_by: 'user' },
153+
{ content: 'b', made_by: 'chatbot' },
154+
{ content: queuedMsg, made_by: 'user' },
155+
]);
156+
157+
expect(global.fetch.mock.calls.length).toEqual(2);
158+
expect(global.fetch.mock.calls[0][0]).toEqual(configuration.hookURL);
159+
expect(global.fetch.mock.calls[0][1]).toEqual(postMsg(queuedMsg));
160+
161+
done();
162+
});
163+
164+
send(queuedMsg);
165+
});
166+
168167
test('onConnectionChangeCallback', done => {
169168
init(configuration, onMessage).then(() => {
170169
expect(connectionChangeListener.mock.calls.length).toBeTruthy();
@@ -345,3 +344,31 @@ describe('API test', () => {
345344
});
346345
});
347346
});
347+
348+
describe('API error test', () => {
349+
const consoleError = console.error;
350+
351+
beforeEach(() => {
352+
console.error = jest.fn();
353+
});
354+
355+
afterEach(() => {
356+
console.error = consoleError;
357+
});
358+
359+
test('Rejects init for invalid Hook URL', done => {
360+
const errorConfiguration = {
361+
apiKey: 'fakeApiKey',
362+
hookURL: '',
363+
};
364+
365+
init(errorConfiguration).then(
366+
f => f,
367+
error => {
368+
expect(error).toEqual('Invalid hook URL');
369+
expect(console.error).toHaveBeenCalled();
370+
done();
371+
}
372+
);
373+
});
374+
});

0 commit comments

Comments
 (0)