<commit_before>'use strict'
/*
  _______                            _         _               ____        _
 |__   __|                          (_)       (_)             |  _ \      | |
    | |_ __ __ _ _ __  ___ _ __ ___  _ ___ ___ _  ___  _ __   | |_) | ___ | |_
    | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \  |  _ < / _ \| __|
    | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_
    |_|_|  \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__|

    © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com)
    Github repository: https://github.com/raffaelecalza/transmission-telegram-bot
*/

const TelegramBot = require('node-telegram-bot-api');
const DateTime = require('date-and-time');
const engine = require('./engine.js');
const NotificationManager = require('./notification-manager.js');

const config = require('./config.json');

var userStates = {};
var userNotification;

if(NotificationManager.fileExists())
    userNotification = NotificationManager.loadFile();
else {
    userNotification = {};
    // Enable notification for each user
    config.bot.users.forEach((user) => {
        userNotification[user] = true;
    });
    NotificationManager.saveFile(userNotification);
}

console.log('Initializing the bot...')
const bot = new TelegramBot(config.bot.token, {
    polling: true
});

bot.getMe().then(function (info) {
    console.log(`
${info.first_name} is ready, the username is @${info.username}
`);
});

// End of configuration

// Display every message in the console
bot.on('message', function (msg) {
    console.log(`
Oh... there's a new incoming message sir!
-------- Here are some details --------
Authorized user: ${config.bot.users.indexOf(msg.from.id) > -1 ? 'yes' : 'no'}
Date: ${DateTime.format(new Date(msg.date * 1000), 'DD/MM HH:mm')}
From user: ${msg.chat.username || 'no username provided'}
Chat ID: ${msg.chat.id}
Name and surname: ${msg.from.first_name} ${msg.from.last_name}
Message id: ${msg.message_id}
Message text: ${msg.text || 'no text'}
`);
});

console.log('-------- Notify autorizhed users that the bot is up --------');
const wokeUpMsg = `Hey, I woke up just now 😎 and I'm ready to respond to your commands 🙌

👉 If you need help, use the /help command

Anyway when a torrent finishes the download, I'll send you a notification 🔔`;

config.bot.users.forEach(user => {
    if(userNotification[user])
        bot.sendMessage(user, wokeUpMsg, engine.listOfCommandsKeyboard);
})

// Start message
bot.onText(/\/start/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var reply = 'Hi ' + msg.chat.first_name + ' 🙌, I\'m your 🤖\nI\'ve been created to give you all the informations regarding the status of your torrents 😊. Start with /help to get a list of all available commands';
    bot.sendMessage(chatId, reply, engine.listOfCommandsKeyboard);
});

// Get the list of all torrents
bot.onText(/\/torrentlist|List of all torrents/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    engine.getTorrentsList((msg) => {
        if (engine.torrents.length == 0)
            bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
        else
            bot.sendMessage(chatId, msg, engine.listOfCommandsKeyboard);
    }, (err) => {
        bot.sendMessage(chatId, err);
    });
});

// Get all details about a torrent
bot.onText(/\/torrentstatus|Status/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboard();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };
    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Select a torrent and you\'ll receive all information about it', opts);
        userStates[chatId] = 'details';
    }
});

// Start torrent
bot.onText(/\/torrentstart|▶ Start/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboardPaused();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else if (keyb.length == 1)
        bot.sendMessage(chatId, 'All torrents are in download queue', engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Please send me a torrent to put in the download queue 😊', opts);
        userStates[chatId] = 'start';
    }
});

// Stop torrent
bot.onText(/\/torrentstop|⏸ Pause/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboardActive();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else if (keyb.length == 1)
        bot.sendMessage(chatId, "All torrents are currently paused", engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Which torrent would you stop?', opts);
        userStates[chatId] = 'stop';
    }
});

// Remove torrent
bot.onText(/\/torrentremove|❌ Remove/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboard();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, '⚠ Be careful! Once you remove it, you can not retrieve it\nSend me the torrent that you would remove 😊', opts);
        userStates[chatId] = 'remove';
    }
})

bot.onText(/Yes|No/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentId = userStates[chatId] || '';
    var answer = msg.text;

    if (answer == 'Yes')
        engine.removeTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly removed\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else
        bot.sendMessage(chatId, 'The operation was canceled, narrow escape 😪', engine.listOfCommandsKeyboard);
})

bot.onText(/\d+\) .+/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentId = msg.text.match(/\d+/)[0];

    var torrentAction = userStates[chatId] || '';

    if (torrentAction == 'stop')
        engine.pauseTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly stopped\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'details')
        engine.getTorrentDetails(torrentId, (details) => {
            bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'start')
        engine.startTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly started\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'remove') {
        userStates[chatId] = torrentId;
        bot.sendMessage(chatId, 'Are you sure you want to remove this torrent?', {
            reply_markup: JSON.stringify({
                keyboard: [['Yes', 'No']]
            })
        });
    }
});

// Add a torrent from url
bot.onText(/\/addtorrent|Add torrent/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, 'Please send me a torrent url or send me a torrent file (e.g. file.torrent)', engine.hideKeyboard);
    userStates[chatId] = 'add';
});

bot.onText(/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentAction = userStates[chatId] || '';
    if (torrentAction == 'add')
        engine.addTorrent(msg.text, (details) => {
            bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
});

// Cancel Operation
bot.onText(/Cancel/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    userStates[chatId] = '';
    bot.sendMessage(chatId, 'The operation was cancelled', engine.listOfCommandsKeyboard);
})

// Receive a document (for add torrent command)
bot.on('document', function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var fileId = msg.document.file_id;
    bot.getFileLink(fileId).then((link) => {
        engine.addTorrent(link, (details) => {
            bot.sendMessage(chatId, 'The torrent was added succesfully, here are some information about it\n' + details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    }, (err) => {
        bot.sendMessage(chatId, 'Oops 😰, something seems to have gone wrong while trying to request the link to Telegram servers 😒... Please try again to send the file\nSome details about the error:\n' + JSON.stringify(err));
    });
});

// Settings command
bot.onText(/\/settings|⚙ Settings/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, 'Please select one voice from the list', engine.settingsKeyboard);
})

bot.onText(/🔙 menu/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;

    var chatId = msg.chat.id;
    bot.sendMessage(chatId, 'What would you see?', engine.listOfCommandsKeyboard);
})

bot.onText(/Transmission info/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    engine.getSessionDetails((msg) => {
        bot.sendMessage(chatId, msg, engine.settingsKeyboard);
    });
})

bot.onText(/User notification/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    bot.sendMessage(chatId, 'Would you enable or disable the notifications?', {
        reply_markup: JSON.stringify({
            keyboard: [['Enable', 'Disable']]
        })
    });
})

bot.onText(/Enable|Disable/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    if(msg.text == 'Enable') {
        userNotification[chatId] = true;
        bot.sendMessage(chatId, 'Notifications enabled 🔔, you\'ll receive a message when a torrent is completely downloaded', engine.settingsKeyboard);
    } else if(msg.text == 'Disable') {
        userNotification[chatId] = false;
        bot.sendMessage(chatId, 'Notification disabled 🔕, you\'ll not receive any notification when a torrent is downloaded completely', engine.settingsKeyboard);
    }
    NotificationManager.saveFile(userNotification);
})

bot.onText(/Set download folder/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    userStates[chatId] = 'set-folder' || '';
    bot.sendMessage(chatId, 'Please send me the new folder where next torrents will be downloaded', engine.hideKeyboard);
})

bot.onText(/(\/\w+)+\//g, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    if(userStates[chatId] == 'set-folder')
        engine.setSettings({'download-dir': msg.text}, () => {
            bot.sendMessage(chatId, 'The download 📂 was changed 👌', engine.settingsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.settingsKeyboard);
        });
})

// End of settings

// Help instructions
bot.onText(/\/help|❔ Help/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, engine.helpMsg, engine.listOfCommandsKeyboard);
});

// Callback when a torrent is completed
engine.torrentCompleted = (msg) => {
    config.bot.users.forEach((userId) => {
        if(userNotification[userId])
            bot.sendMessage(userId, msg, engine.listOfCommandsKeyboard);
    });
};<commit_msg>Added cancel button to enable/disable keyboard<commit_after>'use strict'
/*
  _______                            _         _               ____        _
 |__   __|                          (_)       (_)             |  _ \      | |
    | |_ __ __ _ _ __  ___ _ __ ___  _ ___ ___ _  ___  _ __   | |_) | ___ | |_
    | | '__/ _` | '_ \/ __| '_ ` _ \| / __/ __| |/ _ \| '_ \  |  _ < / _ \| __|
    | | | | (_| | | | \__ \ | | | | | \__ \__ \ | (_) | | | | | |_) | (_) | |_
    |_|_|  \__,_|_| |_|___/_| |_| |_|_|___/___/_|\___/|_| |_| |____/ \___/ \__|

    © 2016 - Calzà Raffaele (raffaelecalza4@gmail.com)
    Github repository: https://github.com/raffaelecalza/transmission-telegram-bot
*/

const TelegramBot = require('node-telegram-bot-api');
const DateTime = require('date-and-time');
const engine = require('./engine.js');
const NotificationManager = require('./notification-manager.js');

const config = require('./config.json');

var userStates = {};
var userNotification;

if(NotificationManager.fileExists())
    userNotification = NotificationManager.loadFile();
else {
    userNotification = {};
    // Enable notification for each user
    config.bot.users.forEach((user) => {
        userNotification[user] = true;
    });
    NotificationManager.saveFile(userNotification);
}

console.log('Initializing the bot...')
const bot = new TelegramBot(config.bot.token, {
    polling: true
});

bot.getMe().then(function (info) {
    console.log(`
${info.first_name} is ready, the username is @${info.username}
`);
});

// End of configuration

// Display every message in the console
bot.on('message', function (msg) {
    console.log(`
Oh... there's a new incoming message sir!
-------- Here are some details --------
Authorized user: ${config.bot.users.indexOf(msg.from.id) > -1 ? 'yes' : 'no'}
Date: ${DateTime.format(new Date(msg.date * 1000), 'DD/MM HH:mm')}
From user: ${msg.chat.username || 'no username provided'}
Chat ID: ${msg.chat.id}
Name and surname: ${msg.from.first_name} ${msg.from.last_name}
Message id: ${msg.message_id}
Message text: ${msg.text || 'no text'}
`);
});

console.log('-------- Notify autorizhed users that the bot is up --------');
const wokeUpMsg = `Hey, I woke up just now 😎 and I'm ready to respond to your commands 🙌

👉 If you need help, use the /help command

Anyway when a torrent finishes the download, I'll send you a notification 🔔`;

config.bot.users.forEach(user => {
    if(userNotification[user])
        bot.sendMessage(user, wokeUpMsg, engine.listOfCommandsKeyboard);
})

// Start message
bot.onText(/\/start/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var reply = 'Hi ' + msg.chat.first_name + ' 🙌, I\'m your 🤖\nI\'ve been created to give you all the informations regarding the status of your torrents 😊. Start with /help to get a list of all available commands';
    bot.sendMessage(chatId, reply, engine.listOfCommandsKeyboard);
});

// Get the list of all torrents
bot.onText(/\/torrentlist|List of all torrents/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    engine.getTorrentsList((msg) => {
        if (engine.torrents.length == 0)
            bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
        else
            bot.sendMessage(chatId, msg, engine.listOfCommandsKeyboard);
    }, (err) => {
        bot.sendMessage(chatId, err);
    });
});

// Get all details about a torrent
bot.onText(/\/torrentstatus|Status/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboard();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };
    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Select a torrent and you\'ll receive all information about it', opts);
        userStates[chatId] = 'details';
    }
});

// Start torrent
bot.onText(/\/torrentstart|▶ Start/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboardPaused();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else if (keyb.length == 1)
        bot.sendMessage(chatId, 'All torrents are in download queue', engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Please send me a torrent to put in the download queue 😊', opts);
        userStates[chatId] = 'start';
    }
});

// Stop torrent
bot.onText(/\/torrentstop|⏸ Pause/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboardActive();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else if (keyb.length == 1)
        bot.sendMessage(chatId, "All torrents are currently paused", engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, 'Which torrent would you stop?', opts);
        userStates[chatId] = 'stop';
    }
});

// Remove torrent
bot.onText(/\/torrentremove|❌ Remove/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var keyb = engine.getKeyboard();
    var opts = {
        reply_markup: JSON.stringify({
            keyboard: keyb
        })
    };

    if (engine.torrents.length == 0)
        bot.sendMessage(chatId, engine.noTorrentsText, engine.listOfCommandsKeyboard);
    else {
        bot.sendMessage(chatId, '⚠ Be careful! Once you remove it, you can not retrieve it\nSend me the torrent that you would remove 😊', opts);
        userStates[chatId] = 'remove';
    }
})

bot.onText(/Yes|No/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentId = userStates[chatId] || '';
    var answer = msg.text;

    if (answer == 'Yes')
        engine.removeTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly removed\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else
        bot.sendMessage(chatId, 'The operation was canceled, narrow escape 😪', engine.listOfCommandsKeyboard);
})

bot.onText(/\d+\) .+/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentId = msg.text.match(/\d+/)[0];

    var torrentAction = userStates[chatId] || '';

    if (torrentAction == 'stop')
        engine.pauseTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly stopped\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'details')
        engine.getTorrentDetails(torrentId, (details) => {
            bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'start')
        engine.startTorrent(torrentId, (details) => {
            bot.sendMessage(chatId, 'Torrent correctly started\nUse /torrentstatus to see the updated torrents list', engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    else if (torrentAction == 'remove') {
        userStates[chatId] = torrentId;
        bot.sendMessage(chatId, 'Are you sure you want to remove this torrent?', {
            reply_markup: JSON.stringify({
                keyboard: [['Yes', 'No']]
            })
        });
    }
});

// Add a torrent from url
bot.onText(/\/addtorrent|Add torrent/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, 'Please send me a torrent url or send me a torrent file (e.g. file.torrent)', engine.hideKeyboard);
    userStates[chatId] = 'add';
});

bot.onText(/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    var torrentAction = userStates[chatId] || '';
    if (torrentAction == 'add')
        engine.addTorrent(msg.text, (details) => {
            bot.sendMessage(chatId, details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
});

// Cancel Operation
bot.onText(/Cancel/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    userStates[chatId] = '';
    bot.sendMessage(chatId, 'The operation was cancelled', engine.listOfCommandsKeyboard);
})

// Receive a document (for add torrent command)
bot.on('document', function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    var fileId = msg.document.file_id;
    bot.getFileLink(fileId).then((link) => {
        engine.addTorrent(link, (details) => {
            bot.sendMessage(chatId, 'The torrent was added succesfully, here are some information about it\n' + details, engine.listOfCommandsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.listOfCommandsKeyboard);
        });
    }, (err) => {
        bot.sendMessage(chatId, 'Oops 😰, something seems to have gone wrong while trying to request the link to Telegram servers 😒... Please try again to send the file\nSome details about the error:\n' + JSON.stringify(err));
    });
});

// Settings command
bot.onText(/\/settings|⚙ Settings/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, 'Please select one voice from the list', engine.settingsKeyboard);
})

bot.onText(/🔙 menu/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;

    var chatId = msg.chat.id;
    bot.sendMessage(chatId, 'What would you see?', engine.listOfCommandsKeyboard);
})

bot.onText(/Transmission info/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    engine.getSessionDetails((msg) => {
        bot.sendMessage(chatId, msg, engine.settingsKeyboard);
    });
})

bot.onText(/User notification/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    bot.sendMessage(chatId, 'Would you enable or disable the notifications?', {
        reply_markup: JSON.stringify({
            keyboard: [['Enable', 'Disable'], ['Cancel']]
        })
    });
})

bot.onText(/Enable|Disable/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    if(msg.text == 'Enable') {
        userNotification[chatId] = true;
        bot.sendMessage(chatId, 'Notifications enabled 🔔, you\'ll receive a message when a torrent is completely downloaded', engine.settingsKeyboard);
    } else if(msg.text == 'Disable') {
        userNotification[chatId] = false;
        bot.sendMessage(chatId, 'Notification disabled 🔕, you\'ll not receive any notification when a torrent is downloaded completely', engine.settingsKeyboard);
    }
    NotificationManager.saveFile(userNotification);
})

bot.onText(/Set download folder/, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    userStates[chatId] = 'set-folder' || '';
    bot.sendMessage(chatId, 'Please send me the new folder where next torrents will be downloaded', engine.hideKeyboard);
})

bot.onText(/(\/\w+)+\//g, function(msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;
    if(userStates[chatId] == 'set-folder')
        engine.setSettings({'download-dir': msg.text}, () => {
            bot.sendMessage(chatId, 'The download 📂 was changed 👌', engine.settingsKeyboard);
        }, (err) => {
            bot.sendMessage(chatId, err, engine.settingsKeyboard);
        });
})

// End of settings

// Help instructions
bot.onText(/\/help|❔ Help/, function (msg) {
    if (config.bot.users.indexOf(msg.from.id) == -1) return;
    var chatId = msg.chat.id;

    bot.sendMessage(chatId, engine.helpMsg, engine.listOfCommandsKeyboard);
});

// Callback when a torrent is completed
engine.torrentCompleted = (msg) => {
    config.bot.users.forEach((userId) => {
        if(userNotification[userId])
            bot.sendMessage(userId, msg, engine.listOfCommandsKeyboard);
    });
};<|endoftext|>


<commit_before>/*global google: false */
Ext.ns('vrs.ux.touch');

/**
* TODO:
*  - Fullscreen toggling needs to move the DOM element when we go fullscreen.
*  - Add support for pan into view
*  - Add specs for the functionality.
*
*  - track location of a moving feature
*/

/**
* Overlay panel holder class.
*
* Acts as the glue to connect an overlay view to the
* Sencha Touch popup panel below.
*/
function OverlayPanelHolder(opts) {
   this.popupPanel = opts.popup;
   this.divElt     = document.createElement('DIV');
}
OverlayPanelHolder.prototype = new google.maps.OverlayView();

OverlayPanelHolder.prototype.draw = function() {
   console.log('panelholder.draw');
   this.popupPanel.updatePosition();
};

/**
* Called when the map panes are available for attaching the popup to the map DOM.
*/
OverlayPanelHolder.prototype.onAdd = function() {
   console.log('panelholder.onAdd');
   this.popupPanel.onAdd();
   // Add the div to the map pane
   var panes = this.getPanes();
   panes.floatPane.appendChild(this.divElt);  // overlayLayer, overlayMouseTarget, floatPane

   this.popupPanel.afterAdd();
};

OverlayPanelHolder.prototype.onRemove = function() {
   console.log('panelholder.onRemove');
   // Remove our div from the map.
   this.divElt.parentNode.removeChild(this.divElt);
   //this.divElt = null;   // keep the div around for the popup to look at and remove.

   this.popupPanel.afterRemove();
};

/**
* A panel that can be used as popup window on a google maps map.
* Handles the anchoring to a location and resizing as needed.
*
* Usage
* =====
*
* window = new GmapPopupPanel({
*    location: marker.position,
*    map     : map,
*    sizeConfig: {
*       size: 'small-wide'
*    },
*    items: [{ html: 'my content' }]
*  });
*/
vrs.ux.touch.GmapPopupPanel = Ext.extend(Ext.Panel, {
   /**
   * The map that we are attached onto.
   */
   map: null,

   /** google.maps.LatLng location of the panel anchor. */
   location: null,

   /** {bool} True if the popup should be anchored to it's location. */
   anchored: true,

   /** {bool} True if we should pan the map so the popup is in view. */
   panIn: true,

   /** A config object to pass to setPanelSize after panel
   * is constructed.
   */
   sizeConfig: null,

   /** Set to allow custom styling. */
   componentCls: 'x-map-popup',

   /**
   * If set, then automatically removes the panel and
   * associated objects when the popup is hidden.
   */
   autoRemoveOnHide : true,

   /** override the panel details. */
   floating      : true,
   hideOnMaskTap : true,  // Don't auto-hide when tap outside the component.

   /**
   * Reference to the overlay holder that manages us.
   */
   _overlayHolder: null,

   /**
   * Construct the popup panel.
   *
   * Note: Since this is a panel, normal panel configuration settings can
   *       be passed in.  (ex: items, dockedItems, layout, ...)
   */
   constructor: function(config) {
      var me = this;

      // Due the way we add to the DOM the scroll would be added before the elements exist
      // Store the scroll flag externally and manually reset it when the Panel is shown
      if (config.scroll) {
         this._config_scroll = config.scroll;
         config.scroll = null;
      }

      // Allocate the holder so we can get the DIV to render into.
      this._overlayHolder = new OverlayPanelHolder({popup: this});

      Ext.apply(config, {
         renderTo: this._overlayHolder.divElt
      });

      this.addEvents({
         /** Fired after the panel has been added to the map DOM.
         * called as: afterAdd(panel)
         */
         'afterAdd': true,

         /** Fired after the panel has been removed from the map DOM.
         * called as: afterRemove(panel)
         */
         'afterRemove': true
      });

      vrs.ux.touch.GmapPopupPanel.superclass.constructor.apply(this, arguments);

      // Add flag we use to track if have toggled ourselves into fullscreen mode
      // used in setPanelSize to handle fullscreen settings.
      this._isFullscreen = false;

      // If setup to auto remove, register to call remove at the end of hide
      if(this.autoRemoveOnHide) {
         this.on({
            hide: function() { me.remove(); }
         });
      }
   },

   initComponent: function() {
      vrs.ux.touch.GmapPopupPanel.superclass.initComponent.apply(this, arguments);

      // Connect it up to the map now that it has been initialized.
      this._overlayHolder.setMap(this.map);
   },

   /**
   * Set the size of the panel to use.
   *
   * @param config
   *      fullscreen: If true, then set to a fullscreen panel centered on screen.
   *                 (still respects width and height settings as computed)
   *      width: if set, sets to this width in pixels.
   *      height: if set, sets to this height in pixels.
   *      size: if set to one of 'small', 'medium', 'large', 'fullscreen'
   *           or variation ending in '-tall' or '-wide'. (ex: 'small-wide')
   *           automatically picks window of good size for the current device.
   *      autoPosition: If true, automatically call the position setting. (default: true)
   *      anchor: If true, we set anchored (if not already set) and update.
   *               (true by default except for fullscreen which is false by default)
   *
   * note: fullscreen is a special variation that will override some other settings.
   */
   setPanelSize: function(config) {
      var vp_size = Ext.Viewport.getSize(),
          tall_wide_factor = 1.5,  // Factor used to increase size of tall/wide panels.
          set_sizes = {},          // map from size name to size values to use.
          size_name, var_name,
          width, height,
          extra_margin = 12,  // extra margin to fix layout issues
          auto_position      = config.autoPosition || true, // if we should update position.
          should_anchor      = config.anchor || true,       // if we should be anchored
          setting_fullscreen = config.fullscreen || false,  // if we are setting to fullscreen
          was_fullscreen     = this._isFullscreen;          // if we were previously fullscreen.

      // Lookup table of named size values
      set_sizes = {
         small: {
            width  : vp_size.width  * 0.2,
            height : vp_size.height * 0.2
         },
         medium: {
            width  : vp_size.width  * 0.35,
            height : vp_size.height * 0.35
         },
         large: {
            width  : vp_size.width  * 0.5,
            height : vp_size.height * 0.5
         }
      };

      // compute the final width and height values
      if(config.size) {
         size_name = config.size.split('-')[0];
         var_name  = config.size.split('-')[1];
         width  = set_sizes[size_name].width;
         height = set_sizes[size_name].height;
         if('tall' === var_name) {
            height = height * tall_wide_factor;
         }
         if('wide' === var_name) {
            width = width * tall_wide_factor;
         }
      } else {
         width  = config.width;
         height = config.height;
      }

      // If fullscreen and no explicit width set, then set defaults.
      if(setting_fullscreen) {
         auto_position = false;
         should_anchor = false;

         if(Ext.isEmpty(width)) {
            width  = vp_size.width  * 0.9;
            height = vp_size.height * 0.9;
         }
      }

      // finalize the size and layout if we are visible.
      // - we don't layout if not visible because that would remove the sizing for first show
      this.setSize(width + extra_margin, height + extra_margin);
      if(this.isVisible()) {
         this.doLayout();
      }

      // If we should be anchoring and it doesn't match previous setting,
      // then change and possible update the rendering to remove/add anchor.
      if(should_anchor !== this.anchored) {
         this.anchored = should_anchor;
         if(this.rendered && this.isVisible()) {
            this.show();        // reshow to get anchor laid out correctly.
         }
      }

      // If we are rendered and we should update position, then do so now.
      if(this.rendered && this.isVisible() && auto_position) {
         this.updatePosition();
      }

      if(setting_fullscreen) {
         this.setCentered(true, true);  // set to centered and update it
      } else if(was_fullscreen && this.centered) {
         this.setCentered(false);      // reset to not be centered
      }

      // update fullscreen flag.
      this._isFullscreen = setting_fullscreen;
   },

   /** Override the show method to position the panel. */
   show: function() {
      vrs.ux.touch.GmapPopupPanel.superclass.show.apply(this, arguments);

      if(this.anchorEl) {
         this.anchorEl.hide();  // If we had anchor in past, hide for now.
      }

      // If we are anchored, position ourselves and potentially pan into view.
      if(this.anchored) {
         // Setup the anchor.
         // - hacked together from showBy and alignTo in Component.js
         //   todo: see if we can use more of alignTo logic.
         if(!this.anchorEl) {
            this.anchorEl = this.el.createChild({
               cls: 'x-anchor'
            });
         }
         this.anchorEl.show();
         this.anchorEl.addCls('x-anchor-bottom');

         // Set the initial position
         /*
         this.position();
         if(this.panIn && !this._mapMove) {
            this.panIntoView();
         }
         */
      }

      // Now that the divs are all added we can set the scroll flag if needed.
      if (this._config_scroll) {
         this.setScrollable(this._config_scroll);
         this.doLayout();
      }
   },

   /**
   * Remove the popup from the map and destroy all the associated objects.
   */
   remove: function() {
      // set map to null to trigger removal.
      // - we finish the removal in the afterRemove callback below.
      this._overlayHolder.setMap(null);
   },

   // --- INTERNAL HELPERS ---- //
   updatePosition: function() {
      var overlay_projection,
          posDiv_pane,
          arrow_size,
          panel_dx, panel_dy,
          arrow_dx, arrow_dy;

      // If we are not anchored, then nothing to update.
      if(!this.anchored) {
         return;
      }

      // Coordinate frames of interest:  (all 0,0 upper left)
      //   - pane: The overlay is added to a google map pane.  (pane 0,0 at upper left of map div)
      //   - panel: panel base div is relative to the pane
      //   - anchor: positioned relative to the panel.

      // Get the projection so we can calculate locations
      overlay_projection = this._overlayHolder.getProjection();

      // Find the location in pixel coordinates relative to the pane.
      posDiv_pane = overlay_projection.fromLatLngToDivPixel(this.location);

      console.log('Position: ', posDiv_pane);

      // Position panel bottom center on top of the point with arrow pointing at it.
      arrow_size = this.anchorEl.getSize();   // Get the box size of the arrow
      panel_dx = (this.getWidth() / 2);
      panel_dy = this.getHeight() + arrow_size.height + 10;
      arrow_dx = arrow_size.width / 2;
      arrow_dy = -arrow_size.height;

      // left, top relative to pane.
      this.setPosition(posDiv_pane.x - panel_dx, posDiv_pane.y - panel_dy);
      // left, top relative to the panel
      this.anchorEl.setBox(panel_dx - arrow_dx,
                           this.getHeight());
   },

   /**
   * Called after the overlay panel holder has added us to a pane.
   *
   * Should only be called once per map.
   */
   afterAdd: function() {
      // Pass the size configuration
      if(this.sizeConfig) {
         this.setPanelSize(this.sizeConfig);
      }

      // Auto show the popup
      this.show();

      this.fireEvent('afterAdd', this);
   },

   /**
   * Called after the overlay panel holder has removed us from the map.
   */
   afterRemove: function() {
      this.fireEvent('afterRemove', this);

      // finish removal by destroying and cleaning up everything here.
      this.map            = null;
      this._overlayHolder = null;
      this.destroy();
   }
});

/**
* Class for managing panel window popups on a google map component.
*
* This class acts as a store for creating popup panels.  It handles
* all the setup with the map and the glue code to connect the overlay
* to a sencha touch panel.
*
* TODO:
*  -
*
* Do we really need manager?
*
*  - It adds ability to close them all
*  - Adds ability to lookup by name/id.
*
* Note: DOES NOT FUNCTION YET.
*/
vrs.ux.touch.MapPanelMgr = Ext.extend(Ext.util.Observable, {
   /**
   * The google map we are connected to.
   */
   map: null,

   /**
   * The popup that we are managing.
   */
   popup: null,

   constructor: function(config) {

      this.addEvents({
         /** Test event.
         * called as: testevent(blah)
         */
         'testevent': true
      });

      vrs.ux.touch.MapPanelMgr.superclass.constructor.call(this, config);
   }

});
<commit_msg>Reorg code for gmaps popup map.<commit_after>/*global google: false */
Ext.ns('vrs.ux.touch');

/**
* TODO:
*  - Fullscreen toggling needs to move the DOM element when we go fullscreen.
*  - Add support for pan into view
*  - Add specs for the functionality.
*
*  - track location of a moving feature
*/

/**
* A panel that can be used as popup window on a google maps map.
* Handles the anchoring to a location and resizing as needed.
*
* Usage
* =====
*
* window = new GmapPopupPanel({
*    location: marker.position,
*    map     : map,
*    sizeConfig: {
*       size: 'small-wide'
*    },
*    items: [{ html: 'my content' }]
*  });
*/
vrs.ux.touch.GmapPopupPanel = Ext.extend(Ext.Panel, {
   /**
   * The map that we are attached onto.
   */
   map: null,

   /** google.maps.LatLng location of the panel anchor. */
   location: null,

   /** {bool} True if the popup should be anchored to it's location. */
   anchored: true,

   /** {bool} True if we should pan the map so the popup is in view. */
   panIn: true,

   /** A config object to pass to setPanelSize after panel
   * is constructed.
   */
   sizeConfig: null,

   /** Set to allow custom styling. */
   componentCls: 'x-map-popup',

   /**
   * If set, then automatically removes the panel and
   * associated objects when the popup is hidden.
   */
   autoRemoveOnHide : true,

   /** override the panel details. */
   floating      : true,
   hideOnMaskTap : true,  // Don't auto-hide when tap outside the component.

   /**
   * Reference to the overlay holder that manages us.
   */
   _overlayHolder: null,

   /**
   * Construct the popup panel.
   *
   * Note: Since this is a panel, normal panel configuration settings can
   *       be passed in.  (ex: items, dockedItems, layout, ...)
   */
   constructor: function(config) {
      var me = this;

      // Due the way we add to the DOM the scroll would be added before the elements exist
      // Store the scroll flag externally and manually reset it when the Panel is shown
      if (config.scroll) {
         this._config_scroll = config.scroll;
         config.scroll = null;
      }

      // Allocate the holder so we can get the DIV to render into.
      this._overlayHolder = new OverlayPanelHolder({popup: this});

      Ext.apply(config, {
         renderTo: this._overlayHolder.divElt
      });

      this.addEvents({
         /** Fired after the panel has been added to the map DOM.
         * called as: afterAdd(panel)
         */
         'afterAdd': true,

         /** Fired after the panel has been removed from the map DOM.
         * called as: afterRemove(panel)
         */
         'afterRemove': true
      });

      vrs.ux.touch.GmapPopupPanel.superclass.constructor.apply(this, arguments);

      // Add flag we use to track if have toggled ourselves into fullscreen mode
      // used in setPanelSize to handle fullscreen settings.
      this._isFullscreen = false;

      // If setup to auto remove, register to call remove at the end of hide
      if(this.autoRemoveOnHide) {
         this.on({
            hide: function() { me.remove(); }
         });
      }
   },

   initComponent: function() {
      vrs.ux.touch.GmapPopupPanel.superclass.initComponent.apply(this, arguments);

      // Connect it up to the map now that it has been initialized.
      this._overlayHolder.setMap(this.map);
   },

   /**
   * Set the size of the panel to use.
   *
   * @param config
   *      fullscreen: If true, then set to a fullscreen panel centered on screen.
   *                 (still respects width and height settings as computed)
   *      width: if set, sets to this width in pixels.
   *      height: if set, sets to this height in pixels.
   *      size: if set to one of 'small', 'medium', 'large', 'fullscreen'
   *           or variation ending in '-tall' or '-wide'. (ex: 'small-wide')
   *           automatically picks window of good size for the current device.
   *      autoPosition: If true, automatically call the position setting. (default: true)
   *      anchor: If true, we set anchored (if not already set) and update.
   *               (true by default except for fullscreen which is false by default)
   *
   * note: fullscreen is a special variation that will override some other settings.
   */
   setPanelSize: function(config) {
      var vp_size = Ext.Viewport.getSize(),
          tall_wide_factor = 1.5,  // Factor used to increase size of tall/wide panels.
          set_sizes = {},          // map from size name to size values to use.
          size_name, var_name,
          width, height,
          extra_margin = 12,  // extra margin to fix layout issues
          auto_position      = config.autoPosition || true, // if we should update position.
          should_anchor      = config.anchor || true,       // if we should be anchored
          setting_fullscreen = config.fullscreen || false,  // if we are setting to fullscreen
          was_fullscreen     = this._isFullscreen;          // if we were previously fullscreen.

      // Lookup table of named size values
      set_sizes = {
         small: {
            width  : vp_size.width  * 0.2,
            height : vp_size.height * 0.2
         },
         medium: {
            width  : vp_size.width  * 0.35,
            height : vp_size.height * 0.35
         },
         large: {
            width  : vp_size.width  * 0.5,
            height : vp_size.height * 0.5
         }
      };

      // compute the final width and height values
      if(config.size) {
         size_name = config.size.split('-')[0];
         var_name  = config.size.split('-')[1];
         width  = set_sizes[size_name].width;
         height = set_sizes[size_name].height;
         if('tall' === var_name) {
            height = height * tall_wide_factor;
         }
         if('wide' === var_name) {
            width = width * tall_wide_factor;
         }
      } else {
         width  = config.width;
         height = config.height;
      }

      // If fullscreen and no explicit width set, then set defaults.
      if(setting_fullscreen) {
         auto_position = false;
         should_anchor = false;

         if(Ext.isEmpty(width)) {
            width  = vp_size.width  * 0.9;
            height = vp_size.height * 0.9;
         }
      }

      // finalize the size and layout if we are visible.
      // - we don't layout if not visible because that would remove the sizing for first show
      this.setSize(width + extra_margin, height + extra_margin);
      if(this.isVisible()) {
         this.doLayout();
      }

      // If we should be anchoring and it doesn't match previous setting,
      // then change and possible update the rendering to remove/add anchor.
      if(should_anchor !== this.anchored) {
         this.anchored = should_anchor;
         if(this.rendered && this.isVisible()) {
            this.show();        // reshow to get anchor laid out correctly.
         }
      }

      // If we are rendered and we should update position, then do so now.
      if(this.rendered && this.isVisible() && auto_position) {
         this.updatePosition();
      }

      if(setting_fullscreen) {
         this.setCentered(true, true);  // set to centered and update it
      } else if(was_fullscreen && this.centered) {
         this.setCentered(false);      // reset to not be centered
      }

      // update fullscreen flag.
      this._isFullscreen = setting_fullscreen;
   },

   /** Override the show method to position the panel. */
   show: function() {
      vrs.ux.touch.GmapPopupPanel.superclass.show.apply(this, arguments);

      if(this.anchorEl) {
         this.anchorEl.hide();  // If we had anchor in past, hide for now.
      }

      // If we are anchored, position ourselves and potentially pan into view.
      if(this.anchored) {
         // Setup the anchor.
         // - hacked together from showBy and alignTo in Component.js
         //   todo: see if we can use more of alignTo logic.
         if(!this.anchorEl) {
            this.anchorEl = this.el.createChild({
               cls: 'x-anchor'
            });
         }
         this.anchorEl.show();
         this.anchorEl.addCls('x-anchor-bottom');

         // Set the initial position
         /*
         this.position();
         if(this.panIn && !this._mapMove) {
            this.panIntoView();
         }
         */
      }

      // Now that the divs are all added we can set the scroll flag if needed.
      if (this._config_scroll) {
         this.setScrollable(this._config_scroll);
         this.doLayout();
      }
   },

   /**
   * Remove the popup from the map and destroy all the associated objects.
   */
   remove: function() {
      // set map to null to trigger removal.
      // - we finish the removal in the afterRemove callback below.
      this._overlayHolder.setMap(null);
   },

   // --- INTERNAL HELPERS ---- //
   updatePosition: function() {
      var overlay_projection,
          posDiv_pane,
          arrow_size,
          panel_dx, panel_dy,
          arrow_dx, arrow_dy;

      // If we are not anchored, then nothing to update.
      if(!this.anchored) {
         return;
      }

      // Coordinate frames of interest:  (all 0,0 upper left)
      //   - pane: The overlay is added to a google map pane.  (pane 0,0 at upper left of map div)
      //   - panel: panel base div is relative to the pane
      //   - anchor: positioned relative to the panel.

      // Get the projection so we can calculate locations
      overlay_projection = this._overlayHolder.getProjection();

      // Find the location in pixel coordinates relative to the pane.
      posDiv_pane = overlay_projection.fromLatLngToDivPixel(this.location);

      console.log('Position: ', posDiv_pane);

      // Position panel bottom center on top of the point with arrow pointing at it.
      arrow_size = this.anchorEl.getSize();   // Get the box size of the arrow
      panel_dx = (this.getWidth() / 2);
      panel_dy = this.getHeight() + arrow_size.height + 10;
      arrow_dx = arrow_size.width / 2;
      arrow_dy = -arrow_size.height;

      // left, top relative to pane.
      this.setPosition(posDiv_pane.x - panel_dx, posDiv_pane.y - panel_dy);
      // left, top relative to the panel
      this.anchorEl.setBox(panel_dx - arrow_dx,
                           this.getHeight());
   },

   /**
   * Called after the overlay panel holder has added us to a pane.
   *
   * Should only be called once per map.
   */
   afterAdd: function() {
      // Pass the size configuration
      if(this.sizeConfig) {
         this.setPanelSize(this.sizeConfig);
      }

      // Auto show the popup
      this.show();

      this.fireEvent('afterAdd', this);
   },

   /**
   * Called after the overlay panel holder has removed us from the map.
   */
   afterRemove: function() {
      this.fireEvent('afterRemove', this);

      // finish removal by destroying and cleaning up everything here.
      this.map            = null;
      this._overlayHolder = null;
      this.destroy();
   }
});

/**
* Overlay panel holder class.
*
* Acts as the glue to connect an overlay view to the
* Sencha Touch popup panel.
*/
function OverlayPanelHolder(opts) {
   this.popupPanel = opts.popup;
   this.divElt     = document.createElement('DIV');
}
OverlayPanelHolder.prototype = new google.maps.OverlayView();

OverlayPanelHolder.prototype.draw = function() {
   console.log('panelholder.draw');
   this.popupPanel.updatePosition();
};

/**
* Called when the map panes are available for attaching the popup to the map DOM.
*/
OverlayPanelHolder.prototype.onAdd = function() {
   console.log('panelholder.onAdd');
   this.popupPanel.onAdd();
   // Add the div to the map pane
   var panes = this.getPanes();
   panes.floatPane.appendChild(this.divElt);  // overlayLayer, overlayMouseTarget, floatPane

   this.popupPanel.afterAdd();
};

OverlayPanelHolder.prototype.onRemove = function() {
   console.log('panelholder.onRemove');
   // Remove our div from the map.
   this.divElt.parentNode.removeChild(this.divElt);
   //this.divElt = null;   // keep the div around for the popup to look at and remove.

   this.popupPanel.afterRemove();
};

/**
* Class for managing panel window popups on a google map component.
*
* This class acts as a store for creating popup panels.  It handles
* all the setup with the map and the glue code to connect the overlay
* to a sencha touch panel.
*
* TODO:
*  -
*
* Do we really need manager?
*
*  - It adds ability to close them all
*  - Adds ability to lookup by name/id.
*
* Note: DOES NOT FUNCTION YET.
*/
vrs.ux.touch.MapPanelMgr = Ext.extend(Ext.util.Observable, {
   /**
   * The google map we are connected to.
   */
   map: null,

   /**
   * The popup that we are managing.
   */
   popup: null,

   constructor: function(config) {

      this.addEvents({
         /** Test event.
         * called as: testevent(blah)
         */
         'testevent': true
      });

      vrs.ux.touch.MapPanelMgr.superclass.constructor.call(this, config);
   }

});
<|endoftext|>


<commit_before>function SocketWrapper(socket) {
    this.socket = socket;
    this.onOpens = [];
    this.onMessages = [];
    this.onErrors = [];
    this.onCloses = [];
    var wrap = this; // wat.

    socket.onopen = function(evt) {
	// APPARENTLY, this is now socket.this
	for (var l in wrap.onOpens.slice(0)) {
	    wrap.onOpens[l](evt);
	}
    }

    socket.onmessage = function(evt) {
	for (var l in wrap.onMessages.slice(0)) {
	    wrap.onMessages[l](JSON.parse(atob(evt.data)));
	}
    }

    socket.onerror = function(evt) {
	for (var l in wrap.onErrors.slice(0)) {
	    wrap.onErrors[l](evt);
	}
    }

    socket.onclose = function(evt) {
	for (var l in wrap.onCloses.slice(0)) {
	    wrap.onCloses[l](evt);
	}
    }
}

SocketWrapper.prototype.addOnOpen = function(cb) {
    this.onOpens.push(cb);
}

SocketWrapper.prototype.addOnMessage = function(cb) {
    this.onMessages.push(cb);
}

SocketWrapper.prototype.addOnError = function(cb) {
    this.onErrors.push(cb);
}

SocketWrapper.prototype.addOnClose = function(cb) {
    this.onCloses.push(cb);
}

SocketWrapper.prototype.send = function(data) {
    this.socket.send(btoa(JSON.stringify(data)));
}

function RemoteFunction(socket, seq, name, callback, timeoutCallback) {
    this.socket = socket;
    this.seq = seq;
    this.name = name;
    this.timer = null;
    this.callback = callback;
    this.timeoutCallback = timeoutCallback;
    this.completed = false;
    this.boundMethod = null;
}

RemoteFunction.prototype.listener = function(data) {
    if (data && "seq" in data) {
	if (data.seq == this.seq) {
	    clearTimeout(this.timer);
	    this.complete = true;
	    this.callback(data);

	    // if we leave this around we get exponential calls, oops
	    var ourIndex = this.socket.onMessages.indexOf(this.boundMethod);
	    delete this.socket.onMessages[ourIndex];
	}
    }
};

RemoteFunction.prototype.call = function(context) {
    var data = {
	"seq": this.seq,
	"op": this.name,
	"args": Array.slice(arguments, 1),
	"kwargs": {},
	"context": context
    };

    var theese = this;

    this.boundMethod = function(data){theese.listener(data);};
    this.socket.addOnMessage(this.boundMethod);

    this.socket.send(data);
    // All functions will have a 5 second timeout I guess
    if (this.callback) {
	this.timer = setTimeout(function() {theese.callback(null);}, 5000);
    } else {
	console.log("this.callback is not anything:");
	console.log(this.callback);
    }
}

window.client = {
    id: null, // This will be updated when connection is successful
    seq: 0,
    socket: null, // This needs to be set before we can initialize
    init: function(socket) {
	this.socket = socket;
	this.socket.addOnOpen(function(evt) {
	    console.log("Socket opened. Now calling whoami...");
	    window.client.call("whoami", null, function(res) {
		this.id = res;
	    });
	});
    },
    call: function(name, context, callback) {
	var tmpSeq = ++this.seq;
	var rf = new RemoteFunction(this.socket, tmpSeq, name, callback, null);
	var newArgs = [context];
	newArgs.concat(Array.slice(arguments, 3));
	rf.call.apply(rf, newArgs);
    }
}

$(function() {
    var wsUri = "ws://echo.websocket.org/";

    window.client.init(new SocketWrapper(new WebSocket("ws://" + location.hostname + ":" + (parseInt(location.port) + 1), ['base64', 'binary'])));

    window.client.socket.addOnOpen(function(evt) { console.log("WebSocket is open!"); registerWithServer();});
    //window.client.socket.addOnMessage(function(data) { console.log(data); });
});

function registerWithServer() {
    window.client.socket.send({"message": "Hello, socket!"});
    $("#center-btn").click(function(){window.client.call("whoami", null, function(res) {console.log("You are " + res.seq);});});
}
<commit_msg>Update ws client to work with new stuff<commit_after>function SocketWrapper(socket) {
    this.socket = socket;
    this.onOpens = [];
    this.onMessages = [];
    this.onErrors = [];
    this.onCloses = [];
    var wrap = this; // wat.

    socket.onopen = function(evt) {
	// APPARENTLY, this is now socket.this
	for (var l in wrap.onOpens.slice(0)) {
	    wrap.onOpens[l](evt);
	}
    }

    socket.onmessage = function(evt) {
	for (var l in wrap.onMessages.slice(0)) {
	    // Might need to do atob() here?
	    wrap.onMessages[l](JSON.parse(evt.data));
	}
    }

    socket.onerror = function(evt) {
	for (var l in wrap.onErrors.slice(0)) {
	    wrap.onErrors[l](evt);
	}
    }

    socket.onclose = function(evt) {
	for (var l in wrap.onCloses.slice(0)) {
	    wrap.onCloses[l](evt);
	}
    }
}

SocketWrapper.prototype.addOnOpen = function(cb) {
    this.onOpens.push(cb);
}

SocketWrapper.prototype.addOnMessage = function(cb) {
    this.onMessages.push(cb);
}

SocketWrapper.prototype.addOnError = function(cb) {
    this.onErrors.push(cb);
}

SocketWrapper.prototype.addOnClose = function(cb) {
    this.onCloses.push(cb);
}

SocketWrapper.prototype.send = function(data) {
    // FIXME maybe need btoa here?
    this.socket.send(JSON.stringify(data));
}

function RemoteFunction(socket, seq, name, callback, timeoutCallback) {
    this.socket = socket;
    this.seq = seq;
    this.name = name;
    this.timer = null;
    this.callback = callback;
    this.timeoutCallback = timeoutCallback;
    this.completed = false;
    this.boundMethod = null;
}

RemoteFunction.prototype.listener = function(data) {
    if (data && "seq" in data) {
	if (data.seq == this.seq) {
	    clearTimeout(this.timer);
	    this.complete = true;
	    this.callback(data);

	    // if we leave this around we get exponential calls, oops
	    var ourIndex = this.socket.onMessages.indexOf(this.boundMethod);
	    delete this.socket.onMessages[ourIndex];
	}
    }
};

RemoteFunction.prototype.call = function(context) {
    var data = {
	"seq": this.seq,
	"op": this.name,
	"args": Array.slice(arguments, 1),
	"kwargs": {},
	"context": context
    };

    var theese = this;

    this.boundMethod = function(data){theese.listener(data);};
    this.socket.addOnMessage(this.boundMethod);

    this.socket.send(data);
    // All functions will have a 5 second timeout I guess
    if (this.callback) {
	this.timer = setTimeout(function() {theese.callback(null);}, 5000);
    } else {
	console.log("this.callback is not anything:");
	console.log(this.callback);
    }
}

window.client = {
    id: null, // This will be updated when connection is successful
    seq: 0,
    socket: null, // This needs to be set before we can initialize
    init: function(socket) {
	this.socket = socket;
	this.socket.addOnOpen(function(evt) {
	    console.log("Socket opened. Now calling whoami...");
	    window.client.call("whoami", null, function(res) {
		this.id = res;
	    });
	});
    },
    call: function(name, context, callback) {
	var tmpSeq = ++this.seq;
	var rf = new RemoteFunction(this.socket, tmpSeq, name, callback, null);
	var newArgs = [context];
	newArgs.concat(Array.slice(arguments, 3));
	rf.call.apply(rf, newArgs);
    }
}

$(function() {
    window.client.init(new SocketWrapper(new WebSocket("ws://" + location.hostname + ":9000/client")));

    window.client.socket.addOnOpen(function(evt) { console.log("WebSocket is open!"); registerWithServer();});
    //window.client.socket.addOnMessage(function(data) { console.log(data); });
});

function registerWithServer() {
    window.client.socket.send({"message": "Hello, socket!"});
    $("#center-btn").click(function(){window.client.call("whoami", null, function(res) {console.log("You are " + res.seq);});});
}
<|endoftext|>


<commit_before>define( [ 'jquery', 'enketo-js/Widget', 'file-manager' ], function( $, Widget, fileManager ) {
    "use strict";

    var pluginName = 'filepicker';

    /**
     * FilePicker that works both offline and online. It abstracts the file storage/cache away
     * with the injected fileManager.
     *
     * @constructor
     * @param {Element} element [description]
     * @param {(boolean|{touch: boolean, maxlength:number})} options options
     * @param {*=} e     event
     */

    function Filepicker( element, options, e ) {
        if ( e ) {
            e.stopPropagation();
            e.preventDefault();
        }
        this.namespace = pluginName;
        Widget.call( this, element, options );
        this._init();
    }

    // copy the prototype functions from the Widget super class
    Filepicker.prototype = Object.create( Widget.prototype );

    // ensure the constructor is the new one
    Filepicker.prototype.constructor = Filepicker;

    /**
     * Initialize
     */
    Filepicker.prototype._init = function() {
        var $input = $( this.element ),
            existingFileName = $input.attr( 'data-loaded-file-name' ),
            that = this;

        this.mediaType = $input.attr( 'accept' );

        $input
            .attr( 'disabled', 'disabled' )
            .addClass( 'transparent' )
            .parent().addClass( 'with-media clearfix' );

        this.$widget = $(
                '
' +
                '

' +
                '

' +
                '

' +
                '
' )
            .insertAfter( $input );
        this.$feedback = this.$widget.find( '.file-feedback' );
        this.$preview = this.$widget.find( '.file-preview' );
        this.$fakeInput = this.$widget.find( '.fake-file-input' );

        // show loaded file name regardless of whether widget is supported
        if ( existingFileName ) {
            this._showFileName( existingFileName, this.mediaType );
            $input.removeAttr( 'data-loaded-file-name' );
        }

        if ( !fileManager || !fileManager.isSupported() ) {
            var advice = ( fileManager.notSupportedAdvisoryMsg ) ? fileManager.notSupportedAdvisoryMsg : '';
            this._showFeedback( 'Media questions are not supported in this browser. ' + advice, 'warning' );
            return;
        }

        if ( fileManager.isWaitingForPermissions() ) {
            this._showFeedback( 'Waiting for user permissions.', 'warning' );
        }

        fileManager.init()
            .then( function() {
                that._showFeedback();
                that._changeListener();
                $input.removeAttr( 'disabled' );
                if ( existingFileName ) {
                    fileManager.getFileUrl( existingFileName )
                        .then( function( url ) {
                            that._showPreview( url, that.mediaType );
                        } )
                        .catch( function() {
                            that._showFeedback( 'File "' + existingFileName + '" could not be found (leave unchanged if already submitted and you want to preserve it).', 'error' );
                        } );
                }
            } )
            .catch( function( error ) {
                that._showFeedback( error.message, 'error' );
            } );
    };

    Filepicker.prototype._getMaxSubmissionSize = function() {
        var maxSize = $( document ).data( 'maxSubmissionSize' );
        return maxSize || 5 * 1024 * 1024;
    };

    Filepicker.prototype._changeListener = function() {
        var that = this;

        $( this.element ).on( 'change.passthrough.' + this.namespace, function( event ) {
            var file,
                $input = $( this );

            // trigger eventhandler to update instance value
            if ( event.namespace === 'passthrough' ) {
                $input.trigger( 'change.file' );
                return false;
            }

            // get the file
            file = this.files[ 0 ];

            // process the file
            fileManager.getFileUrl( file )
                .then( function( url ) {
                    that._showPreview( url, that.mediaType );
                    that._showFeedback( '' );
                    that._showFileName( file );
                    $input.trigger( 'change.passthrough' );
                } )
                .catch( function( error ) {
                    $input.val( '' );
                    that._showPreview( null );
                    that._showFeedback( error.message, 'error' );
                } );
        } );
    };

    Filepicker.prototype._showFileName = function( file ) {
        var fileName = ( typeof file === 'object' && file.name ) ? file.name : file;
        this.$fakeInput.text( fileName );
    };

    Filepicker.prototype._showFeedback = function( message, status ) {
        message = message || '';
        status = status || '';
        // replace text and replace all existing classes with the new status class
        this.$feedback.text( message ).attr( 'class', 'file-feedback ' + status );
    };

    Filepicker.prototype._showPreview = function( url, mediaType ) {
        var $el;

        this.$widget.find( '.file-preview' ).empty();

        switch ( mediaType ) {
            case 'image/*':
                $el = $( '' );
                break;
            case 'audio/*':
                $el = $( '<audio controls="controls"/>' );
                break;
            case 'video/*':
                $el = $( '<video controls="controls"/>' );
                break;
            default:
                $el = $( 'No preview for this mediatype' );
                break;
        }

        if ( url ) {
            this.$preview.append( $el.attr( 'src', url ) );
        }
    };

    $.fn[ pluginName ] = function( options, event ) {

        options = options || {};

        return this.each( function() {
            var $this = $( this ),
                data = $this.data( pluginName );

            //only instantiate if options is an object (i.e. not a string) and if it doesn't exist already
            if ( !data && typeof options === 'object' ) {
                $this.data( pluginName, ( data = new Filepicker( this, options, event ) ) );
            }
            //only call method if widget was instantiated before
            else if ( data && typeof options == 'string' ) {
                //pass the element as a parameter as this is used in fix()
                data[ options ]( this );
            }
        } );
    };

} );
<commit_msg>Yogesh || Platform- retain media file<commit_after>define( [ 'jquery', 'enketo-js/Widget', 'file-manager' ], function( $, Widget, fileManager ) {
    "use strict";

    var pluginName = 'filepicker';

    /**
     * FilePicker that works both offline and online. It abstracts the file storage/cache away
     * with the injected fileManager.
     *
     * @constructor
     * @param {Element} element [description]
     * @param {(boolean|{touch: boolean, maxlength:number})} options options
     * @param {*=} e     event
     */

    function Filepicker( element, options, e ) {
        if ( e ) {
            e.stopPropagation();
            e.preventDefault();
        }
        this.namespace = pluginName;
        Widget.call( this, element, options );
        this._init();
    }

    // copy the prototype functions from the Widget super class
    Filepicker.prototype = Object.create( Widget.prototype );

    // ensure the constructor is the new one
    Filepicker.prototype.constructor = Filepicker;

    /**
     * Initialize
     */
    Filepicker.prototype._init = function() {
        var $input = $( this.element ),
            existingFileName = $input.attr( 'data-loaded-file-name' ),
            that = this;

        this.mediaType = $input.attr( 'accept' );

        $input
            .attr( 'disabled', 'disabled' )
            .addClass( 'transparent' )
            .parent().addClass( 'with-media clearfix' );

        this.$widget = $(
                '
' +
                '

' +
                '

' +
                '

' +
                '
' )
            .insertAfter( $input );
        this.$feedback = this.$widget.find( '.file-feedback' );
        this.$preview = this.$widget.find( '.file-preview' );
        this.$fakeInput = this.$widget.find( '.fake-file-input' );

        // show loaded file name regardless of whether widget is supported
        if ( existingFileName ) {
            this._showFileName( existingFileName, this.mediaType );
        }

        if ( !fileManager || !fileManager.isSupported() ) {
            var advice = ( fileManager.notSupportedAdvisoryMsg ) ? fileManager.notSupportedAdvisoryMsg : '';
            this._showFeedback( 'Media questions are not supported in this browser. ' + advice, 'warning' );
            return;
        }

        if ( fileManager.isWaitingForPermissions() ) {
            this._showFeedback( 'Waiting for user permissions.', 'warning' );
        }

        fileManager.init()
            .then( function() {
                that._showFeedback();
                that._changeListener();
                $input.removeAttr( 'disabled' );
                if ( existingFileName ) {
                    fileManager.getFileUrl( existingFileName )
                        .then( function( url ) {
                            that._showPreview( url, that.mediaType );
                        } )
                        .catch( function() {
                            that._showFeedback( 'File "' + existingFileName + '" could not be found (leave unchanged if already submitted and you want to preserve it).', 'error' );
                        } );
                }
            } )
            .catch( function( error ) {
                that._showFeedback( error.message, 'error' );
            } );
    };

    Filepicker.prototype._getMaxSubmissionSize = function() {
        var maxSize = $( document ).data( 'maxSubmissionSize' );
        return maxSize || 5 * 1024 * 1024;
    };

    Filepicker.prototype._changeListener = function() {
        var that = this;

        $( this.element ).on( 'change.passthrough.' + this.namespace, function( event ) {
            var file,
                $input = $( this );

            // trigger eventhandler to update instance value
            if ( event.namespace === 'passthrough' ) {
                $input.trigger( 'change.file' );
                return false;
            }

            // get the file
            file = this.files[ 0 ];
            $input.removeAttr( 'data-loaded-file-name' );

            // process the file
            fileManager.getFileUrl( file )
                .then( function( url ) {
                    that._showPreview( url, that.mediaType );
                    that._showFeedback( '' );
                    that._showFileName( file );
                    $input.trigger( 'change.passthrough' );
                } )
                .catch( function( error ) {
                    $input.val( '' );
                    that._showPreview( null );
                    that._showFeedback( error.message, 'error' );
                } );
        } );
    };

    Filepicker.prototype._showFileName = function( file ) {
        var fileName = ( typeof file === 'object' && file.name ) ? file.name : file;
        this.$fakeInput.text( fileName );
    };

    Filepicker.prototype._showFeedback = function( message, status ) {
        message = message || '';
        status = status || '';
        // replace text and replace all existing classes with the new status class
        this.$feedback.text( message ).attr( 'class', 'file-feedback ' + status );
    };

    Filepicker.prototype._showPreview = function( url, mediaType ) {
        var $el;

        this.$widget.find( '.file-preview' ).empty();

        switch ( mediaType ) {
            case 'image/*':
                $el = $( '' );
                break;
            case 'audio/*':
                $el = $( '<audio controls="controls"/>' );
                break;
            case 'video/*':
                $el = $( '<video controls="controls"/>' );
                break;
            default:
                $el = $( 'No preview for this mediatype' );
                break;
        }

        if ( url ) {
            this.$preview.append( $el.attr( 'src', url ) );
        }
    };

    $.fn[ pluginName ] = function( options, event ) {

        options = options || {};

        return this.each( function() {
            var $this = $( this ),
                data = $this.data( pluginName );

            //only instantiate if options is an object (i.e. not a string) and if it doesn't exist already
            if ( !data && typeof options === 'object' ) {
                $this.data( pluginName, ( data = new Filepicker( this, options, event ) ) );
            }
            //only call method if widget was instantiated before
            else if ( data && typeof options == 'string' ) {
                //pass the element as a parameter as this is used in fix()
                data[ options ]( this );
            }
        } );
    };

} );
<|endoftext|>


<commit_before>/* eslint-disable no-use-before-define */
/* This should probably be a parser instead of shitty regex.. */

/**
 * Makes 0 to many tabs as two spaces.
 *
 * spaces > tabs // suckers
 *
 * @param {Number} tabs - The number of tabs to create.
 * @return {String} a tab-spaced string
 */
function tab(tabs) {
  if (tabs <= 0) {
    return '';
  }

  return [...new Array(tabs)].reduce(s => `${s}  `, '');
}

/**
 * Formats the `oneOf([ ... ])` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatOneOf(value, tabs) {
  const values = value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
    .split(', ')
    .reduce((vs, v) => `${vs}${tab(tabs + 1)}${toPrettyPropType(v, tabs + 1)},\n`, '');

  return value.replace(/ /g, '')
    .replace(/(\[.*])/, `[\n${values}${tab(tabs)}]`);
}

/**
 * Formats the `oneOfType([ ... ])` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatOneOfType(value, tabs) {
  const types = value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
    // I'm so good at regex. So beautiful
    .split(/((?!\(|\[)(number|string|object|bool|custom|func|element|node|instanceOf),)|((arrayOf|oneOf).+\)|shape.+}\))/)
    // Clean up terrible regex by removing bad matches or empty strings
    .filter(t => !!t && !t.match(/,$/) && !!t.trim() && !t.match(/^(arrayOf|oneOf)$/))
    // If the match someone started with ', ${type}', remove the precending ', '
    .reduce((vs, v) => `${vs}${tab(tabs + 1)}${toPrettyPropType((v.indexOf(',') === 0 ? v.replace(', ', '') : v).trim(), tabs + 1)},\n`, '');

  return `oneOfType([\n${types}${tab(tabs)}])`;
}

/**
 * Formats the `arrayOf( ... )` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatArrayOf(value, tabs) {
  const inner = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')'));
  return `arrayOf(${toPrettyPropType(inner, tabs)})`;
}

/**
 * Extracts a `key`, `value`, and `remaining` string from a shape's value.
 *
 * Will probably need more testing of special shapes.
 *
 * @param {String} shapeValue - The current shape's value or remaining
 *    string.
 * @return {Object} an object containing the current key, value, and remaining string
 *    of the shape's value.
 */
function extractShapeKey(shapeValue) {
  const i = shapeValue.indexOf(': ') + 2;
  const key = shapeValue.substring(0, i);
  let value = '';
  let remaining = shapeValue.substring(i);
  if (isPrimitiveType(remaining)) {
    value = remaining.substring(0, remaining.indexOf(',') + 1);
    remaining = remaining.substring(value.length);
  } else if (remaining.match(/^oneOf/)) {
    value = remaining.substring(0, remaining.indexOf(']') + 3);
    remaining = remaining.substring(value.length);
  }

  return { key, value, remaining };
}

/**
 * Formats the `shape({ ... })` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatShape(value, tabs) {
  const keys = [];
  const values = [];
  let remaining = value.substring(value.indexOf(' ') + 1, value.lastIndexOf('}'));
  while (remaining) {
    const extracted = extractShapeKey(remaining);
    keys.push(extracted.key.trim());
    values.push(extracted.value);
    remaining = extracted.remaining;
  }

  const shape = keys.reduce((all, key, i) => `${all}${tab(tabs + 1)}${key} ${toPrettyPropType(values[i], tabs + 1)}\n`, '');

  return `shape({\n${shape}${tab(tabs)}})`;
}

function isPrimitiveType(type) {
  return !!type.match(/^(number|string|object|bool|custom|deprecated|func|element|node|instanceOf)/);
}

function isOneOf(type) {
  return !!type.match(/^oneOf\(/);
}

function isOneOfType(type) {
  return !!type.match(/^oneOfType/);
}

function isArrayOf(type) {
  return !!type.match(/^arrayOf/);
}

function isShape(type) {
  return !!type.match(/^shape/);
}

/**
 * Takes in a one line string or an already formatted string and prettifies it.. Terribly.
 *
 * Basically:
 *
 * ```js
 * shape({ hello: string, world: arrayOf(string) })
 * ```
 *
 * to
 *
 * ```js
 * shape({
 *   hello: string,
 *   world: arrayOf(string),
 * }),
 * ```
 *
 * This handles some neested structures. But the nested shapes are bit iffy still. Will probably
 * have to update that at some point.
 *
 * @param {String} type - The prop type to prettify.
 * @param {Number=} tabs - The current number of tabs to format with.
 * @return {String} a pretty prop type string!
 */
export default function toPrettyPropType(type, tabs = 0) {
  if (type.match(/\r?\n/) || isPrimitiveType(type)) {
    return type;
  } else if (isOneOf(type)) {
    return formatOneOf(type, tabs);
  } else if (isOneOfType(type)) {
    return formatOneOfType(type, tabs);
  } else if (isArrayOf(type)) {
    return formatArrayOf(type, tabs);
  } else if (isShape(type)) {
    return formatShape(type, tabs);
  }

  return type;
}
<commit_msg>Fixed the shape formatting for sapes that only have one value.<commit_after>/* eslint-disable no-use-before-define */
/* This should probably be a parser instead of shitty regex.. */

/**
 * Makes 0 to many tabs as two spaces.
 *
 * spaces > tabs // suckers
 *
 * @param {Number} tabs - The number of tabs to create.
 * @return {String} a tab-spaced string
 */
function tab(tabs) {
  if (tabs <= 0) {
    return '';
  }

  return [...new Array(tabs)].reduce(s => `${s}  `, '');
}

/**
 * Formats the `oneOf([ ... ])` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatOneOf(value, tabs) {
  const values = value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
    .split(', ')
    .reduce((vs, v) => `${vs}${tab(tabs + 1)}${toPrettyPropType(v, tabs + 1)},\n`, '');

  return value.replace(/ /g, '')
    .replace(/(\[.*])/, `[\n${values}${tab(tabs)}]`);
}

/**
 * Formats the `oneOfType([ ... ])` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatOneOfType(value, tabs) {
  const types = value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
    // I'm so good at regex. So beautiful
    .split(/((?!\(|\[)(number|string|object|bool|custom|func|element|node|instanceOf),)|((arrayOf|oneOf).+\)|shape.+}\))/)
    // Clean up terrible regex by removing bad matches or empty strings
    .filter(t => !!t && !t.match(/,$/) && !!t.trim() && !t.match(/^(arrayOf|oneOf)$/))
    // If the match someone started with ', ${type}', remove the precending ', '
    .reduce((vs, v) => `${vs}${tab(tabs + 1)}${toPrettyPropType((v.indexOf(',') === 0 ? v.replace(', ', '') : v).trim(), tabs + 1)},\n`, '');

  return `oneOfType([\n${types}${tab(tabs)}])`;
}

/**
 * Formats the `arrayOf( ... )` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatArrayOf(value, tabs) {
  const inner = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')'));
  return `arrayOf(${toPrettyPropType(inner, tabs)})`;
}

/**
 * Extracts a `key`, `value`, and `remaining` string from a shape's value.
 *
 * Will probably need more testing of special shapes.
 *
 * @param {String} shapeValue - The current shape's value or remaining
 *    string.
 * @return {Object} an object containing the current key, value, and remaining string
 *    of the shape's value.
 */
function extractShapeKey(shapeValue) {
  const i = shapeValue.indexOf(': ') + 2;
  const key = shapeValue.substring(0, i);
  let value = '';
  let remaining = shapeValue.substring(i);
  if (isPrimitiveType(remaining)) {
    let end = remaining.indexOf(',');
    if (end === -1) {
      end = remaining.indexOf(' ');
    }

    value = remaining.substring(0, end + 1);
    remaining = remaining.substring(value.length);
  } else if (remaining.match(/^oneOf/)) {
    value = remaining.substring(0, remaining.indexOf(']') + 3);
    remaining = remaining.substring(value.length);
  }

  return { key, value, remaining };
}

/**
 * Formats the `shape({ ... })` prop type.
 *
 * @param {String} value - The current prop type's value to format.
 * @param {Number} tabs - the current number of tabs
 * @return {String} a pretty string!
 */
function formatShape(value, tabs) {
  const keys = [];
  const values = [];
  let remaining = value.substring(value.indexOf(' ') + 1, value.lastIndexOf('}'));
  while (remaining) {
    const extracted = extractShapeKey(remaining);
    keys.push(extracted.key.trim());
    values.push(extracted.value);
    remaining = extracted.remaining;
  }

  const shape = keys.reduce((all, key, i) => `${all}${tab(tabs + 1)}${key} ${toPrettyPropType(values[i], tabs + 1)}\n`, '');

  return `shape({\n${shape}${tab(tabs)}})`;
}

function isPrimitiveType(type) {
  return !!type.match(/^(number|string|object|bool|custom|deprecated|func|element|node|instanceOf)/);
}

function isOneOf(type) {
  return !!type.match(/^oneOf\(/);
}

function isOneOfType(type) {
  return !!type.match(/^oneOfType/);
}

function isArrayOf(type) {
  return !!type.match(/^arrayOf/);
}

function isShape(type) {
  return !!type.match(/^shape/);
}

/**
 * Takes in a one line string or an already formatted string and prettifies it.. Terribly.
 *
 * Basically:
 *
 * ```js
 * shape({ hello: string, world: arrayOf(string) })
 * ```
 *
 * to
 *
 * ```js
 * shape({
 *   hello: string,
 *   world: arrayOf(string),
 * }),
 * ```
 *
 * This handles some neested structures. But the nested shapes are bit iffy still. Will probably
 * have to update that at some point.
 *
 * @param {String} type - The prop type to prettify.
 * @param {Number=} tabs - The current number of tabs to format with.
 * @return {String} a pretty prop type string!
 */
export default function toPrettyPropType(type, tabs = 0) {
  if (type.match(/\r?\n/) || isPrimitiveType(type)) {
    return type;
  } else if (isOneOf(type)) {
    return formatOneOf(type, tabs);
  } else if (isOneOfType(type)) {
    return formatOneOfType(type, tabs);
  } else if (isArrayOf(type)) {
    return formatArrayOf(type, tabs);
  } else if (isShape(type)) {
    return formatShape(type, tabs);
  }

  return type;
}
<|endoftext|>


<commit_before>import Flatpickr from 'flatpickr';
import InputWidget from './InputWidget';
import {
  convertFormatToFlatpickr,
  convertFormatToMask,
  convertFormatToMoment,
  currentTimezone,
  formatDate,
  formatOffset,
  getDateSetting,
  getLocaleDateFormatInfo,
  momentDate,
  zonesLoaded,
  shouldLoadZones,
  loadZones
} from '../utils/utils';
import moment from 'moment';
import _ from 'lodash';
const DEFAULT_FORMAT = 'yyyy-MM-dd hh:mm a';
const ISO_8601_FORMAT = 'yyyy-MM-ddTHH:mm:ssZ';

export default class CalendarWidget extends InputWidget {
  /* eslint-disable camelcase */
  static get defaultSettings() {
    return {
      type: 'calendar',
      altInput: true,
      allowInput: true,
      clickOpens: true,
      enableDate: true,
      enableTime: true,
      mode: 'single',
      noCalendar: false,
      format: DEFAULT_FORMAT,
      dateFormat: ISO_8601_FORMAT,
      useLocaleSettings: false,
      language: 'us-en',
      defaultDate: null,
      hourIncrement: 1,
      minuteIncrement: 5,
      time_24hr: false,
      saveAs: 'date',
      displayInTimezone: '',
      timezone: '',
      minDate: '',
      maxDate: ''
    };
  }
  /* eslint-enable camelcase */

  constructor(settings, component) {
    super(settings, component);
    // Change the format to map to the settings.
    if (this.settings.noCalendar) {
      this.settings.format = this.settings.format.replace(/yyyy-MM-dd /g, '');
    }
    if (!this.settings.enableTime) {
      this.settings.format = this.settings.format.replace(/ hh:mm a$/g, '');
    }
    else if (this.settings.time_24hr) {
      this.settings.format = this.settings.format.replace(/hh:mm a$/g, 'HH:mm');
    }
  }

  /**
   * Load the timezones.
   *
   * @return {boolean} TRUE if the zones are loading, FALSE otherwise.
   */
  loadZones() {
    const timezone = this.timezone;
    if (!zonesLoaded() && shouldLoadZones(timezone)) {
      loadZones(timezone).then(() => this.emit('redraw'));

      // Return zones are loading.
      return true;
    }

    // Zones are already loaded.
    return false;
  }

  attach(input) {
    super.attach(input);
    if (input && !input.getAttribute('placeholder')) {
      input.setAttribute('placeholder', this.settings.format);
    }

    const dateFormatInfo = getLocaleDateFormatInfo(this.settings.language);
    this.defaultFormat = {
      date: dateFormatInfo.dayFirst ? 'd/m/Y ' : 'm/d/Y ',
      time: 'G:i K'
    };

    this.closedOn = 0;
    this.valueFormat = this.settings.dateFormat || ISO_8601_FORMAT;
    this.valueMomentFormat = convertFormatToMoment(this.valueFormat);
    this.settings.minDate = getDateSetting(this.settings.minDate);
    this.settings.maxDate = getDateSetting(this.settings.maxDate);
    this.settings.defaultDate = getDateSetting(this.settings.defaultDate);
    this.settings.altFormat = convertFormatToFlatpickr(this.settings.format);
    this.settings.dateFormat = convertFormatToFlatpickr(this.settings.dateFormat);
    this.settings.onChange = () => this.emit('update');
    this.settings.onClose = () => (this.closedOn = Date.now());
    this.settings.formatDate = (date, format) => {
      // Only format this if this is the altFormat and the form is readOnly.
      if (this.settings.readOnly && (format === this.settings.altFormat)) {
        if (this.settings.saveAs === 'text' || this.loadZones()) {
          return Flatpickr.formatDate(date, format);
        }

        return formatOffset(Flatpickr.formatDate.bind(Flatpickr), date, format, this.timezone);
      }

      return Flatpickr.formatDate(date, format);
    };

    if (this._input) {
      // Create a new flatpickr.
      this.calendar = new Flatpickr(this._input, this.settings);

      // Enforce the input mask of the format.
      this.setInputMask(this.calendar._input, convertFormatToMask(this.settings.format));

      // Make sure we commit the value after a blur event occurs.
      ['blir', 'keyup'].map((event) => this.addEventListener(this.calendar._input, event, () =>
        this.calendar.setDate(this.calendar._input.value, true, this.settings.altFormat)
      ));
    }
  }

  get timezone() {
    if (this.settings.timezone) {
      return this.settings.timezone;
    }
    if (this.settings.displayInTimezone === 'submission' && this.settings.submissionTimezone) {
      return this.settings.submissionTimezone;
    }
    if (this.settings.displayInTimezone === 'utc') {
      return 'UTC';
    }

    // Return current timezone if none are provided.
    return currentTimezone();
  }

  get defaultSettings() {
    return CalendarWidget.defaultSettings;
  }

  addSuffix(suffix) {
    this.addEventListener(suffix, 'click', () => {
      if (this.calendar && !this.calendar.isOpen && ((Date.now() - this.closedOn) > 200)) {
        this.calendar.open();
      }
    });
    return suffix;
  }

  set disabled(disabled) {
    super.disabled = disabled;
    if (this.calendar) {
      if (disabled) {
        this.calendar._input.setAttribute('disabled', 'disabled');
      }
      else {
        this.calendar._input.removeAttribute('disabled');
      }
      this.calendar.close();
      this.calendar.redraw();
    }
  }

  get input() {
    return this.calendar ? this.calendar.altInput : null;
  }

  get localeFormat() {
    let format = '';

    if (this.settings.enableDate) {
      format += this.defaultFormat.date;
    }

    if (this.settings.enableTime) {
      format += this.defaultFormat.time;
    }

    return format;
  }

  get dateTimeFormat() {
    return this.settings.useLocaleSettings ? this.localeFormat : convertFormatToFlatpickr(this.dateFormat);
  }

  get dateFormat() {
    return _.get(this.settings, 'format', DEFAULT_FORMAT);
  }

  /**
   * Get the default date for the calendar.
   * @return {*}
   */
  get defaultDate() {
    return getDateSetting(this.settings.defaultDate);
  }

  get defaultValue() {
    const defaultDate = this.defaultDate;
    return defaultDate ? defaultDate.toISOString() : '';
  }

  /**
   * Return the date value.
   *
   * @param date
   * @param format
   * @return {string}
   */
  getDateValue(date, format) {
    return moment(date).format(convertFormatToMoment(format));
  }

  /**
   * Return the value of the selected date.
   *
   * @return {*}
   */
  getValue() {
    // Standard output format.
    if (!this.calendar) {
      return super.getValue();
    }

    // Get the selected dates from the calendar widget.
    const dates = this.calendar.selectedDates;
    if (!dates || !dates.length) {
      return super.getValue();
    }

    if (!(dates[0] instanceof Date)) {
      return 'Invalid Date';
    }

    return this.getDateValue(dates[0], this.valueFormat);
  }

  /**
   * Set the selected date value.
   *
   * @param value
   */
  setValue(value) {
    if (!this.calendar) {
      return super.setValue(value);
    }
    if (value) {
      if ((this.settings.saveAs !== 'text') && this.settings.readOnly && !this.loadZones()) {
        this.calendar.setDate(momentDate(value, this.valueFormat, this.timezone).toDate(), false);
      }
      else {
        this.calendar.setDate(moment(value, this.valueMomentFormat).toDate(), false);
      }
    }
    else {
      this.calendar.clear(false);
    }
  }

  getView(value, format) {
    format = format || this.dateFormat;
    if (this.settings.saveAs === 'text') {
      return this.getDateValue(value, format);
    }

    return formatDate(value, format, this.timezone);
  }

  validationValue(value) {
    if (typeof value === 'string') {
      return new Date(value);
    }
    return value.map(val => new Date(val));
  }

  destroy() {
    super.destroy();
    this.calendar.destroy();
  }
}
<commit_msg>Fixed blir to blur<commit_after>import Flatpickr from 'flatpickr';
import InputWidget from './InputWidget';
import {
  convertFormatToFlatpickr,
  convertFormatToMask,
  convertFormatToMoment,
  currentTimezone,
  formatDate,
  formatOffset,
  getDateSetting,
  getLocaleDateFormatInfo,
  momentDate,
  zonesLoaded,
  shouldLoadZones,
  loadZones
} from '../utils/utils';
import moment from 'moment';
import _ from 'lodash';
const DEFAULT_FORMAT = 'yyyy-MM-dd hh:mm a';
const ISO_8601_FORMAT = 'yyyy-MM-ddTHH:mm:ssZ';

export default class CalendarWidget extends InputWidget {
  /* eslint-disable camelcase */
  static get defaultSettings() {
    return {
      type: 'calendar',
      altInput: true,
      allowInput: true,
      clickOpens: true,
      enableDate: true,
      enableTime: true,
      mode: 'single',
      noCalendar: false,
      format: DEFAULT_FORMAT,
      dateFormat: ISO_8601_FORMAT,
      useLocaleSettings: false,
      language: 'us-en',
      defaultDate: null,
      hourIncrement: 1,
      minuteIncrement: 5,
      time_24hr: false,
      saveAs: 'date',
      displayInTimezone: '',
      timezone: '',
      minDate: '',
      maxDate: ''
    };
  }
  /* eslint-enable camelcase */

  constructor(settings, component) {
    super(settings, component);
    // Change the format to map to the settings.
    if (this.settings.noCalendar) {
      this.settings.format = this.settings.format.replace(/yyyy-MM-dd /g, '');
    }
    if (!this.settings.enableTime) {
      this.settings.format = this.settings.format.replace(/ hh:mm a$/g, '');
    }
    else if (this.settings.time_24hr) {
      this.settings.format = this.settings.format.replace(/hh:mm a$/g, 'HH:mm');
    }
  }

  /**
   * Load the timezones.
   *
   * @return {boolean} TRUE if the zones are loading, FALSE otherwise.
   */
  loadZones() {
    const timezone = this.timezone;
    if (!zonesLoaded() && shouldLoadZones(timezone)) {
      loadZones(timezone).then(() => this.emit('redraw'));

      // Return zones are loading.
      return true;
    }

    // Zones are already loaded.
    return false;
  }

  attach(input) {
    super.attach(input);
    if (input && !input.getAttribute('placeholder')) {
      input.setAttribute('placeholder', this.settings.format);
    }

    const dateFormatInfo = getLocaleDateFormatInfo(this.settings.language);
    this.defaultFormat = {
      date: dateFormatInfo.dayFirst ? 'd/m/Y ' : 'm/d/Y ',
      time: 'G:i K'
    };

    this.closedOn = 0;
    this.valueFormat = this.settings.dateFormat || ISO_8601_FORMAT;
    this.valueMomentFormat = convertFormatToMoment(this.valueFormat);
    this.settings.minDate = getDateSetting(this.settings.minDate);
    this.settings.maxDate = getDateSetting(this.settings.maxDate);
    this.settings.defaultDate = getDateSetting(this.settings.defaultDate);
    this.settings.altFormat = convertFormatToFlatpickr(this.settings.format);
    this.settings.dateFormat = convertFormatToFlatpickr(this.settings.dateFormat);
    this.settings.onChange = () => this.emit('update');
    this.settings.onClose = () => (this.closedOn = Date.now());
    this.settings.formatDate = (date, format) => {
      // Only format this if this is the altFormat and the form is readOnly.
      if (this.settings.readOnly && (format === this.settings.altFormat)) {
        if (this.settings.saveAs === 'text' || this.loadZones()) {
          return Flatpickr.formatDate(date, format);
        }

        return formatOffset(Flatpickr.formatDate.bind(Flatpickr), date, format, this.timezone);
      }

      return Flatpickr.formatDate(date, format);
    };

    if (this._input) {
      // Create a new flatpickr.
      this.calendar = new Flatpickr(this._input, this.settings);

      // Enforce the input mask of the format.
      this.setInputMask(this.calendar._input, convertFormatToMask(this.settings.format));

      // Make sure we commit the value after a blur event occurs.
      ['blur', 'keyup'].map((event) => this.addEventListener(this.calendar._input, event, () =>
        this.calendar.setDate(this.calendar._input.value, true, this.settings.altFormat)
      ));
    }
  }

  get timezone() {
    if (this.settings.timezone) {
      return this.settings.timezone;
    }
    if (this.settings.displayInTimezone === 'submission' && this.settings.submissionTimezone) {
      return this.settings.submissionTimezone;
    }
    if (this.settings.displayInTimezone === 'utc') {
      return 'UTC';
    }

    // Return current timezone if none are provided.
    return currentTimezone();
  }

  get defaultSettings() {
    return CalendarWidget.defaultSettings;
  }

  addSuffix(suffix) {
    this.addEventListener(suffix, 'click', () => {
      if (this.calendar && !this.calendar.isOpen && ((Date.now() - this.closedOn) > 200)) {
        this.calendar.open();
      }
    });
    return suffix;
  }

  set disabled(disabled) {
    super.disabled = disabled;
    if (this.calendar) {
      if (disabled) {
        this.calendar._input.setAttribute('disabled', 'disabled');
      }
      else {
        this.calendar._input.removeAttribute('disabled');
      }
      this.calendar.close();
      this.calendar.redraw();
    }
  }

  get input() {
    return this.calendar ? this.calendar.altInput : null;
  }

  get localeFormat() {
    let format = '';

    if (this.settings.enableDate) {
      format += this.defaultFormat.date;
    }

    if (this.settings.enableTime) {
      format += this.defaultFormat.time;
    }

    return format;
  }

  get dateTimeFormat() {
    return this.settings.useLocaleSettings ? this.localeFormat : convertFormatToFlatpickr(this.dateFormat);
  }

  get dateFormat() {
    return _.get(this.settings, 'format', DEFAULT_FORMAT);
  }

  /**
   * Get the default date for the calendar.
   * @return {*}
   */
  get defaultDate() {
    return getDateSetting(this.settings.defaultDate);
  }

  get defaultValue() {
    const defaultDate = this.defaultDate;
    return defaultDate ? defaultDate.toISOString() : '';
  }

  /**
   * Return the date value.
   *
   * @param date
   * @param format
   * @return {string}
   */
  getDateValue(date, format) {
    return moment(date).format(convertFormatToMoment(format));
  }

  /**
   * Return the value of the selected date.
   *
   * @return {*}
   */
  getValue() {
    // Standard output format.
    if (!this.calendar) {
      return super.getValue();
    }

    // Get the selected dates from the calendar widget.
    const dates = this.calendar.selectedDates;
    if (!dates || !dates.length) {
      return super.getValue();
    }

    if (!(dates[0] instanceof Date)) {
      return 'Invalid Date';
    }

    return this.getDateValue(dates[0], this.valueFormat);
  }

  /**
   * Set the selected date value.
   *
   * @param value
   */
  setValue(value) {
    if (!this.calendar) {
      return super.setValue(value);
    }
    if (value) {
      if ((this.settings.saveAs !== 'text') && this.settings.readOnly && !this.loadZones()) {
        this.calendar.setDate(momentDate(value, this.valueFormat, this.timezone).toDate(), false);
      }
      else {
        this.calendar.setDate(moment(value, this.valueMomentFormat).toDate(), false);
      }
    }
    else {
      this.calendar.clear(false);
    }
  }

  getView(value, format) {
    format = format || this.dateFormat;
    if (this.settings.saveAs === 'text') {
      return this.getDateValue(value, format);
    }

    return formatDate(value, format, this.timezone);
  }

  validationValue(value) {
    if (typeof value === 'string') {
      return new Date(value);
    }
    return value.map(val => new Date(val));
  }

  destroy() {
    super.destroy();
    this.calendar.destroy();
  }
}
<|endoftext|>