1 |
|
2 | import { addEventOnce, removeEvent, addEvent } from './utils/DOM/events';
|
3 | import { BunnyDate } from './BunnyDate';
|
4 |
|
5 | export const MessengerConfig = {
|
6 |
|
7 |
|
8 | storageKeyCurrentUser: 'bunny.messenger.currentChannelUser',
|
9 | storageKeyMessages: 'bunny.messenger.messages',
|
10 | storageKeyAuthUserId: 'bunny.messenger.authUserId',
|
11 | storageKeyCacheDate: 'bunny.messenger.dateCached',
|
12 |
|
13 | messagesPerPage: 20,
|
14 | checkForNewMessagesInterval: 10,
|
15 | cacheLifetime: 3,
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | initAttribute: 'data-messenger',
|
23 |
|
24 |
|
25 | id: 'pm',
|
26 | idName: 'pm_name',
|
27 | idClose: 'pm_close',
|
28 | idBody: 'pm_body',
|
29 | idInput: 'pm_input',
|
30 |
|
31 | idNotificationsBtn: 'pm_notification_btn',
|
32 | attrUnread: 'new',
|
33 | tagNotification: 'PMNOTIFICATION',
|
34 |
|
35 | classActive: 'active'
|
36 |
|
37 | };
|
38 |
|
39 | export const MessengerUI = {
|
40 |
|
41 | Config: MessengerConfig,
|
42 |
|
43 |
|
44 |
|
45 | getBlock() {
|
46 | return document.getElementById(this.Config.id) || false;
|
47 | },
|
48 |
|
49 | getBody() {
|
50 | return document.getElementById(this.Config.idBody) || false;
|
51 | },
|
52 |
|
53 | getNameBlock() {
|
54 | return document.getElementById(this.Config.idName) || false;
|
55 | },
|
56 |
|
57 | getToggleButtons(container = document) {
|
58 | return container.querySelectorAll('[' + this.Config.initAttribute + ']') || false;
|
59 | },
|
60 |
|
61 | getToggleButtonUserId(btn) {
|
62 | const userId = btn.getAttribute(this.Config.initAttribute);
|
63 | if (userId === undefined || userId === '') {
|
64 | throw new Error(`Bunny Messenger: toggle button should have a non-empty attribute "${this.Config.initAttribute}" value representing ID of the second user`)
|
65 | }
|
66 | return userId;
|
67 | },
|
68 |
|
69 | getCloseBtn() {
|
70 | return document.getElementById(this.Config.idClose) || false;
|
71 | },
|
72 |
|
73 | getInput() {
|
74 | return document.getElementById(this.Config.idInput) || false;
|
75 | },
|
76 |
|
77 | getNotificationsBtn() {
|
78 | return document.getElementById(this.Config.idNotificationsBtn) || false;
|
79 | },
|
80 |
|
81 | isNotificationElement(element) {
|
82 | return element.tagName === this.Config.tagNotification;
|
83 | },
|
84 |
|
85 | isNotificationUnread(btn) {
|
86 | return btn.hasAttribute(this.Config.attrUnread);
|
87 | },
|
88 |
|
89 | markNotificationAsRead(btn) {
|
90 | btn.removeAttribute(this.Config.attrUnread);
|
91 | this.decreaseNotificationCounter();
|
92 | },
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | clearBody() {
|
99 | this.getBody().innerHTML = '';
|
100 | },
|
101 |
|
102 | setBlockUserName(name) {
|
103 | this.getNameBlock().textContent = name;
|
104 | },
|
105 |
|
106 | appendAnswer(id, text) {
|
107 | const msg = this.createAnswer(id, text);
|
108 | this.getBody().appendChild(msg);
|
109 | this.scrollToBodyBottom();
|
110 | },
|
111 |
|
112 | appendMessages(messages, currentUser, after = true) {
|
113 | const f = this.createAllMessages(messages, currentUser);
|
114 | const body = this.getBody();
|
115 | if (after) {
|
116 | body.appendChild(f);
|
117 | this.scrollToBodyBottom();
|
118 | } else {
|
119 | const firstChild = body.firstChild;
|
120 | const originalOffsetTop = firstChild.offsetTop;
|
121 | body.insertBefore(f, firstChild);
|
122 | const newOffsetTop = firstChild.offsetTop;
|
123 | const delta = newOffsetTop - originalOffsetTop + body.scrollTop;
|
124 | body.scrollTop = delta;
|
125 | }
|
126 | },
|
127 |
|
128 | openBlockAndAppendMessages(messages, currentUser, focusInput = false) {
|
129 | this.setBlockUserName(currentUser.name);
|
130 | this.showBlock();
|
131 | this.appendMessages(messages, currentUser);
|
132 | if (focusInput) {
|
133 | this.getInput().focus();
|
134 | }
|
135 | },
|
136 |
|
137 | decreaseNotificationCounter() {
|
138 | const btn = this.getNotificationsBtn();
|
139 | if (btn) {
|
140 | if (btn.dataset.count > 1) {
|
141 | btn.dataset.count--;
|
142 | } else {
|
143 | delete btn.dataset.count;
|
144 | }
|
145 | }
|
146 | },
|
147 |
|
148 | increaseNotificationCounter() {
|
149 | const btn = this.getNotificationsBtn();
|
150 | if (btn) {
|
151 | if (btn.dataset.count === undefined) {
|
152 | btn.dataset.count = 1;
|
153 | } else {
|
154 | btn.dataset.count++;
|
155 | }
|
156 | }
|
157 | },
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | blinkBlock() {
|
164 | this.getBlock().classList.add(this.Config.classActive);
|
165 | setTimeout(() => {
|
166 | this.getBlock().classList.remove(this.Config.classActive);
|
167 | }, 1000);
|
168 | },
|
169 |
|
170 | showBlock() {
|
171 | this.getBlock().removeAttribute('hidden');
|
172 | },
|
173 |
|
174 | hideBlock() {
|
175 | this.getBlock().setAttribute('hidden', '');
|
176 | },
|
177 |
|
178 | scrollToBodyBottom() {
|
179 | const body = this.getBody();
|
180 | body.scrollTop = body.scrollHeight;
|
181 | },
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | createMessage(id, text, sender, image = true) {
|
189 | const msg = document.createElement('message');
|
190 | msg.dataset.id = id;
|
191 | const imgWrapper = document.createElement('div');
|
192 | if (image) {
|
193 | const link = document.createElement('a');
|
194 | link.setAttribute('href', sender.profileUrl);
|
195 | const img = new Image;
|
196 | img.src = sender.photoUrl;
|
197 | link.appendChild(img);
|
198 | imgWrapper.appendChild(link);
|
199 | }
|
200 | const p = document.createElement('p');
|
201 | p.textContent = text;
|
202 | p.innerHTML = p.innerHTML.replace(/\r?\n/g, '<br />');
|
203 | msg.appendChild(imgWrapper);
|
204 | msg.appendChild(p);
|
205 | return msg;
|
206 | },
|
207 |
|
208 | createTime(time) {
|
209 | const t = document.createElement('time');
|
210 | t.textContent = time;
|
211 | return t;
|
212 | },
|
213 |
|
214 | createAnswer(id, text) {
|
215 | const msg = document.createElement('message');
|
216 | msg.dataset.id = id;
|
217 | msg.setAttribute('type', 'answer');
|
218 | const p = document.createElement('p');
|
219 | p.textContent = text;
|
220 | p.innerHTML = p.innerHTML.replace(/\r?\n/g, '<br />');
|
221 | msg.appendChild(p);
|
222 | return msg;
|
223 | },
|
224 |
|
225 | createAllMessages(messages, channelCurrentUser) {
|
226 | const f = document.createDocumentFragment();
|
227 | let prevDateRendered = null;
|
228 | messages.forEach(message => {
|
229 |
|
230 | const date = BunnyDate.toEuDate(new Date(message.dateCreated));
|
231 | if (prevDateRendered !== date) {
|
232 | const t = this.createTime(date);
|
233 | f.appendChild(t);
|
234 | prevDateRendered = date;
|
235 | }
|
236 |
|
237 | let msg;
|
238 | if (Server.user.id == message.senderId) {
|
239 | msg = this.createAnswer(message.id, message.message);
|
240 | } else {
|
241 | msg = this.createMessage(message.id, message.message, channelCurrentUser);
|
242 | }
|
243 |
|
244 | f.appendChild(msg);
|
245 | });
|
246 | return f;
|
247 | }
|
248 |
|
249 | };
|
250 |
|
251 | export const MessengerEvents = {
|
252 |
|
253 | UI: MessengerUI,
|
254 | Config: MessengerConfig,
|
255 |
|
256 | |
257 |
|
258 |
|
259 | checkForMessagesIntervalId: null,
|
260 |
|
261 | addMessageEventId: null,
|
262 | blockCloseEventId: null,
|
263 | loadOlderMessagesEventId: null,
|
264 |
|
265 |
|
266 | addToggleButtonsEvent(handler, container = document) {
|
267 | const btns = this.UI.getToggleButtons(container);
|
268 | [].forEach.call(btns, btn => {
|
269 | btn.addEventListener('click', (event) => {
|
270 | handler(this.UI.getToggleButtonUserId(btn), event);
|
271 | })
|
272 | });
|
273 | },
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | addBlockCloseEvent(handler) {
|
280 | this.blockCloseEventId = addEvent(this.UI.getCloseBtn(), 'click', handler);
|
281 | },
|
282 |
|
283 | removeBlockCloseEvent() {
|
284 | this.blockCloseEventId = removeEvent(this.UI.getCloseBtn(), 'click', this.blockCloseEventId);
|
285 | },
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | addIntervalCheckForMessages(handler) {
|
292 | const interval = this.Config.checkForNewMessagesInterval * 1000;
|
293 | this.checkForMessagesIntervalId = setInterval(handler, interval);
|
294 | },
|
295 |
|
296 | removeIntervalCheckForMessages() {
|
297 | clearInterval(this.checkForMessagesIntervalId);
|
298 | this.checkForMessagesIntervalId = null;
|
299 | },
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | addMessageEvent(handler) {
|
306 | const input = this.UI.getInput();
|
307 | this.addMessageEventId = addEventOnce(input, 'keydown', e => {
|
308 | if (!e.shiftKey && e.keyCode === KEY_ENTER) {
|
309 | e.preventDefault();
|
310 | handler(input.value);
|
311 | input.value = '';
|
312 | }
|
313 | }, 100)
|
314 | },
|
315 |
|
316 | removeMessageEvent() {
|
317 | const input = this.UI.getInput();
|
318 | this.addMessageEventId = removeEvent(input, 'keydown', this.addMessageEventId);
|
319 | },
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | addLoadOlderMessagesEvent(handler) {
|
325 | const body = this.UI.getBody();
|
326 | this.loadOlderMessagesEventId = addEventOnce(body, 'scroll', e => {
|
327 | if (body.scrollTop < 100) {
|
328 | handler();
|
329 | }
|
330 | }, 100);
|
331 | },
|
332 |
|
333 | removeLoadOlderMessagesEvent() {
|
334 | this.loadOlderMessagesEventId = removeEvent(this.UI.getBody(), 'scroll', this.scrollEventId);
|
335 | }
|
336 |
|
337 | };
|
338 |
|
339 | export const Messenger = {
|
340 |
|
341 | Config: MessengerConfig,
|
342 | UI: MessengerUI,
|
343 | Events: MessengerEvents,
|
344 |
|
345 | Model: null,
|
346 | curPage: 1,
|
347 |
|
348 | checkForMessagesHandler: null,
|
349 |
|
350 | handlers: {
|
351 | messagesReceived: [],
|
352 | messageSent: []
|
353 | },
|
354 |
|
355 | init(Model, authUserId) {
|
356 | if (this.testInit() === false) {
|
357 | return false;
|
358 | }
|
359 | this.testModel(Model);
|
360 | this.Model = Model;
|
361 | this.authUserId = authUserId;
|
362 |
|
363 | this.initToggles();
|
364 |
|
365 | this.checkChannelAuth(authUserId);
|
366 |
|
367 | if (this.isChannelInitialized()) {
|
368 | this.open();
|
369 | }
|
370 | return true;
|
371 | },
|
372 |
|
373 | checkChannelAuth(authUserId) {
|
374 | const storedAuthUserId = this.getAuthUserId();
|
375 | if (storedAuthUserId !== null && storedAuthUserId != authUserId) {
|
376 |
|
377 | this.destroyChannel();
|
378 | }
|
379 | },
|
380 |
|
381 | initToggles(container = document) {
|
382 | this.Events.addToggleButtonsEvent(this.toggleChannel.bind(this), container)
|
383 | },
|
384 |
|
385 |
|
386 | extendConfig(Config) {
|
387 | const NewConfig = Object.assign(this.Config, Config);
|
388 | this.Config = NewConfig;
|
389 | this.UI.Config = NewConfig;
|
390 | this.Events.Config = NewConfig;
|
391 | },
|
392 |
|
393 | extendUI(UI) {
|
394 | const NewUI = Object.assign(this.UI, UI);
|
395 | this.UI = NewUI;
|
396 | this.Events.UI = NewUI;
|
397 | },
|
398 |
|
399 | extendEvents(Events) {
|
400 | const NewEvents = Object.assign(this.Events, Events);
|
401 | this.Events = NewEvents;
|
402 | },
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | onMessagesReceived(callback) {
|
408 | this.handlers.messagesReceived.push(callback);
|
409 | },
|
410 |
|
411 | onMessageSent(callback) {
|
412 | this.handlers.messageSent.push(callback);
|
413 | },
|
414 |
|
415 | testInit() {
|
416 | return this.UI.getBlock() !== false;
|
417 | },
|
418 |
|
419 | testModel(Model) {
|
420 | if (typeof Model.read !== 'function'
|
421 | && typeof Model.check !== 'function'
|
422 | && typeof Model.create !== 'function')
|
423 | {
|
424 | throw new Error('Bunny Messenger: Model passed to Messenger.init() does not have methods read(), check() and create()');
|
425 | }
|
426 | },
|
427 |
|
428 |
|
429 | toggleChannel(userId, event) {
|
430 | if (this.isChannelInitialized()) {
|
431 | const curUser = this.getChannelCurrentUser();
|
432 | this.close();
|
433 | if (curUser.id != userId) {
|
434 | this.open(userId, true);
|
435 | }
|
436 | } else {
|
437 | this.open(userId, true);
|
438 | }
|
439 |
|
440 | const el = event.currentTarget;
|
441 | if (this.UI.isNotificationElement(el)) {
|
442 | if (this.UI.isNotificationUnread(el)) {
|
443 | this.UI.markNotificationAsRead(el);
|
444 | }
|
445 | }
|
446 | },
|
447 |
|
448 | checkForNewMessages() {
|
449 | const curUser = this.getChannelCurrentUser();
|
450 | this.Model.check(curUser.id, this.getChannelLastMessageId()).then(res => {
|
451 | if (res.messages.length > 0) {
|
452 | this.handlers.messagesReceived.forEach(messagesReceived => messagesReceived(res.messages));
|
453 | this.setChannelMessages(res.messages);
|
454 | this.UI.appendMessages(res.messages, curUser);
|
455 | this.UI.blinkBlock();
|
456 | }
|
457 | });
|
458 | },
|
459 |
|
460 | loadOlderMessages() {
|
461 | const curUser = this.getChannelCurrentUser();
|
462 | this.Model.read(curUser.id, this.curPage + 1).then(res => {
|
463 | if (res.messages.length > 0) {
|
464 | this.curPage = this.curPage + 1;
|
465 | this.UI.appendMessages(res.messages.reverse(), curUser, false);
|
466 | }
|
467 | });
|
468 | },
|
469 |
|
470 |
|
471 |
|
472 | sendMessage(userId, text) {
|
473 | this.Model.create(userId, text).then(res => {
|
474 | this.handlers.messageSent.forEach(messageSent => messageSent(res));
|
475 | this.setChannelMessages(res);
|
476 | this.UI.appendAnswer(res.id, text);
|
477 | });
|
478 | },
|
479 |
|
480 | sendMessageToCurrentUser(text) {
|
481 | const curUser = this.getChannelCurrentUser();
|
482 | this.sendMessage(curUser.id, text);
|
483 | },
|
484 |
|
485 |
|
486 | |
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | open(userId = null, focusInput = false) {
|
495 | this.initChannel(userId).then(messages => {
|
496 | const channelCurrentUser = this.getChannelCurrentUser();
|
497 | this.UI.openBlockAndAppendMessages(messages, channelCurrentUser, focusInput);
|
498 | this.Events.addIntervalCheckForMessages(this.checkForNewMessages.bind(this));
|
499 | this.Events.addBlockCloseEvent(this.close.bind(this));
|
500 | this.Events.addMessageEvent(this.sendMessageToCurrentUser.bind(this));
|
501 | this.Events.addLoadOlderMessagesEvent(this.loadOlderMessages.bind(this));
|
502 | });
|
503 | },
|
504 |
|
505 | close() {
|
506 | this.curPage = 1;
|
507 | this.UI.hideBlock();
|
508 | this.destroyChannel();
|
509 | this.UI.clearBody();
|
510 | this.Events.removeIntervalCheckForMessages();
|
511 | this.Events.removeBlockCloseEvent();
|
512 | this.Events.removeMessageEvent();
|
513 | this.Events.removeLoadOlderMessagesEvent();
|
514 | },
|
515 |
|
516 |
|
517 |
|
518 |
|
519 | |
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 | initChannel(userId = null) {
|
540 | return new Promise(resolve => {
|
541 | if (userId === null) {
|
542 |
|
543 |
|
544 |
|
545 | if (this.isChannelCacheExpired()) {
|
546 | userId = this.getChannelCurrentUser().id;
|
547 | this._initChannelLoad(userId, resolve);
|
548 | } else {
|
549 |
|
550 | const messages = this.getChannelMessages();
|
551 | resolve(messages);
|
552 | }
|
553 | } else {
|
554 | this._initChannelLoad(userId, resolve);
|
555 | }
|
556 | });
|
557 | },
|
558 |
|
559 | _initChannelLoad(userId, resolve) {
|
560 | this.Model.read(userId).then(res => {
|
561 | this.setAuthUserId(this.authUserId);
|
562 | this.setChannelCurrentUser(res.user);
|
563 | this.setChannelMessages(res.messages.reverse());
|
564 | resolve(res.messages);
|
565 | });
|
566 | },
|
567 |
|
568 | isChannelInitialized() {
|
569 | return localStorage.getItem(this.Config.storageKeyCurrentUser) !== null;
|
570 | },
|
571 |
|
572 | getChannelCurrentUser() {
|
573 | return JSON.parse(localStorage.getItem(this.Config.storageKeyCurrentUser));
|
574 | },
|
575 |
|
576 | getChannelMessages() {
|
577 | return JSON.parse(localStorage.getItem(this.Config.storageKeyMessages));
|
578 | },
|
579 |
|
580 | getChannelLastMessageId() {
|
581 | const messages = this.getChannelMessages();
|
582 | return messages[messages.length - 1].id;
|
583 | },
|
584 |
|
585 | setAuthUserId(authUserId) {
|
586 | return localStorage.setItem(this.Config.storageKeyAuthUserId, authUserId);
|
587 | },
|
588 |
|
589 | getAuthUserId() {
|
590 | return localStorage.getItem(this.Config.storageKeyAuthUserId);
|
591 | },
|
592 |
|
593 | setChannelCurrentUser(user) {
|
594 | localStorage.setItem(this.Config.storageKeyCurrentUser, JSON.stringify(user));
|
595 | },
|
596 |
|
597 | updateChannelDateCached() {
|
598 | localStorage.setItem(this.Config.storageKeyCacheDate, (new Date).toISOString());
|
599 | },
|
600 |
|
601 | getChannelDateCached() {
|
602 | return localStorage.getItem(this.Config.storageKeyCacheDate);
|
603 | },
|
604 |
|
605 | isChannelCacheExpired() {
|
606 | const now = new Date();
|
607 | now.setHours(now.getHours() - this.Config.cacheLifetime);
|
608 | const dateCached = new Date(this.getChannelDateCached());
|
609 | return now > dateCached;
|
610 | },
|
611 |
|
612 | _addMessageToQueue(queue, message) {
|
613 | let newQueue = Object.create(queue);
|
614 | if (newQueue.length >= this.Config.messagesPerPage) {
|
615 |
|
616 | newQueue.shift();
|
617 | }
|
618 | newQueue.push(message);
|
619 | return newQueue;
|
620 | },
|
621 |
|
622 | |
623 |
|
624 |
|
625 |
|
626 |
|
627 |
|
628 |
|
629 |
|
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
635 | setChannelMessages(messages) {
|
636 | let currentMessages = this.getChannelMessages();
|
637 | if (currentMessages === null) {
|
638 | currentMessages = [];
|
639 | }
|
640 |
|
641 | if (Array.isArray(messages)) {
|
642 | messages.forEach(message => {
|
643 | currentMessages = this._addMessageToQueue(currentMessages, message);
|
644 | });
|
645 | } else {
|
646 | currentMessages = this._addMessageToQueue(currentMessages, messages);
|
647 | }
|
648 |
|
649 | localStorage.setItem(this.Config.storageKeyMessages, JSON.stringify(currentMessages));
|
650 | this.updateChannelDateCached();
|
651 | return currentMessages;
|
652 | },
|
653 |
|
654 | |
655 |
|
656 |
|
657 | destroyChannel() {
|
658 | localStorage.removeItem(this.Config.storageKeyCurrentUser);
|
659 | localStorage.removeItem(this.Config.storageKeyMessages);
|
660 | localStorage.removeItem(this.Config.storageKeyAuthUserId);
|
661 | localStorage.removeItem(this.Config.storageKeyCacheDate);
|
662 | }
|
663 |
|
664 | };
|