Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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,
1441 ariadna 428
                    iscontact: newOtherUser.iscontact,
429
                    cancreatecontact: newOtherUser.cancreatecontact,
1 efrain 430
                }
431
            };
432
        }
433
 
434
        return null;
435
    };
436
 
437
    /**
438
     * Build a patch for the header of this conversation. Check if this conversation
439
     * is a group conversation.
440
     *
441
     * @param  {Object} state The current state.
442
     * @param  {Object} newState The new state.
443
     * @return {Object} patch
444
     */
445
    var buildHeaderPatchTypeSelf = function(state, newState) {
446
        var shouldRenderHeader = (state.name === null && newState.name !== null);
447
 
448
        if (shouldRenderHeader) {
449
            return {
450
                type: Constants.CONVERSATION_TYPES.SELF,
451
                // Don't display the controls for the self-conversations.
452
                showControls: false,
453
                context: {
454
                    id: newState.id,
455
                    name: newState.name,
456
                    subname: newState.subname,
457
                    imageurl: newState.imageUrl,
458
                    isfavourite: newState.isFavourite,
459
                    // Don't show favouriting if we don't have a conversation.
460
                    showfavourite: newState.id !== null,
461
                    showonlinestatus: true,
462
                }
463
            };
464
        }
465
 
466
        return null;
467
    };
468
 
469
    /**
470
     * Build a patch for the header of this conversation. Check if this conversation
471
     * is a group conversation.
472
     *
473
     * @param  {Object} state The current state.
474
     * @param  {Object} newState The new state.
475
     * @return {Object} patch
476
     */
477
    var buildHeaderPatchTypePublic = function(state, newState) {
478
        var oldMemberCount = state.totalMemberCount;
479
        var newMemberCount = newState.totalMemberCount;
480
 
481
        if (oldMemberCount != newMemberCount) {
482
            return {
483
                type: Constants.CONVERSATION_TYPES.PUBLIC,
484
                showControls: true,
485
                context: {
486
                    id: newState.id,
487
                    name: newState.name,
488
                    subname: newState.subname,
489
                    totalmembercount: newState.totalMemberCount,
490
                    imageurl: newState.imageUrl,
491
                    isfavourite: newState.isFavourite,
492
                    ismuted: newState.isMuted,
493
                    // Don't show favouriting if we don't have a conversation.
494
                    showfavourite: newState.id !== null
495
                }
496
            };
497
        } else {
498
            return null;
499
        }
500
    };
501
 
502
    /**
503
     * Find the newest or oldest message.
504
     *
505
     * @param  {Object} state The current state.
506
     * @param  {Object} newState The new state.
507
     * @return {Number} Oldest or newest message id.
508
     */
509
    var buildScrollToMessagePatch = function(state, newState) {
510
        var oldMessages = state.messages;
511
        var newMessages = newState.messages;
512
 
513
        if (newMessages.length < 1) {
514
            return null;
515
        }
516
 
517
        if (oldMessages.length < 1) {
518
            return newMessages[newMessages.length - 1].id;
519
        }
520
 
521
        var previousNewest = oldMessages[state.messages.length - 1];
522
        var currentNewest = newMessages[newMessages.length - 1];
523
        var previousOldest = oldMessages[0];
524
        var currentOldest = newMessages[0];
525
 
526
        if (previousNewest.id != currentNewest.id) {
527
            return currentNewest.id;
528
        } else if (previousOldest.id != currentOldest.id) {
529
            return previousOldest.id;
530
        }
531
 
532
        return null;
533
    };
534
 
535
    /**
536
     * Check if members should be loaded.
537
     *
538
     * @param  {Object} state The current state.
539
     * @param  {Object} newState The new state.
540
     * @return {Bool|Null}
541
     */
542
    var buildLoadingMembersPatch = function(state, newState) {
543
        if (!state.loadingMembers && newState.loadingMembers) {
544
            return true;
545
        } else if (state.loadingMembers && !newState.loadingMembers) {
546
            return false;
547
        } else {
548
            return null;
549
        }
550
    };
551
 
552
    /**
553
     * Check if the messages are being loaded for the first time.
554
     *
555
     * @param  {Object} state The current state.
556
     * @param  {Object} newState The new state.
557
     * @return {Bool|Null}
558
     */
559
    var buildLoadingFirstMessages = function(state, newState) {
560
        if (state.hasTriedToLoadMessages === newState.hasTriedToLoadMessages) {
561
            return null;
562
        } else if (!newState.hasTriedToLoadMessages && newState.loadingMessages) {
563
            return true;
564
        } else if (newState.hasTriedToLoadMessages && !newState.loadingMessages) {
565
            return false;
566
        } else {
567
            return null;
568
        }
569
    };
570
 
571
    /**
572
     * Check if the messages are still being loaded
573
     *
574
     * @param  {Object} state The current state.
575
     * @param  {Object} newState The new state.
576
     * @return {Bool|Null}
577
     */
578
    var buildLoadingMessages = function(state, newState) {
579
        if (!state.loadingMessages && newState.loadingMessages) {
580
            return true;
581
        } else if (state.loadingMessages && !newState.loadingMessages) {
582
            return false;
583
        } else {
584
            return null;
585
        }
586
    };
587
 
588
    /**
589
     * Determine if we should show the emoji picker.
590
     *
591
     * @param  {Object} state The current state.
592
     * @param  {Object} newState The new state.
593
     * @return {Bool|Null}
594
     */
595
    var buildShowEmojiPicker = function(state, newState) {
596
        if (!state.showEmojiPicker && newState.showEmojiPicker) {
597
            return true;
598
        } else if (state.showEmojiPicker && !newState.showEmojiPicker) {
599
            return false;
600
        } else {
601
            return null;
602
        }
603
    };
604
 
605
    /**
606
     * Determine if we should show the emoji auto complete.
607
     *
608
     * @param  {Object} state The current state.
609
     * @param  {Object} newState The new state.
610
     * @return {Bool|Null}
611
     */
612
    var buildShowEmojiAutoComplete = function(state, newState) {
613
        if (!state.showEmojiAutoComplete && newState.showEmojiAutoComplete) {
614
            return true;
615
        } else if (state.showEmojiAutoComplete && !newState.showEmojiAutoComplete) {
616
            return false;
617
        } else {
618
            return null;
619
        }
620
    };
621
 
622
    /**
623
     * Get the user Object of user to be blocked if pending.
624
     *
625
     * @param  {Object} state The current state.
626
     * @param  {Object} newState The new state.
627
     * @return {Object|Bool|Null} User Object if Object.
628
     */
629
    var buildConfirmBlockUser = function(state, newState) {
630
        if (newState.pendingBlockUserIds.length) {
631
            // We currently only support a single user;
632
            var userId = newState.pendingBlockUserIds[0];
633
            return newState.members[userId];
634
        } else if (state.pendingBlockUserIds.length) {
635
            return false;
636
        }
637
 
638
        return null;
639
    };
640
 
641
    /**
642
     * Get the user Object of user to be unblocked if pending.
643
     *
644
     * @param  {Object} state The current state.
645
     * @param  {Object} newState The new state.
646
     * @return {Object|Bool|Null} User Object if Object.
647
     */
648
    var buildConfirmUnblockUser = function(state, newState) {
649
        if (newState.pendingUnblockUserIds.length) {
650
            // We currently only support a single user;
651
            var userId = newState.pendingUnblockUserIds[0];
652
            return newState.members[userId];
653
        } else if (state.pendingUnblockUserIds.length) {
654
            return false;
655
        }
656
 
657
        return null;
658
    };
659
 
660
    /**
661
     * Get the user Object of user to be added as contact if pending.
662
     *
663
     * @param  {Object} state The current state.
664
     * @param  {Object} newState The new state.
665
     * @return {Object|Bool|Null} User Object if Object.
666
     */
667
    var buildConfirmAddContact = function(state, newState) {
668
        if (newState.pendingAddContactIds.length) {
669
            // We currently only support a single user;
670
            var userId = newState.pendingAddContactIds[0];
671
            return newState.members[userId];
672
        } else if (state.pendingAddContactIds.length) {
673
            return false;
674
        }
675
 
676
        return null;
677
    };
678
 
679
    /**
680
     * Get the user Object of user to be removed as contact if pending.
681
     *
682
     * @param  {Object} state The current state.
683
     * @param  {Object} newState The new state.
684
     * @return {Object|Bool|Null} User Object if Object.
685
     */
686
    var buildConfirmRemoveContact = function(state, newState) {
687
        if (newState.pendingRemoveContactIds.length) {
688
            // We currently only support a single user;
689
            var userId = newState.pendingRemoveContactIds[0];
690
            return newState.members[userId];
691
        } else if (state.pendingRemoveContactIds.length) {
692
            return false;
693
        }
694
 
695
        return null;
696
    };
697
 
698
    /**
699
     * Check if there are any messages to be deleted.
700
     *
701
     * @param  {Object} state The current state.
702
     * @param  {Object} newState The new state.
703
     * @return {Object|Null} The conversation type and if the user can delete  the messages for all users.
704
     */
705
    var buildConfirmDeleteSelectedMessages = function(state, newState) {
706
        var oldPendingCount = state.pendingDeleteMessageIds.length;
707
        var newPendingCount = newState.pendingDeleteMessageIds.length;
708
 
709
        if (newPendingCount && !oldPendingCount) {
710
            return {
711
                show: true,
712
                type: newState.type,
713
                canDeleteMessagesForAllUsers: newState.canDeleteMessagesForAllUsers
714
            };
715
        } else if (oldPendingCount && !newPendingCount) {
716
            return {
717
                show: false
718
            };
719
        }
720
 
721
        return null;
722
    };
723
 
724
    /**
725
     * Check if there is a conversation to be deleted.
726
     *
727
     * @param  {Object} state The current state.
728
     * @param  {Object} newState The new state.
729
     * @return {int|Null} The conversation type to be deleted.
730
     */
731
    var buildConfirmDeleteConversation = function(state, newState) {
732
        if (!state.pendingDeleteConversation && newState.pendingDeleteConversation) {
733
            return newState.type;
734
        } else if (state.pendingDeleteConversation && !newState.pendingDeleteConversation) {
735
            return false;
736
        }
737
 
738
        return null;
739
    };
740
 
741
    /**
742
     * Check if there is a pending contact request to accept or decline.
743
     *
744
     * @param  {Object} state The current state.
745
     * @param  {Object} newState The new state.
746
     * @return {Bool|Null}
747
     */
748
    var buildConfirmContactRequest = function(state, newState) {
749
        var loggedInUserId = state.loggedInUserId;
750
        var oldOtherUser = getOtherUserFromState(state);
751
        var newOtherUser = getOtherUserFromState(newState);
752
        var oldReceivedRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
753
            return request.requesteduserid == loggedInUserId && request.userid == oldOtherUser.id;
754
        });
755
        var newReceivedRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
756
            return request.requesteduserid == loggedInUserId && request.userid == newOtherUser.id;
757
        });
758
        var oldRequest = oldReceivedRequests.length ? oldReceivedRequests[0] : null;
759
        var newRequest = newReceivedRequests.length ? newReceivedRequests[0] : null;
760
 
761
        if (!oldRequest && newRequest) {
762
            return newOtherUser;
763
        } else if (oldRequest && !newRequest) {
764
            return false;
765
        } else {
766
            return null;
767
        }
768
    };
769
 
770
    /**
771
     * Check if there are any changes in blocked users.
772
     *
773
     * @param  {Object} state The current state.
774
     * @param  {Object} newState The new state.
775
     * @return {Bool|Null}
776
     */
777
    var buildIsBlocked = function(state, newState) {
778
        var oldOtherUser = getOtherUserFromState(state);
779
        var newOtherUser = getOtherUserFromState(newState);
780
 
781
        if (!oldOtherUser && !newOtherUser) {
782
            return null;
783
        } else if (!oldOtherUser && newOtherUser) {
784
            return newOtherUser.isblocked ? true : null;
785
        } else if (!newOtherUser && oldOtherUser) {
786
            return oldOtherUser.isblocked ? false : null;
787
        } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) {
788
            return false;
789
        } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) {
790
            return true;
791
        } else {
792
            return null;
793
        }
794
    };
795
 
796
    /**
797
     * Check if there are any changes the conversation favourite state.
798
     *
799
     * @param  {Object} state The current state.
800
     * @param  {Object} newState The new state.
801
     * @return {Bool|Null}
802
     */
803
    var buildIsFavourite = function(state, newState) {
804
        var oldIsFavourite = state.isFavourite;
805
        var newIsFavourite = newState.isFavourite;
806
 
807
        if (state.id === null && newState.id === null) {
808
            // The conversation isn't yet created so don't change anything.
809
            return null;
810
        } else if (state.id === null && newState.id !== null) {
811
            // The conversation was created so we can show the add favourite button.
812
            return 'show-add';
813
        } else if (state.id !== null && newState.id === null) {
814
            // We're changing from a created conversation to a new conversation so hide
815
            // the favouriting functionality for now.
816
            return 'hide';
817
        } else if (oldIsFavourite == newIsFavourite) {
818
            // No change.
819
            return null;
820
        } else if (!oldIsFavourite && newIsFavourite) {
821
            return 'show-remove';
822
        } else if (oldIsFavourite && !newIsFavourite) {
823
            return 'show-add';
824
        } else {
825
            return null;
826
        }
827
    };
828
 
829
    /**
830
     * Check if there are any changes the conversation muted state.
831
     *
832
     * @param  {Object} state The current state.
833
     * @param  {Object} newState The new state.
834
     * @return {string|null}
835
     */
836
    var buildIsMuted = function(state, newState) {
837
        var oldIsMuted = state.isMuted;
838
        var newIsMuted = newState.isMuted;
839
 
840
        if (state.id === null && newState.id === null) {
841
            // The conversation isn't yet created so don't change anything.
842
            return null;
843
        } else if (state.id === null && newState.id !== null) {
844
            // The conversation was created so we can show the mute button.
845
            return 'show-mute';
846
        } else if (state.id !== null && newState.id === null) {
847
            // We're changing from a created conversation to a new conversation so hide
848
            // the muting functionality for now.
849
            return 'hide';
850
        } else if (oldIsMuted == newIsMuted) {
851
            // No change.
852
            return null;
853
        } else if (!oldIsMuted && newIsMuted) {
854
            return 'show-unmute';
855
        } else if (oldIsMuted && !newIsMuted) {
856
            return 'show-mute';
857
        } else {
858
            return null;
859
        }
860
    };
861
 
862
    /**
863
     * Check if there are any changes in the contact status of the current user
864
     * and other user.
865
     *
866
     * @param  {Object} state The current state.
867
     * @param  {Object} newState The new state.
868
     * @return {Bool|Null}
869
     */
870
    var buildIsContact = function(state, newState) {
871
        var loggedInUserId = state.loggedInUserId;
872
        var oldOtherUser = getOtherUserFromState(state);
873
        var newOtherUser = getOtherUserFromState(newState);
874
        var oldContactRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
875
            return (request.userid == loggedInUserId && request.requesteduserid == oldOtherUser.id) ||
876
                (request.userid == oldOtherUser.id && request.requesteduserid == loggedInUserId);
877
        });
878
        var newContactRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
879
            return (request.userid == loggedInUserId && request.requesteduserid == newOtherUser.id) ||
880
                (request.userid == newOtherUser.id && request.requesteduserid == loggedInUserId);
881
        });
882
        var oldHasContactRequests = oldContactRequests.length > 0;
883
        var newHasContactRequests = newContactRequests.length > 0;
884
 
885
        if (!oldOtherUser && !newOtherUser) {
886
            return null;
887
        } else if (oldHasContactRequests && newHasContactRequests) {
888
            return null;
889
        } else if (!oldHasContactRequests && newHasContactRequests && !newOtherUser.iscontact) {
890
            return 'pending-contact';
891
        } else if (!oldOtherUser && newOtherUser) {
892
            return newOtherUser.iscontact ? 'contact' : null;
893
        } else if (!newOtherUser && oldOtherUser) {
894
            return oldOtherUser.iscontact ? 'non-contact' : null;
895
        } else if (oldOtherUser.iscontact && !newOtherUser.iscontact) {
896
            return newHasContactRequests ? 'pending-contact' : 'non-contact';
897
        } else if (!oldOtherUser.iscontact && newOtherUser.iscontact) {
898
            return 'contact';
899
        } else {
900
            return null;
901
        }
902
    };
903
 
904
    /**
905
     * Check if a confirm action is active.
906
     *
907
     * @param  {Object} state The current state.
908
     * @param  {Object} newState The new state.
909
     * @return {Bool|Null}
910
     */
911
    var buildLoadingConfirmationAction = function(state, newState) {
912
        if (!state.loadingConfirmAction && newState.loadingConfirmAction) {
913
            return true;
914
        } else if (state.loadingConfirmAction && !newState.loadingConfirmAction) {
915
            return false;
916
        } else {
917
            return null;
918
        }
919
    };
920
 
921
    /**
922
     * Check if a edit mode is active.
923
     *
924
     * @param  {Object} state The current state.
925
     * @param  {Object} newState The new state.
926
     * @return {Bool|Null}
927
     */
928
    var buildInEditMode = function(state, newState) {
929
        var oldHasSelectedMessages = state.selectedMessageIds.length > 0;
930
        var newHasSelectedMessages = newState.selectedMessageIds.length > 0;
931
        var numberOfMessagesHasChanged = state.messages.length != newState.messages.length;
932
 
933
        if (!oldHasSelectedMessages && newHasSelectedMessages) {
934
            return true;
935
        } else if (oldHasSelectedMessages && !newHasSelectedMessages) {
936
            return false;
937
        } else if (oldHasSelectedMessages && numberOfMessagesHasChanged) {
938
            return true;
939
        } else {
940
            return null;
941
        }
942
    };
943
 
944
    /**
945
     * Build a patch for the messages selected.
946
     *
947
     * @param  {Object} state The current state.
948
     * @param  {Object} newState The new state.
949
     * @return {Object} patch
950
     */
951
    var buildSelectedMessages = function(state, newState) {
952
        var oldSelectedMessages = state.selectedMessageIds;
953
        var newSelectedMessages = newState.selectedMessageIds;
954
 
955
        if (isArrayEqual(oldSelectedMessages, newSelectedMessages)) {
956
            return null;
957
        }
958
 
959
        var diff = diffArrays(oldSelectedMessages, newSelectedMessages, function(a, b) {
960
            return a == b;
961
        });
962
 
963
        return {
964
            count: newSelectedMessages.length,
965
            add: diff.missingFromA,
966
            remove: diff.missingFromB
967
        };
968
    };
969
 
970
    /**
971
     * Get a list of users from the state that are not the logged in user. Use to find group
972
     * message members or the other user in a conversation.
973
     *
974
     * @param  {Object} state State
975
     * @return {Array} List of users.
976
     */
977
    var getOtherUserFromState = function(state) {
978
        return Object.keys(state.members).reduce(function(carry, userId) {
979
            if (userId != state.loggedInUserId && !carry) {
980
                carry = state.members[userId];
981
            }
982
 
983
            return carry;
984
        }, null);
985
    };
986
 
987
    /**
988
     * Check if the given user requires a contact request from the logged in user.
989
     *
990
     * @param  {Integer} loggedInUserId The logged in user id
991
     * @param  {Object} user User record
992
     * @return {Bool}
993
     */
994
    var requiresContactRequest = function(loggedInUserId, user) {
995
        // If a user can message then no contact request is required.
996
        if (user.canmessage) {
997
            return false;
998
        }
999
 
1000
        var contactRequests = user.contactrequests.filter(function(request) {
1001
            return request.userid == loggedInUserId || request.requesteduserid;
1002
        });
1003
        var hasSentContactRequest = contactRequests.length > 0;
1004
        return user.requirescontact && !user.iscontact && !hasSentContactRequest;
1005
    };
1006
 
1007
    /**
1008
     * Check if other users are required to be added as contact.
1009
     *
1010
     * @param  {Object} state The current state.
1011
     * @param  {Object} newState The new state.
1012
     * @return {Object} Object controlling the required to add contact dialog variables.
1013
     */
1014
    var buildRequireAddContact = function(state, newState) {
1015
        var oldOtherUser = getOtherUserFromState(state);
1016
        var newOtherUser = getOtherUserFromState(newState);
1017
        var hadMessages = state.messages.length > 0;
1018
        var hasMessages = newState.messages.length > 0;
1019
        var loggedInUserId = newState.loggedInUserId;
1020
        var prevRequiresContactRequest = oldOtherUser && requiresContactRequest(loggedInUserId, oldOtherUser);
1021
        var nextRequiresContactRequest = newOtherUser && requiresContactRequest(loggedInUserId, newOtherUser);
1022
        var confirmAddContact = buildConfirmAddContact(state, newState);
1023
        var finishedAddContact = confirmAddContact === false;
1024
 
1025
        // Still doing first load.
1026
        if (!state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) {
1027
            return null;
1028
        }
1029
 
1030
        // No users yet.
1031
        if (!oldOtherUser && !newOtherUser) {
1032
            return null;
1033
        }
1034
 
1035
        // We've loaded a new user and they require a contact request.
1036
        if (!oldOtherUser && nextRequiresContactRequest) {
1037
            return {
1038
                show: true,
1039
                hasMessages: hasMessages,
1040
                user: newOtherUser
1041
            };
1042
        }
1043
 
1044
        // The logged in user has completed the confirm contact request dialogue
1045
        // but the other user still requires a contact request which means the logged
1046
        // in user either declined the confirmation or it failed.
1047
        if (finishedAddContact && nextRequiresContactRequest) {
1048
            return {
1049
                show: true,
1050
                hasMessages: hasMessages,
1051
                user: newOtherUser
1052
            };
1053
        }
1054
 
1055
        // Everything is loaded.
1056
        if (state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) {
1057
            if (!prevRequiresContactRequest && nextRequiresContactRequest) {
1058
                return {
1059
                    show: true,
1060
                    hasMessages: hasMessages,
1061
                    user: newOtherUser
1062
                };
1063
            }
1064
 
1065
            if (prevRequiresContactRequest && !nextRequiresContactRequest) {
1066
                return {
1067
                    show: false,
1068
                    hasMessages: hasMessages
1069
                };
1070
            }
1071
        }
1072
 
1073
        // First load just completed.
1074
        if (!state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) {
1075
            if (nextRequiresContactRequest) {
1076
                return {
1077
                    show: true,
1078
                    hasMessages: hasMessages,
1079
                    user: newOtherUser
1080
                };
1081
            }
1082
        }
1083
 
1084
        // Being reset.
1085
        if (state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) {
1086
            if (prevRequiresContactRequest) {
1087
                return {
1088
                    show: false,
1089
                    hasMessages: hadMessages
1090
                };
1091
            }
1092
        }
1093
 
1094
        return null;
1095
    };
1096
 
1097
    /**
1098
     * Check if other users are required to be unblocked.
1099
     *
1100
     * @param  {Object} state The current state.
1101
     * @param  {Object} newState The new state.
1102
     * @return {Bool|Null}
1103
     */
1104
    var buildRequireUnblock = function(state, newState) {
1105
        var oldOtherUser = getOtherUserFromState(state);
1106
        var newOtherUser = getOtherUserFromState(newState);
1107
 
1108
        if (!oldOtherUser && !newOtherUser) {
1109
            return null;
1110
        } else if (oldOtherUser && !newOtherUser) {
1111
            return oldOtherUser.isblocked ? false : null;
1112
        } else if (!oldOtherUser && newOtherUser) {
1113
            return newOtherUser.isblocked ? true : null;
1114
        } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) {
1115
            return true;
1116
        } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) {
1117
            return false;
1118
        }
1119
 
1120
        return null;
1121
    };
1122
 
1123
    /**
1124
     * Check if other users can be messaged.
1125
     *
1126
     * @param  {Object} state The current state.
1127
     * @param  {Object} newState The new state.
1128
     * @return {Bool|Null}
1129
     */
1130
    var buildUnableToMessage = function(state, newState) {
1131
        var oldOtherUser = getOtherUserFromState(state);
1132
        var newOtherUser = getOtherUserFromState(newState);
1133
 
1134
        if (newState.type == Constants.CONVERSATION_TYPES.SELF) {
1135
            // Users always can send message themselves on self-conversations.
1136
            return null;
1137
        }
1138
 
1139
        if (!oldOtherUser && !newOtherUser) {
1140
            return null;
1141
        } else if (oldOtherUser && !newOtherUser) {
1142
            return oldOtherUser.canmessage ? null : true;
1143
        } else if (!oldOtherUser && newOtherUser) {
1144
            return newOtherUser.canmessage ? null : true;
1145
        } else if (!oldOtherUser.canmessage && newOtherUser.canmessage) {
1146
            return false;
1147
        } else if (oldOtherUser.canmessage && !newOtherUser.canmessage) {
1148
            return true;
1149
        }
1150
 
1151
        return null;
1152
    };
1153
 
1154
    /**
1155
     * Build patch for footer information for a private conversation.
1156
     *
1157
     * @param  {Object} state The current state.
1158
     * @param  {Object} newState The new state.
1159
     * @return {Object} containing footer state type.
1160
     */
1161
    var buildFooterPatchTypePrivate = function(state, newState) {
1162
        var loadingFirstMessages = buildLoadingFirstMessages(state, newState);
1163
        var inEditMode = buildInEditMode(state, newState);
1164
        var requireAddContact = buildRequireAddContact(state, newState);
1165
        var requireUnblock = buildRequireUnblock(state, newState);
1166
        var unableToMessage = buildUnableToMessage(state, newState);
1167
        var showRequireAddContact = requireAddContact !== null ? requireAddContact.show && requireAddContact.hasMessages : null;
1168
        var otherUser = getOtherUserFromState(newState);
1169
        var generateReturnValue = function(checkValue, successReturn) {
1170
            if (checkValue) {
1171
                return successReturn;
1172
            } else if (checkValue !== null && !checkValue) {
1173
                if (!otherUser) {
1174
                    return {type: 'content'};
1175
                } else if (otherUser.isblocked) {
1176
                    return {type: 'unblock'};
1177
                } else if (newState.messages.length && requiresContactRequest(newState.loggedInUserId, otherUser)) {
1178
                    return {
1179
                        type: 'add-contact',
1180
                        user: otherUser
1181
                    };
1182
                } else if (!otherUser.canmessage && (otherUser.requirescontact && !otherUser.iscontact)) {
1183
                    return {type: 'unable-to-message'};
1184
                }
1185
            }
1186
 
1187
            return null;
1188
        };
1189
 
1190
        if (
1191
            loadingFirstMessages === null &&
1192
            inEditMode === null &&
1193
            requireAddContact === null &&
1194
            requireUnblock === null
1195
        ) {
1196
            return null;
1197
        }
1198
 
1199
        var checks = [
1200
            [loadingFirstMessages, {type: 'placeholder'}],
1201
            [inEditMode, {type: 'edit-mode'}],
1202
            [unableToMessage, {type: 'unable-to-message'}],
1203
            [requireUnblock, {type: 'unblock'}],
1204
            [showRequireAddContact, {type: 'add-contact', user: otherUser}]
1205
        ];
1206
 
1207
        for (var i = 0; i < checks.length; i++) {
1208
            var checkValue = checks[i][0];
1209
            var successReturn = checks[i][1];
1210
            var result = generateReturnValue(checkValue, successReturn);
1211
 
1212
            if (result !== null) {
1213
                return result;
1214
            }
1215
        }
1216
 
1217
        return {
1218
            type: 'content'
1219
        };
1220
    };
1221
 
1222
    /**
1223
     * Build patch for footer information for a public conversation.
1224
     *
1225
     * @param  {Object} state The current state.
1226
     * @param  {Object} newState The new state.
1227
     * @return {Object} containing footer state type.
1228
     */
1229
    var buildFooterPatchTypePublic = function(state, newState) {
1230
        var loadingFirstMessages = buildLoadingFirstMessages(state, newState);
1231
        var inEditMode = buildInEditMode(state, newState);
1232
 
1233
        if (loadingFirstMessages === null && inEditMode === null) {
1234
            return null;
1235
        }
1236
 
1237
        if (loadingFirstMessages) {
1238
            return {type: 'placeholder'};
1239
        }
1240
 
1241
        if (inEditMode) {
1242
            return {type: 'edit-mode'};
1243
        }
1244
 
1245
        return {
1246
            type: 'content'
1247
        };
1248
    };
1249
 
1250
    /**
1251
     * Check if we're viewing a different conversation. If so then we need to
1252
     * reset the UI.
1253
     *
1254
     * @param  {Object} state The current state.
1255
     * @param  {Object} newState The new state.
1256
     * @return {bool|null} If a reset needs to occur
1257
     */
1258
    var buildReset = function(state, newState) {
1259
        var oldType = state.type;
1260
        var newType = newState.type;
1261
        var oldConversationId = state.id;
1262
        var newConversationId = newState.id;
1263
        var oldMemberIds = Object.keys(state.members);
1264
        var newMemberIds = Object.keys(newState.members);
1265
 
1266
        oldMemberIds.sort();
1267
        newMemberIds.sort();
1268
 
1269
        var membersUnchanged = oldMemberIds.every(function(id, index) {
1270
            return id == newMemberIds[index];
1271
        });
1272
 
1273
        if (oldType != newType) {
1274
            // If we've changed conversation type then we need to reset.
1275
            return true;
1276
        } else if (oldConversationId && !newConversationId) {
1277
            // We previously had a conversation id but no longer do. This likely means
1278
            // the user is viewing the conversation with someone they've never spoken to
1279
            // before.
1280
            return true;
1281
        } else if (oldConversationId && newConversationId && oldConversationId != newConversationId) {
1282
            // If we had a conversation id and it's changed then we need to reset.
1283
            return true;
1284
        } else if (!oldConversationId && !newConversationId && !membersUnchanged) {
1285
            // If we never had a conversation id but the members of the conversation have
1286
            // changed then we need to reset. This can happen if the user goes from viewing
1287
            // a user they've never had a conversation with to viewing a different user that
1288
            // they've never had a conversation with.
1289
            return true;
1290
        }
1291
 
1292
        return null;
1293
    };
1294
 
1295
    /**
1296
     * We should show this message always, for all the self-conversations.
1297
     *
1298
     * The message should be hidden when it's not a self-conversation.
1299
     *
1300
     * @param  {Object} state The current state.
1301
     * @param  {Object} newState The new state.
1302
     * @return {bool}
1303
     */
1304
    var buildSelfConversationMessage = function(state, newState) {
1305
        if (state.type != newState.type) {
1306
            return (newState.type == Constants.CONVERSATION_TYPES.SELF);
1307
        }
1308
 
1309
        return null;
1310
    };
1311
 
1312
    /**
1313
     * We should show the contact request sent message if the user just sent
1314
     * a contact request to the other user
1315
     *
1316
     * @param  {Object} state The current state.
1317
     * @param  {Object} newState The new state.
1318
     * @return {string|false|null}
1319
     */
1320
    var buildContactRequestSent = function(state, newState) {
1321
        var loggedInUserId = newState.loggedInUserId;
1322
        var oldOtherUser = getOtherUserFromState(state);
1323
        var newOtherUser = getOtherUserFromState(newState);
1324
        var oldSentRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) {
1325
            return request.userid == loggedInUserId;
1326
        });
1327
        var newSentRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) {
1328
            return request.userid == loggedInUserId;
1329
        });
1330
        var oldRequest = oldSentRequests.length > 0;
1331
        var newRequest = newSentRequests.length > 0;
1332
 
1333
        if (!oldRequest && newRequest && !newOtherUser.iscontact) {
1334
            return newOtherUser.fullname;
1335
        } else if (oldOtherUser && !oldOtherUser.iscontact && newRequest && newOtherUser.iscontact) {
1336
            // Contact request accepted.
1337
            return false;
1338
        } else if (oldRequest && !newRequest) {
1339
            return false;
1340
        } else {
1341
            return null;
1342
        }
1343
    };
1344
 
1345
    /**
1346
     * Build the full patch comparing the current state and the new state. This patch is used by
1347
     * the conversation renderer to render the UI on any update.
1348
     *
1349
     * @param  {Object} state The current state.
1350
     * @param  {Object} newState The new state.
1351
     * @return {Object} Patch containing all information changed.
1352
     */
1353
    var buildPatch = function(state, newState) {
1354
        var config = {
1355
            all: {
1356
                reset: buildReset,
1357
                conversation: buildConversationPatch,
1358
                scrollToMessage: buildScrollToMessagePatch,
1359
                loadingMembers: buildLoadingMembersPatch,
1360
                loadingFirstMessages: buildLoadingFirstMessages,
1361
                loadingMessages: buildLoadingMessages,
1362
                confirmDeleteSelectedMessages: buildConfirmDeleteSelectedMessages,
1363
                inEditMode: buildInEditMode,
1364
                selectedMessages: buildSelectedMessages,
1365
                isFavourite: buildIsFavourite,
1366
                isMuted: buildIsMuted,
1367
                showEmojiPicker: buildShowEmojiPicker,
1368
                showEmojiAutoComplete: buildShowEmojiAutoComplete
1369
            }
1370
        };
1371
        // These build functions are only applicable to private conversations.
1372
        config[Constants.CONVERSATION_TYPES.PRIVATE] = {
1373
            header: buildHeaderPatchTypePrivate,
1374
            footer: buildFooterPatchTypePrivate,
1375
            confirmBlockUser: buildConfirmBlockUser,
1376
            confirmUnblockUser: buildConfirmUnblockUser,
1377
            confirmAddContact: buildConfirmAddContact,
1378
            confirmRemoveContact: buildConfirmRemoveContact,
1379
            confirmContactRequest: buildConfirmContactRequest,
1380
            confirmDeleteConversation: buildConfirmDeleteConversation,
1381
            isBlocked: buildIsBlocked,
1382
            isContact: buildIsContact,
1383
            loadingConfirmAction: buildLoadingConfirmationAction,
1384
            requireAddContact: buildRequireAddContact,
1385
            contactRequestSent: buildContactRequestSent
1386
        };
1387
        // These build functions are only applicable to public (group) conversations.
1388
        config[Constants.CONVERSATION_TYPES.PUBLIC] = {
1389
            header: buildHeaderPatchTypePublic,
1390
            footer: buildFooterPatchTypePublic,
1391
        };
1392
        // These build functions are only applicable to self-conversations.
1393
        config[Constants.CONVERSATION_TYPES.SELF] = {
1394
            header: buildHeaderPatchTypeSelf,
1395
            footer: buildFooterPatchTypePublic,
1396
            confirmDeleteConversation: buildConfirmDeleteConversation,
1397
            selfConversationMessage: buildSelfConversationMessage
1398
        };
1399
 
1400
        var patchConfig = $.extend({}, config.all);
1401
        if (newState.type && newState.type in config) {
1402
            // Add the type specific builders to the patch config.
1403
            patchConfig = $.extend(patchConfig, config[newState.type]);
1404
        }
1405
 
1406
        return Object.keys(patchConfig).reduce(function(patch, key) {
1407
            var buildFunc = patchConfig[key];
1408
            var value = buildFunc(state, newState);
1409
 
1410
            if (value !== null) {
1411
                patch[key] = value;
1412
            }
1413
 
1414
            return patch;
1415
        }, {});
1416
    };
1417
 
1418
    return {
1419
        buildPatch: buildPatch
1420
    };
1421
});