<commit_before>/**
 * Create self executing funton to avoid global scope creation
 */
(function (angular, tinymce, buildfire) {
    'use strict';
    angular
        .module('placesContent')
    /**
     * Inject dependency
     */
        .controller('ContentItemsCtrl', ['$scope', '$routeParams', 'Buildfire', 'DB', 'COLLECTIONS', 'Modals', 'Orders', 'OrdersItems', 'Messaging', 'EVENTS', 'PATHS', 'Location', 'placesInfo', 'sectionInfo', 'DEFAULT_DATA', '$rootScope',
            function ($scope, $routeParams, Buildfire, DB, COLLECTIONS, Modals, Orders, OrdersItems, Messaging, EVENTS, PATHS, Location, placesInfo, sectionInfo, DEFAULT_DATA, $rootScope) {
                //Show the INT header part.
                Buildfire.appearance.setHeaderVisibility(true);

                //Scroll current view to top when page loaded.
                buildfire.navigation.scrollTop();
                /**
                 * Create instance of Sections,PlaceInfo and Items db collection
                 * @type {DB}
                 */
                var searchOptions;
                var ContentItems = this;
                ContentItems.Sections = new DB(COLLECTIONS.Sections);
                ContentItems.Items = new DB(COLLECTIONS.Items);
                ContentItems.PlaceInfo = new DB(COLLECTIONS.PlaceInfo);
                var tmrDelayForInfo = null
                    , _skip = 0
                    , _limit = 10
                    , _maxLimit = 19;

                if (sectionInfo != 'allitems')
                    ContentItems.sectionInfo = sectionInfo;
                if (placesInfo) {
                    updateMasterInfoData(placesInfo);
                    ContentItems.info = placesInfo;
                } else {
                    updateMasterInfoData(DEFAULT_DATA.PLACE_INFO);
                    ContentItems.info = angular.copy(DEFAULT_DATA.PLACE_INFO);
                }
                if ($routeParams.sectionId) {
                    ContentItems.section = $routeParams.sectionId;
                }
                else {
                    ContentItems.section = 'allitems';
                }
                ContentItems.isBusy = false;
                ContentItems.items = null;
                //ContentItems.masterInfoData = null;
                ContentItems.sortOptions = OrdersItems.options;
                ContentItems.itemSortableOptions = {
                    handle: '> .cursor-grab',
                    disabled: !(ContentItems.info.data.content.sortByItems === Orders.ordersMap.Manually),
                    stop: function (e, ui) {

                        var endIndex = ui.item.sortable.dropindex,
                            maxRank = 0,
                            draggedItem = ContentItems.items[endIndex];
                        if (draggedItem) {
                            var prev = ContentItems.items[endIndex - 1],
                                next = ContentItems.items[endIndex + 1];
                            var isRankChanged = false;
                            if (next) {
                                if (prev) {
                                    draggedItem.data.rank = ((prev.data.rank || 0) + (next.data.rank || 0)) / 2;
                                    isRankChanged = true;
                                } else {
                                    draggedItem.data.rank = (next.data.rank || 0) / 2;
                                    isRankChanged = true;
                                }
                            } else {
                                if (prev) {
                                    draggedItem.data.rank = (((prev.data.rank || 0) * 2) + 10) / 2;
                                    maxRank = draggedItem.data.rank;
                                    isRankChanged = true;
                                }
                            }
                            if (isRankChanged) {
                                console.log('update in index');
                                ContentItems.Items.update(draggedItem.id, draggedItem.data).then(function (updataeditem) {
                                    console.log('Updated item--------------------------------', updataeditem);
                                    if (ContentItems.sectionInfo) {
                                        ContentItems.sectionInfo.data.rankOfLastItem = maxRank;
                                        // Update the rankOfLastItem in a particular section
                                        ContentItems.Sections.update(ContentItems.sectionInfo.id, ContentItems.sectionInfo.data).then(function (data) {
                                                // Do Something on Success
                                            },
                                            function () {
                                                console.error('Error while updating sections Collection data');
                                            });
                                    }
                                }, function () {

                                });

                            }
                        }
                    }
                };
                if (ContentItems.sectionInfo) {
                    searchOptions = {
                        filter: {'$and': [{"$json.itemTitle": {"$regex": '/*'}}, {"$json.sections": {"$all": [ContentItems.section]}}]},
                        skip: _skip,
                        limit: _limit + 1 // the plus one is to check if there are any more
                    };
                }
                else {
                    searchOptions = {
                        filter: {"$json.itemTitle": {"$regex": '/*'}},
                        skip: _skip,
                        limit: _limit + 1 // the plus one is to check if there are any more
                    };
                }

                var updateSearchOptions = function () {
                    var order;
                    if (ContentItems.info && ContentItems.info.data && ContentItems.info.data.content) {
                        order = OrdersItems.getOrder(ContentItems.info.data.content.sortByItems || OrdersItems.ordersMap.Default);
                    }
                    else
                        order = OrdersItems.getOrder(OrdersItems.ordersMap.Default);

                    if (order) {
                        var sort = {};
                        sort[order.key] = order.order;
                        searchOptions.sort = sort;
                        return true;
                    }
                    else {
                        return false;
                    }
                };

                function updateMasterInfoData(infoData) {
                    ContentItems.masterInfoData = angular.copy(infoData);
                }

                /**
                 * isUnChanged to check whether there is change in controller media item or not
                 * @param obj
                 * @returns {*|boolean}
                 */
                function isUnChanged(obj) {
                    console.clear();
                    console.log('obj', obj);
                    console.log('ContentItems.masterInfoData', ContentItems.masterInfoData);
                    return angular.equals(obj, ContentItems.masterInfoData);
                }

                function saveInfoData(_info) {
                    ContentItems.PlaceInfo.save(_info.data).then(function (data) {
                        updateMasterInfoData(_info);
                    }, function (err) {
                        console.error('Error-------', err);
                    });
                }

                function saveInfoDataWithDelay(_info) {
                    if (tmrDelayForInfo) {
                        clearTimeout(tmrDelayForInfo);
                    }
                    if (!isUnChanged(_info)) {
                        tmrDelayForInfo = setTimeout(function () {
                            saveInfoData(_info);
                        }, 1000);
                    }
                }

                ContentItems.deepLinkUrl = function (url) {
                    buildfire.navigation.scrollTop();
                    Modals.DeeplinkPopupModal(url);
                };

                /**
                 * ContentItems.toggleSortOrder() to change the sort by
                 */
                ContentItems.toggleSortOrder = function (name) {
                    if (name) {
                        var sortOrder = OrdersItems.getOrder(name);
                        ContentItems.items = null;
                        ContentItems.noMore = false;
                        searchOptions.skip = 0;
                        ContentItems.isBusy = false;
                        ContentItems.info.data.content.sortByItems = name;
                        ContentItems.getMore();
                        ContentItems.itemSortableOptions.disabled = !(ContentItems.info.data.content.sortByItems === OrdersItems.ordersMap.Manually);
                    }
                };

                /**
                 * ContentItems.noMore tells if all data has been loaded
                 */
                ContentItems.noMore = false;

                /**
                 * ContentItems.getMore is used to load the items
                 */
                ContentItems.getMore = function () {
                    if (ContentItems.isBusy && !ContentItems.noMore) {
                        return;
                    }
                    updateSearchOptions();
                    ContentItems.isBusy = true;
                    ContentItems.Items.find(searchOptions).then(function success(result) {
                        if (result.length <= _limit) {// to indicate there are more
                            ContentItems.noMore = true;
                        }
                        else {
                            result.pop();
                            searchOptions.skip = searchOptions.skip + _limit;
                            ContentItems.noMore = false;
                        }
                        ContentItems.items = ContentItems.items ? ContentItems.items.concat(result) : result;
                        ContentItems.isBusy = false;
                    }, function fail() {
                        ContentItems.isBusy = false;
                    });
                };

                /**
                 * ContentItems.removeListItem() used to delete an item from section list
                 * @param index tells the index of item to be deleted.
                 */
                ContentItems.removeListItem = function (index) {

                    if ("undefined" == typeof index) {
                        return;
                    }
                    var item = ContentItems.items[index];
                    if ("undefined" !== typeof item) {
                        //buildfire.navigation.scrollTop();

                        Modals.removePopupModal({title: ''}).then(function (result) {
                            if (result) {
                                ContentItems.Items.delete(item.id).then(function (data) {
                                    ContentItems.items.splice(index, 1);
                                }, function (err) {
                                    console.error('Error while deleting an item-----', err);
                                });
                            }
                            else {
                                console.info('Unable to load data.');
                            }
                        }, function (cancelData) {
                            //do something on cancel
                        });
                    }
                };

                /**
                 * ContentItems.searchListSection() used to search items section
                 * @param value to be search.
                 */
                ContentItems.searchListItem = function (value) {
                    searchOptions.skip = 0;
                    /*reset the skip value*/
                    ContentItems.isBusy = false;
                    ContentItems.items = null;
                    value = value.trim();
                    if (!value) {
                        value = '/*';
                    }
                    searchOptions.filter = {
                        '$and': [{
                            "$json.itemTitle": {
                                "$regex": value,
                                "$options": "i"
                            }
                        }, {"$json.sections": {"$all": [ContentItems.section]}}]
                    };// {"$json.secTitle": {"$regex": value}};
                    ContentItems.getMore();
                };

                ContentItems.editSections = function (ind) {
                    ContentItems.Sections.find({}).then(function (data) {
                        buildfire.navigation.scrollTop();
                        Modals.editSectionModal(data, ContentItems.items[ind]).then(function (result) {
                            ContentItems.Items.update(result.id, result.data).then(function () {
                                _skip = 0;
                                ContentItems.items = null;
                                ContentItems.getMore();
                            }, function () {
                                console.error('err happened');
                            });

                        }, function (cancelData) {
                            //do something on cancel
                        });
                    }, function (err) {
                    });

                };

                ContentItems.done = function () {
                    Location.goToHome();
                };

                if ($rootScope.dontPropagate === true) {
                    $rootScope.dontPropagate = false;
                }
                else {
                    //syn with widget
                    Messaging.sendMessageToWidget({
                        name: EVENTS.ROUTE_CHANGE,
                        message: {
                            path: PATHS.SECTION,
                            secId: ContentItems.section
                        }
                    });
                }

                $scope.$watch(function () {
                    return ContentItems.info;
                }, saveInfoDataWithDelay, true);

            }]);
})(window.angular, window.tinymce, window.buildfire);<commit_msg>Console.log removed<commit_after>/**
 * Create self executing funton to avoid global scope creation
 */
(function (angular, tinymce, buildfire) {
    'use strict';
    angular
        .module('placesContent')
    /**
     * Inject dependency
     */
        .controller('ContentItemsCtrl', ['$scope', '$routeParams', 'Buildfire', 'DB', 'COLLECTIONS', 'Modals', 'Orders', 'OrdersItems', 'Messaging', 'EVENTS', 'PATHS', 'Location', 'placesInfo', 'sectionInfo', 'DEFAULT_DATA', '$rootScope',
            function ($scope, $routeParams, Buildfire, DB, COLLECTIONS, Modals, Orders, OrdersItems, Messaging, EVENTS, PATHS, Location, placesInfo, sectionInfo, DEFAULT_DATA, $rootScope) {
                //Show the INT header part.
                Buildfire.appearance.setHeaderVisibility(true);

                //Scroll current view to top when page loaded.
                buildfire.navigation.scrollTop();
                /**
                 * Create instance of Sections,PlaceInfo and Items db collection
                 * @type {DB}
                 */
                var searchOptions;
                var ContentItems = this;
                ContentItems.Sections = new DB(COLLECTIONS.Sections);
                ContentItems.Items = new DB(COLLECTIONS.Items);
                ContentItems.PlaceInfo = new DB(COLLECTIONS.PlaceInfo);
                var tmrDelayForInfo = null
                    , _skip = 0
                    , _limit = 10
                    , _maxLimit = 19;

                if (sectionInfo != 'allitems')
                    ContentItems.sectionInfo = sectionInfo;
                if (placesInfo) {
                    updateMasterInfoData(placesInfo);
                    ContentItems.info = placesInfo;
                } else {
                    updateMasterInfoData(DEFAULT_DATA.PLACE_INFO);
                    ContentItems.info = angular.copy(DEFAULT_DATA.PLACE_INFO);
                }
                if ($routeParams.sectionId) {
                    ContentItems.section = $routeParams.sectionId;
                }
                else {
                    ContentItems.section = 'allitems';
                }
                ContentItems.isBusy = false;
                ContentItems.items = null;
                //ContentItems.masterInfoData = null;
                ContentItems.sortOptions = OrdersItems.options;
                ContentItems.itemSortableOptions = {
                    handle: '> .cursor-grab',
                    disabled: !(ContentItems.info.data.content.sortByItems === Orders.ordersMap.Manually),
                    stop: function (e, ui) {

                        var endIndex = ui.item.sortable.dropindex,
                            maxRank = 0,
                            draggedItem = ContentItems.items[endIndex];
                        if (draggedItem) {
                            var prev = ContentItems.items[endIndex - 1],
                                next = ContentItems.items[endIndex + 1];
                            var isRankChanged = false;
                            if (next) {
                                if (prev) {
                                    draggedItem.data.rank = ((prev.data.rank || 0) + (next.data.rank || 0)) / 2;
                                    isRankChanged = true;
                                } else {
                                    draggedItem.data.rank = (next.data.rank || 0) / 2;
                                    isRankChanged = true;
                                }
                            } else {
                                if (prev) {
                                    draggedItem.data.rank = (((prev.data.rank || 0) * 2) + 10) / 2;
                                    maxRank = draggedItem.data.rank;
                                    isRankChanged = true;
                                }
                            }
                            if (isRankChanged) {
                                console.log('update in index');
                                ContentItems.Items.update(draggedItem.id, draggedItem.data).then(function (updataeditem) {
                                    console.log('Updated item--------------------------------', updataeditem);
                                    if (ContentItems.sectionInfo) {
                                        ContentItems.sectionInfo.data.rankOfLastItem = maxRank;
                                        // Update the rankOfLastItem in a particular section
                                        ContentItems.Sections.update(ContentItems.sectionInfo.id, ContentItems.sectionInfo.data).then(function (data) {
                                                // Do Something on Success
                                            },
                                            function () {
                                                console.error('Error while updating sections Collection data');
                                            });
                                    }
                                }, function () {

                                });

                            }
                        }
                    }
                };
                if (ContentItems.sectionInfo) {
                    searchOptions = {
                        filter: {'$and': [{"$json.itemTitle": {"$regex": '/*'}}, {"$json.sections": {"$all": [ContentItems.section]}}]},
                        skip: _skip,
                        limit: _limit + 1 // the plus one is to check if there are any more
                    };
                }
                else {
                    searchOptions = {
                        filter: {"$json.itemTitle": {"$regex": '/*'}},
                        skip: _skip,
                        limit: _limit + 1 // the plus one is to check if there are any more
                    };
                }

                var updateSearchOptions = function () {
                    var order;
                    if (ContentItems.info && ContentItems.info.data && ContentItems.info.data.content) {
                        order = OrdersItems.getOrder(ContentItems.info.data.content.sortByItems || OrdersItems.ordersMap.Default);
                    }
                    else
                        order = OrdersItems.getOrder(OrdersItems.ordersMap.Default);

                    if (order) {
                        var sort = {};
                        sort[order.key] = order.order;
                        searchOptions.sort = sort;
                        return true;
                    }
                    else {
                        return false;
                    }
                };

                function updateMasterInfoData(infoData) {
                    ContentItems.masterInfoData = angular.copy(infoData);
                }

                /**
                 * isUnChanged to check whether there is change in controller media item or not
                 * @param obj
                 * @returns {*|boolean}
                 */
                function isUnChanged(obj) {
                    return angular.equals(obj, ContentItems.masterInfoData);
                }

                function saveInfoData(_info) {
                    ContentItems.PlaceInfo.save(_info.data).then(function (data) {
                        updateMasterInfoData(_info);
                    }, function (err) {
                        console.error('Error-------', err);
                    });
                }

                function saveInfoDataWithDelay(_info) {
                    if (tmrDelayForInfo) {
                        clearTimeout(tmrDelayForInfo);
                    }
                    if (!isUnChanged(_info)) {
                        tmrDelayForInfo = setTimeout(function () {
                            saveInfoData(_info);
                        }, 1000);
                    }
                }

                ContentItems.deepLinkUrl = function (url) {
                    buildfire.navigation.scrollTop();
                    Modals.DeeplinkPopupModal(url);
                };

                /**
                 * ContentItems.toggleSortOrder() to change the sort by
                 */
                ContentItems.toggleSortOrder = function (name) {
                    if (name) {
                        var sortOrder = OrdersItems.getOrder(name);
                        ContentItems.items = null;
                        ContentItems.noMore = false;
                        searchOptions.skip = 0;
                        ContentItems.isBusy = false;
                        ContentItems.info.data.content.sortByItems = name;
                        ContentItems.getMore();
                        ContentItems.itemSortableOptions.disabled = !(ContentItems.info.data.content.sortByItems === OrdersItems.ordersMap.Manually);
                    }
                };

                /**
                 * ContentItems.noMore tells if all data has been loaded
                 */
                ContentItems.noMore = false;

                /**
                 * ContentItems.getMore is used to load the items
                 */
                ContentItems.getMore = function () {
                    if (ContentItems.isBusy && !ContentItems.noMore) {
                        return;
                    }
                    updateSearchOptions();
                    ContentItems.isBusy = true;
                    ContentItems.Items.find(searchOptions).then(function success(result) {
                        if (result.length <= _limit) {// to indicate there are more
                            ContentItems.noMore = true;
                        }
                        else {
                            result.pop();
                            searchOptions.skip = searchOptions.skip + _limit;
                            ContentItems.noMore = false;
                        }
                        ContentItems.items = ContentItems.items ? ContentItems.items.concat(result) : result;
                        ContentItems.isBusy = false;
                    }, function fail() {
                        ContentItems.isBusy = false;
                    });
                };

                /**
                 * ContentItems.removeListItem() used to delete an item from section list
                 * @param index tells the index of item to be deleted.
                 */
                ContentItems.removeListItem = function (index) {

                    if ("undefined" == typeof index) {
                        return;
                    }
                    var item = ContentItems.items[index];
                    if ("undefined" !== typeof item) {
                        //buildfire.navigation.scrollTop();

                        Modals.removePopupModal({title: ''}).then(function (result) {
                            if (result) {
                                ContentItems.Items.delete(item.id).then(function (data) {
                                    ContentItems.items.splice(index, 1);
                                }, function (err) {
                                    console.error('Error while deleting an item-----', err);
                                });
                            }
                            else {
                                console.info('Unable to load data.');
                            }
                        }, function (cancelData) {
                            //do something on cancel
                        });
                    }
                };

                /**
                 * ContentItems.searchListSection() used to search items section
                 * @param value to be search.
                 */
                ContentItems.searchListItem = function (value) {
                    searchOptions.skip = 0;
                    /*reset the skip value*/
                    ContentItems.isBusy = false;
                    ContentItems.items = null;
                    value = value.trim();
                    if (!value) {
                        value = '/*';
                    }
                    searchOptions.filter = {
                        '$and': [{
                            "$json.itemTitle": {
                                "$regex": value,
                                "$options": "i"
                            }
                        }, {"$json.sections": {"$all": [ContentItems.section]}}]
                    };// {"$json.secTitle": {"$regex": value}};
                    ContentItems.getMore();
                };

                ContentItems.editSections = function (ind) {
                    ContentItems.Sections.find({}).then(function (data) {
                        buildfire.navigation.scrollTop();
                        Modals.editSectionModal(data, ContentItems.items[ind]).then(function (result) {
                            ContentItems.Items.update(result.id, result.data).then(function () {
                                _skip = 0;
                                ContentItems.items = null;
                                ContentItems.getMore();
                            }, function () {
                                console.error('err happened');
                            });

                        }, function (cancelData) {
                            //do something on cancel
                        });
                    }, function (err) {
                    });

                };

                ContentItems.done = function () {
                    Location.goToHome();
                };

                if ($rootScope.dontPropagate === true) {
                    $rootScope.dontPropagate = false;
                }
                else {
                    //syn with widget
                    Messaging.sendMessageToWidget({
                        name: EVENTS.ROUTE_CHANGE,
                        message: {
                            path: PATHS.SECTION,
                            secId: ContentItems.section
                        }
                    });
                }

                $scope.$watch(function () {
                    return ContentItems.info;
                }, saveInfoDataWithDelay, true);

            }]);
})(window.angular, window.tinymce, window.buildfire);<|endoftext|>


<commit_before>/* global marked, Push */
/* eslint-disable no-console */

"use strict"

import { StoreUsingServer } from "./StoreUsingServer.js"
import { HashUtils } from "./HashUtils.js"
import { FileUtils } from "./FileUtils.js"
import { FileUploader } from "./FileUploader.js"
import { UUID } from "./UUID.js"

// defines marked
import "./vendor/marked.js"

// defines m
import "./vendor/mithril.js"

let chatRoom = "test"
let userID = localStorage.getItem("userID") || "anonymous"
let chatText = ""
const messages = []
let editedChatMessageUUID = null
let editedChatMessageText = ""

// filterText is split into tags by spaces and used to filter by a logical "and" to include displayed items
let filterText = ""

// hideText is split into tags by spaces and used to filter by a logical "OR" to hide displayed items
let hideText = ""

let sortMessagesByContent = false

let messagesDiv = null

let messagesByUUID = {}

let showEntryArea = true

function startup() {
    chatRoom = HashUtils.getHashParams()["chatRoom"] || chatRoom
    window.onhashchange = () => updateChatRoomFromHash()
    updateHashForChatRoom()
}

function updateTitleForChatRoom() {
    document.title = chatRoom + " -- Twirlip7 Chat"
}

function updateChatRoomFromHash() {
    const hashParams = HashUtils.getHashParams()
    console.log("updateChatRoomFromHash", hashParams)
    const newChatRoom = hashParams["chatRoom"]
    if (newChatRoom !== chatRoom) {
        resetMessagesForChatroomChange()
        chatRoom = newChatRoom
        backend.configure({chatRoom})
        updateTitleForChatRoom()
    }
}

function updateHashForChatRoom() {
    const hashParams = HashUtils.getHashParams()
    hashParams["chatRoom"] = chatRoom
    HashUtils.setHashParams(hashParams)
    updateTitleForChatRoom()
}

function chatRoomChange(event) {
    resetMessagesForChatroomChange()
    chatRoom = event.target.value
    updateHashForChatRoom()
    backend.configure({chatRoom})
}

function resetMessagesForChatroomChange() {
    messages.splice(0)
    messagesByUUID = {}
}

function userIDChange(event) {
    userID = event.target.value
    backend.configure(undefined, userID)
    localStorage.setItem("userID", userID)
}

function chatTextChange(event) {
    chatText = event.target.value
}

function sendChatMessage() {
    const timestamp = new Date().toISOString()
    const uuid = "chatMessage:" + UUID.uuidv4()

    const newMessage = { chatText, userID, timestamp, uuid }

    sendMessage({ chatText, userID, timestamp, uuid })
    chatText = ""
    if (!hasFilterText(newMessage)) {
        setTimeout(() => alert("The message you just added is currently\nnot displayed due to show/hide filtering."))
        /*
        filterText = ""
        hideText = "
        */
    }
    setTimeout(() => {
        // Scroll to bottom always when sending -- but defer it just in case was filtering
        if (messagesDiv) {
            messagesDiv.scrollTop = messagesDiv.scrollHeight
        }
    }, 0)
}

function sendMessage(message) {
    // Call addItem after a delay to give socket.io a chance to reconnect
    // as socket.io will timeout if a prompt (or alert?) is up for very long
    setTimeout(() => backend.addItem(message), 10)
}

function sendEditedChatMessage() {
    const timestamp = new Date().toISOString()
    const uuid = editedChatMessageUUID

    sendMessage({ chatText: editedChatMessageText, userID, timestamp, uuid })
    editedChatMessageUUID = null
    editedChatMessageText = ""
}

function sendIfCtrlEnter(event, text, callbackToSendMessage) {
    if (text.trim() && event.key === "Enter" && event.ctrlKey ) {
        callbackToSendMessage()
        return false
    }
    event.redraw = false
    return true
}

function editedChatMessageKeyDown(event) {
    return sendIfCtrlEnter(event, editedChatMessageText, sendEditedChatMessage)
}

function textAreaKeyDown(event) {
    return sendIfCtrlEnter(event, chatText, sendChatMessage)
}

function formatChatMessage(text) {
    return m.trust(marked(text))
}

function getSortedMessages() {
    if (!sortMessagesByContent) return messages
    // console.log("sorting messages")
    const sortedMessages = messages.slice()
    sortedMessages.sort((a, b) => {
        if (a.chatText < b.chatText) return -1
        if (a.chatText > b.chatText) return 1
        return 0
    })
    return sortedMessages
}

function hasFilterText(message) {
    if (message.chatText === "DELETED" && filterText !== "DELETED") return false
    const localMessageTimestamp = makeLocalMessageTimestamp(message.timestamp)
    if ((filterText || hideText) && typeof message.chatText === "string") {
        let lowerCaseText = message.chatText.toLowerCase() + " " + ("" + message.userID).toLowerCase() + " " + localMessageTimestamp.toLowerCase()

        if (filterText) {
            const tags = filterText.split(" ")
            for (let tag of tags) {
                if (tag && !lowerCaseText.includes(tag.toLowerCase())) return false
            }
        }
        if (hideText) {
            const tags = hideText.split(" ")
            for (let tag of tags) {
                if (tag && lowerCaseText.includes(tag.toLowerCase())) return false
            }
        }
    }

    return true
}

function exportChatAsMarkdownClicked() {
    let text = ""

    getSortedMessages().forEach(function (message, index) {
        if (!hasFilterText(message)) return
        text += "\n----\n"
        text += "author: " + message.userID + " " + makeLocalMessageTimestamp(message.timestamp) + "\n"
        text += message.editedTimestamp ? "last edited: " + makeLocalMessageTimestamp(message.editedTimestamp) + "\n": ""
        text += "\n"
        text += message.chatText
    })

    FileUtils.saveToFile(chatRoom + " " + new Date().toISOString(), text, ".md")
}

function exportChatAsJSONClicked() {
    const messagesToExport = []

    getSortedMessages().forEach(function (message, index) {
        if (!hasFilterText(message)) return
        messagesToExport.push(message)
    })

    FileUtils.saveToFile(chatRoom + " " + new Date().toISOString(), JSON.stringify(messagesToExport, null, 4), ".json")
}

function importChatFromJSONClicked() {
    FileUtils.loadFromFile(false, (filename, contents, bytes) => {
        console.log("JSON filename, contents", filename, bytes, contents)
    })
}

function uploadDocumentClicked() {
    FileUtils.loadFromFile(true, (filename, contents, bytes) => {
        // console.log("loadFromFile result", filename, contents, bytes)

        const uploadResult = FileUploader.upload(backend, userID, filename, contents, bytes)

        let textToAdd = `[${filename}](${uploadResult.url})`
        // Format as markdown image if it might be an image
        if (uploadResult.isImageFile) textToAdd = `![${filename}](${uploadResult.url} "${filename}")`

        if (chatText) chatText += ""
        chatText += textToAdd

        m.redraw()
    })
}

function makeLocalMessageTimestamp(timestamp) {
    // Derived from: https://stackoverflow.com/questions/17415579/how-to-iso-8601-format-a-date-with-timezone-offset-in-javascript
    const date = new Date(Date.parse(timestamp))
    // const tzo = -date.getTimezoneOffset()
    // const dif = tzo >= 0 ? "+" : "-"
    const pad = function(num) {
        var norm = Math.floor(Math.abs(num))
        return (norm < 10 ? "0" : "") + norm
    }
    return date.getFullYear() +
        "-" + pad(date.getMonth() + 1) +
        "-" + pad(date.getDate()) +
        " " + pad(date.getHours()) +
        ":" + pad(date.getMinutes()) +
        ":" + pad(date.getSeconds())
    // + dif + pad(tzo / 60) +
    // ":" + pad(tzo % 60)
}

const TwirlipChat = {
    view: function () {
        return m("div.pa2.overflow-hidden.flex.flex-column.h-100.w-100", [
            // m("h4.tc", "Twirlip Chat"),
            m("div.mb3",
                m("span.dib.tr", "Space:"),
                m("input.w5.ml2", {value: chatRoom, onchange: chatRoomChange}),
                m("span.dib.tr.ml2", "User:"),
                m("input.w4.ml2", {value: userID, onchange: userIDChange, title: "Your user id or handle"}),
                m("div.dib",
                    m("span.ml2" + (filterText ? ".green" : ""), "Show:"),
                    m("input.ml2" + (filterText ? ".green" : ""), {value: filterText, oninput: (event) => { filterText = event.target.value; scrollToBottomLater() }, title: "Only display messages with all entered words"}),
                    m("span.ml2" + (hideText ? ".orange" : ""), "Hide:"),
                    m("input.ml2" + (hideText ? ".orange" : ""), {value: hideText, oninput: (event) => { hideText = event.target.value; scrollToBottomLater() }, title: "Hide messages with any entered words"}),
                    m("span.ml2",  { title: "Sort alphabetically by chat message text" },
                        m("input[type=checkbox].ma1", { checked: sortMessagesByContent, onchange: (event) => sortMessagesByContent = event.target.checked }),
                        "sort"
                    )
                ),
            ),
            m("div.overflow-auto.flex-auto",
                {
                    oncreate: (vnode) => {
                        messagesDiv = (vnode.dom)
                    },
                },
                getSortedMessages().map(function (message, index) {
                    if (!hasFilterText(message)) return []
                    return m("div", /* Causes ordering issue: {key: message.uuid || ("" + index)}, */ [
                        m("hr.b--light-gray"),
                        m("span",
                            m("i", makeLocalMessageTimestamp(message.timestamp) + " " + message.userID),
                            (message.previousVersion)
                                ? m("span.ml2", {
                                    title: "show history in console",
                                    onclick: () => {
                                        let messageVersion = message
                                        while (messageVersion) {
                                            console.log("messageVersion", messageVersion)
                                            messageVersion = messageVersion.previousVersion
                                        }
                                        alert("History of message put in console")
                                    }}, "⌚")
                                : [],
                            message.editedTimestamp ? m("b.ml1", {title: makeLocalMessageTimestamp(message.editedTimestamp) }, "edited")  : [],
                            // support editing
                            (message.userID === userID && message.uuid)
                                ? m("span.ml2", {
                                    title: "edit",
                                    onclick: () => {
                                        if (editedChatMessageUUID === message.uuid) {
                                            editedChatMessageUUID = null
                                        } else {
                                            editedChatMessageUUID = message.uuid || null
                                            editedChatMessageText = message.chatText
                                        }
                                    }}, "✎")
                                : []),
                        editedChatMessageUUID === message.uuid
                            // if editing
                            ? m("div.ba.bw1.ma3.ml4.pa3",
                                m("textarea.h5.w-80.ma2.ml3.f4", {value: editedChatMessageText, onkeydown: editedChatMessageKeyDown, oninput: (event) => editedChatMessageText = event.target.value}),
                                m("div",
                                    m("button.ml2.mt2", {onclick: () => sendEditedChatMessage() }, "Update (ctrl-enter)"),
                                    m("button.ml2.mt2", {onclick: () => editedChatMessageUUID = null}, "Cancel"),
                                ),
                            )
                            : m(".pl4.pr4", formatChatMessage(message.chatText))
                    ])
                })
            ),
            // showEntryArea && m("br"),
            m("div",
                m("span.ml2",  { title: "Show entry area" },
                    m("input[type=checkbox].ma1", { checked: showEntryArea, onchange: (event) => showEntryArea = event.target.checked }),
                    "entry area"
                ),
                !showEntryArea && m("span.w-80",
                    m("input.ml2.w-70", {value: chatText, oninput: chatTextChange, onkeydown: textAreaKeyDown}),
                    m("button.ml2.mt2.w-10", {onclick: sendChatMessage, title: "Ctrl-Enter to send"}, "Send"),
                ),
                showEntryArea && m("div.dib",
                    m("a.pl2", {href: "https://github.github.com/gfm/", target: "_blank"}, "Markdown"),
                    m("a.pl2", {href: "https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html", target: "_blank"}, "SVGEdit"),
                    m("button.ml2.mt2", {onclick: sendChatMessage}, "Send (ctrl-enter)"),
                    m("button.ml2.mt2", {onclick: uploadDocumentClicked}, "Upload document..."),
                    m("button.ml2.mt2", {onclick: exportChatAsMarkdownClicked, title: "Export filtered chat as Markdown"}, "Export Markdown..."),
                    m("button.ml2.mt2", {onclick: exportChatAsJSONClicked, title: "Export filtered chat as JSON"}, "Export JSON..."),
                    m("button.ml2.mt2", {onclick: importChatFromJSONClicked, title: "Import chat messages from JSON"}, "Import JSON..."),
                )
            ),
            showEntryArea && m("div.pb1.f4" + (editedChatMessageUUID ? ".dn" : ""),
                m("textarea.h4.w-80.ma1.ml3", {value: chatText, oninput: chatTextChange, onkeydown: textAreaKeyDown}),
            )
        ])
    }
}

let isLoaded = false

function scrollToBottomLater() {
    setTimeout(() => {
        // Scroll to bottom when loaded everything
        if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight + 10000
    }, 0)
}

const chatRoomResponder = {
    onLoaded: () => {
        isLoaded = true
        console.log("onLoaded")
        scrollToBottomLater()
    },
    onAddItem: (item) => {
        // console.log("onAddItem", item)
        let edited = false
        // Complexity needed to support editing
        if (messagesByUUID[item.uuid] === undefined) {
            if (item.uuid !== undefined) messagesByUUID[item.uuid] = messages.length
            messages.push(item)
        } else {
            if (messagesByUUID[item.uuid] !== undefined) {
                const previousVersion = messages[messagesByUUID[item.uuid]]
                // console.log("message is edited", item, messagesByUUID[item.uuid])
                item.editedTimestamp = item.timestamp
                item.timestamp = previousVersion.timestamp
                messages[messagesByUUID[item.uuid]] = item
                edited = true
                item.previousVersion = previousVersion
            }
        }
        const itemIsNotFiltered = hasFilterText(item)
        if (isLoaded) {
            // Only scroll if scroll is already near bottom and not filtering or editing to avoid messing up editing or browsing previous items
            if (itemIsNotFiltered && !edited && messagesDiv && (item.userID === userID || messagesDiv.scrollTop >= (messagesDiv.scrollHeight - messagesDiv.clientHeight - 300))) {
                setTimeout(() => {
                    // Add some because height may not include new item
                    messagesDiv.scrollTop = messagesDiv.scrollHeight + 10000
                }, 100)
            }
            if (!document.hasFocus()) {
                // Notify the user about a new message in this window
                Push.create(item.userID + ": " + item.chatText, {timeout: 4000})
            }
        }
    }
}

startup()

const backend = StoreUsingServer(m.redraw, {chatRoom}, userID)

backend.connect(chatRoomResponder)
try {
    backend.setup()
} catch(e) {
    alert("This Chat app requires a backend server supporting socket.io (i.e. won't work correctly on rawgit)")
}

m.mount(document.body, TwirlipChat)
<commit_msg>chat: use uploader promise to wait on upload completion<commit_after>/* global marked, Push */
/* eslint-disable no-console */

"use strict"

import { StoreUsingServer } from "./StoreUsingServer.js"
import { HashUtils } from "./HashUtils.js"
import { FileUtils } from "./FileUtils.js"
import { FileUploader } from "./FileUploader.js"
import { UUID } from "./UUID.js"
import { Toast } from "./Toast.js"

// defines marked
import "./vendor/marked.js"

// defines m
import "./vendor/mithril.js"

let chatRoom = "test"
let userID = localStorage.getItem("userID") || "anonymous"
let chatText = ""
const messages = []
let editedChatMessageUUID = null
let editedChatMessageText = ""

// filterText is split into tags by spaces and used to filter by a logical "and" to include displayed items
let filterText = ""

// hideText is split into tags by spaces and used to filter by a logical "OR" to hide displayed items
let hideText = ""

let sortMessagesByContent = false

let messagesDiv = null

let messagesByUUID = {}

let showEntryArea = true

function startup() {
    chatRoom = HashUtils.getHashParams()["chatRoom"] || chatRoom
    window.onhashchange = () => updateChatRoomFromHash()
    updateHashForChatRoom()
}

function updateTitleForChatRoom() {
    document.title = chatRoom + " -- Twirlip7 Chat"
}

function updateChatRoomFromHash() {
    const hashParams = HashUtils.getHashParams()
    console.log("updateChatRoomFromHash", hashParams)
    const newChatRoom = hashParams["chatRoom"]
    if (newChatRoom !== chatRoom) {
        resetMessagesForChatroomChange()
        chatRoom = newChatRoom
        backend.configure({chatRoom})
        updateTitleForChatRoom()
    }
}

function updateHashForChatRoom() {
    const hashParams = HashUtils.getHashParams()
    hashParams["chatRoom"] = chatRoom
    HashUtils.setHashParams(hashParams)
    updateTitleForChatRoom()
}

function chatRoomChange(event) {
    resetMessagesForChatroomChange()
    chatRoom = event.target.value
    updateHashForChatRoom()
    backend.configure({chatRoom})
}

function resetMessagesForChatroomChange() {
    messages.splice(0)
    messagesByUUID = {}
}

function userIDChange(event) {
    userID = event.target.value
    backend.configure(undefined, userID)
    localStorage.setItem("userID", userID)
}

function chatTextChange(event) {
    chatText = event.target.value
}

function sendChatMessage() {
    const timestamp = new Date().toISOString()
    const uuid = "chatMessage:" + UUID.uuidv4()

    const newMessage = { chatText, userID, timestamp, uuid }

    sendMessage({ chatText, userID, timestamp, uuid })
    chatText = ""
    if (!hasFilterText(newMessage)) {
        Toast.toast("The message you just added is currently\nnot displayed due to show/hide filtering.")
        /*
        filterText = ""
        hideText = "
        */
    }
    setTimeout(() => {
        // Scroll to bottom always when sending -- but defer it just in case was filtering
        if (messagesDiv) {
            messagesDiv.scrollTop = messagesDiv.scrollHeight
        }
    }, 0)
}

function sendMessage(message) {
    // Call addItem after a delay to give socket.io a chance to reconnect
    // as socket.io will timeout if a prompt (or alert?) is up for very long
    setTimeout(() => backend.addItem(message), 10)
}

function sendEditedChatMessage() {
    const timestamp = new Date().toISOString()
    const uuid = editedChatMessageUUID

    sendMessage({ chatText: editedChatMessageText, userID, timestamp, uuid })
    editedChatMessageUUID = null
    editedChatMessageText = ""
}

function sendIfCtrlEnter(event, text, callbackToSendMessage) {
    if (text.trim() && event.key === "Enter" && event.ctrlKey ) {
        callbackToSendMessage()
        return false
    }
    event.redraw = false
    return true
}

function editedChatMessageKeyDown(event) {
    return sendIfCtrlEnter(event, editedChatMessageText, sendEditedChatMessage)
}

function textAreaKeyDown(event) {
    return sendIfCtrlEnter(event, chatText, sendChatMessage)
}

function formatChatMessage(text) {
    return m.trust(marked(text))
}

function getSortedMessages() {
    if (!sortMessagesByContent) return messages
    // console.log("sorting messages")
    const sortedMessages = messages.slice()
    sortedMessages.sort((a, b) => {
        if (a.chatText < b.chatText) return -1
        if (a.chatText > b.chatText) return 1
        return 0
    })
    return sortedMessages
}

function hasFilterText(message) {
    if (message.chatText === "DELETED" && filterText !== "DELETED") return false
    const localMessageTimestamp = makeLocalMessageTimestamp(message.timestamp)
    if ((filterText || hideText) && typeof message.chatText === "string") {
        let lowerCaseText = message.chatText.toLowerCase() + " " + ("" + message.userID).toLowerCase() + " " + localMessageTimestamp.toLowerCase()

        if (filterText) {
            const tags = filterText.split(" ")
            for (let tag of tags) {
                if (tag && !lowerCaseText.includes(tag.toLowerCase())) return false
            }
        }
        if (hideText) {
            const tags = hideText.split(" ")
            for (let tag of tags) {
                if (tag && lowerCaseText.includes(tag.toLowerCase())) return false
            }
        }
    }

    return true
}

function exportChatAsMarkdownClicked() {
    let text = ""

    getSortedMessages().forEach(function (message, index) {
        if (!hasFilterText(message)) return
        text += "\n----\n"
        text += "author: " + message.userID + " " + makeLocalMessageTimestamp(message.timestamp) + "\n"
        text += message.editedTimestamp ? "last edited: " + makeLocalMessageTimestamp(message.editedTimestamp) + "\n": ""
        text += "\n"
        text += message.chatText
    })

    FileUtils.saveToFile(chatRoom + " " + new Date().toISOString(), text, ".md")
}

function exportChatAsJSONClicked() {
    const messagesToExport = []

    getSortedMessages().forEach(function (message, index) {
        if (!hasFilterText(message)) return
        messagesToExport.push(message)
    })

    FileUtils.saveToFile(chatRoom + " " + new Date().toISOString(), JSON.stringify(messagesToExport, null, 4), ".json")
}

function importChatFromJSONClicked() {
    FileUtils.loadFromFile(false, (filename, contents, bytes) => {
        console.log("JSON filename, contents", filename, bytes, contents)
    })
}

let isUploading = false

function uploadDocumentClicked() {
    FileUtils.loadFromFile(true, async (filename, contents, bytes) => {
        // console.log("loadFromFile result", filename, contents, bytes)
        isUploading = true
        m.redraw()

        let uploadResult
        try {
            uploadResult = await FileUploader.upload(backend, userID, filename, contents, bytes)
        } catch (error) {
            console.log("upload error", error)
            isUploading = false
            Toast.toast("Upload failed")
            return
        }

        // console.log("uploadResult", uploadResult)

        isUploading = false

        let textToAdd = `[${filename}](${uploadResult.url})`
        // Format as markdown image if it might be an image
        if (uploadResult.isImageFile) textToAdd = `![${filename}](${uploadResult.url} "${filename}")`

        if (chatText) chatText += ""
        chatText += textToAdd

        Toast.toast("Uploaded " + uploadResult.filename)

        m.redraw()
    })
}

function makeLocalMessageTimestamp(timestamp) {
    // Derived from: https://stackoverflow.com/questions/17415579/how-to-iso-8601-format-a-date-with-timezone-offset-in-javascript
    const date = new Date(Date.parse(timestamp))
    // const tzo = -date.getTimezoneOffset()
    // const dif = tzo >= 0 ? "+" : "-"
    const pad = function(num) {
        var norm = Math.floor(Math.abs(num))
        return (norm < 10 ? "0" : "") + norm
    }
    return date.getFullYear() +
        "-" + pad(date.getMonth() + 1) +
        "-" + pad(date.getDate()) +
        " " + pad(date.getHours()) +
        ":" + pad(date.getMinutes()) +
        ":" + pad(date.getSeconds())
    // + dif + pad(tzo / 60) +
    // ":" + pad(tzo % 60)
}

const TwirlipChat = {
    view: function () {
        return m("div.pa2.overflow-hidden.flex.flex-column.h-100.w-100", [
            Toast.viewToast(),
            // m("h4.tc", "Twirlip Chat"),
            m("div.mb3",
                m("span.dib.tr", "Space:"),
                m("input.w5.ml2", {value: chatRoom, onchange: chatRoomChange}),
                m("span.dib.tr.ml2", "User:"),
                m("input.w4.ml2", {value: userID, onchange: userIDChange, title: "Your user id or handle"}),
                m("div.dib",
                    m("span.ml2" + (filterText ? ".green" : ""), "Show:"),
                    m("input.ml2" + (filterText ? ".green" : ""), {value: filterText, oninput: (event) => { filterText = event.target.value; scrollToBottomLater() }, title: "Only display messages with all entered words"}),
                    m("span.ml2" + (hideText ? ".orange" : ""), "Hide:"),
                    m("input.ml2" + (hideText ? ".orange" : ""), {value: hideText, oninput: (event) => { hideText = event.target.value; scrollToBottomLater() }, title: "Hide messages with any entered words"}),
                    m("span.ml2",  { title: "Sort alphabetically by chat message text" },
                        m("input[type=checkbox].ma1", { checked: sortMessagesByContent, onchange: (event) => sortMessagesByContent = event.target.checked }),
                        "sort"
                    )
                ),
            ),
            m("div.overflow-auto.flex-auto",
                {
                    oncreate: (vnode) => {
                        messagesDiv = (vnode.dom)
                    },
                },
                getSortedMessages().map(function (message, index) {
                    if (!hasFilterText(message)) return []
                    return m("div", /* Causes ordering issue: {key: message.uuid || ("" + index)}, */ [
                        m("hr.b--light-gray"),
                        m("span",
                            m("i", makeLocalMessageTimestamp(message.timestamp) + " " + message.userID),
                            (message.previousVersion)
                                ? m("span.ml2", {
                                    title: "show history in console",
                                    onclick: () => {
                                        let messageVersion = message
                                        while (messageVersion) {
                                            console.log("messageVersion", messageVersion)
                                            messageVersion = messageVersion.previousVersion
                                        }
                                        Toast.toast("History of message put in console")
                                    }}, "⌚")
                                : [],
                            message.editedTimestamp ? m("b.ml1", {title: makeLocalMessageTimestamp(message.editedTimestamp) }, "edited")  : [],
                            // support editing
                            (message.userID === userID && message.uuid)
                                ? m("span.ml2", {
                                    title: "edit",
                                    onclick: () => {
                                        if (editedChatMessageUUID === message.uuid) {
                                            editedChatMessageUUID = null
                                        } else {
                                            editedChatMessageUUID = message.uuid || null
                                            editedChatMessageText = message.chatText
                                        }
                                    }}, "✎")
                                : []),
                        editedChatMessageUUID === message.uuid
                            // if editing
                            ? m("div.ba.bw1.ma3.ml4.pa3",
                                m("textarea.h5.w-80.ma2.ml3.f4", {value: editedChatMessageText, onkeydown: editedChatMessageKeyDown, oninput: (event) => editedChatMessageText = event.target.value}),
                                m("div",
                                    m("button.ml2.mt2", {onclick: () => sendEditedChatMessage() }, "Update (ctrl-enter)"),
                                    m("button.ml2.mt2", {onclick: () => editedChatMessageUUID = null}, "Cancel"),
                                ),
                            )
                            : m(".pl4.pr4", formatChatMessage(message.chatText))
                    ])
                })
            ),
            // showEntryArea && m("br"),
            m("div",
                m("span.ml2",  { title: "Show entry area" },
                    m("input[type=checkbox].ma1", { checked: showEntryArea, onchange: (event) => showEntryArea = event.target.checked }),
                    "entry area"
                ),
                !showEntryArea && m("span.w-80",
                    m("input.ml2.w-70", {value: chatText, oninput: chatTextChange, onkeydown: textAreaKeyDown}),
                    m("button.ml2.mt2.w-10", {onclick: sendChatMessage, title: "Ctrl-Enter to send"}, "Send"),
                ),
                showEntryArea && m("div.dib",
                    m("a.pl2", {href: "https://github.github.com/gfm/", target: "_blank"}, "Markdown"),
                    m("a.pl2", {href: "https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html", target: "_blank"}, "SVGEdit"),
                    m("button.ml2.mt2", {onclick: sendChatMessage}, "Send (ctrl-enter)"),
                    m("button.ml2.mt2", {onclick: uploadDocumentClicked}, m("i.fa.mr1" + (isUploading ? ".fa-refresh.fa-spin" : ".fa-upload")), "Upload document..."),
                    m("button.ml2.mt2", {onclick: exportChatAsMarkdownClicked, title: "Export filtered chat as Markdown"}, "Export Markdown..."),
                    m("button.ml2.mt2", {onclick: exportChatAsJSONClicked, title: "Export filtered chat as JSON"}, "Export JSON..."),
                    m("button.ml2.mt2", {onclick: importChatFromJSONClicked, title: "Import chat messages from JSON"}, "Import JSON..."),
                )
            ),
            showEntryArea && m("div.pb1.f4" + (editedChatMessageUUID ? ".dn" : ""),
                m("textarea.h4.w-80.ma1.ml3", {value: chatText, oninput: chatTextChange, onkeydown: textAreaKeyDown}),
            )
        ])
    }
}

let isLoaded = false

function scrollToBottomLater() {
    setTimeout(() => {
        // Scroll to bottom when loaded everything
        if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight + 10000
    }, 0)
}

const chatRoomResponder = {
    onLoaded: () => {
        isLoaded = true
        console.log("onLoaded")
        scrollToBottomLater()
    },
    onAddItem: (item) => {
        // console.log("onAddItem", item)
        let edited = false
        // Complexity needed to support editing
        if (messagesByUUID[item.uuid] === undefined) {
            if (item.uuid !== undefined) messagesByUUID[item.uuid] = messages.length
            messages.push(item)
        } else {
            if (messagesByUUID[item.uuid] !== undefined) {
                const previousVersion = messages[messagesByUUID[item.uuid]]
                // console.log("message is edited", item, messagesByUUID[item.uuid])
                item.editedTimestamp = item.timestamp
                item.timestamp = previousVersion.timestamp
                messages[messagesByUUID[item.uuid]] = item
                edited = true
                item.previousVersion = previousVersion
            }
        }
        const itemIsNotFiltered = hasFilterText(item)
        if (isLoaded) {
            // Only scroll if scroll is already near bottom and not filtering or editing to avoid messing up editing or browsing previous items
            if (itemIsNotFiltered && !edited && messagesDiv && (item.userID === userID || messagesDiv.scrollTop >= (messagesDiv.scrollHeight - messagesDiv.clientHeight - 300))) {
                setTimeout(() => {
                    // Add some because height may not include new item
                    messagesDiv.scrollTop = messagesDiv.scrollHeight + 10000
                }, 100)
            }
            if (!document.hasFocus()) {
                // Notify the user about a new message in this window
                Push.create(item.userID + ": " + item.chatText, {timeout: 4000})
            }
        }
    }
}

startup()

const backend = StoreUsingServer(m.redraw, {chatRoom}, userID)

backend.connect(chatRoomResponder)
try {
    backend.setup()
} catch(e) {
    alert("This Chat app requires a backend server supporting socket.io (i.e. won't work correctly on rawgit)")
}

m.mount(document.body, TwirlipChat)
<|endoftext|>


<commit_before>var go = {};
go;

var _ = require('lodash');
var vumigo = require('vumigo_v02');
var JsonApi = vumigo.http.api.JsonApi;

go.utils = {
    // Shared utils lib

    get_today: function(config) {
        var today;
        if (config.testing_today) {
            today = new Date(config.testing_today);
        } else {
            today = new Date();
        }
        return today;
    },

    check_valid_number: function(input){
        // an attempt to solve the insanity of JavaScript numbers
        var numbers_only = new RegExp('^\\d+$');
        if (input !== '' && numbers_only.test(input) && !Number.isNaN(Number(input))){
            return true;
        } else {
            return false;
        }
    },

    check_number_in_range: function(input, start, end){
        return go.utils.check_valid_number(input) && (parseInt(input, 10) >= start) && (parseInt(input, 10) <= end);
    },

    is_true: function(bool) {
        //If is is not undefined and boolean is true
        return (!_.isUndefined(bool) && (bool==='true' || bool===true));
    },

    get_snappy_faqs: function (im) {
        var http = new JsonApi(im, {
            auth: {
                username: im.config.snappy.username,
                password: 'x'
            }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs', {
            data: JSON.stringify(),
            headers: {
                'Content-Type': ['application/json']
            }
        });
    },

    get_snappy_topics: function (im, faq_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

    get_snappy_topic_content: function(im, faq_id, topic_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics/'+topic_id+'/questions', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

};

go.app = function() {
    var vumigo = require('vumigo_v02');
    var _ = require('lodash');
    var App = vumigo.App;
    var Choice = vumigo.states.Choice;
    var EndState = vumigo.states.EndState;
    var PaginatedState = vumigo.states.PaginatedState;
    var PaginatedChoiceState = vumigo.states.PaginatedChoiceState;

    var GoFAQBrowser = App.extend(function(self) {
        App.call(self, 'states_start');
        var $ = self.$;

        // Start - select topic
        self.states.add('states_start', function(name) {
          if(self.im.config.snappy.default_faq) {
            return self.states.create('states_topics', {
                faq_id: self.im.config.snappy.default_faq
            });
          } else {
            return self.states.create('states_faqs');
          }
        });

        self.states.add('states_faqs', function (name, opts) {
            return go.utils.get_snappy_faqs(self.im)
                .then(function (response) {
                    if(typeof response.data.error !== 'undefined') {
                        return error;
                    } else {
                        return _.sortBy(response.data, function (d) {
                                return parseInt(d.order, 10);
                            })
                            .map(function (d) {
                                return new Choice(d.id, d.title);
                            });
                    }
                })
                .then(function (choices) {
                    return new PaginatedChoiceState(name, {
                        question: $('Welcome to FAQ Browser. Choose FAQ:'),
                        choices: choices,
                        options_per_page: 8,
                        next: function (choice) {
                            return {
                                name: 'states_topics',
                                creator_opts: {
                                    faq_id: choice.value
                                }
                            };
                        }
                    });
                });
        });

        self.states.add('states_topics', function (name, opts) {
            return go.utils.get_snappy_topics(self.im, opts.faq_id)
                .then(function(response) {
                    if (typeof response.data.error  !== 'undefined') {
                        // TODO Throw proper error
                        return error;
                    } else {
                        return _.sortBy(response.data, function (d) {
                                return parseInt(d.order, 10);
                            })
                            .map(function(d) {
                                return new Choice(d.id, d.topic);
                            });
                    }
                })
                .then(function(choices) {
                    return new PaginatedChoiceState(name, {
                        question: $('Welcome to FAQ Browser. Choose topic:'),
                        choices: choices,
                        options_per_page: 8,
                        next: function(choice) {
                            return {
                                name: 'states_questions',
                                creator_opts: {
                                    faq_id: opts.faq_id
                                }
                            };
                        }
                    });
                });
        });

        // Show questions in selected topic
        self.states.add('states_questions', function(name, opts) {
            return go.utils.get_snappy_topic_content(self.im,
                        opts.faq_id, self.im.user.answers.states_topics)
                .then(function(response) {
                    if (typeof response.data.error  !== 'undefined') {
                        // TODO Throw proper error
                        return error;
                    } else {
                        var choices = _.sortBy(response.data, function (d) {
                                return parseInt(d.pivot.order, 10);
                            })
                            .map(function(d) {
                                return new Choice(d.id, d.question);
                            });

                        return new PaginatedChoiceState(name, {
                            question: $('Please choose a question:'),
                            choices: choices,
                            options_per_page: null,
                            next: function(choice) {
                                var question_id = choice.value;
                                var index = _.findIndex(response.data, { 'id': question_id});
                                var answer = response.data[index].answer.trim();
                                return {
                                    name: 'states_answers',
                                    creator_opts: {
                                        answer: answer
                                    }
                                };
                            }
                        });
                    }
                });
        });

        // Show answer to selected question
        self.states.add('states_answers', function(name, opts) {
            return new PaginatedState(name, {
                text: opts.answer,
                characters_per_page: 800,
                more: $('More'),
                back: $('Back'),
                exit: $('Exit'),
                next: function() {
                    return {
                        name: 'states_end',
                        creator_opts: {
                            answer: opts.answer
                        }
                    };
                }
            });
        });

        // End
        self.states.add('states_end', function(name, opts) {
            return new EndState(name, {
                text: $('Thank you and visit again!'),
                next: 'states_start'
            });
        });

    });

    return {
        GoFAQBrowser: GoFAQBrowser
    };
}();

go.init = function() {
    var vumigo = require('vumigo_v02');
    var InteractionMachine = vumigo.InteractionMachine;
    var GoFAQBrowser = go.app.GoFAQBrowser;

    return {
        im: new InteractionMachine(api, new GoFAQBrowser())
    };
}();
<commit_msg>fix character size with new build<commit_after>var go = {};
go;

var _ = require('lodash');
var vumigo = require('vumigo_v02');
var JsonApi = vumigo.http.api.JsonApi;

go.utils = {
    // Shared utils lib

    get_today: function(config) {
        var today;
        if (config.testing_today) {
            today = new Date(config.testing_today);
        } else {
            today = new Date();
        }
        return today;
    },

    check_valid_number: function(input){
        // an attempt to solve the insanity of JavaScript numbers
        var numbers_only = new RegExp('^\\d+$');
        if (input !== '' && numbers_only.test(input) && !Number.isNaN(Number(input))){
            return true;
        } else {
            return false;
        }
    },

    check_number_in_range: function(input, start, end){
        return go.utils.check_valid_number(input) && (parseInt(input, 10) >= start) && (parseInt(input, 10) <= end);
    },

    is_true: function(bool) {
        //If is is not undefined and boolean is true
        return (!_.isUndefined(bool) && (bool==='true' || bool===true));
    },

    get_snappy_faqs: function (im) {
        var http = new JsonApi(im, {
            auth: {
                username: im.config.snappy.username,
                password: 'x'
            }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs', {
            data: JSON.stringify(),
            headers: {
                'Content-Type': ['application/json']
            }
        });
    },

    get_snappy_topics: function (im, faq_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

    get_snappy_topic_content: function(im, faq_id, topic_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics/'+topic_id+'/questions', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

};

go.app = function() {
    var vumigo = require('vumigo_v02');
    var _ = require('lodash');
    var App = vumigo.App;
    var Choice = vumigo.states.Choice;
    var EndState = vumigo.states.EndState;
    var PaginatedState = vumigo.states.PaginatedState;
    var PaginatedChoiceState = vumigo.states.PaginatedChoiceState;

    var GoFAQBrowser = App.extend(function(self) {
        App.call(self, 'states_start');
        var $ = self.$;

        // Start - select topic
        self.states.add('states_start', function(name) {
          if(self.im.config.snappy.default_faq) {
            return self.states.create('states_topics', {
                faq_id: self.im.config.snappy.default_faq
            });
          } else {
            return self.states.create('states_faqs');
          }
        });

        self.states.add('states_faqs', function (name, opts) {
            return go.utils.get_snappy_faqs(self.im)
                .then(function (response) {
                    if(typeof response.data.error !== 'undefined') {
                        return error;
                    } else {
                        return _.sortBy(response.data, function (d) {
                                return parseInt(d.order, 10);
                            })
                            .map(function (d) {
                                return new Choice(d.id, d.title);
                            });
                    }
                })
                .then(function (choices) {
                    return new PaginatedChoiceState(name, {
                        question: $('Welcome to FAQ Browser. Choose FAQ:'),
                        choices: choices,
                        options_per_page: 8,
                        next: function (choice) {
                            return {
                                name: 'states_topics',
                                creator_opts: {
                                    faq_id: choice.value
                                }
                            };
                        }
                    });
                });
        });

        self.states.add('states_topics', function (name, opts) {
            return go.utils.get_snappy_topics(self.im, opts.faq_id)
                .then(function(response) {
                    if (typeof response.data.error  !== 'undefined') {
                        // TODO Throw proper error
                        return error;
                    } else {
                        return _.sortBy(response.data, function (d) {
                                return parseInt(d.order, 10);
                            })
                            .map(function(d) {
                                return new Choice(d.id, d.topic);
                            });
                    }
                })
                .then(function(choices) {
                    return new PaginatedChoiceState(name, {
                        question: $('Welcome to FAQ Browser. Choose topic:'),
                        choices: choices,
                        options_per_page: 8,
                        next: function(choice) {
                            return {
                                name: 'states_questions',
                                creator_opts: {
                                    faq_id: opts.faq_id
                                }
                            };
                        }
                    });
                });
        });

        // Show questions in selected topic
        self.states.add('states_questions', function(name, opts) {
            return go.utils.get_snappy_topic_content(self.im,
                        opts.faq_id, self.im.user.answers.states_topics)
                .then(function(response) {
                    if (typeof response.data.error  !== 'undefined') {
                        // TODO Throw proper error
                        return error;
                    } else {
                        var choices = _.sortBy(response.data, function (d) {
                                return parseInt(d.pivot.order, 10);
                            })
                            .map(function(d) {
                                return new Choice(d.id, d.question);
                            });

                        return new PaginatedChoiceState(name, {
                            question: $('Please choose a question:'),
                            choices: choices,
                            options_per_page: null,
                            next: function(choice) {
                                var question_id = choice.value;
                                var index = _.findIndex(response.data, { 'id': question_id});
                                var answer = response.data[index].answer.trim();
                                return {
                                    name: 'states_answers',
                                    creator_opts: {
                                        answer: answer
                                    }
                                };
                            }
                        });
                    }
                });
        });

        // Show answer to selected question
        self.states.add('states_answers', function(name, opts) {
            return new PaginatedState(name, {
                text: opts.answer,
                characters_per_page: 320,
                more: $('More'),
                back: $('Back'),
                exit: $('Exit'),
                next: function() {
                    return {
                        name: 'states_end',
                        creator_opts: {
                            answer: opts.answer
                        }
                    };
                }
            });
        });

        // End
        self.states.add('states_end', function(name, opts) {
            return new EndState(name, {
                text: $('Thank you and visit again!'),
                next: 'states_start'
            });
        });

    });

    return {
        GoFAQBrowser: GoFAQBrowser
    };
}();

go.init = function() {
    var vumigo = require('vumigo_v02');
    var InteractionMachine = vumigo.InteractionMachine;
    var GoFAQBrowser = go.app.GoFAQBrowser;

    return {
        im: new InteractionMachine(api, new GoFAQBrowser())
    };
}();
<|endoftext|>


<commit_before>'use babel';
'use strict';

import { Collection } from 'discord.js';

/**
 * Stores all guild-specific storages as <[id]{@link GuildStorage#id}, {@link GuildStorage}> pairs
 * @class GuildStorageRegistry
 * @extends {external:Collection}
 */
export default class GuildStorageRegistry extends Collection
{
	constructor()
	{
		super();
	}

	/**
	 * Get the GuildStorage by [Guild]{@link external:Guild} or guild id string
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {(external:Guild|string)} guild - Guild object or guild id string
	 * @returns {GuildStorage}
	 */
	get(guild)
	{
		return super.get(guild.id ? guild.id : guild);
	}

	/**
	 * Return a [Collection]{@link external:Collection} of GuildStorage items that
	 * have the provided key and value
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {string} key - Setting key to match
	 * @param {*} value - Value to match
	 * @returns {external:Collection<string, GuildStorage>}
	 */
	findAll(key, value)
	{
		let collection = new Collection();
		this.forEach(guild =>
		{
			if (guild.getItem(key) === value) collection.set(guild.id, guild);
		});
		if (collection.size === 0) return null;
		return collection;
	}

	/**
	 * Return a [Collection]{@link external:Collection} of GuildStorage items that
	 * have the provided setting key and value
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {string} key - Setting key to match
	 * @param {*} value - Value to match
	 * @returns {external:Collection<string, GuildStorage>}
	 */
	findAllBySetting(key, value)
	{
		let collection = new Collection();
		this.forEach(guild =>
		{
			if (guild.getSetting(key) === value) collection.set(guild.id, guild);
		});
		if (collection.size === 0) return null;
		return collection;
	}

	/**
	 * Reset all guild settings to default, deleting any extra settings that are
	 * not part of the [defaultGuildSettings]{@link Bot#defaultGuildSettings}
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {Object} defaults - Should always use [defaultGuildSettings]{@link Bot#defaultGuildSettings}
	 */
	resetAllGuildSettings(defaults)
	{
		super.forEach(guild => guild.resetSettings(defaults));
	}
}
<commit_msg>Simplify GuildStorageRegistry methods<commit_after>'use babel';
'use strict';

import { Collection } from 'discord.js';

/**
 * Stores all guild-specific storages as <{@link string}, {@link GuildStorage}> pairs,
 * where {@link string} is the guild's ID string
 * @class GuildStorageRegistry
 * @extends {external:Collection}
 */
export default class GuildStorageRegistry extends Collection
{
	constructor()
	{
		super();
	}

	/**
	 * Get the GuildStorage by [Guild]{@link external:Guild} or guild id string
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {(external:Guild|string)} guild - Guild object or guild id string
	 * @returns {GuildStorage}
	 */
	get(guild) { return super.get(guild.id ? guild.id : guild); }

	/**
	 * Return a [Collection]{@link external:Collection} of GuildStorage items that
	 * have the provided key and value
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {string} key - Setting key to match
	 * @param {*} value - Value to match
	 * @returns {external:Collection<string, GuildStorage>}
	 */
	findAll(key, value)
	{
		return super.filter(a => a.getItem(key) === value);
	}

	/**
	 * Return a [Collection]{@link external:Collection} of GuildStorage items that
	 * have the provided setting key and value
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {string} key - Setting key to match
	 * @param {*} value - Value to match
	 * @returns {external:Collection<string, GuildStorage>}
	 */
	findAllBySetting(key, value)
	{
		return super.filter(a => a.getSetting(key) === value);
	}

	/**
	 * Reset all guild settings to default, deleting any extra settings that are
	 * not part of the [defaultGuildSettings]{@link Bot#defaultGuildSettings}
	 * @memberof GuildStorageRegistry
	 * @instance
	 * @param {Object} defaults - Should always use [defaultGuildSettings]{@link Bot#defaultGuildSettings}
	 */
	resetAllGuildSettings(defaults)
	{
		super.forEach(guild => guild.resetSettings(defaults));
	}
}
<|endoftext|>


<commit_before>var AI = function() {
    var root = this;

    var AI = function() {}

    var switchPlayer = function(crt) {
        return 1 - crt;
    }

    var maxList = [0x7fffffff + 1];
    for (var i = 1; i < 8; i++) {
        maxList.push(maxList[i - 1] / 8);
    }

    var genRules = function(crt) {
        var otr = switchPlayer(crt);
        return [
            [[crt, 5, 0], maxList[0]],
            [[crt, 5, 1], maxList[0]],
            [[crt, 5, 2], maxList[0]],
            [[otr, 5, 0], -maxList[1]],
            [[otr, 5, 1], -maxList[1]],
            [[otr, 5, 2], -maxList[1]],
            [[crt, 4, 1], maxList[2]],
            [[crt, 4, 2], maxList[2]],
            [[otr, 4, 2], -maxList[3]],
            [[crt, 3, 2], maxList[4]],
            [[otr, 3, 2], -maxList[5]],
            [[otr, 4, 1], -maxList[5]],
            [[crt, 2, 2], maxList[6]],
            [[otr, 2, 2], -maxList[6]],
            [[crt, 2, 1], maxList[7]],
            [[otr, 2, 1], -maxList[7]]
        ];
    };
    var rules = [];
    rules[0] = genRules(0);
    rules[1] = genRules(1);

    var orgs = function(grid) {
        var row = grid.length;
        var col = grid[0].length;
        return [
            [_.map(_.range(0, col), function(v) {return [0, v];}),
            [[1, 0], [1, 1], [1, -1]]],
            [_.map(_.range(0, col), function(v) {return [row - 1, v];}),
            [[-1, 1], [-1, -1]]],
            [_.map(_.range(0, row), function(v) {return [v, 0];}),
            [[0, 1]]]
        ];
    }

    var getRow = function(grid, org, dir) {
        var row = grid.length;
        var col = grid[0].length;
        var i = org[0];
        var j = org[1];
        var ret = [[]];
        var cnt = 0;
        while (i < row && j < col && i >= 0 && j >= 0) {
            if (_.isEmpty(ret[cnt]) || _.last(ret[cnt]) == grid[i][j])
                ret[cnt].push(grid[i][j]);
            else {
                cnt += 1;
                ret[cnt] = [];
                ret[cnt].push(grid[i][j]);
            }
            i += dir[0];
            j += dir[1];
        }
        return ret;
    }

    var calcState = function(grid) {
        var ret = {};
        var createOrIncrease = function(ctr, l, h) {
            if (!ret[ctr]) ret[ctr] = {};
            if (!ret[ctr][l]) ret[ctr][l] = {};
            if (!ret[ctr][l][h])
                ret[ctr][l][h] = 1;
            else
                ret[ctr][l][h]++;
        }
        _.each(orgs(grid), function(org) {
            for (var i = 0; i < org[0].length; i++) { // orgs
                for (var j = 0; j < org[1].length; j++) {  // dirs
                    var rows = getRow(grid, org[0][i], org[1][j]);
                    for (var k = 0; k < rows.length; k++) {
                        if (rows[k][0] == -1 || rows[k].length < 2) continue;
                        var head = 0;
                        if (k != 0 && rows[k - 1][0] == -1) head += 1;
                        if (k != rows.length - 1 && rows[k + 1][0] == -1) head += 1;
                        createOrIncrease(rows[k][0], rows[k].length, head);
                    }
                }
            }
        });
        return ret;
    }

    var chance = function(grid, crt) {
        var state = calcState(grid);
        var getInState = function(r) {
            var tmp = state[r[0]];
            if (!tmp) return 0;
            tmp = tmp[r[1]];
            if (!tmp) return 0;
            tmp = tmp[r[2]];
            if (!tmp) return 0;
            return tmp;
        };
        var ret = 0;
        _.each(rules[crt], function(rule) {
            ret += getInState(rule[0]) * rule[1];
        });
        return ret;
    }
    //chance = _.memoize(chance, function(grid, crt) {
    //    return [grid, crt];
    //});

    AI.play = function(grid, crtPlayer, depth, maxChance) {
        if (depth == 0)
            return [chance(grid, crtPlayer), null];
        var row = grid.length;
        var col = grid[0].length;

        var notAlone = function(grid, r, c) {
            for (var i = -1; i <= 1; i++)
                for (var j = -1; j <= 1; j++) {
                    var newi = i + r;
                    var newj = j + c;
                    if (newi < 0 || newj < 0 || newi >= grid.length || newj >= grid[0].length)
                        continue;
                    if ((i != 0 || j != 0) &&
                            (grid[i + r][j + c] == 0 || grid[i + r][j + c] == 1))
                        return true;
                }
            return false;
        }
        var ret = [-0x7fffffff, null];
        for (var i = 0; i < row; i++) {
            for (var j = 0; j < col; j++) {
                if (grid[i][j] != -1) continue;
                if (!notAlone(grid, i, j)) continue;
                grid[i][j] = crtPlayer;
                if (depth != 1) {
                    // FIXME: check the victory directly
                    var tmp_cs = chance(grid, crtPlayer);
                    if (tmp_cs >= maxList[0]) {
                        grid[i][j] = -1;
                        return [tmp_cs, [i, j]];
                    }
                }
                var enm_ret = AI.play(grid, switchPlayer(crtPlayer), depth - 1, -ret[0]);
                grid[i][j] = -1;

                var enm_cs = -enm_ret[0];
                if (maxChance && enm_cs >= maxChance)
                    return [0x7fffffff * 100, [i, j]];
                if (enm_cs > ret[0])
                    ret = [enm_cs, [i, j]];
            }
        }
        return ret;
    }
    return AI;
}();
<commit_msg>add todo<commit_after>var AI = function() {
    var root = this;

    var AI = function() {}

    var switchPlayer = function(crt) {
        return 1 - crt;
    }

    var maxList = [0x7fffffff + 1];
    for (var i = 1; i < 8; i++) {
        maxList.push(maxList[i - 1] / 8);
    }

    var genRules = function(crt) {
        var otr = switchPlayer(crt);
        return [
            [[crt, 5, 0], maxList[0]],
            [[crt, 5, 1], maxList[0]],
            [[crt, 5, 2], maxList[0]],
            [[otr, 5, 0], -maxList[1]],
            [[otr, 5, 1], -maxList[1]],
            [[otr, 5, 2], -maxList[1]],
            [[crt, 4, 1], maxList[2]],
            [[crt, 4, 2], maxList[2]],
            [[otr, 4, 2], -maxList[3]],
            [[crt, 3, 2], maxList[4]],
            [[otr, 3, 2], -maxList[5]],
            [[otr, 4, 1], -maxList[5]],
            [[crt, 2, 2], maxList[6]],
            [[otr, 2, 2], -maxList[6]],
            [[crt, 2, 1], maxList[7]],
            [[otr, 2, 1], -maxList[7]]
        ];
    };
    var rules = [];
    rules[0] = genRules(0);
    rules[1] = genRules(1);

    var orgs = function(grid) {
        var row = grid.length;
        var col = grid[0].length;
        return [
            [_.map(_.range(0, col), function(v) {return [0, v];}),
            [[1, 0], [1, 1], [1, -1]]],
            [_.map(_.range(0, col), function(v) {return [row - 1, v];}),
            [[-1, 1], [-1, -1]]],
            [_.map(_.range(0, row), function(v) {return [v, 0];}),
            [[0, 1]]]
        ];
    }

    var getRow = function(grid, org, dir) {
        var row = grid.length;
        var col = grid[0].length;
        var i = org[0];
        var j = org[1];
        var ret = [[]];
        var cnt = 0;
        while (i < row && j < col && i >= 0 && j >= 0) {
            if (_.isEmpty(ret[cnt]) || _.last(ret[cnt]) == grid[i][j])
                ret[cnt].push(grid[i][j]);
            else {
                cnt += 1;
                ret[cnt] = [];
                ret[cnt].push(grid[i][j]);
            }
            i += dir[0];
            j += dir[1];
        }
        return ret;
    }

    var calcState = function(grid) {
        var ret = {};
        var createOrIncrease = function(ctr, l, h) {
            if (!ret[ctr]) ret[ctr] = {};
            if (!ret[ctr][l]) ret[ctr][l] = {};
            if (!ret[ctr][l][h])
                ret[ctr][l][h] = 1;
            else
                ret[ctr][l][h]++;
        }
        _.each(orgs(grid), function(org) {
            for (var i = 0; i < org[0].length; i++) { // orgs
                for (var j = 0; j < org[1].length; j++) {  // dirs
                    var rows = getRow(grid, org[0][i], org[1][j]);
                    for (var k = 0; k < rows.length; k++) {
                        if (rows[k][0] == -1 || rows[k].length < 2) continue;
                        var head = 0;
                        if (k != 0 && rows[k - 1][0] == -1) head += 1;
                        if (k != rows.length - 1 && rows[k + 1][0] == -1) head += 1;
                        createOrIncrease(rows[k][0], rows[k].length, head);
                    }
                }
            }
        });
        return ret;
    }

    var chance = function(grid, crt) {
        var state = calcState(grid);
        var getInState = function(r) {
            var tmp = state[r[0]];
            if (!tmp) return 0;
            tmp = tmp[r[1]];
            if (!tmp) return 0;
            tmp = tmp[r[2]];
            if (!tmp) return 0;
            return tmp;
        };
        var ret = 0;
        _.each(rules[crt], function(rule) {
            ret += getInState(rule[0]) * rule[1];
        });
        return ret;
    }
    //chance = _.memoize(chance, function(grid, crt) {
    //    return [grid, crt];
    //});

    AI.play = function(grid, crtPlayer, depth, maxChance) {
        if (depth == 0)
            return [chance(grid, crtPlayer), null];
        var row = grid.length;
        var col = grid[0].length;

        var notAlone = function(grid, r, c) {
            for (var i = -1; i <= 1; i++)
                for (var j = -1; j <= 1; j++) {
                    var newi = i + r;
                    var newj = j + c;
                    if (newi < 0 || newj < 0 || newi >= grid.length || newj >= grid[0].length)
                        continue;
                    if ((i != 0 || j != 0) &&
                            (grid[i + r][j + c] == 0 || grid[i + r][j + c] == 1))
                        return true;
                }
            return false;
        }
        var ret = [-0x7fffffff, null];
        for (var i = 0; i < row; i++) {
            for (var j = 0; j < col; j++) {
                if (grid[i][j] != -1) continue;
                if (!notAlone(grid, i, j)) continue;
                grid[i][j] = crtPlayer;
                if (depth != 1) {
                    // FIXME: check the victory directly
                    var tmp_cs = chance(grid, crtPlayer);
                    if (tmp_cs >= maxList[0]) {
                        grid[i][j] = -1;
                        return [tmp_cs, [i, j]];
                    }
                }
                var enm_ret = AI.play(grid, switchPlayer(crtPlayer), depth - 1, -ret[0]);
                grid[i][j] = -1;

                // TODO: add random reaction
                var enm_cs = -enm_ret[0];
                if (maxChance && enm_cs >= maxChance)
                    return [0x7fffffff * 100, [i, j]];
                if (enm_cs > ret[0])
                    ret = [enm_cs, [i, j]];
            }
        }
        return ret;
    }
    return AI;
}();
<|endoftext|>