Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * This module will take 2 view states from the message_drawer_view_conversation
18
 * module and generate a patch that can be given to the
19
 * message_drawer_view_conversation_renderer module to update the UI.
20
 *
21
 * This module should never modify either state. It's purely a read only
22
 * module.
23
 *
24
 * @module     core_message/message_drawer_view_conversation_patcher
25
 * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
26
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
define(
29
[
30
    'jquery',
31
    'core/user_date',
32
    'core_message/message_drawer_view_conversation_constants'
33
],
34
function(
35
    $,
36
    UserDate,
37
    Constants
38
) {
39
    /**
40
     * Sort messages by day.
41
     *
42
     * @param  {Array} messages The list of messages to sort.
43
     * @param  {Number} midnight User's midnight timestamp.
44
     * @return {Array} messages sorted by day.
45
     */
46
    var sortMessagesByDay = function(messages, midnight) {
47
        var messagesByDay = messages.reduce(function(carry, message) {
48
            var timeCreated = message.timeCreated ? message.timeCreated : midnight;
49
            var dayTimestamp = UserDate.getUserMidnightForTimestamp(timeCreated, midnight);
50
 
51
            if (carry.hasOwnProperty(dayTimestamp)) {
52
                carry[dayTimestamp].push(message);
53
            } else {
54
                carry[dayTimestamp] = [message];
55
            }
56
 
57
            return carry;
58
        }, {});
59
 
60
        return Object.keys(messagesByDay).map(function(dayTimestamp) {
61
            return {
62
                timestamp: dayTimestamp,
63
                messages: messagesByDay[dayTimestamp]
64
            };
65
        });
66
    };
67
 
68
    /**
69
     * Diff 2 arrays using a match function
70
     *
71
     * @param  {Array} a The first array.
72
     * @param  {Array} b The second array.
73
     * @param  {Function} matchFunction Function used for matching array items.
74
     * @return {Object} Object containing array items missing from a, array items missing from b
75
     * and matches
76
     */
77
    var diffArrays = function(a, b, matchFunction) {
78
        // Make copy of it.
79
        b = b.slice();
80
        var missingFromA = [];
81
        var missingFromB = [];
82
        var matches = [];
83
 
84
        a.forEach(function(current) {
85
            var found = false;
86
            var index = 0;
87
 
88
            for (; index < b.length; index++) {
89
                var next = b[index];
90
 
91
                if (matchFunction(current, next)) {
92
                    found = true;
93
                    matches.push({
94
                        a: current,
95
                        b: next
96
                    });
97
                    break;
98
                }
99
            }
100
 
101
            if (found) {
102
                // This day has been processed so removed it from the list.
103
                b.splice(index, 1);
104
            } else {
105
                // If we couldn't find it in the next messages then it means
106
                // it needs to be added.
107
                missingFromB.push(current);
108
            }
109
        });
110
 
111
        missingFromA = b;
112
 
113
        return {
114
            missingFromA: missingFromA,
115
            missingFromB: missingFromB,
116
            matches: matches
117
        };
118
    };
119
 
120
    /**
121
     * Find an element in a array based on a matching function.
122
     *
123
     * @param  {array} array Array to search.
124
     * @param  {Function} breakFunction Function to run on array item.
125
     * @return {*} The array item.
126
     */
127
    var findPositionInArray = function(array, breakFunction) {
128
        var before = null;
129
 
130
        for (var i = 0; i < array.length; i++) {
131
            var candidate = array[i];
132
 
133
            if (breakFunction(candidate)) {
134
                return candidate;
135
            }
136
        }
137
 
138
        return before;
139
    };
140
 
141
    /**
142
     * Check if 2 arrays are equal.
143
     *
144
     * @param  {Array} a The first array.
145
     * @param  {Array} b The second array.
146
     * @return {Boolean} Are arrays equal.
147
     */
148
    var isArrayEqual = function(a, b) {
149
        // Make shallow copies so that we don't mess with the array sorting.
150
        a = a.slice();
151
        b = b.slice();
152
        a.sort();
153
        b.sort();
154
        var aLength = a.length;
155
        var bLength = b.length;
156
 
157
        if (aLength < 1 && bLength < 1) {
158
            return true;
159
        }
160
 
161
        if (aLength != bLength) {
162
            return false;
163
        }
164
 
165
        return a.every(function(item, index) {
166
            return item == b[index];
167
        });
168
    };
169
 
170
    /**
171
     * Do a shallow check to see if two objects appear to be equal. This should
172
     * only be used for pretty basic objects.
173
     *
174
     * @param {Object} a First object to compare.
175
     * @param {Object} b Second object to compare
176
     * @return {Bool}
177
     */
178
    var isObjectEqual = function(a, b) {
179
        var aKeys = Object.keys(a);
180
        var bKeys = Object.keys(b);
181
 
182
        if (aKeys.length != bKeys.length) {
183
            return false;
184
        }
185
 
186
        return aKeys.every(function(key) {
187
            var aVal = a[key];
188
            var bVal = b[key];
189
            var aType = typeof aVal;
190
            var bType = typeof bVal;
191
            aType = (aVal === null) ? 'null' : aType;
192
            bType = (aVal === null) ? 'null' : bType;
193
            aType = (aType === 'object' && Array.isArray(aType)) ? 'array' : aType;
194
            bType = (bType === 'object' && Array.isArray(bType)) ? 'array' : bType;
195
 
196
            if (aType !== bType) {
197
                return false;
198
            }
199
 
200
            switch (aType) {
201
                case 'object':
202
                    return isObjectEqual(aVal, bVal);
203
                case 'array':
204
                    return isArrayEqual(aVal, bVal);
205
                default:
206
                    return a[key] == b[key];
207
            }
208
        });
209
    };
210
 
211
    /**
212
     * Compare two messages to check if they are equal. This function only checks a subset
213
     * of the message properties which we know will change rather than all properties.
214
     *
215
     * @param {Object} a The first message
216
     * @param {Object} b The second message
217
     * @return {Bool}
218
     */
219
    var isMessageEqual = function(a, b) {
220
        return isObjectEqual(
221
            {
222
                id: a.id,
223
                state: a.sendState,
224
                text: a.text,
225
                timeCreated: a.timeCreated
226
            },
227
            {
228
                id: b.id,
229
                state: b.sendState,
230
                text: b.text,
231
                timeCreated: b.timeCreated
232
            }
233
        );
234
    };
235
 
236
    /**
237
     * Build a patch based on days.
238
     *
239
     * @param  {Object} current Current list current items.
240
     * @param  {Array} remove List of days to remove.
241
     * @param  {Array} add List of days to add.
242
     * @return {Object} Patch with elements to add and remove.
243
     */
244
    var buildDaysPatch = function(current, remove, add) {
245
        return {
246
            remove: remove,
247
            add: add.map(function(day) {
248
                // Any days left over in the "next" list weren't in the "current" list
249
                // so they will need to be added.
250
                var before = findPositionInArray(current, function(candidate) {
251
                    return day.timestamp < candidate.timestamp;
252
                });
253
 
254
                return {
255
                    before: before,
256
                    value: day
257
                };
258
            })
259
        };
260
    };
261
 
262
    /**
263
     * Build the messages patch for each day.
264
     *
265
     * @param {Array} matchingDays Array of old and new messages sorted by day.
266
     * @return {Object} patch.
267
     */
268
    var buildMessagesPatch = function(matchingDays) {
269
        var remove = [];
270
        var add = [];
271
        var update = [];
272
 
273
        // Iterate over the list of days and determine which messages in those days
274
        // have been changed.
275
        matchingDays.forEach(function(days) {
276
            var dayCurrent = days.a;
277
            var dayNext = days.b;
278
            // Find out which messages have changed in this day. This will return a list of messages
279
            // from the current state that couldn't be found in the next state and a list of messages in
280
            // the next state which couldn't be count in the current state.
281
            var messagesDiff = diffArrays(dayCurrent.messages, dayNext.messages, isMessageEqual);
282
            // Take the two arrays (list of messages changed from dayNext and list of messages changed
283
            // from dayCurrent) any work out which messages have been added/removed from the list and
284
            // which messages were just updated.
285
            var patch = diffArrays(
286
                // The messages from dayCurrent.message that weren't in dayNext.messages.
287
                messagesDiff.missingFromB,
288
                // The messages from dayNext.message that weren't in dayCurrent.messages.
289
                messagesDiff.missingFromA,
290
                function(a, b) {
291
                    // This function is going to determine if the messages were
292
                    // added/removed from either list or if they were simply an updated.
293
                    //
294
                    // If the IDs match or it was a state change (i.e. message with a temp
295
                    // ID goes from pending to sent and receives an actual id) then they are
296
                    // the same message which should be an update not an add/remove.
297
                    return a.id == b.id || (a.sendState != b.sendState && a.timeAdded == b.timeAdded);
298
                }
299
            );
300
 
301
            // Any messages from the current state for this day which aren't in the next state
302
            // for this day (i.e. the user deleted the message) means we need to remove them from
303
            // the UI.
304
            remove = remove.concat(patch.missingFromB);
305
 
306
            // Any messages not in the current state for this day which are in the next state
307
            // for this day (i.e. it's a new message) means we need to add it to the UI so work
308
            // out where in the list of messages it should appear (it could be a new message the
309
            // user has sent or older messages loaded as part of the conversation scroll back).
310
            patch.missingFromA.forEach(function(message) {
311
                // By default a null value for before will render the message at the bottom of
312
                // the message UI (i.e. it's the newest message).
313
                var before = null;
314
 
315
                if (message.timeCreated) {
316
                    // If this message has a time created then find where it sits in the list of
317
                    // message to insert it into the correct position.
318
                    before = findPositionInArray(dayCurrent.messages, function(candidate) {
319
                        if (message.timeCreated == candidate.timeCreated) {
320
                            return message.id < candidate.id;
321
                        } else {
322
                            return message.timeCreated < candidate.timeCreated;
323
                        }
324
                    });
325
                }
326
 
327
                add.push({
328
                    before: before,
329
                    value: message,
330
                    day: dayCurrent
331
                });
332
            });
333
 
334
            // Any message that appears in both the current state for this day and the next state
335
            // for this day means something in the message was updated.
336
            update = update.concat(patch.matches.map(function(message) {
337
                return {
338
                    before: message.a,
339
                    after: message.b
340
                };
341
            }));
342
        });
343
 
344
        return {
345
            add: add,
346
            remove: remove,
347
            update: update
348
        };
349
    };
350
 
351
    /**
352
     * Build a patch for this conversation.
353
     *
354
     * @param  {Object} state The current state of this conversation.
355
     * @param  {Object} newState The new state of this conversation.
356
     * @returns {Object} Patch with days and messsages for each day.
357
     */
358
    var buildConversationPatch = function(state, newState) {
359
        var diff = diffArrays(state.messages, newState.messages, isMessageEqual);
360
 
361
        if (diff.missingFromA.length || diff.missingFromB.length) {
362
            // Some messages have changed so let's work out which ones by sorting
363
            // them into their respective days.
364
            var current = sortMessagesByDay(state.messages, state.midnight);
365
            var next = sortMessagesByDay(newState.messages, newState.midnight);
366
            // This diffs the arrays to work out if there are any missing days that need
367
            // to be added (i.e. we've got some new messages on a new day) or if there
368
            // are any days that need to be deleted (i.e. the user has deleted some old messages).
369
            var daysDiff = diffArrays(current, next, function(dayCurrent, dayNext) {
370
                return dayCurrent.timestamp == dayNext.timestamp;
371
            });
372
 
373
            return {
374
                // Handle adding or removing whole days.
375
                days: buildDaysPatch(current, daysDiff.missingFromB, daysDiff.missingFromA),
376
                // Handle updating messages that don't require adding/removing a whole day.
377
                messages: buildMessagesPatch(daysDiff.matches)
378
            };
379
        } else {
380
            return null;
381
        }
382
    };
383
 
384
    /**
385
     * Build a patch for the header of this conversation. Check if this conversation
386
     * is a group conversation.
387
     *
388
     * @param  {Object} state The current state.
389
     * @param  {Object} newState The new state.
390
     * @return {Object} patch
391
     */
392
    var buildHeaderPatchTypePrivate = function(state, newState) {
393
        var requireAddContact = buildRequireAddContact(state, newState);
394
        var confirmContactRequest = buildConfirmContactRequest(state, newState);
395
        var oldOtherUser = getOtherUserFromState(state);
396
        var newOtherUser = getOtherUserFromState(newState);
397
        var requiresAddContact = requireAddContact && requireAddContact.show && !requireAddContact.hasMessages;
398
        var requiredAddContact = requireAddContact && !requireAddContact.show;
399
        // Render the header once we've got a user.
400
        var shouldRenderHeader = !oldOtherUser && newOtherUser;
401
        // We should also re-render the header if the other user requires
402
        // being added as a contact or if they did but no longer do.
403
        shouldRenderHeader = shouldRenderHeader || requiresAddContact || requiredAddContact;
404
        // Finally, we should re-render if the other user has sent this user
405
        // a contact request that is waiting for approval or if it's been approved/declined.
406
        shouldRenderHeader = shouldRenderHeader || confirmContactRequest !== null;
407
 
408
        if (shouldRenderHeader) {
409
            return {
410
                type: Constants.CONVERSATION_TYPES.PRIVATE,
411
                // We can show controls if the other user doesn't require add contact
412
                // and we aren't waiting for this user to respond to a contact request.
413
                showControls: !requiresAddContact && !confirmContactRequest,
414
                context: {
415
                    id: newState.id,
416
                    name: newState.name,
417
                    subname: newState.subname,
418
                    totalmembercount: newState.totalMemberCount,
419
                    imageurl: newState.imageUrl,
420
                    isfavourite: newState.isFavourite,
421
                    ismuted: newState.isMuted,
422
                    // Don't show favouriting if we don't have a conversation.
423
                    showfavourite: newState.id !== null,
424
                    userid: newOtherUser.id,
425
                    showonlinestatus: newOtherUser.showonlinestatus,
426
                    isonline: newOtherUser.isonline,
427
                    isblocked: newOtherUser.isblocked,
428
                    iscontact: newOtherUser.iscontact
429
                }
430
            };
431
        }
432
 
433
        return null;
434
    };
435
 
436
    /**
437
     * Build a patch for the header of this conversation. Check if this conversation
438
     * is a group conversation.
439
     *
440
     * @param  {Object} state The current state.
441
     * @param  {Object} newState The new state.
442
     * @return {Object} patch
443
     */
444
    var buildHeaderPatchTypeSelf = function(state, newState) {
445
        var shouldRenderHeader = (state.name === null && newState.name !== null);
446
 
447
        if (shouldRenderHeader) {
448
            return {
449
                type: Constants.CONVERSATION_TYPES.SELF,
450
                // Don't display the controls for the self-conversations.
451
                showControls: false,
452
                context: {
453
                    id: newState.id,
454
                    name: newState.name,
455
                    subname: newState.subname,
456
                    imageurl: newState.imageUrl,
457
                    isfavourite: newState.isFavourite,
458
                    // Don't show favouriting if we don't have a conversation.
459
                    showfavourite: newState.id !== null,
460
                    showonlinestatus: true,
461
                }
462
            };
463
        }
464
 
465
        return null;
466
    };
467
 
468
    /**
469
     * Build a patch for the header of this conversation. Check if this conversation
470
     * is a group conversation.
471
     *
472
     * @param  {Object} state The current state.
473
     * @param  {Object} newState The new state.
474
     * @return {Object} patch
475
     */
476
    var buildHeaderPatchTypePublic = function(state, newState) {
477
        var oldMemberCount = state.totalMemberCount;
478
        var newMemberCount = newState.totalMemberCount;
479
 
480
        if (oldMemberCount != newMemberCount) {
481
            return {
482
                type: Constants.CONVERSATION_TYPES.PUBLIC,
483
                showControls: true,
484
                context: {
485
                    id: newState.id,
486
                    name: newState.name,
487
                    subname: newState.subname,
488
                    totalmembercount: newState.totalMemberCount,
489
                    imageurl: newState.imageUrl,
490
                    isfavourite: newState.isFavourite,
491
                    ismuted: newState.isMuted,
492
                    // Don't show favouriting if we don't have a conversation.
493
                    showfavourite: newState.id !== null
494
                }
495
            };
496
        } else {
497
            return null;
498
        }
499
    };
500
 
501
    /**
502
     * Find the newest or oldest message.
503
     *
504
     * @param  {Object} state The current state.
505
     * @param  {Object} newState The new state.
506
     * @return {Number} Oldest or newest message id.
507
     */
508
    var buildScrollToMessagePatch = function(state, newState) {
509
        var oldMessages = state.messages;
510
        var newMessages = newState.messages;
511
 
512
        if (newMessages.length < 1) {
513
            return null;
514
        }
515
 
516
        if (oldMessages.length < 1) {
517
            return newMessages[newMessages.length - 1].id;
518
        }
519
 
520
        var previousNewest = oldMessages[state.messages.length - 1];
521
        var currentNewest = newMessages[newMessages.length - 1];
522
        var previousOldest = oldMessages[0];
523
        var currentOldest = newMessages[0];
524
 
525
        if (previousNewest.id != currentNewest.id) {
526
            return currentNewest.id;
527
        } else if (previousOldest.id != currentOldest.id) {
528
            return previousOldest.id;
529
        }
530
 
531
        return null;
532
    };
533
 
534
    /**
535
     * Check if members should be loaded.
536
     *
537
     * @param  {Object} state The current state.
538
     * @param  {Object} newState The new state.
539
     * @return {Bool|Null}
540
     */
541
    var buildLoadingMembersPatch = function(state, newState) {
542
        if (!state.loadingMembers && newState.loadingMembers) {
543
            return true;
544
        } else if (state.loadingMembers && !newState.loadingMembers) {
545
            return false;
546
        } else {
547
            return null;
548
        }
549
    };
550
 
551
    /**
552
     * Check if the messages are being loaded for the first time.
553
     *
554
     * @param  {Object} state The current state.
555
     * @param  {Object} newState The new state.
556
     * @return {Bool|Null}
557
     */
558
    var buildLoadingFirstMessages = function(state, newState) {
559
        if (state.hasTriedToLoadMessages === newState.hasTriedToLoadMessages) {
560
            return null;
561
        } else if (!newState.hasTriedToLoadMessages && newState.loadingMessages) {
562
            return true;
563
        } else if (newState.hasTriedToLoadMessages && !newState.loadingMessages) {
564
            return false;
565
        } else {
566
            return null;
567
        }
568
    };
569
 
570
    /**
571
     * Check if the messages are still being loaded
572
     *
573
     * @param  {Object} state The current state.
574
     * @param  {Object} newState The new state.
575
     * @return {Bool|Null}
576
     */
577
    var buildLoadingMessages = function(state, newState) {
578
        if (!state.loadingMessages && newState.loadingMessages) {
579
            return true;
580
        } else if (state.loadingMessages && !newState.loadingMessages) {
581
            return false;
582
        } else {
583
            return null;
584
        }
585
    };
586
 
587
    /**
588
     * Determine if we should show the emoji picker.
589
     *
590
     * @param  {Object} state The current state.
591
     * @param  {Object} newState The new state.
592
     * @return {Bool|Null}
593
     */
594
    var buildShowEmojiPicker = function(state, newState) {
595
        if (!state.showEmojiPicker && newState.showEmojiPicker) {
596
            return true;
597
        } else if (state.showEmojiPicker && !newState.showEmojiPicker) {
598
            return false;
599
        } else {
600
            return null;
601
        }
602
    };
603
 
604
    /**
605
     * Determine if we should show the emoji auto complete.
606
     *
607
     * @param  {Object} state The current state.
608
     * @param  {Object} newState The new state.
609
     * @return {Bool|Null}
610
     */
611
    var buildShowEmojiAutoComplete = function(state, newState) {
612
        if (!state.showEmojiAutoComplete && newState.showEmojiAutoComplete) {
613
            return true;
614
        } else if (state.showEmojiAutoComplete && !newState.showEmojiAutoComplete) {
615
            return false;
616
        } else {
617
            return null;
618
        }
619
    };
620
 
621
    /**
622
     * Get the user Object of user to be blocked if pending.
623
     *
624
     * @param  {Object} state The current state.
625
     * @param  {Object} newState The new state.
626
     * @return {Object|Bool|Null} User Object if Object.
627
     */
628
    var buildConfirmBlockUser = function(state, newState) {
629
        if (newState.pendingBlockUserIds.length) {
630
            // We currently only support a single user;
631
            var userId = newState.pendingBlockUserIds[0];
632
            return newState.members[userId];
633
        } else if (state.pendingBlockUserIds.length) {
634
            return false;
635
        }
636
 
637
        return null;
638
    };
639
 
640
    /**
641
     * Get the user Object of user to be unblocked if pending.
642
     *
643
     * @param  {Object} state The current state.
644
     * @param  {Object} newState The new state.
645
     * @return {Object|Bool|Null} User Object if Object.
646
     */
647
    var buildConfirmUnblockUser = function(state, newState) {
648
        if (newState.pendingUnblockUserIds.length) {
649
            // We currently only support a single user;
650
            var userId = newState.pendingUnblockUserIds[0];
651
            return newState.members[userId];
652
        } else if (state.pendingUnblockUserIds.length) {
653
            return false;
654
        }
655
 
656
        return null;
657
    };
658
 
659
    /**
660
     * Get the user Object of user to be added as contact if pending.
661
     *
662
     * @param  {Object} state The current state.
663
     * @param  {Object} newState The new state.
664
     * @return {Object|Bool|Null} User Object if Object.
665
     */
666
    var buildConfirmAddContact = function(state, newState) {
667
        if (newState.pendingAddContactIds.length) {
668
            // We currently only support a single user;
669
            var userId = newState.pendingAddContactIds[0];
670
            return newState.members[userId];
671
        } else if (state.pendingAddContactIds.length) {
672
            return false;
673
        }
674
 
675
        return null;
676
    };
677
 
678
    /**
679
     * Get the user Object of user to be removed as contact if pending.
680
     *
681
     * @param  {Object} state The current state.
682
     * @param  {Object} newState The new state.
683
     * @return {Object|Bool|Null} User Object if Object.
684
     */
685
    var buildConfirmRemoveContact = function(state, newState) {
686
        if (newState.pendingRemoveContactIds.length) {
687
            // We currently only support a single user;
688
            var userId = newState.pendingRemoveContactIds[0];
689
            return newState.members[userId];
690
        } else if (state.pendingRemoveContactIds.length) {
691
            return false;
692
        }
693
 
694
        return null;
695
    };
696
 
697
    /**
698
     * Check if there are any messages to be deleted.
699
     *
700
     * @param  {Object} state The current state.
701
     * @param  {Object} newState The new state.
702
     * @return {Object|Null} The conversation type and if the user can delete  the messages for all users.
703
     */
704
    var buildConfirmDeleteSelectedMessages = function(state, newState) {
705
        var oldPendingCount = state.pendingDeleteMessageIds.length;
706
        var newPendingCount = newState.pendingDeleteMessageIds.length;
707
 
708
        if (newPendingCount && !oldPendingCount) {
709
            return {
710
                show: true,
711
                type: newState.type,
712
                canDeleteMessagesForAllUsers: newState.canDeleteMessagesForAllUsers
713
            };
714
        } else if (oldPendingCount && !newPendingCount) {
715
            return {
716
                show: false
717
            };
718
        }
719
 
720
        return null;
721
    };
722
 
723
    /**
724
     * Check if there is a conversation to be deleted.
725
     *
726
     * @param  {Object} state The current state.
727
     * @param  {Object} newState The new state.
728
     * @return {int|Null} The conversation type to be deleted.
729
     */
730
    var buildConfirmDeleteConversation = function(state, newState) {
731
        if (!state.pendingDeleteConversation && newState.pendingDeleteConversation) {
732
            return newState.type;
733
        } else if (state.pendingDeleteConversation && !newState.pendingDeleteConversation) {
734
            return false;
735
        }
736
 
737
        return null;
738
    };
739
 
740
    /**
741
     * Check if there is a pending contact request to accept or decline.
742
     *
743
     * @param  {Object} state The current state.
744
     * @param  {Object} newState The new state.
745
     * @return {Bool|Null}
746
     */
747
    var buildConfirmContactRequest = function(state, newState) {
748
        var loggedInUserId = state.loggedInUserId;
749
        var oldOtherUser = getOtherUserFromState(state);
750
        var newOtherUser = getOtherUserFromState(newState);
751
        var oldReceivedRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
752
            return request.requesteduserid == loggedInUserId && request.userid == oldOtherUser.id;
753
        });
754
        var newReceivedRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
755
            return request.requesteduserid == loggedInUserId && request.userid == newOtherUser.id;
756
        });
757
        var oldRequest = oldReceivedRequests.length ? oldReceivedRequests[0] : null;
758
        var newRequest = newReceivedRequests.length ? newReceivedRequests[0] : null;
759
 
760
        if (!oldRequest && newRequest) {
761
            return newOtherUser;
762
        } else if (oldRequest && !newRequest) {
763
            return false;
764
        } else {
765
            return null;
766
        }
767
    };
768
 
769
    /**
770
     * Check if there are any changes in blocked users.
771
     *
772
     * @param  {Object} state The current state.
773
     * @param  {Object} newState The new state.
774
     * @return {Bool|Null}
775
     */
776
    var buildIsBlocked = function(state, newState) {
777
        var oldOtherUser = getOtherUserFromState(state);
778
        var newOtherUser = getOtherUserFromState(newState);
779
 
780
        if (!oldOtherUser && !newOtherUser) {
781
            return null;
782
        } else if (!oldOtherUser && newOtherUser) {
783
            return newOtherUser.isblocked ? true : null;
784
        } else if (!newOtherUser && oldOtherUser) {
785
            return oldOtherUser.isblocked ? false : null;
786
        } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) {
787
            return false;
788
        } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) {
789
            return true;
790
        } else {
791
            return null;
792
        }
793
    };
794
 
795
    /**
796
     * Check if there are any changes the conversation favourite state.
797
     *
798
     * @param  {Object} state The current state.
799
     * @param  {Object} newState The new state.
800
     * @return {Bool|Null}
801
     */
802
    var buildIsFavourite = function(state, newState) {
803
        var oldIsFavourite = state.isFavourite;
804
        var newIsFavourite = newState.isFavourite;
805
 
806
        if (state.id === null && newState.id === null) {
807
            // The conversation isn't yet created so don't change anything.
808
            return null;
809
        } else if (state.id === null && newState.id !== null) {
810
            // The conversation was created so we can show the add favourite button.
811
            return 'show-add';
812
        } else if (state.id !== null && newState.id === null) {
813
            // We're changing from a created conversation to a new conversation so hide
814
            // the favouriting functionality for now.
815
            return 'hide';
816
        } else if (oldIsFavourite == newIsFavourite) {
817
            // No change.
818
            return null;
819
        } else if (!oldIsFavourite && newIsFavourite) {
820
            return 'show-remove';
821
        } else if (oldIsFavourite && !newIsFavourite) {
822
            return 'show-add';
823
        } else {
824
            return null;
825
        }
826
    };
827
 
828
    /**
829
     * Check if there are any changes the conversation muted state.
830
     *
831
     * @param  {Object} state The current state.
832
     * @param  {Object} newState The new state.
833
     * @return {string|null}
834
     */
835
    var buildIsMuted = function(state, newState) {
836
        var oldIsMuted = state.isMuted;
837
        var newIsMuted = newState.isMuted;
838
 
839
        if (state.id === null && newState.id === null) {
840
            // The conversation isn't yet created so don't change anything.
841
            return null;
842
        } else if (state.id === null && newState.id !== null) {
843
            // The conversation was created so we can show the mute button.
844
            return 'show-mute';
845
        } else if (state.id !== null && newState.id === null) {
846
            // We're changing from a created conversation to a new conversation so hide
847
            // the muting functionality for now.
848
            return 'hide';
849
        } else if (oldIsMuted == newIsMuted) {
850
            // No change.
851
            return null;
852
        } else if (!oldIsMuted && newIsMuted) {
853
            return 'show-unmute';
854
        } else if (oldIsMuted && !newIsMuted) {
855
            return 'show-mute';
856
        } else {
857
            return null;
858
        }
859
    };
860
 
861
    /**
862
     * Check if there are any changes in the contact status of the current user
863
     * and other user.
864
     *
865
     * @param  {Object} state The current state.
866
     * @param  {Object} newState The new state.
867
     * @return {Bool|Null}
868
     */
869
    var buildIsContact = function(state, newState) {
870
        var loggedInUserId = state.loggedInUserId;
871
        var oldOtherUser = getOtherUserFromState(state);
872
        var newOtherUser = getOtherUserFromState(newState);
873
        var oldContactRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
874
            return (request.userid == loggedInUserId && request.requesteduserid == oldOtherUser.id) ||
875
                (request.userid == oldOtherUser.id && request.requesteduserid == loggedInUserId);
876
        });
877
        var newContactRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
878
            return (request.userid == loggedInUserId && request.requesteduserid == newOtherUser.id) ||
879
                (request.userid == newOtherUser.id && request.requesteduserid == loggedInUserId);
880
        });
881
        var oldHasContactRequests = oldContactRequests.length > 0;
882
        var newHasContactRequests = newContactRequests.length > 0;
883
 
884
        if (!oldOtherUser && !newOtherUser) {
885
            return null;
886
        } else if (oldHasContactRequests && newHasContactRequests) {
887
            return null;
888
        } else if (!oldHasContactRequests && newHasContactRequests && !newOtherUser.iscontact) {
889
            return 'pending-contact';
890
        } else if (!oldOtherUser && newOtherUser) {
891
            return newOtherUser.iscontact ? 'contact' : null;
892
        } else if (!newOtherUser && oldOtherUser) {
893
            return oldOtherUser.iscontact ? 'non-contact' : null;
894
        } else if (oldOtherUser.iscontact && !newOtherUser.iscontact) {
895
            return newHasContactRequests ? 'pending-contact' : 'non-contact';
896
        } else if (!oldOtherUser.iscontact && newOtherUser.iscontact) {
897
            return 'contact';
898
        } else {
899
            return null;
900
        }
901
    };
902
 
903
    /**
904
     * Check if a confirm action is active.
905
     *
906
     * @param  {Object} state The current state.
907
     * @param  {Object} newState The new state.
908
     * @return {Bool|Null}
909
     */
910
    var buildLoadingConfirmationAction = function(state, newState) {
911
        if (!state.loadingConfirmAction && newState.loadingConfirmAction) {
912
            return true;
913
        } else if (state.loadingConfirmAction && !newState.loadingConfirmAction) {
914
            return false;
915
        } else {
916
            return null;
917
        }
918
    };
919
 
920
    /**
921
     * Check if a edit mode is active.
922
     *
923
     * @param  {Object} state The current state.
924
     * @param  {Object} newState The new state.
925
     * @return {Bool|Null}
926
     */
927
    var buildInEditMode = function(state, newState) {
928
        var oldHasSelectedMessages = state.selectedMessageIds.length > 0;
929
        var newHasSelectedMessages = newState.selectedMessageIds.length > 0;
930
        var numberOfMessagesHasChanged = state.messages.length != newState.messages.length;
931
 
932
        if (!oldHasSelectedMessages && newHasSelectedMessages) {
933
            return true;
934
        } else if (oldHasSelectedMessages && !newHasSelectedMessages) {
935
            return false;
936
        } else if (oldHasSelectedMessages && numberOfMessagesHasChanged) {
937
            return true;
938
        } else {
939
            return null;
940
        }
941
    };
942
 
943
    /**
944
     * Build a patch for the messages selected.
945
     *
946
     * @param  {Object} state The current state.
947
     * @param  {Object} newState The new state.
948
     * @return {Object} patch
949
     */
950
    var buildSelectedMessages = function(state, newState) {
951
        var oldSelectedMessages = state.selectedMessageIds;
952
        var newSelectedMessages = newState.selectedMessageIds;
953
 
954
        if (isArrayEqual(oldSelectedMessages, newSelectedMessages)) {
955
            return null;
956
        }
957
 
958
        var diff = diffArrays(oldSelectedMessages, newSelectedMessages, function(a, b) {
959
            return a == b;
960
        });
961
 
962
        return {
963
            count: newSelectedMessages.length,
964
            add: diff.missingFromA,
965
            remove: diff.missingFromB
966
        };
967
    };
968
 
969
    /**
970
     * Get a list of users from the state that are not the logged in user. Use to find group
971
     * message members or the other user in a conversation.
972
     *
973
     * @param  {Object} state State
974
     * @return {Array} List of users.
975
     */
976
    var getOtherUserFromState = function(state) {
977
        return Object.keys(state.members).reduce(function(carry, userId) {
978
            if (userId != state.loggedInUserId && !carry) {
979
                carry = state.members[userId];
980
            }
981
 
982
            return carry;
983
        }, null);
984
    };
985
 
986
    /**
987
     * Check if the given user requires a contact request from the logged in user.
988
     *
989
     * @param  {Integer} loggedInUserId The logged in user id
990
     * @param  {Object} user User record
991
     * @return {Bool}
992
     */
993
    var requiresContactRequest = function(loggedInUserId, user) {
994
        // If a user can message then no contact request is required.
995
        if (user.canmessage) {
996
            return false;
997
        }
998
 
999
        var contactRequests = user.contactrequests.filter(function(request) {
1000
            return request.userid == loggedInUserId || request.requesteduserid;
1001
        });
1002
        var hasSentContactRequest = contactRequests.length > 0;
1003
        return user.requirescontact && !user.iscontact && !hasSentContactRequest;
1004
    };
1005
 
1006
    /**
1007
     * Check if other users are required to be added as contact.
1008
     *
1009
     * @param  {Object} state The current state.
1010
     * @param  {Object} newState The new state.
1011
     * @return {Object} Object controlling the required to add contact dialog variables.
1012
     */
1013
    var buildRequireAddContact = function(state, newState) {
1014
        var oldOtherUser = getOtherUserFromState(state);
1015
        var newOtherUser = getOtherUserFromState(newState);
1016
        var hadMessages = state.messages.length > 0;
1017
        var hasMessages = newState.messages.length > 0;
1018
        var loggedInUserId = newState.loggedInUserId;
1019
        var prevRequiresContactRequest = oldOtherUser && requiresContactRequest(loggedInUserId, oldOtherUser);
1020
        var nextRequiresContactRequest = newOtherUser && requiresContactRequest(loggedInUserId, newOtherUser);
1021
        var confirmAddContact = buildConfirmAddContact(state, newState);
1022
        var finishedAddContact = confirmAddContact === false;
1023
 
1024
        // Still doing first load.
1025
        if (!state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) {
1026
            return null;
1027
        }
1028
 
1029
        // No users yet.
1030
        if (!oldOtherUser && !newOtherUser) {
1031
            return null;
1032
        }
1033
 
1034
        // We've loaded a new user and they require a contact request.
1035
        if (!oldOtherUser && nextRequiresContactRequest) {
1036
            return {
1037
                show: true,
1038
                hasMessages: hasMessages,
1039
                user: newOtherUser
1040
            };
1041
        }
1042
 
1043
        // The logged in user has completed the confirm contact request dialogue
1044
        // but the other user still requires a contact request which means the logged
1045
        // in user either declined the confirmation or it failed.
1046
        if (finishedAddContact && nextRequiresContactRequest) {
1047
            return {
1048
                show: true,
1049
                hasMessages: hasMessages,
1050
                user: newOtherUser
1051
            };
1052
        }
1053
 
1054
        // Everything is loaded.
1055
        if (state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) {
1056
            if (!prevRequiresContactRequest && nextRequiresContactRequest) {
1057
                return {
1058
                    show: true,
1059
                    hasMessages: hasMessages,
1060
                    user: newOtherUser
1061
                };
1062
            }
1063
 
1064
            if (prevRequiresContactRequest && !nextRequiresContactRequest) {
1065
                return {
1066
                    show: false,
1067
                    hasMessages: hasMessages
1068
                };
1069
            }
1070
        }
1071
 
1072
        // First load just completed.
1073
        if (!state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) {
1074
            if (nextRequiresContactRequest) {
1075
                return {
1076
                    show: true,
1077
                    hasMessages: hasMessages,
1078
                    user: newOtherUser
1079
                };
1080
            }
1081
        }
1082
 
1083
        // Being reset.
1084
        if (state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) {
1085
            if (prevRequiresContactRequest) {
1086
                return {
1087
                    show: false,
1088
                    hasMessages: hadMessages
1089
                };
1090
            }
1091
        }
1092
 
1093
        return null;
1094
    };
1095
 
1096
    /**
1097
     * Check if other users are required to be unblocked.
1098
     *
1099
     * @param  {Object} state The current state.
1100
     * @param  {Object} newState The new state.
1101
     * @return {Bool|Null}
1102
     */
1103
    var buildRequireUnblock = function(state, newState) {
1104
        var oldOtherUser = getOtherUserFromState(state);
1105
        var newOtherUser = getOtherUserFromState(newState);
1106
 
1107
        if (!oldOtherUser && !newOtherUser) {
1108
            return null;
1109
        } else if (oldOtherUser && !newOtherUser) {
1110
            return oldOtherUser.isblocked ? false : null;
1111
        } else if (!oldOtherUser && newOtherUser) {
1112
            return newOtherUser.isblocked ? true : null;
1113
        } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) {
1114
            return true;
1115
        } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) {
1116
            return false;
1117
        }
1118
 
1119
        return null;
1120
    };
1121
 
1122
    /**
1123
     * Check if other users can be messaged.
1124
     *
1125
     * @param  {Object} state The current state.
1126
     * @param  {Object} newState The new state.
1127
     * @return {Bool|Null}
1128
     */
1129
    var buildUnableToMessage = function(state, newState) {
1130
        var oldOtherUser = getOtherUserFromState(state);
1131
        var newOtherUser = getOtherUserFromState(newState);
1132
 
1133
        if (newState.type == Constants.CONVERSATION_TYPES.SELF) {
1134
            // Users always can send message themselves on self-conversations.
1135
            return null;
1136
        }
1137
 
1138
        if (!oldOtherUser && !newOtherUser) {
1139
            return null;
1140
        } else if (oldOtherUser && !newOtherUser) {
1141
            return oldOtherUser.canmessage ? null : true;
1142
        } else if (!oldOtherUser && newOtherUser) {
1143
            return newOtherUser.canmessage ? null : true;
1144
        } else if (!oldOtherUser.canmessage && newOtherUser.canmessage) {
1145
            return false;
1146
        } else if (oldOtherUser.canmessage && !newOtherUser.canmessage) {
1147
            return true;
1148
        }
1149
 
1150
        return null;
1151
    };
1152
 
1153
    /**
1154
     * Build patch for footer information for a private conversation.
1155
     *
1156
     * @param  {Object} state The current state.
1157
     * @param  {Object} newState The new state.
1158
     * @return {Object} containing footer state type.
1159
     */
1160
    var buildFooterPatchTypePrivate = function(state, newState) {
1161
        var loadingFirstMessages = buildLoadingFirstMessages(state, newState);
1162
        var inEditMode = buildInEditMode(state, newState);
1163
        var requireAddContact = buildRequireAddContact(state, newState);
1164
        var requireUnblock = buildRequireUnblock(state, newState);
1165
        var unableToMessage = buildUnableToMessage(state, newState);
1166
        var showRequireAddContact = requireAddContact !== null ? requireAddContact.show && requireAddContact.hasMessages : null;
1167
        var otherUser = getOtherUserFromState(newState);
1168
        var generateReturnValue = function(checkValue, successReturn) {
1169
            if (checkValue) {
1170
                return successReturn;
1171
            } else if (checkValue !== null && !checkValue) {
1172
                if (!otherUser) {
1173
                    return {type: 'content'};
1174
                } else if (otherUser.isblocked) {
1175
                    return {type: 'unblock'};
1176
                } else if (newState.messages.length && requiresContactRequest(newState.loggedInUserId, otherUser)) {
1177
                    return {
1178
                        type: 'add-contact',
1179
                        user: otherUser
1180
                    };
1181
                } else if (!otherUser.canmessage && (otherUser.requirescontact && !otherUser.iscontact)) {
1182
                    return {type: 'unable-to-message'};
1183
                }
1184
            }
1185
 
1186
            return null;
1187
        };
1188
 
1189
        if (
1190
            loadingFirstMessages === null &&
1191
            inEditMode === null &&
1192
            requireAddContact === null &&
1193
            requireUnblock === null
1194
        ) {
1195
            return null;
1196
        }
1197
 
1198
        var checks = [
1199
            [loadingFirstMessages, {type: 'placeholder'}],
1200
            [inEditMode, {type: 'edit-mode'}],
1201
            [unableToMessage, {type: 'unable-to-message'}],
1202
            [requireUnblock, {type: 'unblock'}],
1203
            [showRequireAddContact, {type: 'add-contact', user: otherUser}]
1204
        ];
1205
 
1206
        for (var i = 0; i < checks.length; i++) {
1207
            var checkValue = checks[i][0];
1208
            var successReturn = checks[i][1];
1209
            var result = generateReturnValue(checkValue, successReturn);
1210
 
1211
            if (result !== null) {
1212
                return result;
1213
            }
1214
        }
1215
 
1216
        return {
1217
            type: 'content'
1218
        };
1219
    };
1220
 
1221
    /**
1222
     * Build patch for footer information for a public conversation.
1223
     *
1224
     * @param  {Object} state The current state.
1225
     * @param  {Object} newState The new state.
1226
     * @return {Object} containing footer state type.
1227
     */
1228
    var buildFooterPatchTypePublic = function(state, newState) {
1229
        var loadingFirstMessages = buildLoadingFirstMessages(state, newState);
1230
        var inEditMode = buildInEditMode(state, newState);
1231
 
1232
        if (loadingFirstMessages === null && inEditMode === null) {
1233
            return null;
1234
        }
1235
 
1236
        if (loadingFirstMessages) {
1237
            return {type: 'placeholder'};
1238
        }
1239
 
1240
        if (inEditMode) {
1241
            return {type: 'edit-mode'};
1242
        }
1243
 
1244
        return {
1245
            type: 'content'
1246
        };
1247
    };
1248
 
1249
    /**
1250
     * Check if we're viewing a different conversation. If so then we need to
1251
     * reset the UI.
1252
     *
1253
     * @param  {Object} state The current state.
1254
     * @param  {Object} newState The new state.
1255
     * @return {bool|null} If a reset needs to occur
1256
     */
1257
    var buildReset = function(state, newState) {
1258
        var oldType = state.type;
1259
        var newType = newState.type;
1260
        var oldConversationId = state.id;
1261
        var newConversationId = newState.id;
1262
        var oldMemberIds = Object.keys(state.members);
1263
        var newMemberIds = Object.keys(newState.members);
1264
 
1265
        oldMemberIds.sort();
1266
        newMemberIds.sort();
1267
 
1268
        var membersUnchanged = oldMemberIds.every(function(id, index) {
1269
            return id == newMemberIds[index];
1270
        });
1271
 
1272
        if (oldType != newType) {
1273
            // If we've changed conversation type then we need to reset.
1274
            return true;
1275
        } else if (oldConversationId && !newConversationId) {
1276
            // We previously had a conversation id but no longer do. This likely means
1277
            // the user is viewing the conversation with someone they've never spoken to
1278
            // before.
1279
            return true;
1280
        } else if (oldConversationId && newConversationId && oldConversationId != newConversationId) {
1281
            // If we had a conversation id and it's changed then we need to reset.
1282
            return true;
1283
        } else if (!oldConversationId && !newConversationId && !membersUnchanged) {
1284
            // If we never had a conversation id but the members of the conversation have
1285
            // changed then we need to reset. This can happen if the user goes from viewing
1286
            // a user they've never had a conversation with to viewing a different user that
1287
            // they've never had a conversation with.
1288
            return true;
1289
        }
1290
 
1291
        return null;
1292
    };
1293
 
1294
    /**
1295
     * We should show this message always, for all the self-conversations.
1296
     *
1297
     * The message should be hidden when it's not a self-conversation.
1298
     *
1299
     * @param  {Object} state The current state.
1300
     * @param  {Object} newState The new state.
1301
     * @return {bool}
1302
     */
1303
    var buildSelfConversationMessage = function(state, newState) {
1304
        if (state.type != newState.type) {
1305
            return (newState.type == Constants.CONVERSATION_TYPES.SELF);
1306
        }
1307
 
1308
        return null;
1309
    };
1310
 
1311
    /**
1312
     * We should show the contact request sent message if the user just sent
1313
     * a contact request to the other user
1314
     *
1315
     * @param  {Object} state The current state.
1316
     * @param  {Object} newState The new state.
1317
     * @return {string|false|null}
1318
     */
1319
    var buildContactRequestSent = function(state, newState) {
1320
        var loggedInUserId = newState.loggedInUserId;
1321
        var oldOtherUser = getOtherUserFromState(state);
1322
        var newOtherUser = getOtherUserFromState(newState);
1323
        var oldSentRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
1324
            return request.userid == loggedInUserId;
1325
        });
1326
        var newSentRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
1327
            return request.userid == loggedInUserId;
1328
        });
1329
        var oldRequest = oldSentRequests.length > 0;
1330
        var newRequest = newSentRequests.length > 0;
1331
 
1332
        if (!oldRequest && newRequest && !newOtherUser.iscontact) {
1333
            return newOtherUser.fullname;
1334
        } else if (oldOtherUser && !oldOtherUser.iscontact && newRequest && newOtherUser.iscontact) {
1335
            // Contact request accepted.
1336
            return false;
1337
        } else if (oldRequest && !newRequest) {
1338
            return false;
1339
        } else {
1340
            return null;
1341
        }
1342
    };
1343
 
1344
    /**
1345
     * Build the full patch comparing the current state and the new state. This patch is used by
1346
     * the conversation renderer to render the UI on any update.
1347
     *
1348
     * @param  {Object} state The current state.
1349
     * @param  {Object} newState The new state.
1350
     * @return {Object} Patch containing all information changed.
1351
     */
1352
    var buildPatch = function(state, newState) {
1353
        var config = {
1354
            all: {
1355
                reset: buildReset,
1356
                conversation: buildConversationPatch,
1357
                scrollToMessage: buildScrollToMessagePatch,
1358
                loadingMembers: buildLoadingMembersPatch,
1359
                loadingFirstMessages: buildLoadingFirstMessages,
1360
                loadingMessages: buildLoadingMessages,
1361
                confirmDeleteSelectedMessages: buildConfirmDeleteSelectedMessages,
1362
                inEditMode: buildInEditMode,
1363
                selectedMessages: buildSelectedMessages,
1364
                isFavourite: buildIsFavourite,
1365
                isMuted: buildIsMuted,
1366
                showEmojiPicker: buildShowEmojiPicker,
1367
                showEmojiAutoComplete: buildShowEmojiAutoComplete
1368
            }
1369
        };
1370
        // These build functions are only applicable to private conversations.
1371
        config[Constants.CONVERSATION_TYPES.PRIVATE] = {
1372
            header: buildHeaderPatchTypePrivate,
1373
            footer: buildFooterPatchTypePrivate,
1374
            confirmBlockUser: buildConfirmBlockUser,
1375
            confirmUnblockUser: buildConfirmUnblockUser,
1376
            confirmAddContact: buildConfirmAddContact,
1377
            confirmRemoveContact: buildConfirmRemoveContact,
1378
            confirmContactRequest: buildConfirmContactRequest,
1379
            confirmDeleteConversation: buildConfirmDeleteConversation,
1380
            isBlocked: buildIsBlocked,
1381
            isContact: buildIsContact,
1382
            loadingConfirmAction: buildLoadingConfirmationAction,
1383
            requireAddContact: buildRequireAddContact,
1384
            contactRequestSent: buildContactRequestSent
1385
        };
1386
        // These build functions are only applicable to public (group) conversations.
1387
        config[Constants.CONVERSATION_TYPES.PUBLIC] = {
1388
            header: buildHeaderPatchTypePublic,
1389
            footer: buildFooterPatchTypePublic,
1390
        };
1391
        // These build functions are only applicable to self-conversations.
1392
        config[Constants.CONVERSATION_TYPES.SELF] = {
1393
            header: buildHeaderPatchTypeSelf,
1394
            footer: buildFooterPatchTypePublic,
1395
            confirmDeleteConversation: buildConfirmDeleteConversation,
1396
            selfConversationMessage: buildSelfConversationMessage
1397
        };
1398
 
1399
        var patchConfig = $.extend({}, config.all);
1400
        if (newState.type && newState.type in config) {
1401
            // Add the type specific builders to the patch config.
1402
            patchConfig = $.extend(patchConfig, config[newState.type]);
1403
        }
1404
 
1405
        return Object.keys(patchConfig).reduce(function(patch, key) {
1406
            var buildFunc = patchConfig[key];
1407
            var value = buildFunc(state, newState);
1408
 
1409
            if (value !== null) {
1410
                patch[key] = value;
1411
            }
1412
 
1413
            return patch;
1414
        }, {});
1415
    };
1416
 
1417
    return {
1418
        buildPatch: buildPatch
1419
    };
1420
});